“一切皆文件”:从文件视角看透容器技术的本质
提到容器(Container),我们脑海中浮现的往往是 Docker、Kubernetes 这些高大上的名词,或者是“轻量级虚拟机”、“沙箱”这些抽象的概念。但如果你是一名 Linux 老兵,或者信奉 Unix 哲学,你会发现容器并没有那么神秘。
容器的本质,不过是宿主机上两个看起来很特别的“文件集合”:Namespace(隔离)和 Cgroups(限制)。
今天,我们就抛开复杂的架构图,仅仅通过 Linux 的文件系统(特别是 /proc),来扒一扒容器的“底裤”。
一、 /proc:内核的“透视窗”
在 Linux 中,/proc 是一个非常有意思的地方。它是一个虚拟文件系统(Pseudo-filesystem)。
什么叫虚拟?就是说它并不占用你硬盘里实际的物理空间(你往里写数据也没用),它其实是内核在内存中维护的一个“窗口”。通过这个窗口,内核把内部的数据结构、进程状态映射成了大家都能看懂的“文件”和“目录”。
这就好比内核在为你实时直播它的内部运行情况。
进程的“档案室”:/proc/[PID]
内核为每一个正在运行的进程,都在 /proc 下建立了一个以 PID 命名的目录。只要进程还在呼吸,这个目录就在;进程一挂,目录自动消失。
这个目录简直就是该进程的“个人档案室”,里面存放了它的一切秘密。下面这张表总结了这里面常见的“档案”及其作用:
| 路径 | 类型 | 说明 | 常见用途 (DevOps 必看) |
|---|---|---|---|
cmdline |
文件 | 启动命令行(\0 分隔) |
查锅:确认进程到底是用什么参数启动的 |
environ |
文件 | 环境变量(\0 分隔) |
排错:配置没生效?看看环境变量是不是传丢了 |
status |
文件 | 进程综合状态 | 体检:最常用,看内存峰值、线程数、UID、Cap 权限 |
cwd |
link | 当前工作目录 | 寻路:排查相对路径找不到文件的问题 |
exe |
link | 进程可执行文件 | 溯源:确认跑的到底是哪个版本的二进制 |
fd/ |
目录 | 打开的文件描述符 | 排漏:ls -l 进去,排查 FD 泄漏或 Socket 占用 |
maps / smaps |
文件 | 内存映射详情 | 深挖:内存泄漏分析,看堆栈、共享库的内存分布 |
mountinfo |
文件 | 挂载点信息 | 容器核心:查看 Overlayfs 挂载情况 |
ns/ |
目录 | Namespace 链接 | 隔离核心:容器隔离排查的入口 |
cgroup |
文件 | 所属 cgroup 路径 | 限额核心:查看该进程属于哪个资源限制组 |
oom_score |
文件 | OOM 被杀评分 | 求生:分越高,内存不足时死得越快 |
二、 Namespace:看不见的墙
容器最神奇的地方在于“隔离”。在容器里,你觉得自己拥有整个世界(独立的主机名、独立的进程号、独立的网络),但实际上你只是被“蒙蔽”了。
这种蒙蔽机制,就藏在 /proc/[PID]/ns/ 目录下。
1. 它是如何工作的?
如果你查看任何一个进程的 ns 目录,你会看到一堆符号链接:
ls -l /proc/self/ns
# 输出示例:
# lrwxrwxrwx ... cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx ... ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx ... mnt -> 'mnt:[4026531840]'
# lrwxrwxrwx ... net -> 'net:[4026531992]'
# lrwxrwxrwx ... pid -> 'pid:[4026531836]'
# ...
注意看箭头后面的数字(如 4026531836)。这串数字是内核中 Namespace 对象的 Inode 号。
- 规则很简单:如果两个进程的某个 Namespace(比如
net)的 Inode 号相同,它们就在同一个“房间”里,能互相看到;如果不同,它们就处于平行时空,互不可见。
2. 为什么容器里 ps 看不到外面的进程?
很多初学者疑惑:“容器不就是个进程吗?为什么我在容器里 ps -ef 只能看到我自己?”
既然我们知道了“一切皆文件”,这个原理就很好解释了。ps 命令并不是魔法,它也是通过读取 /proc 目录来工作的。
当你在容器内执行 ps 时,发生了以下过程:
graph TD
A[用户态执行 ps] --> B(遍历 /proc 目录);
B --> C{调用内核接口 proc_pid_readdir};
C --> D[读取当前进程的 task_struct];
D --> E[获取当前进程的 Namespace 视图];
E --> F{目标 PID 是否属于当前 Namespace?};
F -- 是 (匹配) --> G[返回 PID 信息];
F -- 否 (不匹配) --> H[忽略/不可见];
style F stroke:#f66,stroke-width:2px
简单来说,因为容器进程处于一个独立的 PID Namespace 中,内核在处理文件系统请求时,不仅检查权限,还检查 Namespace 上下文。如果发现你要访问的 PID 不在你当前的“平行时空”里,内核直接就不会在 /proc 下为你展示那个目录。
你看不到文件,自然就以为那些进程不存在。
三、 Cgroups:隐形的枷锁
如果说 Namespace 是通过“欺骗”进程的视觉来做隔离,那么 Cgroups(Control Groups)就是通过“控制”进程的供给来做限制。
Namespace 决定了你能看到谁,Cgroups 决定了你能用多少资源。
同样地,我们要用“文件”的方式来理解它。
1. 找到资源控制的“遥控器”
在 Linux 中,Cgroups 的接口通常挂载在 /sys/fs/cgroup 下(这又是另一个文件系统)。
如果说 /proc/[PID] 是用来读信息的,那么 /sys/fs/cgroup 主要是用来写规则的。
2. 也是通过文件操作
假设我们要限制一个容器(或进程组)只能使用 200MB 内存,内核是这样通过文件与之交互的:
创建组:
你只需要在/sys/fs/cgroup/memory/下mkdir一个目录(比如docker/container_id)。内核会自动在这个新目录里生成一堆文件。设置限制(写文件):
你往memory.limit_in_bytes(v1) 或memory.max(v2) 这个文件里写入数字209715200(200MB)。echo 209715200 > /sys/fs/cgroup/memory/docker/container_id/memory.limit_in_bytes这就好比把电表上的保险丝换成了 200MB 的规格。
加入进程(写文件):
你把容器里进程的 PID 写入cgroup.procs文件。echo [PID] > /sys/fs/cgroup/memory/docker/container_id/cgroup.procs这就相当于把这个进程的电源插头插到了我们刚才设定的“电表”上。
3. 如何在 /proc 中确认?
回到我们的老朋友 /proc。如果你想确认一个进程到底被加上了什么枷锁,可以看前文表中提到的 /proc/[PID]/cgroup 文件:
cat /proc/12345/cgroup
# 输出示例 (Cgroup v1):
# 11:memory:/docker/8f3a2b...
# 4:cpu,cpuacct:/docker/8f3a2b...
# 1:name=systemd:/docker/8f3a2b...
- 解读:这行字明明白白地告诉你,PID 12345 目前归属于
/docker/8f3a2b...这个控制组。 - 关联:系统会根据这个路径,去
/sys/fs/cgroup/memory/docker/8f3a2b...里查找对应的资源限制文件。
总结
所以,容器到底是什么?
- Namespace (ns):就是给进程换了一副“特制眼镜”,让它只能看到文件系统
/proc的一部分,从而实现隔离。 - Cgroups:就是给进程所在的“房间”装上了水电表,通过读写
/sys/fs/cgroup下的配置文件,精准控制它的资源消耗。
剥去 Docker 和 K8s 华丽的外衣,你会发现底层的技术其实非常朴素且优雅:一切,真的都是文件。