namespaces + cgroups —— 命名空间与资源限额(容器的内核底座)
这是 task-struct 里
task->nsproxy和 cgroup 那一项的展开。容器(Docker/K8s)不是虚拟机——它就是"一组被 namespace 圈起来看不见外面、被 cgroup 限量的普通进程"。这两样都是task_struct挂着的资源对象:namespace 决定进程"看得见什么"(隔离)、cgroup 决定它"能用多少"(限额)。本篇讲清这两套机制、各自的种类、以及它们怎么合起来撑起容器。
零、一句话认知:隔离(看得见什么) + 限额(能用多少) = 容器
核心记忆:namespace 管"隔离"——进程看到的是被裁剪过的系统视图;cgroup 管"限额"——进程能消耗的资源有上限。 两者正交、都作用于普通进程,合起来就是容器。容器和宿主共享同一个内核(区别于虚拟机的 guest 内核)。
一、namespace:8 种隔离视图
每个进程通过 task->nsproxy 指向它所属的一组 namespace。同一 namespace 里的进程共享某类资源视图,不同 namespace 互相看不见。
| namespace | 隔离什么 | 效果 |
|---|---|---|
| PID | 进程 ID 空间 | 容器内进程从 PID 1 开始编号,看不到宿主/别的容器的进程 |
| NET | 网络栈 | 独立的网卡、IP、端口、路由表、防火墙——容器有自己的 eth0 |
| MNT | 文件系统挂载点 | 独立的挂载树(容器的 / 是自己的镜像) |
| UTS | 主机名/域名 | 容器有自己的 hostname |
| IPC | System V IPC / POSIX 消息队列 | 隔离共享内存、信号量 |
| USER | uid/gid 映射 | 容器内 uid=0(root) 映射到宿主的非特权 uid——容器内 root ≠ 宿主 root(安全关键) |
| CGROUP | cgroup 根视图 | 隐藏容器所属 cgroup 的真实层级 |
| TIME | 系统时钟偏移 | 容器可有不同的启动时间基准 |
ls -l /proc/<pid>/ns/ # 该进程所属的每个 namespace(inode 号相同=在同一 ns)
# pid -> pid:[4026531836] net -> net:[4026531840] ...
unshare --net --pid --fork bash # 手动创建新 net+pid namespace 跑一个 shell
nsenter -t <pid> -n ip addr # 进入某进程的 net namespace 执行命令- PID namespace 的层级:容器里的 PID 1 是那个 namespace 的"init",它在宿主机里其实是另一个大 PID。
/proc/<pid>/ns/pid的 inode 相同 = 在同一 PID namespace。 - USER namespace 是安全基石:容器内进程可以是"root"(uid 0),但通过 uid 映射,它在宿主看来只是个普通用户——即使逃逸也没有宿主 root 权限。呼应 cred 的 capabilities:容器内的 root 权限也被 capability 边界裁剪。
二、cgroup:资源限额与统计
cgroup(control group) 把一组进程归到一起,对它们的资源用量限额 + 计量。task 记录自己属于哪些 cgroup。
| 控制器(controller) | 限制/统计什么 |
|---|---|
| cpu | CPU 时间配额(cpu.max:每周期最多用多少)、权重(cpu.weight) |
| cpuset | 把一组 CPU 核/NUMA 节点划给这组进程(见 thread-affinity) |
| memory | 内存上限(memory.max);超限触发cgroup 内 OOM kill |
| io | 块设备读写带宽/IOPS 上限 |
| pids | 最多能创建多少进程(防 fork 炸弹) |
cgroup v1 vs v2
- v1:每个控制器一棵独立层级树,
/sys/fs/cgroup/cpu/、/sys/fs/cgroup/memory/… 各管各的,一个进程在不同控制器里可属于不同组——灵活但混乱。 - v2(统一层级):所有控制器共用一棵层级树(
/sys/fs/cgroup/下统一),一个进程只属于一个 cgroup 节点,在该节点上启用需要的控制器。现在的主流。
# cgroup v2:看某进程属于哪个 cgroup
cat /proc/<pid>/cgroup # 0::/system.slice/docker-xxx.scope
# 看/设某个 cgroup 的限额(v2)
cat /sys/fs/cgroup/<path>/memory.max # 内存上限
cat /sys/fs/cgroup/<path>/cpu.max # CPU 配额 "50000 100000" = 每 100ms 最多 50ms
cat /sys/fs/cgroup/<path>/memory.current # 当前用量- K8s 的 CPU limit/request 底层就是设 cgroup 的
cpu.max/cpu.weight;memory limit 就是memory.max,容器 OOM 常是撞了它。 - cgroup 配置属于
/sys入口(见 kernel-tuning)。
三、容器 = 怎么拼起来的
一个容器启动时,runtime(runc 等)大致做:
clone(CLONE_NEWPID|CLONE_NEWNET|CLONE_NEWNS|...):创建带一组新 namespace 的进程(见 process-creation §三 的 clone flags)。- 配 namespace 内容:挂镜像文件系统(mnt)、建虚拟网卡(net)、设 hostname(uts)、uid 映射(user)。
- 加进 cgroup:把这个进程放进一个新 cgroup 节点,设好 CPU/内存上限。
execve容器里的入口程序。
于是这个进程(及其子孙)看到的是裁剪过的系统、能用的资源有上限——这就是容器。
四、观测
ls -l /proc/<pid>/ns/ # 所属各 namespace(对比两进程 inode 是否相同=同 ns)
cat /proc/<pid>/cgroup # 所属 cgroup 路径
cat /proc/<pid>/status | grep -E 'NSpid|Uid' # NSpid:在各层 PID ns 里的 pid;Uid:user ns 映射后
systemd-cgls # 树状看 cgroup 层级和里面的进程
systemd-cgtop # 按 cgroup 看实时资源用量(类似 top)- 容器里看
/proc/cpuinfo/meminfo是宿主机的值(procfs 不感知 cgroup,见 procfs §五)——要看容器真实可用资源得读 cgroup 文件,或用 lxcfs 矫正。这是容器内监控的经典坑。
五、和其它文档的关系
- 容器专题:本篇是容器的内核底座;容器完整实现原理(+overlayfs 镜像分层)、与虚拟机的差异、性能损耗见独立簇 ../../container/。
- 父篇:task-struct(nsproxy/cgroup 是 task 的资源对象)。
- 创建:process-creation(clone 的
CLONE_NEW*flags 建 namespace)。 - cpuset 绑核:thread-affinity(cgroup cpuset 把核划给一组进程)。
- 权限:cred(user namespace 与 capabilities 一起裁剪容器内 root)。
- 配置入口:kernel-tuning(cgroup 走
/sys/fs/cgroup)。 - 观测坑:procfs(容器内 /proc 显示宿主值)。
六、一句话总结
namespace 和 cgroup 是容器的内核底座,都作用于普通进程、和宿主共享内核(非虚拟机)。namespace 管隔离——8 种(PID/NET/MNT/UTS/IPC/USER/CGROUP/TIME)各裁剪一类系统视图,让容器以为自己独占系统;其中 user namespace 把容器内 root 映射成宿主非特权用户,是安全关键。cgroup 管限额——cpu/cpuset/memory/io/pids 等控制器限制并计量一组进程的资源用量(v2 用统一层级),K8s 的 CPU/内存 limit 就落在 cpu.max/memory.max。容器 = clone 出带新 namespace 的进程 + 塞进限额 cgroup + exec 入口。透过
/proc/<pid>/ns/和/proc/<pid>/cgroup观测;注意容器内 /proc 的 cpu/mem 是宿主值。