容器内显示 PID 1,却不代表宿主也看到 PID 1;新建挂载空间,也没有复制一份文件内容。namespace 为各类资源建立视图和权限边界,cgroup 则管理资源用量。
容器还需组合根文件系统、capability、seccomp 与安全策略,不存在一个包办全部隔离的 namespace 对象。
| 查阅目标 | 阅读位置 |
| 对象范围与引用 | 类型、创建与进入 |
| 身份和文件视图 | PID、User、Mount |
| 可回收实验与其余边界 | 隔离示例、网络与时间 |
各类 namespace 管什么
| 类型 | 标志 | 范围与边界 |
| Mount | CLONE_NEWNS | 挂载树视图;文件内容可能仍共享,事件可能传播 |
| PID | CLONE_NEWPID | PID 编号与可见层级;不是每个成员都变成 PID 1 |
| User | CLONE_NEWUSER | UID/GID 映射和 capability 作用域 |
| Network | CLONE_NEWNET | 网络设备、路由、协议栈状态和端口等 |
| IPC | CLONE_NEWIPC | SysV IPC、POSIX 消息队列等 |
| UTS | CLONE_NEWUTS | hostname 和 NIS domainname;不是独立 DNS 系统 |
| Cgroup | CLONE_NEWCGROUP | cgroup 路径视图;不创建资源额度 |
| Time | CLONE_NEWTIME | monotonic/boottime 等时钟偏移;不虚拟化所有时钟 |
具体能力依赖内核配置,创建和进入也受权限限制。namespace 概览
创建、进入与持有引用
clone/clone3 可以让新任务进入新 namespace;unshare 为调用者解除某些共享关系;setns 进入由 fd 指定的已有 namespace。各类型有特殊规则,不能把三者视为完全等价的“更换容器”。
/proc/PID/ns 下的链接可用于比较成员关系,打开相应 fd 或保留 bind mount 可以维持 namespace 引用。进程退出不一定立刻销毁仍被持有的 namespace 对象。
nsproxy 保存多类 namespace 引用,但 user namespace 关联进程凭据,当前 PID namespace 又与 PID 层级有关;不能把 nsproxy 简化为包含一切的完整快照。pid_for_children 和 time_for_children 专门提示“当前所属”与“后续子进程所属”的区别。
PID namespace:编号是相对的
新 PID namespace 的第一个进程成为其中的 PID 1,后续成员有各自编号。同一任务在祖先 PID namespace 中还有其他 PID;祖先可看见后代,子 namespace 不借此获得查看全部祖先进程的能力。
unshare(CLONE_NEWPID) 不改变调用者自己的 PID namespace,而是影响之后创建的子进程;setns 对 PID namespace 也有对应的子进程语义。这解释了 unshare 命令通常需要 --fork 才能让执行的程序进入新 PID 视图。PID namespace
先创建一个子进程,再 unshare 并创建后续子进程。从两个观察者切换视图,比较同一个任务的身份与可见编号,而不是把 namespace 当成复制了一份进程。
正在呈现知识画面
unshare 以后,父进程的 PID 为什么没变?unshare(CLONE_NEWPID) 改变 pid_for_children,不迁移调用者当前 PID namespace。同一子任务可在祖先和自身空间拥有不同 PID,并没有复制两个进程。
{"id":"pid-namespace-identity","title":"unshare 以后,父进程的 PID 为什么没变?","summary":"unshare(CLONE_NEWPID) 改变 pid_for_children,不迁移调用者当前 PID namespace。同一子任务可在祖先和自身空间拥有不同 PID,并没有复制两个进程。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"fork\" data-i18n=\"fork\"></button><button id=\"unshare\" data-i18n=\"unshare\"></button></div><div class=\"track-row\"><span data-i18n=\"current\"></span><div id=\"current\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"future\"></span><div id=\"future\" class=\"track\"></div></div><div class=\"presets\" role=\"group\" data-i18n-label=\"view\"><button data-mode=\"host\" data-i18n=\"host\"></button><button data-mode=\"child\" data-i18n=\"child\"></button></div><div class=\"track-row\"><span data-i18n=\"identity\"></span><div id=\"identity\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"visible\"></span><div id=\"visible\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n#identity,#visible{display:grid;grid-template-columns:1fr}#identity .cell{min-height:45px;font:12px var(--viz-font-ui)}#visible .cell{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let created=false,children=[],serial=0,view='host';function draw(){cells('current',[{text:'NS0 · getpid() = 400',cls:'on'}]);cells('future',[{text:created?'NS1':'NS0',cls:created?'gold':'on'}]);const all=[{host:400,local:null},...children];cells('identity',Array.from({length:4},(_,i)=>{const p=all[i];return{text:p?'task '+p.host+' → NS0:'+p.host+' / NS1:'+(p.local??'—'):'—',cls:p?.local?'gold':p?'on':'empty'};}));cells('visible',Array.from({length:4},(_,i)=>{const p=all[i];return{text:!p?'—':view==='host'?'PID '+p.host:p.local?'PID '+p.local:t('hidden'),cls:p&&(view==='host'||p.local)?'on':'empty'};}));$('#fork').disabled=children.length===3;$('#unshare').disabled=created;pressed(view);set('explanation',t('note'));}$('#fork').onclick=()=>{children.push({host:401+children.length,local:created?++serial:null});draw();};$('#unshare').onclick=()=>{created=true;draw();};document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{view=e.dataset.mode;draw();});$('#reset').onclick=()=>{created=false;children=[];serial=0;draw();};draw();","audio":false,"strings":{"child":"新子空间","current":"父任务当前 PID namespace","fork":"父进程创建一个子进程","future":"父任务 pid_for_children","heading":"unshare 以后,父进程的 PID 为什么没变?","hidden":"在此视图不可见","host":"祖先空间","identity":"一个对象,两层编号","intro":"先创建一个子进程,再为后续子进程建立 PID namespace,继续创建。切换观察视图,对照同一任务在祖先和子空间中的编号。","note":"unshare 前已存在的子进程也不会搬家。只有此后创建的子任务进入新空间,第一个成为那里 PID 1,父任务 getpid() 仍返回 400。","reset":"重置","scope":"父任务宿主 PID=400,最多创建三个子任务,只创建一层 PID namespace。编号按教学顺序分配,实际 PID 分配不保证连续。省略权限、init 退出、收养、setns 和 proc 挂载;子空间视图只展示其可见成员。","scopeLabel":"模型范围与约定","unshare":"为后续子进程 unshare PID","view":"观察者所在空间","visible":"当前视图里的 PID"}}
PID 1 具有特殊信号规则:namespace 内其他成员通常只能向它发送其已安装处理器的信号,SIGKILL/SIGSTOP 不能像普通进程一样在内部生效;来自祖先 namespace 的 SIGKILL/SIGSTOP 有特定例外。因此“内部不能 kill,但可以 SIGSTOP”是错误概括。
PID 1 退出时,内核会杀死该 namespace 中其余进程,后续向这个 namespace 创建进程也会受限制;对象仍可能被 fd 保持,不能等同于立即释放所有引用。
验证“当前空间没变,后续子进程变了”
下面用 Python 3.12+ 的 Linux os.unshare() 接口直接观察这两个成员关系。先从命令行创建一个临时 user namespace,让程序获得创建自己 PID namespace 所需的作用域内权限;程序保留外层 proc 挂载,便于父进程按外层 PID 检查子进程。
保存为 pid_view.py,用 unshare --user --map-root-user python3 pid_view.py 运行。程序只创建一个子进程,并用管道控制其退出时机。
import errno
import os
def ns_identity(path):
info = os.stat(path)
return info.st_dev, info.st_ino
before_pid = os.getpid()
before_ns = ns_identity("/proc/self/ns/pid")
os.unshare(os.CLONE_NEWPID)
assert os.getpid() == before_pid
assert ns_identity("/proc/self/ns/pid") == before_ns
read_fd, write_fd = os.pipe()
child = os.fork()
if child == 0:
os.close(write_fd)
try:
assert os.getpid() == 1 and os.getppid() == 0
assert os.read(read_fd, 1) == b"x"
except BaseException:
os._exit(1)
os._exit(0)
os.close(read_fd)
try:
child_ns = ns_identity(f"/proc/{child}/ns/pid")
assert child_ns != before_ns
assert ns_identity("/proc/self/ns/pid_for_children") == child_ns
assert ns_identity("/proc/self/ns/pid") == before_ns
assert os.write(write_fd, b"x") == 1
finally:
os.close(write_fd)
_, status = os.waitpid(child, 0)
assert os.waitstatus_to_exitcode(status) == 0
print(f"parent PID unchanged: {before_pid}; child seen outside: {child}; inside: 1")
try:
unexpected = os.fork()
except OSError as error:
assert error.errno == errno.ENOMEM
else:
if unexpected == 0:
os._exit(1)
os.waitpid(unexpected, 0)
raise AssertionError("已退出 init 的 PID namespace 不应接受新进程")
PID 数字随每次运行改变,真正要检查的是四个关系:父进程自己的编号未变;子进程在内部是 1;子进程看到外部父进程的 PPID 为 0;父进程的 pid_for_children 与子进程当前 PID namespace 相同。pid_for_children 在第一次创建子进程前还有特殊可见性,因此代码等到 fork 之后才比较它。
最后的 ENOMEM 在这个受控轨迹里来自 namespace init 已退出,不能一概解释为物理内存不足。实际程序仍应保留 errno 并结合前置操作诊断。实验不重新挂载 proc,所以它没有演示容器内 ps 的完整视图;需要该视图时再配合独立 mount namespace 和新的 proc 实例。PID namespace 的成员与 proc 语义
作为 PID 1 运行,还需承担信号与回收责任
孤儿由适当的 subreaper 或 namespace init 收养,负责方应 wait 回收。应用作为 PID 1 时,需要正确处理终止信号与子进程回收,不能只依赖普通交互式程序的默认行为。
User namespace:root 的意义取决于映射
在非初始 user namespace 内读取自己的 /proc/self/uid_map,若看到 0 1000 1,可按三个字段解释:
| 字段 | 数值 | 含义 |
| 内部起始 UID | 0 | 新空间内的身份 |
| 父空间起始 UID | 1000 | 对应外部身份 |
| 范围长度 | 1 | 仅映射这一个 UID |
它不会自动创建其他用户映射,也不能把内部 root 简单换算成宿主 root。
内部 root 不等于宿主 nobody;也不是自动无法读取任何宿主文件。若挂载视图暴露了宿主文件,映射后的身份仍可能拥有普通访问权限。user namespace 内的 capability 主要对其权限作用域内资源生效,不会自动获得初始 user namespace 的全局权限。User namespace
多 UID/GID 映射涉及 subuid/subgid、映射助手和 setgroups 限制等。无特权 user namespace 是否可创建,还可能受发行版安全策略、LSM 和数量限制影响;kernel.unprivileged_userns_clone 不是所有 Linux 上都统一存在的开关。
嵌套映射要逐层合成,也要记录读者视角
设第一层 namespace 到初始 namespace 的范围为 0 100000 65536,第二层到第一层只映射 0 1000 1。那么第二层 UID 0 先映成第一层 UID 1000,再映成初始 namespace UID 101000;第一层自己的 UID 0 则对应初始 UID 100000。两层都叫 root,并不意味着它们代表同一个外部身份。
def mapped(uid, ranges):
for inside, outside, count in ranges:
if inside <= uid < inside + count:
return outside + uid - inside
return None
first_to_initial = [(0, 100000, 65536)]
second_to_first = [(0, 1000, 1)]
intermediate = mapped(0, second_to_first)
assert intermediate == 1000
assert mapped(intermediate, first_to_initial) == 101000
assert mapped(0, first_to_initial) == 100000
assert mapped(65535, first_to_initial) == 165535
assert mapped(65536, first_to_initial) is None
assert mapped(1, second_to_first) is None
模型里的 None 表示没有对应映射,不是 UID 0,也不等于自动拥有 nobody 的权限。内核在不同接口中可能用 overflow UID 显示无法表示的所有者,或者拒绝需要有效映射的操作;显示结果不能替代授权判断。上述范围也只是映射计算示例,实际创建它们仍需满足映射写入权限与父层可映射范围。
还有一个容易漏掉的细节:如果读取者与目标进程在同一个 user namespace,uid_map 第二列按目标的父 namespace 解释;如果两者在不同 user namespace,第二列按打开该文件的读取者的 namespace 解释。因此采集映射时应同时记录读取者的位置,不能把任意 /proc/PID/uid_map 的第二列都标成“宿主 UID”或“父 UID”。User namespace:UID/GID 映射的读取规则
Mount namespace:独立挂载表不等于独立文件内容
新空间通常从现有挂载视图复制而来,底层文件系统仍可能相同。共享子树的 shared/slave/private 等传播属性决定挂载事件如何跨视图传播;因此“一个空间 mount 永远不影响另一个”不成立。挂载 namespace
根目录隔离还需要合适的挂载和根切换设计。chroot 或新 mount namespace 本身都不会复制所有文件,也不会自动撤销已打开 fd 的访问能力。
一个不改宿主 hostname 的局部示例
以下命令在允许无特权 user namespace 的环境中创建 user、UTS、PID 和 mount 空间,子进程退出后结束实验:
unshare --user --map-root-user --uts --pid --fork --mount --mount-proc sh -c 'hostname kb-demo; printf "pid=%s hostname=%s\n" "$$" "$(hostname)"; cat /proc/self/uid_map'
hostname 修改发生在新 UTS namespace;--mount-proc 为新 PID 视图挂载 proc,避免 ps 等工具仍读取外部 proc 实例。输出的 UID 映射取决于执行者,不能预设一定为宿主 1000。权限拒绝时先识别环境策略,不要为运行教学例子直接放宽整机安全配置。
网络与时间空间的常见误解
容器可以共享网络 namespace,甚至使用宿主网络,所以不是每个容器必有独立 netns。新网络空间也不自动得到可用外网,需要按方案配置设备、地址、路由或用户态网络后端。
time namespace 为相应 monotonic/boottime 时钟提供偏移,适合保持迁移后的时间视图;CLOCK_REALTIME 不因此获得每容器独立墙钟。创建后的子进程与偏移配置时机也有专门限制。Time namespace
进一步理解资源上限与委派,见 cgroup。