首页 › programming › 内核里没有容器:只是个被骗的进程

内核里没有容器:只是个被骗的进程

导语:你以为的"Docker魔法",其实是Linux的一场骗局

你大概写过无数遍 docker run,看着一个环境在毫秒之间起好,心里默认"容器就是个轻量级虚拟机"。这个默认认知,恰恰是理解容器最大的障碍。

State & Flow 频道那支《How Docker Containers Actually Work — Namespaces, Cgroups, and Union Filesystems》之所以值得反复看,是因为它干了一件大多数 Docker 教程刻意回避的事:把"容器"这个词从内核里彻底删掉,让你看见底下真正在转的三样东西——namespace、cgroup、联合文件系统。

结论先抛出来:Linux 内核里根本没有"container"这个对象。你 ps 出来的那个进程,和你 docker exec 进去看到的进程,是同一个进程,跑在同一个内核上,挤在同一颗 CPU 上。它之所以显得"被关起来了",纯粹是因为内核对它撒了一系列谎——告诉它"你是 PID 1""这台机器只有一张网卡""根目录长这样"。容器不是被造出来的,是被裁剪出来的。

理解了这一点,你才会明白为什么容器启动只要几毫秒、为什么跨内核架构跑不了、为什么"隔离"和"限流"是两件根本不相干的事。这篇文章就把视频里的内核三件套拆开揉碎,再补上视频没讲透的工程细节,最后落到通信人最关心的场景:5G 核心网为什么非要把网元塞进容器。

视频背景卡片

项目 内容
视频标题 How Docker Containers Actually Work — Namespaces, Cgroups, and Union Filesystems
频道 State & Flow(专注云原生与系统底层原理解读)
时长 深度讲解长视频,节奏紧凑、代码与图示穿插
播放量 技术圈高口碑,持续被引用为"最该补的一课"
核心主题 容器不是虚拟机,本质是 namespaces + cgroups + union filesystem

一、内核里压根没有"容器"这个东西

这是全片最反直觉的一句话,也是整个容器技术被发明的起点。你去翻 Linux 内核源码,搜 container,搜不到一个叫"容器"的系统调用。内核只提供了一批原语(primitive):clone()、unshare()、setns()、cgroup 文件系统、OverlayFS……所谓"容器",是用户态把这些原语按特定方式组合后涌现出来的效果。

Aqua Security 的 Liz Rice 在那场著名的《Containers from Scratch》演讲里,用大约 100 行 Go 代码就"造"出了一个能跑的容器。核心就一句:调用 syscall.Clone 时带上 CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWIPC 这些标志位,再 pivot_root 换个根目录。就这些。没有魔法,没有 hypervisor,就是几个系统调用。

这个认知的价值在于:它把"容器"从一个黑盒名词,还原成了一组可解释、可调试、可单独开关的开关。下次你排查"为什么容器里看不到宿主机进程",你就知道是 PID namespace 在干活,而不是去怀疑 Docker 出 bug。

二、容器不是小号虚拟机:差在那一层内核

很多人把容器类比成"瘦身版虚拟机",这个比喻方便但有害。两者的分野只有一条:谁拥有内核。

虚拟机走的是硬件级虚拟化:hypervisor(KVM、ESXi)模拟出一套硬件,每个 VM 里跑一个完整的客户机内核(guest kernel)。一个 Ubuntu VM 里,那个 systemd、那套网卡驱动、那套调度器,全都是真实存在的第二份拷贝。所以 VM 动辄几个 GB,启动要几十秒——它真的在"开机"。

容器走的是操作系统级虚拟化:所有容器共用宿主机的同一个内核。容器里那个"内核"根本不存在,它调用的系统调用,直接被宿主机内核接住处理。所以一个 Alpine 镜像可以只有 5MB,容器可以毫秒级启动——因为它只是 fork 出一个进程,给它换了一副"眼镜",根本没开机。

这条分野有一个非常硬核的推论,也是面试常考的点:容器必须和宿主机共享内核架构。你不能在 Linux 宿主机上原生跑一个 Windows 容器,因为内核对不上。这恰恰反证了"容器=轻量VM"是错的——如果真有独立的内核,架构本不该是约束。通信圈做 5G 云核心网时,所有 NF(Network Function)都必须基于同一套 Linux 内核镜像,根源就在这里。

三、Namespace:骗进程"你独占了整个世界"

如果说容器是一场骗局,namespace 就是骗术本身。它干的事情用一句话概括:控制一个进程能"看见"什么。

Linux 内核到现在一共提供了 8 种 namespace,每一种都对应一种"世界观"的裁剪:

注意一个被反复忽略的事实:这 8 种 namespace 是相互独立的。你可以只给一个进程套上 net namespace(让它有独立网络),却不给它 pid namespace(它能看见宿主机所有进程)。实际上 ip netns 创建网络命名空间、Kubernetes 里 hostNetwork: true、Docker 的 --network host 都是在做"部分隔离"的取舍。namespace 不是非开即关的开关组,而是一个 8 维的布尔向量。

一个最直观的验证:你在容器里 ps aux 看到自己是 PID 1,但跑到宿主机上 ps aux,能找到同一个进程,只是它的 PID 是 4567。进程只有一个,只是两副眼镜看到的编号不同。 这就是"被骗的进程"的字面含义。

四、Cgroup:管的是"你能用多少",跟隔离无关

namespace 解决了"看得见什么",但完全没解决"能用多少"。一个只套了 namespace 的进程,理论上可以吃光宿主机全部内存、占满所有 CPU、fork 出十万个子进程把系统拖垮。这就是 cgroup(control group)登场的理由。

cgroup 干的是资源记账与限制:把进程分组,对每组施加配额。它的控制器(controller)按资源类型拆开:memory 限内存、cpu/cpuacct 限和统计 CPU、blkio(v2 里叫 io)限磁盘 IO、pids 限进程数、devices 控制能访问哪些设备文件、net_cls/net_prio 给网络流量打标签。你在 Docker 里写 --memory=512m --cpus=0.5,本质就是往 cgroup 文件系统里写两个数字。

cgroup 有 v1 和 v2 两代,区别不小:v1 每种资源一棵独立的层级树,资源之间没法联动(一个进程在内存树和 CPU 树里是两套路径,常打架);v2 改成统一层级(unified hierarchy),一棵树管所有控制器,2014 年合入内核,到现在 Docker/K8s 才基本完成迁移。这事工程上坑极多,是很多"为什么我的 cgroup 限制不生效"问题的根源。

五、隔离和限流,是两条完全独立的轴

这是我这篇解读最想强调、也是视频点破的一个概念性洞见:namespace(隔离)和 cgroup(限流)是正交的两条轴,谁也不包含谁。

把它们摆在一个二维坐标系里就清楚了:

一个真正的生产容器,必须两条轴都拉满:既看不见宿主机别的进程,也不能吃光内存。但现实中很多人把它们混为一谈,说"Docker 提供了资源隔离"——这句话是错的,或者说至少是不精确的。Docker 提供隔离(namespace)+限制(cgroup),这是两件事。

正交性带来的实战推论很有意思: - 只隔离不限流:容器能看到的世界很小,但一个失控的 Java 进程照样能 OOM 整台宿主机,把隔壁容器一起带走(这是 K8s 早期"邻居噪音"问题的根)。 - 只限流不隔离:一个进程被限了 CPU,但它能 cat /etc/passwd 读到宿主机所有用户、能看到所有进程的网络连接——限制毫无安全意义。

通信云核心网里,UPF(用户面功能)转发流量大、内存敏感,cgroup 限流是性能 SLA 的命根子;而 AMF/SMF 处理信令、涉及用户数据,namespace 隔离是安全合规的底线。同一种技术,两条轴的权重完全不同。理解了正交性,你才会针对性地配 resource request/limit 和 securityContext,而不是无脑复制 YAML。

六、镜像不是复制:OverlayFS 靠分层叠加

namespace 和 cgroup 解决了"进程"的问题,但容器还得有个"文件系统"——这就是视频标题里第三个关键词 Union Filesystem(联合文件系统)出场的地方。它解释了一个常被忽略的奇迹:为什么 100 个容器基于同一个镜像,磁盘上只存一份。

OverlayFS(以及早期的 AUFS、device-mapper thin pool)的核心是分层 + 写时复制(Copy-on-Write)。一个容器运行时的文件系统视图,由几层拼出来:

关键机制是 copy_up:当容器要修改一个来自 lowerdir 的文件时,OverlayFS 不会去动只读层,而是先把这个文件复制到 upperdir,再在副本上改。这就是写时复制。

这套机制带来三个工程后果,每一个都和你日常相关:第一,镜像可以共享层,pull 一个新镜像时已存在的层会跳过,所以拉镜像才那么快。第二,容器是临时的,删掉容器,upperdir 里的改动全没了——这就是为什么"别把数据写进容器,要用 volume"。第三,写性能有额外开销,第一次修改一个只读层文件会触发 copy_up,IO 密集场景要留意。Docker 镜像之所以设计成一层层 RUN 指令,本质就是在和 OverlayFS 的分层模型对齐,把变动频繁的层放上面,提升共享率。

七、光有隔离还不够:seccomp 与 capabilities 补刀

namespace+cgroup+unionfs 已经能跑出一个像样的容器,但安全上还有两个洞,视频点到为止,我展开讲。

第一个洞:系统调用没拦。Linux 有大约 300 个系统调用,其中不少是给特权操作用的——reboot 重启机器、ptrace 窥探别的进程、mount 挂载文件系统。即便容器里的 root 映射成了宿主机的 nobody,有些 syscall 仍可能被滥用。seccomp(secure computing mode) 就是来堵这个洞的:它用 BPF 过滤器在内核里规定"这个进程只能调哪些 syscall"。Docker 默认的 seccomp profile 大约屏蔽了 44 个危险 syscall。

第二个洞:root 权限太粗。传统 Unix 的 root 是个二进制开关,要么全能要么无能。Linux capabilities 把 root 拆成了约 40 个细粒度权限,比如 CAP_NET_BIND_SERVICE(绑定 1024 以下端口)、CAP_SYS_ADMIN(几乎等于万能,尽量别给)、CAP_NET_RAW(发原始网络包)。容器安全的核心原则是 drop all, then add only needed——先把所有 cap 删掉,再按需加回去。K8s 的 Pod Security Standards 里 restricted 级别干的就是这件事。

把 seccomp、capabilities 和前面三件套叠在一起,才是一个真正生产可用的容器安全模型。这也是为什么"容器逃逸"(container escape)一直是云安全的高危话题:只要这些原语里任何一层配错,进程就可能突破谎言、看到真实世界。

八、从 docker run 到内核:一整条调用链

把前面所有零件串起来,你执行 docker run nginx 时,背后是这么一条链:

flowchart TD
    A["docker run nginx<br/>(CLI 命令)"] --> B["dockerd<br/>(守护进程,解析参数)"]
    B --> C["containerd<br/>(高层运行时,管生命周期)"]
    C --> D["runc<br/>(OCI 低层运行时,符合 runtime-spec)"]
    D --> E["Linux 内核原语"]
    E --> F["namespaces<br/>裁剪视野"]
    E --> G["cgroups<br/>限制资源"]
    E --> H["OverlayFS<br/>分层文件系统"]
    E --> I["seccomp + capabilities<br/>收窄权限"]
    F & G & H & I --> J(("一个'被骗'的普通进程<br/>= 容器"))

    style J fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style E fill:#fff3e0,stroke:#ef6c00

这条链对应一个行业标准 OCI(Open Container Initiative),它定义了两份规范:image-spec(镜像长什么样)和 runtime-spec(容器实例长什么样)。runc 是 reference implementation。Docker、containerd、Kubernetes 的 containerd runtime、CRI-O,底层最后都汇聚到 runc,再汇聚到那几个内核原语。理解了这条链,你就理解了为什么"换容器运行时"(比如 Docker 换 containerd)对应用透明——因为 OCI 把接口标准化了。

知识结构图:容器的完整拼图

flowchart LR
    subgraph 神话["大众认知的'容器'"]
        M["一个黑盒<br/>≈ 轻量虚拟机"]
    end
    subgraph 真相["内核里真实存在的原语"]
        N["namespaces<br/>8种 · 管视野/隔离<br/>'看不见'"]
        C["cgroups<br/>v1/v2 · 管资源/限制<br/>'用不了'"]
        U["unionfs/OverlayFS<br/>分层+写时复制<br/>'磁盘省一份'"]
        S["seccomp + capabilities<br/>收窄syscall与特权<br/>'干不了坏事'"]
    end
    M -.被还原为.-> N
    M -.被还原为.-> C
    M -.被还原为.-> U
    M -.被还原为.-> S
    N --> P["正交的两条轴"]
    C --> P
    P --> R(("容器 = 普通进程<br/>+ 一组精心组合的内核谎言"))

    style M fill:#ffebee,stroke:#c62828
    style R fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style P fill:#e3f2fd,stroke:#1565c0

实战启示:知道是"被骗的进程"之后,你能做什么

对通信工程师(这是写给你看的): 5G 核心网从 Release 16 起全面云原生化,AMF、SMF、UPF、PCF 全部以容器形态部署在 Kubernetes 上(即所谓的 CBA,Cloud-native Based Architecture)。理解 namespace+cgroup,你才看得懂为什么 UPF 要单独做资源隔离(cgroup 限 CPU/内存保转发 SLA),为什么不同租户的 NF 要强 namespace 隔离(多租户安全),为什么容器网络(CNI、Calico/VXLAN)本质是在 net namespace 之间架 veth pair 和路由。容器不是通信人可以跳过的"互联网技术",它就是下一代核心网的运行底座。

对后端/云原生开发者: 第一,排查问题换个坐标系——容器里 OOM 了,先去宿主机看那个真实 PID 的 cgroup memory.usage_in_bytes,而不是只看应用日志。第二,写 Dockerfile 时脑子里要有 OverlayFS 分层图,把变动少的放底层、变动多的放上层,合并 RUN 减少层数,缩小镜像。第三,做安全加固时走"默认拒绝"——cap drop all、seccomp default、非 root 用户运行,按需放开,而不是反过来。

对所有写代码的人: 这套"内核不提供容器,只提供原语;容器是原语组合的涌现"的设计哲学,是 Unix 哲学的极致体现——不要造大而全的新概念,要提供正交、可组合的小原语。你设计自己的系统时,与其定义一个"插件"巨对象,不如想清楚有哪几条独立的轴,让用户自由组合。这正是容器能横扫整个软件交付链的根本原因:它没有发明任何新东西,只是把内核已有的几块乐高,拼出了一个所有人都想要的形状。

延伸学习路径

  1. Liz Rice —《Containers From Scratch》(GopherCon 演讲 + GitHub lizrice/containers-from-scratch):100 行 Go 手搓容器,看完"容器"祛魅彻底完成。
  2. 《Linux namespaces》系列 — ENCCS 官方容器教程(enccs.github.io/containers):学术机构出品,namespace/cgroup 讲得最严谨免费教材。
  3. Henry S. Warren《Hacker's Delight》与内核文档 Documentation/cgroups/:前者是底层位运算圣典,后者是 cgroup v1→v2 迁移权威说明。
  4. OCI 官网(opencontainers.org)+ runc 源码:想真正理解 runtime-spec,没有比读 runc 的 create.go 更快的路。
  5. Julia Evans 的《How Containers Work》zine(wizardzines.com):手绘风把 namespace/cgroup/overlayfs 画得清清楚楚,适合视觉型学习者。

一句话总结

Linux 内核里从来没有"容器",只有一个被 namespace 捏造了世界观、被 cgroup 限制了配额、靠 OverlayFS 省下磁盘的普通进程——理解了这场精心设计的"骗局",你就拿到了云原生时代的底层源代码。

参考来源

  1. How Docker Containers Actually Work — Namespaces, Cgroups, and Union Filesystems — 本文解读的核心视频(State & Flow 频道)
  2. Namespaces and cgroups — ENCCS Introduction to Containers — 容器原语权威免费教材,namespace 自 2002 年、容器支持 2013 年合入内核的出处
  3. containers-from-scratch — Liz Rice (GitHub) — 100 行 Go 造容器的原始代码
  4. How Container Runtimes Work Internally: Namespaces, Cgroups, and the OCI Stack — docker→containerd→runc→内核 调用链详解
  5. Open Container Initiative (OCI) — image-spec 与 runtime-spec 标准定义

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入