从文件的视角看容器

31 阅读6分钟

“一切皆文件”:从文件视角看透容器技术的本质

提到容器(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 权限
cwdlink当前工作目录寻路:排查相对路径找不到文件的问题
exelink进程可执行文件溯源:确认跑的到底是哪个版本的二进制
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 内存,内核是这样通过文件与之交互的:

  1. 创建组: 你只需要在 /sys/fs/cgroup/memory/mkdir 一个目录(比如 docker/container_id)。内核会自动在这个新目录里生成一堆文件。

  2. 设置限制(写文件): 你往 memory.limit_in_bytes (v1) 或 memory.max (v2) 这个文件里写入数字 209715200 (200MB)。

    echo 209715200 > /sys/fs/cgroup/memory/docker/container_id/memory.limit_in_bytes
    

    这就好比把电表上的保险丝换成了 200MB 的规格。

  3. 加入进程(写文件): 你把容器里进程的 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 华丽的外衣,你会发现底层的技术其实非常朴素且优雅:一切,真的都是文件。