docker run 背后的内核机制就两项:namespace(视图隔离)和 cgroup(资源限制)。本文专讲 namespace——如何把全局内核资源切成多份、让一群进程”活在自己的宇宙”里。
一、总览
flowchart TB
T[task_struct]-->N[nsproxy]
N-->MNT[mnt_namespace<br/>挂载视图]
N-->PID[pid_namespace<br/>PID 空间]
N-->NET[net_namespace<br/>网络栈]
N-->IPC[ipc_namespace<br/>SysV/POSIX/mq]
N-->UTS[uts_namespace<br/>hostname/domain]
N-->TIME[time_namespace<br/>CLOCK_MONOTONIC 偏移]
N-->CG[cgroup_namespace<br/>cgroup 视图]
T-->U[user_namespace<br/>uid/gid 映射 + capabilities]
classDef k fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef u fill:#f0883e22,stroke:#f0883e,color:#adbac7;
class T,N,MNT,PID,NET,IPC,UTS,TIME,CG k
class U u
注意:user namespace 不在 nsproxy 里,挂在 task 直接引用。它是最特别的一种——是其他 ns 的”权限根”。
二、七种 namespace 逐一看
2.1 mount namespace(MNT)
每个 mnt ns 有独立的挂载表。namespace 内的 mount/umount 不影响外部。
mount
propagation:private、shared、slave、unbindable。容器运行时用
slave 继承宿主机的
/proc、/sys,但自己的 mount
不传回宿主。
pivot_root vs chroot:容器一般用 pivot_root 切根——比 chroot 安全(不能”退出”),且需要 mount ns 支持。
2.2 pid namespace(PID)
pid ns 内部的进程看到的 PID 从 1 开始。
- 第一个进程是该 ns 的 init,pid ns 里别的进程孤儿化时 reparent 到它
- 如果 ns init 退了,整个 ns 里的进程全被 SIGKILL(内核强制清理)
- ns init 对信号有特殊待遇:没装 handler
的信号默认被忽略(和全局 PID 1 一样)——所以容器里
--init很常用 - 嵌套:pid ns 可以嵌套,内层进程在外层也有不同的 pid 号
2.3 net namespace(NET)
独立的网络栈:接口、路由表、firewall、socket port、sysctl net。
创建 net ns 后默认只有
lo。要让容器能通信:
vethpair:一端在 host,一端在容器;host 端接 bridge(docker0)- 或
macvlan/ipvlan:直接分父接口的 MAC/IP - 或 eBPF/Cilium 等替代方案
ip netns add foo →
/var/run/netns/foo 持久化 net ns。
2.4 ipc namespace
SysV shm/sem/msg、POSIX mqueue 被隔离。不同 ns 互不可见。
2.5 uts namespace
hostname 和 domainname。docker run -h myhost
就是 UTS ns + sethostname。
2.6 user namespace(关键)
最重要且最复杂。提供 uid/gid 映射:容器内 uid 0(root)可以映射到 host 上的 uid 100000(非特权)。
/proc/<pid>/uid_map:
0 100000 65536
含义:容器 uid 0-65535 映射到 host uid 100000-165535。
capability 的语义也被改写:user ns 内 root 对本 ns 内的资源有特权,对外部无特权。
这是 rootless 容器(podman、rootless docker、unpriv Firejail)的技术基础。
坑:
- 很多文件系统操作(mount、setuid binary 等)在 user ns 里有限制
- setgroups 映射需要
deny或配合 /proc/setgroups - 外部文件系统看到的是 host uid,容器内看到的是映射后的 uid;共享目录权限容易混
2.7 cgroup namespace
v2 引入。容器看到的 cgroup 路径以自己所在 cgroup 为根。避免容器里看到 host 的 cgroup 结构。
2.8 time namespace
5.6+ 引入。独立的 CLOCK_MONOTONIC 和
CLOCK_BOOTTIME
偏移。checkpoint/restore(CRIU)迁移时恢复单调时钟不倒退。
不隔离
CLOCK_REALTIME——墙钟是全局的,不适合多租户。
三、API 三件套
3.1 clone3 / clone
struct clone_args args = {
.flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET |
CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER,
.exit_signal = SIGCHLD,
};
pid_t pid = syscall(SYS_clone3, &args, sizeof(args));在 fork 时直接建新 ns。这是 docker/containerd/runc 标准路径。
3.2 unshare
unshare(CLONE_NEWNS | CLONE_NEWPID);
// 注意 CLONE_NEWPID 不影响当前进程,只对后续 fork 的子生效命令行:unshare -pfu bash 在新 ns 里起
bash。
3.3 setns
int fd = open("/proc/<pid>/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);加入已有 ns。nsenter 命令行封装。
四、ns 的生命周期
每个 ns 是内核对象,引用计数:
- task 引用(task 在此 ns 内)
- bind mount 引用(
/proc/<pid>/ns/netbind 到持久路径) - ns fd open 引用
全部归零时销毁。这是为什么 ip netns 要 bind
到 /var/run/netns/X——让 netns
在最后一个进程退出后仍持久。
五、rootless 的现实
rootless 容器最大优势:host 上不需要 root。故障面小、安全性高。
但坑:
- 只能用
newuidmap/newgidmap分配 subuid/subgid(/etc/subuid) - 不能直接挂 overlayfs(需 kernel 5.11+ 支持 user ns 挂)
- 不能直接 bind 1024 以下端口(需 cap_net_bind_service 或
net.ipv4.ip_unprivileged_port_start) - 不能创建所有 cgroup 子系统(部分需要 delegation)
podman 的 rootless 模式成熟得多;docker rootless 仍需 daemon 侧配置。
六、namespace 与 cgroup 分工
- namespace = 看到什么(命名、视图、ID)
- cgroup = 能用多少(CPU、内存、I/O、PID 数、网络带宽)
两者正交:namespace 不限资源,cgroup 不隔视图。docker 两者都用。
七、观察 namespace
ls -l /proc/<pid>/ns/
# mnt -> mnt:[4026532...]
# net -> net:[...]
# ...inode 号相同 = 两进程在同一 ns。
lsns # util-linux 提供
lsns -t net # 按类型nsenter -t <pid> -a 进入某 pid
所在的全部 ns。
八、常见 bug
bug A:容器看到 host 的 /proc
原因:没 remount /proc(新 pid ns 后要
mount -t proc proc /proc)
bug B:docker exec 的命令在容器里 PID 不是 1 原因:exec 把进程加入容器的 pid ns,但顶层是容器 init;exec 的是其子孙
bug C:rootless 容器里 ping
不能用 原因:ping 要 cap_net_raw;user ns
里默认无
解决:sysctl net.ipv4.ping_group_range;或
setcap
bug D:user ns 映射后文件权限混乱 原因:容器内 uid 1000 映射 host uid 101000;host 上没这个 uid 的文件所有者 解决:挂载前 chown,或用 id-mapped mount(5.12+)
bug E:pid ns init 死了,里面进程全没 原因:内核会 SIGKILL ns 内所有进程 理解:这是 feature;容器要由 init 兜底
九、现代化趋势
- id-mapped mount(5.12+):同一 mount 在不同 user ns 里看到不同 uid。避免 chown 大文件系统。
- cgroup v2 + PSI(C-18)已取代 v1
- binfmt_misc + user ns + qemu-user:跨架构容器
- sandboxing(Firejail、bwrap):脱离容器,用 ns + seccomp + Landlock 做单应用隔离
十、小结
- 7 种 ns 切 7 类全局资源;user ns 是权限根
- clone3 / unshare / setns 三件套
- rootless 容器 = user ns 深度使用
- ns 做视图、cgroup 做额度,协作才是完整容器
lsns、nsenter、/proc/<pid>/ns/*是诊断起点
下一篇 B-18 讲 cgroup v2——统一的资源控制模型,PSI 压力指标,以及 systemd slice 体系。
参考文献
- Kerrisk, M. The Linux Programming Interface, Ch. 28, 34-36, 45
- Linux source:
kernel/nsproxy.c、kernel/pid_namespace.c、net/core/net_namespace.c、kernel/user_namespace.c - Kerrisk, M. “Namespaces in operation” 系列。LWN.net 2013
- Biederman, E. “User namespaces” 提交日志
- Documentation:
Documentation/admin-guide/namespaces/,user_namespaces(7),pid_namespaces(7) - Brauner, C. “Mount, FS, namespace” 相关 patch(id-mapped mount)
工具
lsns/nsenter/unshare—— 查看、进入、创建ip netns—— net ns 管理podman unshare—— rootless 辅助bpftrace -e 'kprobe:copy_namespaces { ... }'crictl inspect/docker inspect—— 容器 ns 引用
上一篇:消息队列 下一篇:cgroup v2:资源控制的统一模型
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】OverlayFS
OverlayFS 用 lower/upper/work 三层实现联合挂载——容器镜像层栈的内核根基。本文讲 copy_up、whiteout、opaque、metacopy、redirect_dir、性能与 Docker overlay2 驱动的工程细节。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。