容器 17:OCI runtime 的 create 与 start 分别做什么
生命周期为何拆成两段
OCI Runtime Specification v1.3.0 定义了 create、start、state、kill、delete 的状态与条件。create 必须按照 config.json 构造执行环境,但此时不执行用户指定程序;state 的 created 带有 Linux PID,却不等于应用开始响应。start 才使用户程序开始执行;程序退出后进入 stopped,满足删除前提后 delete 撤销运行时创建的资源。runtime 对象 ID、容器进程 PID、bundle 的绝对路径分别是三种字段;别用 ID 当 PID。即使 running,也仍需实际请求 /ready 才能讨论服务可用性。
bundle 与镜像不是两种叫法
OCI image-spec 规定的是可存储、可分发的 index、manifest、config 与 layers;runtime-spec 的 bundle 则以目录中的 config.json 和已准备好的 rootfs 作为运行入口。image config 里的默认命令、用户和环境可能为生成 bundle 的过程提供输入,但它不等于运行时最终接收的 config.json,也不能把压缩 tar 的路径写在 bundle 的 root 字段里就让 runc 自动展开镜像。中间还需要内容校验、平台选择、层解包、挂载和运行时配置转换等步骤;这部分通常由上层管理组件组织,而不是一个 runc create 自己联网拉镜像完成。10 的示例层甚至只有 Python 脚本、没有解释器,不具备运行所需 rootfs,因此不能只复制那份离线 layout 就构造可工作的 bundle。
同一个 bundle 目录还可能被误认为容器身份。规范的 state 对象同时给出运行时作用域内唯一 ID、当前状态、bundle 的绝对路径,以及 Linux 上 created/running 状态要求的 PID。ID 只在该宿主运行时管理范围内唯一,不是跨宿主的全局 Pod UID;bundle 路径描述配置与根文件系统的位置,不是容器内 / 一定等于宿主该字符串。运行者必须在相近时间点采集 runtime state、实际进程宿主 PID、对应 namespace/cgroup 与 rootfs 挂载,否则容器退出后宿主 PID 重用会让旧记录指向另一个进程。
规范里的 created 究竟完成了什么
runtime-spec v1.3.0 对 create 的要求是应用 config.json 中除 process 的具体运行参数外的配置,process.args 直到 start 前不得应用;其余 process 属性在 create 阶段有可选行为。配置无法应用时必须报错并不能留下一个声称成功的新容器;调用 create 之后再编辑 config.json 不应改变已经创建的那个对象。created 状态意味着执行环境准备好,而用户指定程序尚未执行,Linux 的 state PID 仍已存在。因此在 create 后通过宿主 /proc/<pid> 看到了进程,与业务 /ready 无响应可以同时成立。不能简单地把 created 翻译成“镜像没下载完”,镜像输入是否完整应在更上游取证。
start 的前提是存在处于 created 的对象;调用 start 时才运行 process 配置指定的用户程序。若容器已经 running 或 stopped,再调用 start 按规范要报错且不得改变已有状态,不能在教学脚本里忽略错误继续复用同一个 ID。用户程序运行并不保证服务已经在 socket 上监听、业务准备就绪或持久状态完整,state=running 与 HTTP /ready=200 仍然是两项观察。kill 是向 created/running 对象进程投递指定信号;信号处理后的实际退出路径要结合 03 的 PID 1 行为和应用处理逻辑,不能从 kill 调用返回码直接推出工作已完成。
delete 的规范前提是 stopped,它应撤销 create 阶段创建的资源;与该容器相关但非由这个容器创建的资源不得随意删除。这解释了为什么删除 runtime 对象并不自动等于删除镜像内容、外部 volume 或用户提前创建的 bind 目录。若 start 失败还留有 runtime 状态,要先查状态与子进程,清理只属于本次自建 bundle/root 的对象,不能拿 rm -rf 清理整台 VM 的容器内容存储。规范还规定错误操作除日志等附带变化外应保持环境效果如未尝试,实践验收时应真的查本次 ID、PID、挂载与 cgroup,而不是只凭 CLI 非零退出就认为已经自动回滚。
用固定的 runc 版本阅读实现,但不伪造运行
为了对照这些规范步骤,这里选择 runc 源码 tag v1.3.0,只针对源码结构作 DOC_VERIFIED,并不宣称本机安装了 runc。该版本 CLI 的 create.go 将 create 调用交给 startContainer(..., CT_ACT_CREATE, ...);start.go 先获取对象并读取状态,只有 libcontainer.Created 分支调用 container.Exec(),对 Stopped 与 Running 返回不同错误。libcontainer/container_linux.go 的 Run() 路径会调用启动过程并在 init 情形调用 c.exec(),而 Exec() 经过 FIFO 协调 user process 的开始。这些是所选 tag 源码对规范 create/start 区分的一种实现路径,不等于我们已经在这台机器看到实际 FIFO、shim 或某个宿主 PID。cli 输出格式、rootfs 挂载和 hook 行为还需对运行在专用 VM 中的确切 runc 二进制重新检查。
对照源码要避免把名称 startContainer 误读成“一律启动业务程序”:create.go 传入的是 CT_ACT_CREATE,run.go 则传入 CT_ACT_RUN,路径里会选择不同后续动作。真正在 runc start 触发的 container.Exec() 检查既有状态,且源码按不同状态直接拒绝执行;这与规范给出的合法状态前提相符。若未来升级 runc,不能继续给新版本安上旧文件和行号;把 tag、文件的 Git blob 哈希和访问日期写入版本记录,在 VM 取 runc --version 后再重新追相应实现。OCI runtime-spec 版本、源码 tag 以及实际安装的二进制版本是三种不同字段,不能仅凭其中一个推断另两个完全相同。
在专用 VM 设计状态对照
准备一份可信 probe-app 镜像,先固定其 digest,按所选工具安全解包完整 rootfs;在自己拥有的 bundle 目录生成并人工核查 config.json,尤其核对 root.path、process 的 args/env/uid/gid、mounts、namespaces 与 Linux cgroup 配置。runc spec 只是生成参考配置的工具调用,不为任意系统自动提供与探针匹配的目录结构;若权限不足导致挂载和 namespace 配置不能应用,记录失败和前提,不以禁用宿主防护作为教学捷径。使用独立 --root 管理本次运行时状态,在不同步骤都记录绝对 bundle 路径、容器 ID 与 state 原始 JSON,采样该 PID 的宿主 /proc 和与它关联的 cgroup。
在 create 之后、start 之前请求探针,应预期业务服务尚未启动;但网络请求失败也可能因为端口尚未配置,须与 state=created、进程已存在且 process.args 未执行的证据一起解释。执行 start 再请求 /health 和 /ready,保存 HTTP 状态与调用时刻;探针设置了就绪延迟时,running 仍可能对应 503。待进程退出后查看 stopped、删除本次对象并验证同一个 ID 的 state 查询已不存在、实验挂载和自建运行时根目录无残留。整个链路要写每条命令、参数、退出码、原始输出、预期与实际差异,不能只保留最后一行 runc state。
失败分支复制本次 bundle 后只改变其中一个可审计条件,例如把 root.path 改为一个不存在的自建目录,让 create 返回确切错误;随后查本次 ID 是否意外存在、宿主是否残留进程、mountinfo 中是否有自建挂载、cgroup 子组是否仍存在。若另一环境对配置进行预检,失败可能发生在不同一条 CLI 子命令,记录实际版本与失败点比声称“所有 runc 必定在 create 的第 N 行报 ENOENT”更准确。失败时删除副本不等于清理成功:要核对自建资源是否实际未创建或已回收。现有云端无 runc/可用 mount/OCI bundle,这条正负路径都保持 NOT_RUN,不能把规范与源码阅读升格为实验通过。
正例的实验要在独立 VM 将 13 的可信 probe-app 通过选定工具解包到 bundle,使用 runc spec 按固定版本生成配置,核查 process、root、mounts、namespaces 和 user 的精确内容;分步执行 create → state → start → state → delete,记录外侧 PID 和每步退出码。负例把仅属于实验 bundle 的配置改成非法 rootfs 或权限参数,记录 create 错误并确认没有遗留进程与挂载。运行入口和自含探针源码给出前提;当前没有 runc、OCI bundle 或所需挂载权限,运行实验仍 NOT_RUN。固定 tag 的源码已静态核对,可重复查 CLI 分支,但不能从 container.Exec 代码推出本机允许挂载、namespace 创建或子进程处理信号;本机 runc 二进制行为不能声称已经过验收。
生命周期中的检查点不只有两个
规范状态 creating、created、running、stopped 对应的不是 CLI 输出的自然语言同义词。进入 created 之前允许实现做预检;create 成功后在 Linux 上应有容器进程 PID 且业务尚未执行;start 使用户程序执行,随后 runtime 会按规范调用 poststart hooks;程序可能立即退出,使监控方第一次查询 state 时已看见 stopped。因而“start 返回后 state 没看见 running”不能自动判定 runtime 没执行用户程序,还须结合进程退出状态、日志及时间窗口。业务服务瞬间退出、钩子失败导致停止、HTTP 就绪一直未到达都是不同路径,要分别保留错误来源。
OCI 运行时也规定错误操作的环境效果。若请求删除一个尚在 created 的容器,规范要求报错且不得产生状态变化;不能把 CLI 报错当成“容器已删除”。若程序退出、进入 stopped,delete 要撤销 create 所有的资源,但外部创建的 bind 目录与共享层 blob 不在“由它创建”的范围。poststop 钩子即使失败,规范要求记警告并继续 lifecycle,而不是因此保留所有由 runtime 创建的内核对象。实验中选用的 config 如包含 hooks,还必须明确哪些 hook 实际存在、输出与退出码;无 hooks 的最小示例不应凭空写“钩子执行成功”。
在专用 VM 设计重复调用的失败场景:对刚 create 的自建容器先调两次 start,正常的一次之后另一调用应依当前 state 报错;观察应用是否只启动了一份,而不只是记录最后一次 stderr。再在 stopped 状态请求 start,查状态与宿主 PID,确保没有绕过新的 create 使用旧 ID。如果只保留一次 HTTP 200 而没有对应 state 时间点,重复 start 是否创建了第二个进程仍无法排除。此类测试必须使用可信短命程序和唯一实验 ID,避免向其他任务同名对象发送信号。
源码中的 FIFO 应如何解读
runc v1.3.0 的 libcontainer/container_linux.go 中,init 进程的 start 路径会准备 exec FIFO,随后 Exec() 路径借该 FIFO 与进程协调用户程序的执行;Run() 在启动进程后可以紧接着走 exec,而 CLI create 和 start 通过不同动作分开。这给“为什么 created 状态有 PID 但 process.args 未执行”提供一条该版本的实现线索。FIFO 是 runtime 内部同步的一环,不等于 app 的标准输入、业务日志通道或 containerd shim。需要在 VM 中追踪 FIFO 文件、进程状态、FD 与关闭时刻,才能确定实际部署的那版 runtime 是否沿同样路径执行。
源码还在 Exec() 路径中检查进程是否已死亡,并区分 FIFO 打开后 user process 是否可能瞬间结束的情形;因此一次 state 采样没看见 running,不是充分的反证。拿 runc run 与 runc create/start 比较时,固定同一个可信 bundle 与相同 --root 参数,并注意 run 一般是合并调用入口,不应只根据 CLI 数量得出底层内核“少创建了一个 namespace”。这篇只确认所选源码 tag 的分支和规范语义,未对本机 exec FIFO 的长度、关闭时序或运行性能做任何数字结论。
配置冻结的时间边界
runtime-spec 明确 create 之后更改 config.json 不影响这个已创建容器。这不是说磁盘上的配置文件永远不可编辑,而是说此次容器执行应按 create 时被接受的配置决定。测试时把 config 的输入哈希连同状态记录保存,create 后在私有实验副本修改默认 command,再 start,观察是否仍运行原命令;若检查到另一份 bundle 的输出,不得拿那个差异宣称某 runtime 违反规范。为避免危险或混乱,实验程序只在本目录的可信探针中选择两个不同的、有界响应,不修改共享 rootfs 或其他容器的配置。
即使不做这个附加测试,排障时也应问“现在磁盘上看到的 config 是否就是当时 create 的输入”。运行时生成配置过程中可能合并镜像默认 USER、命令、工作目录、capabilities、挂载和环境变量,某字段被 Docker/CRI 覆盖后,直接回头看 OCI image config 会看错执行参数。config.json 的哈希、runtime 对象 state、宿主 PID 和一次实际 HTTP 请求是四类不同时间的证据;只有相互对齐才能确认应用这次运行的根文件和身份。若没有容器运行时,唯有镜像配置的静态描述而没有 bundle 执行结果,应保留空白状态列,不能复制上次本机普通探针日志填进去。
bundle 安全边界仍需运行时版本
把不可信 rootfs 直接交给教学 runc 示例并不安全。测试所用 rootfs 由本系列自编探针构建,按实际 digest 验证且只在专用 VM 中挂载;配置中声明禁用或允许的权限要按该版 runc、kernel 和目标 namespace 检查,不应该为了看到 running 把所有限制关掉。--root 指 runtime 存放本次对象状态的路径,不能当作进程的新 /;bundle 的 root.path 所指 rootfs 又不是镜像内容存储的全局目录。三者用途不同,清理时应先删除本次 runc 对象,再核验属于本次的 bundle 与运行时状态目录;不直接删除共享基础镜像 blob 或其他实验的 --root。
若 config 中设了只读根、bind mount、capabilities 或 seccomp,需要同时验证应用运行和预期拒绝;单靠 runc state 中的 running 不能证明 rootfs 实际只读。想把 08 的本地 seccomp 小程序嵌入 bundle,也要记得它只是可信演示、不是审计整套运行时默认策略。用户在自建 VM 无法获得所需挂载和 cgroup 委派时,不应强行绕开共享宿主策略,应保存内核、能力、runc tag/二进制版本、完整错误输入和 NOT_RUN 补跑命令。规范与固定源码能解释前提,但只有隔离环境下的状态、PID 与对象清理记录才能做容器级验收。
出错时先定位 create 还是 start
如果 create 的输入 rootfs 路径不合法或挂载无法应用,规范要求拒绝创建这个新容器。应先保存被拒配置的绝对路径和 sha256,再记录 runc create 的退出码及 stderr,紧接着 runc state 查询这个实验 ID,最后在外侧核对是否有以该 ID 标识的进程、挂载和 cgroup 残留。如果 state 不存在却仍有自己创建的挂载,说明清理尚不能称为通过;如果 state 还存在则要按实际状态检查运行时如何恢复,而不是再次执行 create 冒险覆盖现场。二次重跑要换用干净的私有副本,首先验证上一轮资源已由本次实验正确清理。
另一种错误发生于 start:bundle 已建立、state 显示 created,却因为配置的程序路径或执行权限不满足导致用户程序未能执行。对照时保持 rootfs 与 mount 相同,只在自己的 bundle 里选择一个刻意不存在的入口路径,并记录 start 的准确 stderr、state 转移和相关进程是否退出。create 成功与 start 失败可以同时成立,不能回填“create 没运行过”。反过来,程序执行后立刻以非零状态退出也不等于 runc start 没触发它:区别依赖进程自己的日志、宿主 PID 和退出事件时间线。必须先实测选定 runc 和内核,再解释返回码;这里没有任何一个合成的 VM 错误码作为真实输出。
一个可迁移的诊断顺序是“配置文件/根目录是否准备好 → 对象是否 created → 用户进程是否已执行 → 服务是否就绪 → stopped 后资源是否撤销”。每一段都对应不同的证明方式:静态核对 config 与 blob,state/PID 记录,进程实际标准输出或 exec 错误,HTTP 请求,最后 mountinfo 与本次运行时 root 目录的残留核查。若出现 created PID,流程已跨过第一段,但不能跨过第三段;若出现 HTTP 200,则至少有服务能处理请求,还须核对其确实属于刚刚这个容器 ID 与镜像 digest。排障时按证据最早断裂的位置回溯,避免把应用未就绪归咎于镜像拉取。
分工边界也决定源码应查哪里
规范告诉运行时必须接受的状态、操作和错误约束,不规定某个 daemon 的 API,也不指定 runc 使用 FIFO 是唯一合法实现。因此这里只把 create.go、start.go 和 libcontainer/container_linux.go 作为 runc v1.3.0 的静态实现选择,再将其与 runtime-spec v1.3.0 的 create/start 前提逐条对照;containerd 是否额外拉镜像、shim 怎样管理 task、Docker daemon 是否调用哪条 API,属于 18–19 的另一个版本化实现问题。把 runc run 退出 0 当作“containerd、CRI、Kubernetes 节点链路全部成功”,是把后续每个组件的对象与失败条件压扁了。
同理,runc 的 CLI 源码 create.go 调用 startContainer 这个函数名,不足以判断它在 create 分支实际执行了用户 process args;必须连同 CT_ACT_CREATE 的分支含义和 runc start 对 Created 状态的后续调用一起读。当前获取到的源码 blob 是特定 tag 的 Git 对象,能供读者复核该版本选择的路径,却无法证明未来某个镜像里打包的是同一个提交的二进制。VM 的 /usr/bin/runc --version、构建来源及其 sha256sum 必须另行记录,不能用网页上 v1.3.0 三个字代填本机版本。
最后还要对照本系列的教学启动器:09 只在自己进程里 unshare user/UTS/IPC、fork/exec 固定探针,未创建具有规范状态的 OCI runtime 对象;它没有给出 config.json、bundle 路径,也不会对 start 在错误状态下返回规范要求的错误。两篇在进程、namespace 和 exec 的基础现象上可以相互验证,但不能称它已实现 OCI runtime。现实运行时还要在内核支持与用户权限下处理 rootfs、挂载传播、cgroup、FD、信号、权限下降及外部状态清理;本文不为没有运行环境的组件凭空标记合规。
这一差别还能帮助验收一个出错的启动链:如果离线 OCI layout 的层通过 digest 校验,证明的是分发字节;如果 bundle config.json 通过语法检查,证明的是结构;如果 runc 在专用 VM 报 created,证明的是运行环境和状态;如果探针真正回答请求,才证明了选定入口在指定条件下能服务。每项要用同一份制品 digest、bundle 输入哈希、容器 ID 和采样时刻串起来。中间任何一步缺证据都保留未知值,既不说“运行失败一定是 runc”,也不说“构建和文档正确就算验收”。
清理也是验收的一环:停止程序、查询状态、删除对象、核对挂载与 cgroup、再以新 ID 复跑,必须有顺序且限定在本次 VM 自建资源。若还没有真正执行过 create,就没有由这个命令创建的容器可删;把清理步骤写在 RUN 中只表示已设计补跑入口,不表示云端已经出现并回收过那个对象。
| 现象 | 核查字段 | 不可直接推出 |
|---|---|---|
created 有 PID |
runtime state、宿主 /proc |
业务程序已执行 |
| start 返回 0 | 应用 PID、HTTP 就绪 | /ready 必为 200 |
| delete 失败 | runtime 状态、残留资源 | 所有旧卷已被删除 |
练习
- 先预测在
create后、start前请求服务是否能得到业务响应,专用 VM 中记录 state、PID 和 HTTP 结果并与规范的状态阶段对照。 - 先预测一个 rootfs 路径不存在的 bundle 在哪一阶段失败,保存 runc 版本、配置、退出码、残留进程/挂载及清理结果。
上一篇:16:数据位置;下一篇:18:containerd 对象。
系列总目录:从进程隔离到运行时与编排。






