容器技术在现代软件开发和运维中已经成为基础设施级别的存在。无论是 Docker、Podman 还是 Kubernetes,它们都依赖 Linux 内核提供的两项核心机制来实现隔离:命名空间(Namespaces) 和 控制组(Cgroups)。理解这两项技术,就理解了容器“看起来像虚拟机”背后的真实原理。
容器不是虚拟机
很多人习惯把容器类比为轻量级虚拟机,但两者在实现层面有本质区别。虚拟机通过 Hypervisor 模拟硬件,每台虚拟机运行独立的内核;而容器共享宿主机的同一个 Linux 内核,只是通过内核提供的隔离机制,让进程“以为自己”运行在独立系统中。
因此,容器隔离的本质可以概括为两句话:
- 命名空间负责“看不看得见”:隔离进程能看到的系统资源视图。
- Cgroups 负责“用多少”:限制和统计进程组对 CPU、内存、I/O 等资源的使用。
一、Linux 命名空间:隔离视图
命名空间是 Linux 内核的一种特性,它允许不同进程组拥有不同的系统资源视图。每个命名空间内部看起来像一套独立的系统环境,但底层仍然是同一个内核。
目前 Linux 主要提供以下几类命名空间:
1. PID 命名空间
PID 命名空间隔离进程 ID。在容器内部,进程可能看到自己的 PID 是 1,但在宿主机上它可能是一个普通的用户进程,PID 为 12345。这样一来,容器内的进程无法看到宿主机上的其他进程,也无法被宿主机上其他进程直接感知。
PID 命名空间的一个关键特点是:容器内的 1 号进程承担类似 init 的职责,需要负责回收孤儿进程。如果它退出,整个 PID 命名空间中的进程都会被终止。
2. Mount 命名空间
Mount 命名空间隔离文件系统挂载点。每个容器可以拥有自己的根文件系统、挂载目录和挂载传播属性。容器内看到的 / 可能来自镜像中的 rootfs,而宿主机看到的 / 是另一套完全不同的目录结构。
这也是容器镜像能够“打包运行环境”的基础:镜像本质上就是一份准备好的 rootfs,通过 Mount 命名空间挂载为容器根目录。
3. Network 命名空间
Network 命名空间隔离网络协议栈,包括网卡、IP 地址、路由表、端口、防火墙规则等。每个容器可以拥有独立的 lo、eth0 和 IP 地址,彼此之间通过 veth pair、网桥等机制通信。
Docker 默认会为容器创建独立的 Network 命名空间,并通过 NAT 或桥接方式与外界通信。
4. UTS 命名空间
UTS 命名空间隔离主机名和域名。容器可以拥有自己的 hostname,而不会影响宿主机。
5. IPC 命名空间
IPC 命名空间隔离 System V IPC 和 POSIX 消息队列。不同容器之间的进程无法通过共享内存、信号量等方式直接通信。
6. User 命名空间
User 命名空间隔离用户和用户组 ID。它允许容器内的 root 用户在宿主机上被映射为非 root 用户,从而提升安全性。这是 Rootless 容器的重要基础。
7. Cgroup 命名空间
Cgroup 命名空间隔离 cgroup 视图,使容器内看到的 cgroup 层级与宿主机不同。它在较新的内核中引入,用于更好地支持容器内资源管理。
这些命名空间可以组合使用。通过 clone()、unshare()、setns() 等系统调用,进程可以创建或加入命名空间。容器运行时(如 runc)在启动容器时,正是通过指定一组命名空间,让容器进程拥有独立的视图。
二、Cgroups:限制与统计资源
命名空间解决了“隔离视图”的问题,但如果没有资源限制,一个容器仍然可能耗尽宿主机的 CPU 或内存,影响其他容器。Cgroups 就是为此而生。
Cgroups(Control Groups)是 Linux 内核用于限制、记录和隔离进程组资源使用的机制。它按层级组织,每个层级可以附加一个或多个子系统(controller)。
常见的 Cgroup 子系统包括:
- cpu:限制 CPU 使用时间或权重。
- cpuset:绑定进程到特定 CPU 核心和内存节点。
- memory:限制内存使用量、统计内存用量、控制 OOM 行为。
- blkio:限制块设备 I/O 带宽。
- pids:限制进程数量,防止 fork 炸弹。
- devices:控制设备访问权限。
- freezer:暂停或恢复进程组。
Cgroups 有两个主要版本:v1 和 v2。v1 允许多个层级并存,每个子系统可以挂载到不同层级;v2 采用统一层级,结构更清晰,功能也更现代。目前主流发行版和容器运行时正在逐步迁移到 cgroup v2。
在容器场景中,Cgroups 的作用非常直接:
- 限制容器最多使用 2 个 CPU 核心;
- 限制容器最多使用 512MB 内存;
- 限制容器最多创建 100 个进程;
- 统计容器实际消耗的 CPU 和内存,用于监控和计费。
例如,Docker 的 --cpus、--memory、--pids-limit 等参数,最终都会转化为对 Cgroup 文件的写入。
三、命名空间与 Cgroups 如何协同工作
一个典型的容器启动过程大致如下:
- 容器运行时准备 rootfs 和配置。
- 调用
clone()或unshare()创建新的命名空间,包括 PID、Mount、Network、UTS、IPC、User 等。 - 将容器进程加入指定的 Cgroup,设置 CPU、内存、pids 等限制。
- 在 Mount 命名空间中切换根目录到容器 rootfs。
- 启动容器入口进程,该进程在容器内通常 PID 为 1。
此时,容器进程拥有独立的进程视图、网络栈、文件系统和主机名,同时受到 Cgroup 的资源约束。宿主机上的其他进程看不到容器内部进程,容器也无法感知宿主机上的其他进程。这就是容器隔离的基本图景。
需要强调的是,容器隔离并不等同于虚拟机级别的安全隔离。由于共享内核,内核漏洞可能成为逃逸通道。因此,生产环境中还需要结合 Seccomp、AppArmor、SELinux、Capabilities 等安全机制,形成多层防御。
四、常用观察命令
理解原理之后,可以通过一些命令观察命名空间和 Cgroups:
- 查看进程命名空间:
ls -l /proc/<pid>/ns/ - 查看命名空间列表:
lsns - 进入命名空间:
nsenter -t <pid> -n - 查看 Cgroup v2 层级:
mount | grep cgroup2 - 查看进程 Cgroup:
cat /proc/<pid>/cgroup - 查看内存限制:
cat /sys/fs/cgroup/<group>/memory.max
这些工具可以帮助你验证容器运行时的实际行为,也能在排查资源问题时提供第一手信息。
总结
Linux 命名空间和 Cgroups 是容器技术的两大基石。命名空间通过 PID、Mount、Network、UTS、IPC、User 等隔离机制,让进程拥有独立的系统视图;Cgroups 通过 cpu、memory、blkio、pids 等子系统,限制和统计进程组的资源使用。两者结合,再加上 rootfs、Capabilities、Seccomp 等机制,才构成了我们今天所熟知的容器。
理解这些底层原理,不仅有助于日常使用 Docker 和 Kubernetes,也能在性能调优、安全加固和故障排查中提供更清晰的思路。容器并不神秘,它只是 Linux 内核能力的一次巧妙组合。
未经允许不得转载:任鹏个人博客 » Linux 命名空间与 Cgroups:容器隔离的底层原理详解


朋友圈点赞图在线生成源码