摘要
Kubernetes 和 Docker 表面上是“容器平台”和“容器工具”,但它们真正依赖的是 Linux 内核已经存在的进程、namespace、cgroup、mount、capability、seccomp、文件系统和网络能力。我的判断是:容器不是虚拟机,容器本质上是被 Linux 内核隔离、限制、改造过运行环境的一组进程。Docker 官方也明确说,容器是在宿主机上运行的一个进程,只是拥有独立文件系统、网络和进程树等隔离视图。(Docker Documentation[1])
本文按论文式结构,沿着 Kubernetes Pod 生命周期 → CRI → containerd / Docker → runc → Linux 内核系统调用 → 容器内应用 syscall 路径 展开,重点解释 nsproxy、namespace、cgroup 在容器创建、启动、运行、销毁中的作用,并给出一个 Pod 和一个容器启动过程中通常涉及的 Linux 系统调用与关键内核函数。
关键词:Kubernetes,Docker,containerd,runc,namespace,nsproxy,cgroup,clone3,setns,mount,execve,seccomp,OCI Runtime
1. 引言:Kubernetes 和 Docker 到底在 Linux 上运行了什么?
先给结论:Kubernetes 不直接“运行容器”,Docker 也不直接等于“容器本身”。它们最终都是在驱动 Linux 创建一个或多个普通进程,只是这些进程被放进了特定 namespace、cgroup、rootfs、capability、seccomp profile 和网络环境里。
在现代 Kubernetes 中,kubelet 通过 CRI 和容器运行时通信;CRI 是 kubelet 与 container runtime 之间的主协议,kubelet 作为 gRPC 客户端连接容器运行时,运行时负责真正创建 Pod 和容器。(Kubernetes[2]) Kubernetes 官方也说明,从 v1.24 开始,内置 dockershim 已经移除;Docker Engine 本身不实现 CRI,因此 Kubernetes 需要通过 CRI 兼容运行时,例如 containerd、CRI-O,或者额外的 cri-dockerd 适配 Docker。(Kubernetes[3])
因此,在 Linux 上看容器技术,正确分层应该是:
Kubernetes API / PodSpec ↓kubelet ↓ CRI gRPCcontainerd / CRI-O / cri-dockerd ↓containerd-shim / runtime shim ↓ OCI Runtimerunc ↓ Linux syscallsclone3 / clone / unshare / setns / mount / pivot_root / execve / prctl / seccomp / kill / wait4 ↓Linux kerneltask_struct / nsproxy / cgroup / VFS / netns / scheduler / memory manager
OCI Runtime Specification 定义了容器配置、执行环境和生命周期,config.json 描述容器创建所需字段,runtime 负责基于这些字段创建一致的执行环境。(GitHub[4]) 这就是为什么 Docker、containerd、CRI-O 最终都可以落到 runc 或其他 OCI runtime 上。
2. 研究对象与边界
本文讨论的是 Linux 容器主路径,尤其是 Kubernetes + containerd + runc,以及 Docker Engine + containerd + runc 这两条常见链路。不同版本、不同 runtime、不同发行版、是否启用 cgroup v1 / v2、rootless、user namespace、systemd cgroup driver、CNI 插件实现,都会导致细节差异,但主干机制是一致的。containerd 官方说明它管理宿主机上的完整容器生命周期,包括镜像传输、存储、容器执行、监督、底层存储和网络附件等。(containerd.io[5])
本文不把 Kubernetes 的调度器、控制器、API Server 作为重点,而是聚焦在 Pod 已经调度到某个 Node 后,kubelet 如何把 PodSpec 转换为 Linux 进程。Kubernetes 官方 Pod 生命周期文档说明:Pod 被调度绑定到 Node 后,kubelet 会管理容器,并把 Pod spec 翻译给容器运行时。(Kubernetes[6])
3. Linux 容器的三个核心事实
3.1 容器首先是进程
容器里运行的 Java、Go、Nginx、MySQL,本质上仍然是宿主机 Linux 上的进程。区别在于,这些进程看到的 PID、网络设备、mount 树、hostname、IPC、cgroup 路径等不是宿主机全局视图,而是 namespace 裁剪后的视图。Docker 官方对 docker run 的描述非常直接:容器进程运行在宿主机上,并拥有自己的文件系统、网络和隔离的进程树。(Docker Documentation[7])
3.2 namespace 负责“看见什么”
namespace 的职责是隔离内核资源视图。比如:
| |
|---|
| |
| |
| |
| 容器内看到自己的 hostname、domainname |
| 隔离 System V IPC、POSIX message queue |
| |
| 隔离 /proc/self/cgroup 看到的 cgroup 路径 |
| |
Linux man-pages 说明,setns(2) 可以让调用进程加入已有 namespace,而 namespace 通常可以通过 /proc/pid/ns 文件描述符引用。(man7.org[8]) clone(2) / clone3(2) 则是创建新进程、线程以及新 namespace 的关键系统调用接口。(man7.org[9])
3.3 cgroup 负责“能用多少”
cgroup 不主要负责“看见什么”,而是负责资源归属、限制、统计和控制。Linux cgroup v2 官方文档明确说明,它是 cgroup v2 设计、接口和约定的权威文档。(Linux内核文档[10]) 在 cgroup v2 中,cgroup.procs 可以列出属于该 cgroup 的进程 PID,也可以通过向目标 cgroup 的 cgroup.procs 写入 PID 来迁移进程。(Linux内核文档[11])
cgroup controller 决定资源如何分配。例如 cpu、memory、io controller 可以控制子 cgroup 对 CPU、内存、IO 的使用;父 cgroup 通过开启 controller 来控制其子层级的资源分配。(Linux内核文档[12]) Kubernetes 的 CPU limit、memory limit、QoS、Pod cgroup 层级,本质都要落到这些内核接口上。
4. nsproxy:namespace 在内核里的“指针集合”
nsproxy 是理解容器隔离的关键。Linux 内核源码中的 struct nsproxy 包含多个 namespace 指针,例如 uts_ns、ipc_ns、mnt_ns、pid_ns_for_children、net_ns、time_ns、cgroup_ns。源码注释还说明:共享所有 namespace 的 task 可以共享同一个 nsproxy;一旦 clone 或 unshare 某个 namespace,nsproxy 就会被复制。(GitHub[13])
简化理解:
struct task_struct { // ... struct nsproxy *nsproxy; // ...};struct nsproxy { struct uts_namespace *uts_ns; struct ipc_namespace *ipc_ns; struct mnt_namespace *mnt_ns; struct pid_namespace *pid_ns_for_children; struct net *net_ns; struct cgroup_namespace *cgroup_ns; // ...};
这说明一件很重要的事:容器隔离不是靠给进程打一个“container”标签实现的,而是 task_struct 间接引用了一组 namespace 对象。 当 runc 创建容器 init 进程时,它通过 clone / clone3 传入 CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWNET、CLONE_NEWUTS、CLONE_NEWIPC、CLONE_NEWCGROUP 等 flags,内核创建或复制 namespace 对象,再把新进程的 task_struct->nsproxy 指向这组 namespace。clone(2) man page 明确说明它描述了 glibc clone() wrapper、底层系统调用以及较新的 clone3() 系统调用。(man7.org[14])
我的判断是:nsproxy 是 Linux 容器隔离的“索引结构”,namespace 对象才是隔离视图本身。 没有 nsproxy,你很难把“一个进程同时处于多个 namespace 中”这个事实组织起来。
5. Kubernetes 中一个 Pod 的启动路径
一个 Pod 的创建,不是直接启动业务容器,而是通常先创建 Pod sandbox。Kubernetes CRI 把 kubelet 与 runtime 的交互标准化,kubelet 通过 CRI gRPC 与 runtime 通信。(Kubernetes[15])
典型调用链如下:
1. 用户提交 Pod YAML2. API Server 持久化 PodSpec3. Scheduler 把 Pod 绑定到某个 Node4. 该 Node 上的 kubelet 发现待运行 Pod5. kubelet 调用 CRI RuntimeService.RunPodSandbox6. runtime 创建 Pod sandbox7. CNI 为 sandbox 配置网络8. kubelet 调用 CreateContainer9. kubelet 调用 StartContainer10. 容器内业务进程 execve 启动11. kubelet 持续探测、重启、停止、删除
Kubernetes 官方文档描述 Pod 生命周期时说明:Pod 从 Pending 开始,如果至少一个主容器成功启动则进入 Running,之后根据容器是否失败进入 Succeeded 或 Failed;同时 kubelet 在 Pod 运行期间管理容器,并把 Pod spec 翻译给容器 runtime。(Kubernetes[16])
Pod sandbox 的核心作用是为 Pod 建立共享环境,尤其是 network namespace。对于普通 Linux runtime,Pod sandbox 通常对应 pause 容器或类似 infra 容器:它先创建并持有 Pod 的 network namespace,后续业务容器通过 setns 加入该 namespace。Kubernetes 文档没有把 pause 容器实现细节固定死,因为 CRI 留给 runtime 一定解释空间;CRI 的核心是 kubelet 与 runtime 的协议,而不是强制某一种底层实现。(Kubernetes[17])
6. 一个容器的 OCI 生命周期
OCI Runtime Specification 给出的生命周期是理解 runc 的主线。OCI runtime 的 create 命令会被调用,随后执行 hooks;start 命令被调用后,runtime 必须运行用户指定程序;进程退出可能来自错误、正常退出、崩溃或 runtime 的 kill 操作;最后 delete 被调用,容器必须通过撤销 create 阶段的步骤被销毁。(GitHub[18])
简化成工程视角:
Create 阶段: - 准备 rootfs - 准备 mount namespace - 配置 cgroup - 配置 namespace - 配置 capabilities - 配置 seccomp / AppArmor / SELinux - 创建但不一定立即执行用户程序Start 阶段: - 放行容器 init 进程 - execve 用户指定 command - 业务进程成为容器内主进程Run 阶段: - 业务进程发起 syscall - 内核基于 namespace/cgroup/security profile 处理Stop / Kill 阶段: - runtime / kubelet 发送 SIGTERM - 超时后 SIGKILL - wait4 / waitid 回收退出状态Delete 阶段: - 删除 cgroup - 卸载 mount - 删除 runtime state - 清理网络 namespace / CNI 配置
这里最容易误解的是 create 和 start。create 不是简单 fork 一个进程就完事,它要先把 Linux 执行环境准备好;start 才真正让用户指定的程序运行。OCI 生命周期文档明确把 create、start、运行用户程序、退出、delete 分成了不同阶段。(GitHub[19])
7. runc 创建容器时通常涉及的系统调用
下面是容器创建启动过程中最关键的一批 Linux syscall。不同 runtime 版本可能用 clone 或 clone3,不同配置可能增减 unshare、setns、pivot_root、chroot、seccomp 等调用,但主干不会变。
| | |
|---|
| clone3() | 创建容器 init 进程,并指定 namespace flags |
| setns() | 业务容器加入 Pod sandbox 的 netns 等 |
| unshare() | |
| mount() | 挂载 overlayfs、proc、sysfs、tmpfs、bind mount |
| pivot_root() | |
| chdir() | |
| setuid() / setgid() / setgroups() / capset() | |
| prctl() | 设置 no_new_privs、seccomp 过滤 |
| mkdir() | 创建 cgroup 目录、写入 cgroup.procs、memory.max、cpu.max 等 |
| execve() | |
| kill() | |
| wait4() | |
| umount2() | |
clone 是这里的核心,因为它既能创建进程,也能通过 flags 创建新的 namespace;man page 明确把 clone() wrapper、底层 syscall 和 clone3() 放在同一页讨论。(man7.org[20]) setns 则用于加入已有 namespace,这正是 Kubernetes Pod 内多个容器共享网络 namespace 的关键机制。(man7.org[21])
8. 从内核函数看 clone 如何创建容器进程
用户态调用 clone3() / clone() 后,内核大致会走到进程创建路径。不同内核版本函数细节会变化,但典型主线可以抽象为:
sys_clone / sys_clone3 → kernel_clone → copy_process → copy_namespaces → create_new_namespaces / copy_namespaces → copy_mnt_ns → copy_utsname → copy_ipcs → copy_pid_ns → copy_net_ns → copy_cgroup_ns → cgroup_can_fork / cgroup_post_fork → sched_fork → copy_files / copy_fs / copy_sighand / copy_mm → wake_up_new_task
这条路径的关键不是“函数名背诵”,而是理解 copy_process 会复制或共享进程资源。Linux Kernel Labs 对进程的讲解说明,Linux 的基本执行单元是 task,也就是 task_struct;线程和进程都用 task 表示,并通过指针引用共享或独立的资源结构。(Linux Kernel Labs[22])
当 clone flags 包含 CLONE_NEWNET、CLONE_NEWPID、CLONE_NEWNS 等标志时,内核不会让新进程继续共享父进程的对应 namespace,而是创建新的 namespace 对象,并通过新的 nsproxy 组织这些对象。nsproxy.h 源码注释明确说明,共享全部 namespace 的 task 共享 nsproxy;一旦某个 namespace 被 cloned 或 unshared,nsproxy 会被复制。(GitHub[23])
9. Pod sandbox 与业务容器的 namespace 关系
一个 Pod 里面可以有多个容器,但它们通常共享网络 namespace。这就是为什么同一个 Pod 里的容器可以通过 localhost 互相访问,也为什么同一个 Pod 内端口不能冲突。
典型过程如下:
RunPodSandbox: runc 创建 pause / sandbox 进程 pause 进程拥有新的 netns、utsns、ipcns 等 CNI 插件把 veth 一端放入 sandbox netns Pod 获得 IPCreateContainer / StartContainer: runc 创建业务容器进程 对于需要共享的 namespace,打开 sandbox 的 /proc/<pause-pid>/ns/net 调用 setns(fd, CLONE_NEWNET) 业务容器进入同一个 Pod 网络命名空间 execve 启动业务命令
这里 pause / sandbox 进程的价值不是执行业务,而是“持有 namespace”。namespace 的生命周期通常由引用决定,只要还有进程或打开的 namespace fd 引用它,namespace 就不会消失。cgroup namespace 文档也说明,cgroup namespace 只要内部还有进程或 mount 引用就存活,最后一个使用消失后 namespace 被销毁。(Linux内核文档[24])
10. cgroup 在 Pod / 容器中的作用
cgroup 在 Kubernetes 中至少承担五类职责:
第一,资源限制。Pod / container 的 CPU、memory、pids、hugepages、IO 等限制,最终都要落到 cgroup controller 文件。cgroup v2 文档说明,controller 可用时会暴露对应 interface files,使目标资源的分配可以被观察或控制。(Linux内核文档[25])
第二,资源统计。容器 CPU 使用量、内存使用量、OOM、IO 等指标,通常来自 cgroup 文件或内核统计,再被 kubelet、container runtime、cAdvisor、Prometheus 采集。
第三,进程归属。runtime 会把容器 init 进程 PID 写入对应 cgroup 的 cgroup.procs,子进程 fork 后默认出生在父进程所在 cgroup 中;cgroup v2 文档明确说明,进程 fork 出来的子进程会出生在 fork 时父进程所属的 cgroup。(Linux内核文档[26])
第四,生命周期清理。容器退出后,runtime 可以检查 cgroup 中是否还有残留进程;cgroup v2 还提供 cgroup.kill 机制,写入 1 可以对该 cgroup 及其后代发送 SIGKILL。(Linux内核文档[27])
第五,视图隔离。cgroup namespace 可以让容器内 /proc/self/cgroup 只看到相对路径,而不是宿主机完整 cgroup 路径;内核文档明确指出,没有 cgroup namespace 时 /proc/$PID/cgroup 会显示完整路径,这可能泄漏系统级信息,而 cgroup namespace 可以限制这种可见性。(Linux内核文档[28])
我的判断是:namespace 让容器“像一台独立机器”,cgroup 让容器“不能像一台独立机器那样无限消耗资源”。二者缺一不可。
11. 容器 rootfs 与 mount namespace
容器文件系统不是 magically isolated。runtime 通常先准备镜像层,例如 overlayfs merged 目录,然后在新的 mount namespace 中挂载:
/├── bin├── etc├── proc├── sys├── dev├── tmp└── app
典型操作包括:
mount("overlay", merged, "overlay", ...)mount("proc", "/proc", "proc", ...)mount("tmpfs", "/dev", "tmpfs", ...)mount("/host/path", "/container/path", MS_BIND, ...)pivot_root(new_root, put_old)umount2(put_old, MNT_DETACH)
Mount namespace 的意义是:容器里看到自己的挂载树,宿主机不会因为容器里 mount /proc、bind mount volume、切换 rootfs 而污染全局挂载视图。clone / unshare 创建新 namespace 的能力,是 mount namespace 生效的前提。(man7.org[29])
这里要强调:rootfs 隔离不是安全边界的全部。 如果没有合适的 capability、seccomp、只读挂载、device cgroup、LSM 限制,容器仍然可能通过内核攻击面或错误挂载逃逸。
12. 容器内应用执行系统调用时发生了什么?
容器内的 Java / Go / C 程序调用 open()、read()、socket()、connect()、fork()、mmap() 时,并不会调用什么“容器专用内核”。它调用的是同一个宿主机 Linux kernel。
路径大致是:
应用代码 → libc wrapper / Go runtime syscall wrapper → CPU syscall 指令 → 进入内核 syscall entry → 根据 syscall number 分发到 sys_openat / sys_socket / sys_clone / ... → seccomp 检查是否允许 → capability / LSM 权限检查 → namespace 视图转换 → cgroup 资源计费和限制 → VFS / network / memory / scheduler 子系统执行 → 返回用户态
Linux man-pages 明确说,system call 是应用程序和 Linux 内核之间的基本接口。(man7.org[30]) syscall(2) 也说明,syscall() 是一个小型库函数,用指定系统调用号和参数触发汇编层面的系统调用接口。(man7.org[31])
seccomp 是容器 syscall 安全的重要一层。seccomp(2) 文档指出,seccomp 过滤基于系统调用号;应用通常不会直接调用系统调用,而是调用 C library wrapper,再由 wrapper 触发真正的 syscall。文档还提醒,同一个 wrapper 在不同 glibc 版本或架构上可能调用不同底层 syscall,例如较新的 glibc 中 open() 可能走 openat()。(man7.org[32]) Kubernetes 官方也说明 seccomp 可以限制进程从 userspace 调入 kernel 的系统调用,并可应用到 Pod 和容器。(Kubernetes[33])
举几个具体例子:
12.1 容器内调用 gethostname()
app gethostname() → syscall → kernel 读取 current->nsproxy->uts_ns → 返回该 UTS namespace 中的 hostname
所以容器里看到的是容器 hostname,不是宿主机 hostname。原因不是应用被改写了,而是内核根据当前 task 的 namespace 指针返回不同数据。
12.2 容器内调用 socket() / connect()
app socket(AF_INET, ...) → kernel 根据 current->nsproxy->net_ns 创建 socket → socket 属于容器所在 netns → 路由表、iptables/nftables、网卡都是该 netns 视图
所以同一 Pod 内容器共享 network namespace 时,它们共享 localhost 和端口空间;不同 Pod 拥有不同 netns,所以同端口可以重复监听。
12.3 容器内调用 fork() / Go 创建线程
fork / clone → 内核检查 pids cgroup 限制 → 如果超过 pids.max,返回错误 → 子进程继承父进程 cgroup
cgroup v2 文档说明,pids controller 用于在指定限制达到后阻止新的 fork 或 clone;如果创建新进程会违反 cgroup PID policy,fork() 或 clone() 会返回 -EAGAIN。(Linux内核文档[34])
12.4 容器内申请内存
malloc / mmap / brk → 内核分配虚拟内存 → 实际 page fault 时分配物理页 → memory cgroup 计费 → 超过 memory.max 触发 reclaim / OOM
所以容器内应用看到的“内存不足”,很多时候不是宿主机真的没内存,而是该容器所在 cgroup 的 memory limit 被打满。
13. 容器启动全过程:从 PodSpec 到 execve
下面用一个更完整的时间线串起来。
13.1 kubelet 接收 Pod
Pod 被调度到 Node 后,kubelet 通过 watch / sync loop 发现本机应运行该 Pod。Kubernetes 官方文档说明,Pod 一旦被调度并绑定到 Node,Kubernetes 会尝试在该 Node 上运行该 Pod。(Kubernetes[35])
13.2 kubelet 调用 CRI 创建 Pod sandbox
kubelet → RuntimeService.RunPodSandbox(PodSandboxConfig)
这一阶段 runtime 会创建 sandbox 容器,并准备 Pod 级 namespace。CRI 是 kubelet 与 container runtime 通信的主协议;kubelet 需要 Node 上有可工作的 container runtime 才能启动 Pods 和 containers。(Kubernetes[36])
13.3 runtime 调用 CNI 配置网络
containerd / CRI-O → CNI ADD → 创建 veth pair → 一端放入 Pod netns → 配置 IP、路由、DNS、iptables / nftables
CNI 本身不是 Linux syscall,而是 runtime 调用的网络插件协议;插件内部最终仍然使用 netlink、mount namespace、setns 等 Linux 能力。
13.4 kubelet 创建业务容器
kubelet → RuntimeService.CreateContainer → RuntimeService.StartContainer
runtime 会准备 OCI bundle,其中包括 config.json 和 rootfs。OCI spec 明确规定了容器配置、执行环境和生命周期。(GitHub[37])
13.5 runc create
runc create <container-id> → 读取 config.json → clone / clone3 创建 init 进程 → 设置 namespace → 设置 cgroup → 设置 rootfs mount → 设置 capability / seccomp → 等待 start
这一步最关键的是 clone3() / clone()、setns()、mount()、pivot_root()、写 cgroup 文件。clone(2) 文档明确覆盖了 clone() 和 clone3() 两类接口。(man7.org[38])
13.6 runc start
runc start <container-id> → 释放 init 进程 → execve(argv[0], argv, envp) → 用户业务进程开始运行
OCI runtime lifecycle 明确把 start 与运行用户指定程序作为生命周期中的独立步骤。(GitHub[39])
14. Docker 的路径与 Kubernetes 的路径有什么不同?
Docker 本地执行:
大致路径是:
docker CLI → dockerd → containerd → containerd-shim → runc → Linux kernel
Kubernetes 执行 Pod,大致路径是:
kubelet → CRI → containerd / CRI-O / cri-dockerd → containerd-shim / runtime shim → runc → Linux kernel
所以,Docker 和 Kubernetes 的差异主要在上层编排、API、生命周期管理、网络模型和 runtime 接口;到底层 Linux 容器创建,最终都会收敛到 OCI runtime + Linux kernel primitives。
这也是为什么 Kubernetes 移除 dockershim 并不意味着 Linux 容器机制改变了。Kubernetes 官方解释,CRI 是为了让 Kubernetes 可以和多种 runtime 互操作;Docker Engine 不实现 CRI,所以 Kubernetes 曾经维护 dockershim 作为临时方案,后来在 v1.24 移除。(Kubernetes[40])
15. 容器停止与销毁过程
容器销毁不是简单 kill 一个进程。典型过程如下:
1. kubelet 收到删除 Pod / 停止容器需求2. 执行 preStop hook3. CRI StopContainer4. runtime 向容器 init 进程发送 SIGTERM5. 等待 terminationGracePeriodSeconds6. 超时后发送 SIGKILL7. wait4 / waitid 回收进程8. CRI RemoveContainer9. 删除 cgroup10. 卸载 mount11. CNI DEL 删除网络12. RemovePodSandbox
OCI lifecycle 明确说明,容器进程退出可能由于错误、崩溃或 runtime kill 操作;之后 runtime delete 被调用,容器必须通过撤销 create 阶段执行的步骤被销毁。(GitHub[41])
在 cgroup v2 上,清理时要特别关注残留进程。如果进程没有退出,cgroup 目录通常无法正常删除。cgroup v2 提供 cgroup.kill,向非 root cgroup 的该文件写入 1 会导致该 cgroup 及其后代中的所有进程被 SIGKILL。(Linux内核文档[42])
16. 关键函数与关键结构总结
16.1 用户态关键组件
| | |
|---|
| Kubernetes API Server / Scheduler / Controller | |
| | |
| containerd / CRI-O / cri-dockerd | |
| | |
| | 根据 OCI spec 调 Linux syscall 创建容器 |
| | |
| | 提供 namespace、cgroup、VFS、net、scheduler |
16.2 Linux syscall
| |
|---|
clone3 | |
unshare | |
setns | 加入 Pod sandbox 已有 namespace |
mount | |
pivot_root | |
execve | |
prctl | 设置 no_new_privs、dumpable 等 |
seccomp | |
capset | |
setuid | |
kill | |
wait4 | |
umount2 | |
openat | |
16.3 内核关键结构
| |
|---|
task_struct | |
nsproxy | |
mnt_namespace | |
pid_namespace | |
net | |
uts_namespace | |
ipc_namespace | |
cgroup_namespace | |
css_set | |
cred | |
mm_struct | |
fs_struct | |
files_struct | |
17. 一个最小容器启动的伪代码模型
下面不是 runc 源码原文,而是把主路径抽象成可理解的模型:
int start_container(struct oci_spec *spec) { // Create cgroup directory and configure resource limits. mkdir("/sys/fs/cgroup/kubepods/.../container-id", 0755); write_file("memory.max", spec->memory_limit); write_file("cpu.max", spec->cpu_quota); // Create container init process with namespace isolation. pid_t pid = clone3(CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWCGROUP | maybe(CLONE_NEWNET), child_stack); if (pid == 0) { // Join pod network namespace if this is an app container. if (spec->join_pod_netns) { int fd = open("/proc/<pause-pid>/ns/net", O_RDONLY); setns(fd, CLONE_NEWNET); } // Build container filesystem view. mount("overlay", spec->rootfs, "overlay", 0, spec->overlay_opts); mount("proc", "/proc", "proc", 0, NULL); mount("tmpfs", "/dev", "tmpfs", 0, NULL); pivot_root(spec->rootfs, spec->oldroot); chdir("/"); // Apply process credentials and security profile. setgroups(...); setgid(spec->gid); setuid(spec->uid); capset(spec->capabilities); prctl(PR_SET_NO_NEW_PRIVS, 1); seccomp(SECCOMP_SET_MODE_FILTER, 0, spec->seccomp_prog); // Replace init process image with the user command. execve(spec->argv[0], spec->argv, spec->envp); _exit(127); } // Move process into configured cgroup. write_file("cgroup.procs", pid); return pid;}
这个模型的核心判断是:容器创建不是一个 syscall 完成的,而是多个 syscall 和内核子系统协同完成的状态构造过程。
18. 结论
Kubernetes 和 Docker 在 Linux 上运行容器,本质不是创建虚拟机,而是创建被内核隔离和约束的进程集合。Kubernetes 通过 kubelet 和 CRI 驱动 container runtime;Docker 通过 dockerd 驱动 containerd;二者最终都可以落到 OCI runtime,例如 runc。OCI Runtime Specification 定义容器生命周期,runc 则把生命周期落实为 Linux 系统调用。(GitHub[43])
nsproxy 是进程与 namespace 集合之间的关键连接点。它把 mount、PID、network、UTS、IPC、cgroup、time 等 namespace 聚合到 task_struct 可引用的结构中。内核源码明确显示,nsproxy 包含这些 namespace 指针,并且在 clone 或 unshare namespace 时会复制。(GitHub[44])
cgroup 是资源治理核心。它不负责制造“容器看到的世界”,而是负责控制“容器能消耗多少资源”,并提供统计、限制、迁移、冻结、kill 等能力。cgroup v2 官方文档说明,进程可以通过写 cgroup.procs 迁移到 cgroup,子进程默认出生在父进程所在 cgroup;controller 则控制 CPU、memory、IO 等资源分配。(Linux内核文档[45])
容器内应用执行系统调用时,调用的是同一个宿主机内核。所谓“容器内 syscall”,只是该 syscall 在进入内核后,会受到当前 task 的 namespace、cgroup、capability、seccomp、LSM、mount namespace、network namespace 等上下文影响。Linux 文档明确指出 system call 是应用与 Linux kernel 之间的基本接口,而 seccomp 过滤也是基于 syscall number 工作。(man7.org[46])
最终可以用一句话概括:Kubernetes 负责声明和编排,containerd / Docker 负责运行时管理,runc 负责把 OCI 规范翻译成 Linux syscall,Linux 内核通过 namespace 决定进程“看见什么”,通过 cgroup 决定进程“能用多少”,通过 syscall 路径真正执行容器内应用的所有操作。
[1]Running containers | Docker Docs: undefined
[2]Container Runtime Interface (CRI) | Kubernetes: undefined
[3]Updated: Dockershim Removal FAQ | Kubernetes: undefined
[4]runtime-spec/spec.md at main · opencontainers/runtime-spec · GitHub: undefined
[5]containerd – An industry-standard container runtime with an ...: undefined
[6]Pod Lifecycle | Kubernetes: undefined
[7]Running containers | Docker Docs: undefined
[8]namespaces(7) - Linux manual page: undefined
[9]clone(2) - Linux manual page: undefined
[10]Control Group v2 — The Linux Kernel documentation: undefined
[11]Control Group v2 — The Linux Kernel documentation: undefined
[12]Control Group v2 — The Linux Kernel documentation: undefined
[13]linux/include/linux/nsproxy.h at master · torvalds/linux · GitHub: undefined
[14]clone(2) - Linux manual page: undefined
[15]Container Runtime Interface (CRI) | Kubernetes: undefined
[16]Pod Lifecycle | Kubernetes: undefined
[17]Container Runtime Interface (CRI) | Kubernetes: undefined
[18]runtime-spec/runtime.md at main · opencontainers/runtime-spec · GitHub: undefined
[19]runtime-spec/runtime.md at main · opencontainers/runtime-spec · GitHub: undefined
[20]clone(2) - Linux manual page: undefined
[21]namespaces(7) - Linux manual page: undefined
[22]Processes — The Linux Kernel documentation: undefined
[23]linux/include/linux/nsproxy.h at master · torvalds/linux · GitHub: undefined
[24]Control Group v2 — The Linux Kernel documentation: undefined
[25]Control Group v2 — The Linux Kernel documentation: undefined
[26]Control Group v2 — The Linux Kernel documentation: undefined
[27]Control Group v2 — The Linux Kernel documentation: undefined
[28]Control Group v2 — The Linux Kernel documentation: undefined
[29]clone(2) - Linux manual page: undefined
[30]syscalls(2) - Linux manual page: undefined
[31]syscall(2) - Linux manual page: undefined
[32]seccomp(2) - Linux manual page: undefined
[33]Restrict a Container's Syscalls with seccomp: undefined
[34]Control Group v2 — The Linux Kernel documentation: undefined
[35]Pod Lifecycle | Kubernetes: undefined
[36]Container Runtime Interface (CRI) | Kubernetes: undefined
[37]runtime-spec/spec.md at main · opencontainers/runtime-spec · GitHub: undefined
[38]clone(2) - Linux manual page: undefined
[39]runtime-spec/runtime.md at main · opencontainers/runtime-spec · GitHub: undefined
[40]Updated: Dockershim Removal FAQ | Kubernetes: undefined
[41]runtime-spec/runtime.md at main · opencontainers/runtime-spec · GitHub: undefined
[42]Control Group v2 — The Linux Kernel documentation: undefined
[43]runtime-spec/runtime.md at main · opencontainers/runtime-spec · GitHub: undefined
[44]linux/include/linux/nsproxy.h at master · torvalds/linux · GitHub: undefined
[45]Control Group v2 — The Linux Kernel documentation: undefined
[46]syscalls(2) - Linux manual page: undefined