mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4001 字
11 分钟
Cgroups 与 Namespaces
2024-12-01

第 3 章:进程管理中,理解了 task_struct 和进程的完整生命周期;在第 14 章:内核同步机制中,掌握了内核如何保护共享数据。现在,走进 Linux 内核中最深刻的一次抽象跃迁——如何让一组进程以为自己独占了整台机器

这就是容器技术的内核根基:Namespace 提供资源视图隔离,Cgroup 提供资源使用限制。两者组合,构成了 Docker、Kubernetes 等容器生态的底层基石。理解它们,你才能真正理解”容器到底是什么”——它不是虚拟机,不是沙箱,而是内核对进程施加的一组视角限制配额约束

本章将从容器与虚拟机的本质区别出发,逐一剖析 7 种 Namespace 的隔离能力与内核实现,深入 Cgroup v1/v2 的架构演进与控制器机制,最终揭示容器运行时如何将两者编织为现代云原生的基石。

一、容器 vs 虚拟机:两种隔离哲学#

1.1 虚拟机:硬件级隔离#

虚拟机(Virtual Machine)通过 Hypervisor 在物理硬件之上模拟出完整的硬件环境,每个虚拟机运行独立的操作系统内核。这种隔离是硬件级别的——不同的内核实例、不同的系统调用表、不同的设备驱动。

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ App A │ │ App B │ │ App C │
│ Guest OS │ │ Guest OS │ │ Guest OS │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ vCPU │ │ vCPU │ │ vCPU │
│ vMem │ │ vMem │ │ vMem │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
└───────────────┼───────────────┘
Hypervisor
┌─────────┴─────────┐
│ Host Kernel │
└───────────────────┘
┌───────────────────┐
│ Hardware │
└───────────────────┘

1.2 容器:内核级隔离#

容器则完全不同——所有容器共享同一个内核。容器本质上只是一组进程,内核通过 Namespace 让它们”看”到不同的资源视图,通过 Cgroup 限制它们能使用的资源量。没有硬件模拟,没有额外的内核实例,只有同一个内核中的不同视角和约束

┌──────────┐ ┌──────────┐ ┌──────────┐
│ App A │ │ App B │ │ App C │
│ NS+Cgrp │ │ NS+Cgrp │ │ NS+Cgrp │
└────┬─────┘ └────┬─────┘ └────┬─────┘
└─────────────┼─────────────┘
Shared Linux Kernel
┌─────────────┴─────────────┐
│ Namespaces + Cgroups │
└───────────────────────────┘
┌───────────────────────────┐
│ Hardware │
└───────────────────────────┘

1.3 核心差异对比#

维度虚拟机容器
隔离级别硬件级(独立内核)内核级(共享内核)
启动速度分钟级(需启动完整 OS)毫秒级(仅创建进程)
资源开销GB 级(完整 OS 镜像)MB 级(仅应用+依赖)
隔离强度强(硬件强制)弱(内核机制,共享内核漏洞影响全局)
密度低(通常数十个)高(可达数千个)
技术基础VT-x/AMD-V 硬件虚拟化Namespace + Cgroup
Note

容器的”弱隔离”并非缺陷,而是设计权衡。共享内核意味着零虚拟化开销,但也意味着内核漏洞(如 Dirty COW)可以突破容器边界。这就是为什么安全敏感场景仍需虚拟机,而高密度部署场景偏好容器。

二、Linux Namespace:资源视图隔离#

2.1 Namespace 的核心思想#

Namespace 的设计哲学极其优雅:不创建新的资源,只改变进程能看到什么。它为进程提供了一种”视障”——每个 Namespace 中的进程都以为自己拥有系统的全部资源,而实际上它们只是看到了内核为它们定制的一份”视图”。

内核中每个 Namespace 类型对应 task_struct 中的一个指针:

// include/linux/sched.h 中的 nsproxy 结构
struct task_struct {
// ...
struct nsproxy *nsproxy; // 指向进程的命名空间代理
// ...
};
// kernel/nsproxy.c
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns; // UTS: 主机名与域名
struct ipc_namespace *ipc_ns; // IPC: System V IPC 与 POSIX MQ
struct mnt_namespace *mnt_ns; // Mount: 文件系统挂载点
struct pid_namespace *pid_ns_for_children; // PID: 进程号
struct net *net_ns; // Network: 网络栈
struct cgroup_namespace *cgroup_ns; // Cgroup: cgroup 根目录视图
struct user_namespace *user_ns; // User: 用户与组 ID
};

2.2 七种 Namespace 详解#

Linux 内核目前实现了 7 种 Namespace,每种隔离一类系统资源:

graph TB subgraph "Linux 7 种 Namespace" MOUNT["Mount Namespace<br/>隔离文件系统挂载点<br/>CLONE_NEWNS<br/>Linux 2.4.19"] PID["PID Namespace<br/>隔离进程 ID 号<br/>CLONE_NEWPID<br/>Linux 2.6.24"] NET["Network Namespace<br/>隔离网络栈<br/>CLONE_NEWNET<br/>Linux 2.6.29"] USER["User Namespace<br/>隔离用户/组 ID<br/>CLONE_NEWUSER<br/>Linux 3.8"] UTS["UTS Namespace<br/>隔离主机名与域名<br/>CLONE_NEWUTS<br/>Linux 2.6.19"] IPC["IPC Namespace<br/>隔离 System V IPC<br/>CLONE_NEWIPC<br/>Linux 2.6.19"] CGRP["Cgroup Namespace<br/>隔离 cgroup 根视图<br/>CLONE_NEWCGROUP<br/>Linux 4.6"] end style MOUNT fill:#e74c3c,color:#fff style PID fill:#3498db,color:#fff style NET fill:#2ecc71,color:#fff style USER fill:#9b59b6,color:#fff style UTS fill:#f39c12,color:#fff style IPC fill:#1abc9c,color:#fff style CGRP fill:#e67e22,color:#fff

Mount Namespace(文件系统隔离)#

Mount Namespace 是 Linux 历史上第一个 Namespace(2002 年,Linux 2.4.19),它隔离的是进程看到的文件系统挂载点视图。不同 Mount Namespace 中的进程可以拥有完全不同的目录结构。

关键机制:Mount Namespace 使用传播类型(Shared Slaves)控制挂载事件如何在 Namespace 之间传播:

传播类型行为
shared挂载/卸载事件传播到对等组中的其他 Namespace
slave只接收来自主 Namespace 的事件,不反向传播
private不传播也不接收任何挂载事件
unbindable与 private 相同,且禁止被 bind mount

PID Namespace(进程号隔离)#

PID Namespace 让不同 Namespace 中的进程拥有独立的进程号空间。在子 PID Namespace 中,第一个进程的 PID 为 1,扮演 init 进程的角色。当 PID 1 退出时,该 Namespace 中的所有进程都被杀死。

// kernel/pid_namespace.c 中的核心结构
struct pid_namespace {
struct idr idr; // PID 分配器
unsigned int pid_allocated;
struct task_struct *child_reaper; // PID 1 进程
struct kmem_cache *pid_cachep;
unsigned int level; // 嵌套深度
struct pid_namespace *parent; // 父 Namespace
// ...
};

PID Namespace 支持嵌套——子 Namespace 中的进程在父 Namespace 中可见,但 PID 号不同。一个进程在不同层级的 Namespace 中拥有不同的 PID:

全局 PID Namespace (level 0)
└── PID 1 (systemd)
└── 容器 PID Namespace (level 1)
└── PID 1 (容器 init)
└── PID 47 (容器内进程)
→ 在全局 Namespace 中 PID 为 12345

Network Namespace(网络栈隔离)#

Network Namespace 隔离了完整的网络协议栈——包括网络设备、IP 地址、路由表、端口号、iptables 规则、/proc/net/sys/class/net 目录。每个 Network Namespace 拥有独立的 lo(loopback)设备,可以拥有独立的虚拟以太网设备(veth pair)。

这是容器网络实现的基石:Docker 的 bridge 网络模式、Kubernetes 的 Pod 网络、CNI 插件——它们都基于 Network Namespace 构建。

User Namespace(用户身份隔离)#

User Namespace 是最强大的 Namespace 类型——它允许普通用户在子 User Namespace 中”成为 root”。具体来说,User Namespace 建立了 UID/GID 映射:子 Namespace 中的 UID 0(root)映射到父 Namespace 中的普通 UID。

include/linux/user_namespace.h
struct user_namespace {
struct uid_gid_map uid_map; // UID 映射表
struct uid_gid_map gid_map; // GID 映射表
struct uid_gid_map projid_map; // 项目配额映射
struct user_namespace *parent; // 父 Namespace
int level; // 嵌套深度
kuid_t owner; // 创建者 UID
// ...
};

User Namespace 的嵌套深度限制为 32 层(USER_NS_HIERARCHY_MAX)。正是 User Namespace 使得非特权用户也能创建容器——这是 rootless container 的技术基础。

UTS Namespace(主机名隔离)#

UTS(Unix Time-sharing System)Namespace 隔离的是 hostnamedomainname 两个系统标识符,对应 sethostname()setdomainname() 系统调用。每个容器可以有自己的主机名,而不影响宿主机。

IPC Namespace(进程间通信隔离)#

IPC Namespace 隔离了 System V IPC 对象(信号量、消息队列、共享内存段)和 POSIX 消息队列。不同 IPC Namespace 中的进程无法通过这些机制通信,确保了容器间 IPC 资源的隔离。

Cgroup Namespace(cgroup 视图隔离)#

Cgroup Namespace(Linux 4.6)是最晚加入的 Namespace,它隔离的是进程看到的 cgroup 层级视图。在 Cgroup Namespace 中,进程的 cgroup 根目录被映射为 /,使得容器内的进程无法看到宿主机的完整 cgroup 层次结构。

2.3 /proc/[pid]/ns/:Namespace 的文件表示#

每个进程的 Namespace 都在 /proc/[pid]/ns/ 目录下以符号链接的形式暴露:

$ ls -la /proc/self/ns/
total 0
lrwxrwxrwx 1 root root 0 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 uts -> 'uts:[4026531838]'

方括号中的数字是 Namespace 的唯一标识符(inode 号)。两个进程如果指向同一个 inode,说明它们在同一个 Namespace 中。这就是判断两个进程是否共享某个 Namespace 的方法。

2.4 unsharesetns:操作 Namespace 的系统调用#

内核提供了两个关键系统调用来操作 Namespace:

unshare(flags)——将调用进程从当前 Namespace 中移出,创建并加入新的 Namespace:

kernel/fork.c
SYSCALL_DEFINE1(unshare, unsigned long, unshare_flags)
{
// 根据 flags 创建新的 Namespace
// CLONE_NEWNS → 新 Mount Namespace
// CLONE_NEWUTS → 新 UTS Namespace
// CLONE_NEWIPC → 新 IPC Namespace
// CLONE_NEWPID → 新 PID Namespace(仅影响子进程)
// CLONE_NEWNET → 新 Network Namespace
// CLONE_NEWUSER → 新 User Namespace
// ...
}

setns(fd, flags)——将调用进程加入由文件描述符 fd 指定的已有 Namespace:

kernel/nsproxy.c
SYSCALL_DEFINE2(setns, int, fd, int, nstype)
{
// fd 指向 /proc/[pid]/ns/ 下的某个符号链接
// nstype 用于验证 Namespace 类型是否匹配
// 将当前进程切换到目标 Namespace
}
Note

unshare(CLONE_NEWPID) 有一个微妙之处:它只为后续创建的子进程建立新的 PID Namespace,调用进程自身的 PID 不会改变。这是因为当前进程已经在父 Namespace 中注册了 PID,无法撤销。

三、Cgroup:资源使用限制#

如果说 Namespace 是”你能看到什么”,那么 Cgroup 就是”你能用多少”。Cgroup(Control Group)将进程组织成层次化的组,对每组施加资源限制、优先级分配和审计统计。

3.1 Cgroup v1 vs v2:架构演进#

Cgroup 经历了从 v1 到 v2 的重大架构变革:

graph LR subgraph "Cgroup v1:多层级" V1_CPU["cpu 控制器<br/>/sys/fs/cgroup/cpu/"] V1_MEM["memory 控制器<br/>/sys/fs/cgroup/memory/"] V1_BLK["blkio 控制器<br/>/sys/fs/cgroup/blkio/"] V1_PID["pids 控制器<br/>/sys/fs/cgroup/pids/"] V1_CPUSET["cpuset 控制器<br/>/sys/fs/cgroup/cpuset/"] V1_NET["net_cls/net_prio<br/>/sys/fs/cgroup/net_cls/"] V1_DEV["devices 控制器<br/>/sys/fs/cgroup/devices/"] V1_FRZ["freezer 控制器<br/>/sys/fs/cgroup/freezer/"] end subgraph "Cgroup v2:统一层级" V2_ROOT["统一根<br/>/sys/fs/cgroup/"] V2_ROOT --> V2_A["group_a/<br/>cpu.max + memory.max"] V2_ROOT --> V2_B["group_b/<br/>cpu.max + memory.max"] V2_A --> V2_A1["group_a/child/<br/>继承父级约束"] end style V1_CPU fill:#e74c3c,color:#fff style V1_MEM fill:#3498db,color:#fff style V1_BLK fill:#2ecc71,color:#fff style V2_ROOT fill:#9b59b6,color:#fff style V2_A fill:#f39c12,color:#fff style V2_B fill:#1abc9c,color:#fff
维度Cgroup v1Cgroup v2
层级结构每个控制器独立层级统一单层级(unified hierarchy)
挂载方式每个控制器单独挂载一次挂载,所有控制器共享
进程归属同一进程可在不同层级的不同组一个进程只能在一个 cgroup 中
控制器启用挂载即启用通过 cgroup.subtree_control 按需启用
线程粒度部分控制器支持线程模式原生支持线程模式(cgroup.type
压力通知memory.pressure_level + PSI
默认行为无默认限制memory.swap.max 可限制 swap

Cgroup v2 的统一层级是最核心的设计改进。在 v1 中,cpu 控制器和 memory 控制器有各自独立的树形结构,一个进程可以位于 cpu 层级的 A 组、memory 层级的 B 组——这导致资源所有权模糊,管理复杂。v2 强制所有控制器共享同一棵树,一个进程只能属于一个 cgroup,资源所有权清晰明确。

3.2 Cgroup v2 核心控制器#

cpu 控制器#

cpu 控制器限制 cgroup 的 CPU 使用时间。核心接口文件:

  • cpu.max:格式为 "max 100000""50000 100000",第一个值是配额(微秒),第二个值是周期(微秒)。max 表示无限制。50000 100000 表示每个 100ms 周期内最多使用 50ms CPU 时间(即 50% CPU)。
  • cpu.weight:1-10000 的权重值,用于在兄弟 cgroup 之间按比例分配 CPU 时间。这是 CFS 调度器在 cgroup 粒度的扩展。
# 限制容器最多使用 1.5 个 CPU 核心
echo "150000 100000" > /sys/fs/cgroup/mypod/cpu.max
# 设置相对权重(默认 100)
echo 200 > /sys/fs/cgroup/mypod/cpu.weight

memory 控制器#

memory 控制器限制 cgroup 的内存使用。核心接口文件:

  • memory.max:内存使用硬限制(字节)。超过此限制会触发 OOM killer。
  • memory.high:内存使用软限制。超过此限制后内核会积极回收内存,但不会杀进程。
  • memory.swap.max:swap 使用限制。设为 0 可完全禁止 swap 使用。
  • memory.current:当前内存使用量(只读)。
  • memory.oom.group:设为 1 时,OOM killer 会杀死该 cgroup 中的所有进程。
# 限制最多使用 512MB 内存
echo 536870912 > /sys/fs/cgroup/mypod/memory.max
# 禁止使用 swap
echo 0 > /sys/fs/cgroup/mypod/memory.swap.max
# 设置软限制,超过时触发回收
echo 4294967296 > /sys/fs/cgroup/mypod/memory.high

io 控制器#

io 控制器限制 cgroup 的块设备 I/O 带宽和 IOPS。核心接口文件:

  • io.max:格式为 "8:0 rbps=max wbps=10485760 riops=max wiops=1000",按设备号设置读写带宽和 IOPS 限制。
  • io.weight:1-10000 的权重值,类似 cpu.weight。
# 限制 /dev/sda (8:0) 的写带宽为 10MB/s
echo "8:0 wbps=10485760" > /sys/fs/cgroup/mypod/io.max

pids 控制器#

pids 控制器限制 cgroup 中可以创建的进程数量,防止 fork 炸弹:

# 限制最多 100 个进程
echo 100 > /sys/fs/cgroup/mypod/pids.max
# 查看当前进程数
cat /sys/fs/cgroup/mypod/pids.current

cpuset 控制器#

cpuset 控制器限制 cgroup 可以使用的 CPU 核心和内存节点(NUMA):

# 只允许使用 CPU 0-1
echo "0-1" > /sys/fs/cgroup/mypod/cpuset.cpus
# 只允许使用 NUMA 节点 0 的内存
echo "0" > /sys/fs/cgroup/mypod/cpuset.mems

3.3 内核实现:css_set 与 cgroup_subsys#

Cgroup 的内核实现围绕两个核心数据结构展开:

kernel/cgroup/cgroup-internal.h
// css_set:连接进程与 cgroup 的桥梁
struct css_set {
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
// 每个控制器对应一个 css 指针
struct list_head tasks; // 属于此 css_set 的进程列表
struct list_head mg_tasks; // 正在迁移的进程
// ...
};
// include/linux/cgroup-defs.h
// cgroup_subsys_state:控制器在 cgroup 中的实例
struct cgroup_subsys_state {
struct cgroup *cgroup; // 所属 cgroup
struct cgroup_subsys *ss; // 所属控制器
struct percpu_ref refcnt; // 引用计数
// ...
};
// cgroup_subsys:控制器的操作接口
struct cgroup_subsys {
const char *name; // 控制器名称
struct cgroup_subsys_state *(*css_alloc)(struct cgroup_subsys_state *parent_css);
int (*css_online)(struct cgroup_subsys_state *css);
void (*css_offline)(struct cgroup_subsys_state *css);
void (*css_free)(struct cgroup_subsys_state *css);
// 控制器特有的方法...
};

进程与 cgroup 的关系通过 css_set 间接建立:task_struct → css_set → cgroup_subsys_state[] → cgroup。这种间接设计使得多个进程可以共享同一组 cgroup 归属,避免为每个进程维护独立的关联关系。

当进程被移入新的 cgroup 时,内核会查找或创建匹配的 css_set,更新进程的 cgroups 指针,并调用各控制器的 css_online() 回调完成初始化。

3.4 PSI:资源压力感知#

Cgroup v2 引入了 PSI(Pressure Stall Information)机制,让用户空间可以感知资源竞争程度:

# 查看 CPU 压力
cat /sys/fs/cgroup/mypod/cpu.pressure
# some avg10=0.00 avg60=0.12 avg300=0.05 total=123456
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
# 查看内存压力
cat /sys/fs/cgroup/mypod/memory.pressure
# some avg10=2.34 avg60=1.56 avg300=0.89 total=9876543
# 查看 IO 压力
cat /sys/fs/cgroup/mypod/io.pressure
  • some:至少有一个任务等待资源的时间占比
  • full:所有任务都在等待资源的时间占比(完全停滞)

PSI 是 Kubernetes 基于资源压力进行调度决策的基础设施。

四、容器运行时:Namespace + Cgroup 的编织者#

4.1 runc:从 OCI 规范到运行容器#

容器运行时(如 runc)是 Namespace 和 Cgroup 的”编织者”——它按照 OCI(Open Container Initiative)运行时规范,依次创建各种 Namespace、配置 Cgroup 限制、设置 rootfs,最终启动容器进程。

runc 创建容器的大致流程:

  1. 创建 User Namespace(如果配置了非 root 容器)
  2. 创建 PID Namespaceunshare(CLONE_NEWPID)
  3. 创建 Mount Namespaceunshare(CLONE_NEWNS)),挂载 rootfs
  4. 创建 Network Namespaceunshare(CLONE_NEWNET)),配置 veth pair
  5. 创建 UTS/IPC/Cgroup Namespace
  6. 配置 Cgroup 限制(写入 cpu.maxmemory.max 等)
  7. 执行容器入口进程execve 容器镜像中的 CMD/ENTRYPOINT)
// runc/libcontainer/process_linux.go(简化)
func (p *initProcess) start() error {
// 1. 创建 namespace
cmd.SysProcAttr.Cloneflags = p.config.Namespaces.CloneFlags()
// CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS
// 2. 启动子进程(此时已在新 namespace 中)
if err := cmd.Start(); err != nil {
return err
}
// 3. 配置 cgroup
if err := p.manager.Apply(p.pid()); err != nil {
return err
}
// 4. 设置网络(veth pair, bridge, iptables)
if err := p.network.Setup(p.config.Networks); err != nil {
return err
}
// 5. 发送 execv 指令,执行容器入口
return p.sendConfig()
}

4.2 容器 = Namespace + Cgroup + rootfs#

一个完整的容器由三部分组成:

组件作用内核机制
Namespace资源视图隔离unshare/setns 系统调用
Cgroup资源使用限制cgroup 文件系统接口
rootfs文件系统隔离Mount Namespace + pivot_root

rootfs 是容器的文件系统视图——通过 Mount Namespace + pivot_root()(或 chroot()),将容器的根目录切换为镜像解压后的目录。容器内进程看到的 / 是镜像中的文件系统,而非宿主机的根文件系统。

五、eBPF 在 Cgroup 中的应用#

eBPF(extended Berkeley Packet Filter)与 Cgroup 的结合是现代容器网络和安全的重要基础设施。内核提供了 cgroup-bpf 机制,允许 eBPF 程序附加到 cgroup 上,对该 cgroup 中所有进程的网络行为进行过滤或修改。

5.1 cgroup-bpf 附加类型#

include/linux/bpf-cgroup.h
enum cgroup_bpf_attach_type {
CGROUP_INET_INGRESS, // 入站网络包过滤
CGROUP_INET_EGRESS, // 出站网络包过滤
CGROUP_INET_SOCK_CREATE, // socket 创建拦截
CGROUP_SOCK_OPS, // socket 操作回调
CGROUP_DEVICE, // 设备访问控制(替代 cgroup v1 devices 控制器)
CGROUP_INET4_BIND, // IPv4 bind 拦截
CGROUP_INET6_BIND, // IPv6 bind 拦截
CGROUP_INET4_CONNECT, // IPv4 connect 拦截
CGROUP_INET6_CONNECT, // IPv6 connect 拦截
// ...
};

5.2 典型应用场景#

  • Cilium:使用 cgroup-bpf 实现 socket-level 的网络策略,绕过 iptables,性能提升显著
  • Facebook Katran:基于 CGROUP_INET_SOCK_CREATE 实现 L4 负载均衡
  • 设备控制CGROUP_DEVICE 替代了 cgroup v1 的 devices 控制器,用 eBPF 程序决定容器可以访问哪些设备节点
# 将 eBPF 程序附加到 cgroup
bpftool cgroup attach /sys/fs/cgroup/mypod/ ingress pinned /sys/fs/bpf/my_filter
# 查看附加到 cgroup 的 eBPF 程序
bpftool cgroup show /sys/fs/cgroup/mypod/

六、从 Namespace + Cgroup 到 Kubernetes Pod#

6.1 Pod 的内核本质#

Kubernetes 的 Pod 是一组共享某些 Namespace 的容器。从内核视角看,Pod 的实现非常直接:

  • 同一个 Pod 内的所有容器共享 Network Namespace、IPC Namespace 和 UTS Namespace
  • 每个容器有独立的 Mount Namespace 和 PID Namespace
  • 整个 Pod 对应一个 cgroup(Kubernetes 通常使用 cgroup v2 的层级结构管理)
graph TB subgraph "Kubernetes Pod (内核视角)" POD_CGRP["Pod cgroup<br/>cpu.max / memory.max"] subgraph "共享 Namespace" NET_NS["Network NS<br/>共享 IP/端口/路由"] IPC_NS["IPC NS<br/>共享 System V IPC"] UTS_NS["UTS NS<br/>共享 hostname"] end subgraph "Container A" PA_MNT["Mount NS (独立)"] PA_PID["PID NS (独立)"] PA_CGRP["Container A cgroup"] end subgraph "Container B" PB_MNT["Mount NS (独立)"] PB_PID["PID NS (独立)"] PB_CGRP["Container B cgroup"] end POD_CGRP --> PA_CGRP POD_CGRP --> PB_CGRP end style NET_NS fill:#2ecc71,color:#fff style IPC_NS fill:#1abc9c,color:#fff style UTS_NS fill:#f39c12,color:#fff style POD_CGRP fill:#9b59b6,color:#fff

6.2 Pause 容器:Pod 的 Namespace 锚点#

Kubernetes 使用 Pause 容器(也叫 sandbox 容器)作为 Pod 中共享 Namespace 的锚点。Pause 容器是一个极简进程(只执行 pause() 系统调用,永久睡眠),它的作用是:

  1. 创建 Network Namespace、IPC Namespace、UTS Namespace
  2. 作为这些 Namespace 的”持有者”——即使 Pod 中的业务容器崩溃重启,Namespace 也不会被销毁
  3. 为后续加入的容器提供 setns() 的目标

当业务容器启动时,它通过 setns() 加入 Pause 容器已创建的共享 Namespace,然后创建自己的 Mount Namespace 和 PID Namespace。

6.3 资源限制的层级传递#

Kubernetes 的 resources.limitsresources.requests 最终映射为 cgroup 参数:

Kubernetes 字段cgroup v2 接口含义
resources.limits.cpu: "2"cpu.max = "200000 100000"2 个 CPU 核心的硬限制
resources.limits.memory: "512Mi"memory.max = 536870912512MB 内存硬限制
resources.requests.cpu: "1"cpu.weight = 100(按比例)CPU 调度权重
resources.requests.memory: "256Mi"用于调度决策不直接映射为 cgroup 参数

Pod 级别的 cgroup 是所有容器 cgroup 的父节点,Pod 的资源限制是容器资源限制的上限。这种层级传递确保了 Pod 不会超过其总配额,即使单个容器的限制尚未触达。

七、动手实践#

实践 1:用 unshare 创建隔离环境#

# 创建新的 UTS、IPC、PID、Mount Namespace
# --fork: 在子进程中运行(PID NS 需要)
# --mount-proc: 挂载新的 /proc(否则看到宿主机的进程)
unshare --uts --ipc --pid --mount --fork --mount-proc /bin/bash
# 在新 Namespace 中验证隔离效果
hostname isolated-host
hostname # 输出: isolated-host
# 另一个终端中检查宿主机 hostname 未改变
hostname # 输出: 原始主机名
# 在新 PID Namespace 中
ps aux # 只能看到 bash 和 ps 两个进程
exit # 退出后 Namespace 自动销毁

实践 2:查看进程的 Namespace#

# 查看当前进程的所有 Namespace
ls -la /proc/self/ns/
# 比较两个进程是否在同一个 Network Namespace
readlink /proc/1/ns/net
readlink /proc/$$/ns/net
# 如果 inode 号相同,则在同一个 Network Namespace
# 查看系统中所有不同的 Namespace
ls -la /proc/*/ns/* 2>/dev/null | awk '{print $NF}' | sort -u

实践 3:用 cgcreate 和 cgset 管理 Cgroup v1#

# 安装 cgroup 工具
sudo apt install cgroup-tools
# 创建一个新的 cgroup(v1)
sudo cgcreate -g cpu,memory:/mygroup
# 设置 CPU 限制(50% CPU)
sudo cgset -r cpu.cfs_quota_us=50000 mygroup
sudo cgset -r cpu.cfs_period_us=100000 mygroup
# 设置内存限制(256MB)
sudo cgset -r memory.limit_in_bytes=268435456 mygroup
# 将进程加入 cgroup
sudo cgclassify -g cpu,memory:mygroup $$
# 验证
cat /proc/$$/cgroup

实践 4:直接操作 Cgroup v2 文件#

# 创建子 cgroup
sudo mkdir /sys/fs/cgroup/mycontainer
# 将当前进程加入 cgroup
echo $$ | sudo tee /sys/fs/cgroup/mycontainer/cgroup.procs
# 设置 CPU 限制(1 核心)
echo "100000 100000" | sudo tee /sys/fs/cgroup/mycontainer/cpu.max
# 设置内存限制(128MB)
echo 134217728 | sudo tee /sys/fs/cgroup/mycontainer/memory.max
# 禁止 swap
echo 0 | sudo tee /sys/fs/cgroup/mycontainer/memory.swap.max
# 限制进程数
echo 50 | sudo tee /sys/fs/cgroup/mycontainer/pids.max
# 查看当前资源使用
cat /sys/fs/cgroup/mycontainer/memory.current
cat /sys/fs/cgroup/mycontainer/pids.current
# 查看 PSI 压力指标
cat /sys/fs/cgroup/mycontainer/cpu.pressure
cat /sys/fs/cgroup/mycontainer/memory.pressure
cat /sys/fs/cgroup/mycontainer/io.pressure

实践 5:用 setns 加入已有 Namespace#

# 终端 1:创建一个新的 Network Namespace 并运行一个进程
unshare --net /bin/bash
# 在新 Network Namespace 中启动一个简单的服务
nc -l 8080 &
# 终端 2:找到该 Namespace 的文件描述符
NS_INODE=$(readlink /proc/$(pgrep -f "unshare --net")/ns/net)
echo "Namespace: $NS_INODE"
# 使用 nsenter 加入该 Namespace
sudo nsenter --net --target $(pgrep -f "unshare --net") /bin/bash
ip link # 只看到 lo 设备

参考资料#

内核源码#

路径内容
kernel/cgroup/Cgroup 核心实现(cgroup.c、rstat.c、legacy_freezer.c)
kernel/cgroup/cgroup-v1.cCgroup v1 兼容层
kernel/cgroup/rstat.cCgroup 递归统计
kernel/pid_namespace.cPID Namespace 实现
kernel/user_namespace.cUser Namespace 实现
kernel/nsproxy.cNamespace 代理管理
net/core/net_namespace.cNetwork Namespace 实现
fs/namespace.cMount Namespace 实现
include/linux/cgroup-defs.hCgroup 核心数据结构定义
include/linux/nsproxy.hnsproxy 结构定义

手册页#

  • man 7 namespaces——Linux Namespace 概述,7 种 Namespace 的详细说明
  • man 7 cgroups——Cgroup 概述,v1/v2 接口说明
  • man 2 unshare——unshare 系统调用
  • man 2 setns——setns 系统调用
  • man 1 nsenter——nsenter 工具,加入已有 Namespace

官方文档#

深入阅读#

  • 《Container Security》——Liz Rice,从安全视角剖析容器技术的内核基础
  • 《Linux Containers and Virtualization》——Shashank Mohan Jain,容器技术的系统级解析
  • Cilium eBPF Documentation——eBPF 在容器网络中的实践
  • PSI — Pressure Stall Information——PSI 机制文档

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Cgroups 与 Namespaces
https://blog.souloss.com/posts/linux-internals/cgroups-and-namespaces/
作者
Souloss
发布于
2024-12-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时