前言
在前序文章中,我们依次分析了容器的命名空间(Namespaces)、文件系统(Filesystem)、资源控制(Cgroups 与 Intel RDT)以及安全防线(Security)。当所有静态配置和隔离环境准备就绪后,容器的启动进入了实质性的执行阶段。
本文将解析 SPEC.md 中的 Runtime and Init Process(运行时与 Init 进程)章节。该章节明确了容器创建时的进程间同步机制、标准 I/O 处理方式、由运行时注入的关键文件,以及容器进程环境的默认参数设定。
父进程与 Init 进程的同步机制
在 Linux 系统中创建容器时,宿主机上的父进程(Parent process,即运行时环境)需要与容器内的主进程(Init process)进行状态同步。容器进程不能在被创建后立刻执行用户业务代码,因为父进程还需要时间去完成一些依赖 PID 的后续配置。
SPEC.md 中这样描述该同步过程:
During container creation the parent process needs to talk to the container’s init process and have a form of synchronization. This is accomplished by creating a pipe that is passed to the container’s init. When the init process first spawns it will block on its side of the pipe until the parent closes its side.
规范指定了通过**匿名管道(Pipe)**来实现这种同步。具体流程如下:
- 父进程创建一个管道,并将其文件描述符(规范中指明为
FD 3)传递给新拉起的容器 Init 进程。 - Init 进程启动后,会在该管道的读取端阻塞等待。
- 父进程利用这段时间完成诸如将新进程 PID 加入 Cgroup 层级结构,或者为 User Namespace 写入 UID/GID 映射关系等操作。
- 父进程完成上述配置后,关闭其持有的管道写入端。
- Init 进程检测到管道关闭,结束阻塞状态,继续执行后续的初始化操作。
这种设计严格避免了竞态条件(Race condition),确保容器内的进程在正式执行前,已经被正确纳入了所有资源和权限的限制框架内。
Init 进程执行模型
关于容器的 Init 进程,规范提出了两点明确的要求:
The application consuming libcontainer should be compiled statically. libcontainer does not define any init process and the arguments provided are used to
execthe process inside the application. There should be no long running init within the container spec.
- 无常驻 Init 守护进程:与传统 Linux 操作系统不同,容器内部不应该存在一个类似于
systemd或sysvinit的长生命周期的 Init 进程。libcontainer不提供默认的守护进程,而是直接利用提供的参数通过系统调用exec启动目标应用。 - 静态编译:文档建议使用
libcontainer的应用程序采用静态编译,以减少对宿主机动态链接库的依赖,提升可移植性和环境隔离的纯粹性。
伪终端与标准流重定向
对于需要交互的容器任务,终端的配置是必不可少的。
If a pseudo tty is provided to a container it will open and
dup2the console as the container’s STDIN, STDOUT, STDERR as well as mounting the console as/dev/console.
如果在配置中为容器指定了伪终端(Pseudo TTY),运行时会进行以下处理:
- 打开该终端设备。
- 使用
dup2系统调用,将容器的标准输入(STDIN)、标准输出(STDOUT)和标准错误输出(STDERR)全部重定向到该控制台。 - 将该控制台设备挂载至容器文件系统的
/dev/console路径。
运行时注入的文件
容器的 rootfs(根文件系统)通常来自静态的镜像。然而,某些涉及网络解析和主机标识的文件具有很强的环境相关性。如果直接使用镜像中自带的文件,可能会在进程执行时产生不符合预期的副作用。
为了解决这一问题,规范指出运行时(Runtime)会介入这些特定文件的创建和填充:
An extra set of mounts are provided to a container and setup for use. A container’s rootfs can contain some non portable files inside that can cause side effects during execution of a process. These files are usually created and populated with the container specific information via the runtime.
SPEC.md 完整列出了以下 4 个由运行时注入并管理的额外文件(Extra runtime files):
/etc/hosts/etc/resolv.conf/etc/hostname/etc/localtime
在实际实现中,运行时通常会在宿主机上生成这些文件,然后通过 Bind Mount 的方式将它们覆盖挂载到容器根文件系统的对应位置。
默认参数配置 (Defaults)
SPEC.md 规定了一组默认的环境参数。在用户没有显式覆盖这些配置项的情况下,容器内的进程将适用以下默认值。此处完整列出文档中的所有默认项,不作任何省略:
| 参数类型 (Type) | 默认值 (Value) |
|---|---|
| Parent Death Signal | SIGKILL |
| UID | 0 |
| GID | 0 |
| GROUPS | 0, NULL |
| CWD | "/" |
| $HOME | Current user’s home dir or "/" |
| Readonly rootfs | false |
| Pseudo TTY | false |
容器标准操作 API (Public API)
规范在 Actions 章节中明确指出:
After a container is created there is a standard set of actions that can be done to the container. These actions are part of the public API for a container.
一旦容器被创建,便有一组标准的操作可以应用于该容器。为了保证规范的完整性,以下原封不动地列出 SPEC.md 中定义的所有 5 个核心操作:
| 操作名称 (Action) | 描述 (Description) |
|---|---|
| Get processes | Return all the pids for processes running inside a container (返回所有运行在容器内的进程的 PID) |
| Get Stats | Return resource statistics for the container as a whole (返回容器作为一个整体的资源使用统计数据) |
| Wait | Waits on the container’s init process ( pid 1 ) (等待容器的 init 进程,即 PID 1 执行结束) |
| Wait Process | Wait on any of the container’s processes returning the exit status (等待容器内任意进程执行结束,并返回其退出状态码) |
| Destroy | Kill the container’s init process and remove any filesystem state (杀死容器的 init 进程,并移除任何残留的文件系统状态) |
这些操作看似简单,但它们屏蔽了极其复杂的底层系统调用。下面我们逐一分析这 5 个 API 在 libcontainer 底层的实现原理。
1. Get processes(获取进程列表)
容器本质上是一组被隔离的进程,但在宿主机的视角下,它们依然是普通的 Linux 进程。如何精确地定位哪些进程属于某个特定的容器?
底层机制:libcontainer 并不会去遍历整个 /proc 目录。由于每个容器都会被放入一个独立的 Cgroup 中,获取容器进程列表的最严谨、开销最小的方法,是直接读取该容器对应的 Cgroup 控制器(通常是 pids 或 cpu 控制器)目录下的 cgroup.procs 文件。该文件由 Linux 内核维护,天然包含了归属于该 Cgroup 的所有进程 PID(宿主机视角)。
2. Get Stats(获取资源统计)
上层命令如 docker stats 或 kubectl top 能够实时显示容器的 CPU、内存及网络使用情况,其数据来源正是这个 Get Stats 操作。
底层机制:与 Get processes 类似,这极度依赖于 Cgroups 暴露的文件系统接口。libcontainer 会访问 /sys/fs/cgroup/ 下对应容器的子目录:
- 读取
memory.stat和memory.usage_in_bytes获取内存和 Swap 使用量及 Page Cache 统计。 - 读取
cpuacct.usage和cpuacct.stat获取 CPU 时间片消耗(区分 User 态和 System 态)。 - 读取
blkio.throttle.io_service_bytes获取磁盘 I/O 统计。
该接口通过解析这些由内核实时更新的文件,聚合成结构化的数据对象返回给调用方。
3. Wait 与 Wait Process(进程等待与状态捕获)
容器的生命周期通常与其内部的主进程(Init 进程)绑定。当需要同步等待容器退出时,提供了两个层级的 Wait 方法。
底层机制:
- 对于 Wait (等待 Init 进程):父进程利用 Linux 的
waitpid()系统调用来挂起自身,监听目标 PID(此时通常传入容器的 Init 进程在宿主机上的映射 PID)的状态变化。当收到SIGCHLD信号时,表明 Init 进程退出,系统即可回收资源。 - 对于 Wait Process (等待任意进程):这通常用于通过
runc exec(即 Docker 的docker exec)进入容器执行的附加进程。父进程同样通过waitpid()或更底层的文件描述符事件驱动(如epoll)来等待特定子进程结束,并准确捕获其退出状态码(Exit Code),以便反馈给终端用户。
4. Destroy(销毁容器)
当容器结束任务或被强制停止时,Destroy 操作负责彻底回收所有系统资源,防止出现僵尸进程(Zombie Process)或挂载点泄漏。
底层机制:这是一个具有严格执行顺序的复合操作:
- 清理进程:向容器的 Init 进程发送
SIGKILL信号。基于 Linux PID Namespace 的重要特性——当 PID Namespace 中的 PID 1 进程终止时,内核会自动向该 Namespace 中的所有其他进程发送 SIGKILL 信号。这确保了容器内的所有衍生进程被瞬间且彻底地清理。 - 卸载文件系统:进程清理完毕后,
libcontainer会执行umount操作,卸载之前为容器准备的 rootfs、/dev、/proc、/sys等虚拟文件系统挂载点。 - 销毁控制组:最后,删除
/sys/fs/cgroup/下该容器专用的各个子目录。当目录中的进程被全部清空后,这些 Cgroup 目录允许被rmdir删除。
结语
Actions 章节作为 SPEC.md 的收尾部分,定义了上层容器引擎与底层运行时环境交互的最小必要操作集。通过这 5 个朴素的 API 接口,我们将底层的 Cgroups 状态、系统调用(waitpid、kill)以及 PID Namespace 的清理特性封装成了对用户友好的生命周期管理逻辑。
至此,我们完成了对 runc 核心规范库 libcontainer/SPEC.md 的全系列逐节解读。从隔离机制到生命周期控制,这份文档是理解现代 Linux 容器技术底座的绝佳教材。