runc/libcontainer · 初始化进程握手

先保存容器状态,再允许初始化进程继续

runc 用 procReady/procRun 握手建立父子进程的执行边界:父进程完成限制设置并保存容器状态后,才让子进程继续初始化。

v1.2.3并发与一致性核验 2026-10-12

工程问题#

容器子进程和管理它的父进程在不同执行环境中完成设置。如果父进程过早放行,然后在 state.json 写出之前退出,子进程可能仍然存活,而后续管理命令缺少定位和清理它所需的状态。单靠进程创建成功不能表示容器初始化已经完成。

实际实现#

标准初始化子进程发送 procReady 后阻塞等待 procRun。父进程收到 ready,先设置 rlimit、建立 created 状态,再调用 updateState 保存初始化进程信息;saveState 写临时文件,关闭后 rename 为正式状态文件。只有这些步骤成功,父进程才发送 procRun。接收端检查预期消息类型,父进程也要求整个握手中确实见过 procReady,防止把提前关闭连接误判为初始化成功。

处理流程#

  1. 子进程完成前期设置,在安装后续 seccomp 规则前发送 procReady
  2. 子进程等待 procRun,父进程设置限制并记录 created 状态
  3. 父进程把容器状态写入临时文件,再 rename 为 state.json
  4. 保存成功后父进程发送 procRun,子进程继续剩余初始化
  5. 子进程最终还要经过 exec FIFO 的启动等待,才执行目标程序

源码片段#

固定版本 · 原文摘录
libcontainer/standard_init_linux.gov1.2.3 · L169–L178
查看原文件
169	// Tell our parent that we're ready to exec. This must be done before the170	// Seccomp rules have been applied, because we need to be able to read and171	// write to a socket.172	if err := syncParentReady(l.pipe); err != nil {173		return fmt.Errorf("sync ready: %w", err)174	}175	if err := selinux.SetExecLabel(l.config.ProcessLabel); err != nil {176		return fmt.Errorf("can't set process label: %w", err)177	}178	defer selinux.SetExecLabel("") //nolint: errcheck
Init · 在后续安全设置前同步

此时仍需要读写同步 socket,因此先完成 ready 握手;返回后还会继续安全和身份相关设置。

libcontainer/init_linux.gov1.2.3 · L414–L424
查看原文件
414// syncParentReady sends to the given pipe a JSON payload which indicates that415// the init is ready to Exec the child process. It then waits for the parent to416// indicate that it is cleared to Exec.417func syncParentReady(pipe *syncSocket) error {418	// Tell parent.419	if err := writeSync(pipe, procReady); err != nil {420		return err421	}422	// Wait for parent to give the all-clear.423	return readSync(pipe, procRun)424}
syncParentReady · 请求并等待放行

发送 procReady 不会直接继续执行;必须成功读到预期的 procRun 回复。

libcontainer/process_linux.gov1.2.3 · L710–L722
查看原文件
710		case procReady:711			seenProcReady = true712			// Set rlimits, this has to be done here because we lose permissions713			// to raise the limits once we enter a user-namespace714			if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {715				return fmt.Errorf("error setting rlimits for ready process: %w", err)716			}717718			// generate a timestamp indicating when the container was started719			p.container.created = time.Now().UTC()720			p.container.state = &createdState{721				c: p.container,722			}
initProcess.start · 父进程处理 ready

父进程先设置 rlimit,再建立 created 状态。此处的 ready 是阶段边界,并不表示最终工作负载已启动。

libcontainer/process_linux.gov1.2.3 · L724–L742
查看原文件
724			// NOTE: If the procRun state has been synced and the725			// runc-create process has been killed for some reason,726			// the runc-init[2:stage] process will be leaky. And727			// the runc command also fails to parse root directory728			// because the container doesn't have state.json.729			//730			// In order to cleanup the runc-init[2:stage] by731			// runc-delete/stop, we should store the status before732			// procRun sync.733			state, uerr := p.container.updateState(p)734			if uerr != nil {735				return fmt.Errorf("unable to store init state: %w", uerr)736			}737			p.container.initProcessStartTime = state.InitProcessStartTime738739			// Sync with child.740			if err := writeSync(p.comm.syncSockParent, procRun); err != nil {741				return err742			}
initProcess.start · 保存状态后回复

注释直接说明过早发送 procRun 可能产生的清理问题。updateState 返回成功后才发送放行消息,写状态失败时不会走到放行步骤。

libcontainer/container_linux.gov1.2.3 · L825–L835
查看原文件
825	err = utils.WriteJSON(tmpFile, s)826	if err != nil {827		return err828	}829	err = tmpFile.Close()830	if err != nil {831		return err832	}833834	stateFilePath := filepath.Join(c.stateDir, stateFilename)835	return os.Rename(tmpFile.Name(), stateFilePath)
saveState · 写临时文件后 rename

JSON 写入临时文件并成功关闭后,再替换正式状态文件。这里没有 fsync,不能从 rename 推导出掉电持久性保证。

libcontainer/process_linux.gov1.2.3 · L780–L789
查看原文件
780	if err := p.comm.syncSockParent.Shutdown(unix.SHUT_WR); err != nil && ierr == nil {781		return err782	}783	if !seenProcReady && ierr == nil {784		ierr = errors.New("procReady not received")785	}786	if ierr != nil {787		return fmt.Errorf("error during container init: %w", ierr)788	}789	return nil
initProcess.start · 缺少 ready 就报错

parseSync 允许 EOF 结束接收,但调用者额外检查 seenProcReady,防止把子进程提前断开当作成功。

六段原文展示子进程调用、等待协议、父进程先保存再放行、文件替换以及缺少就绪消息的错误处理;完整状态保存与最终 exec FIFO 路径附在参考来源。

收益与取舍#

工程分析

带来的收益

  • 把必须成立的顺序写进协议:子进程获准继续之前,管理状态已经写出
  • 同目录临时文件加 rename 避免读者看到逐步写入的半个 JSON 文件
  • 显式校验消息类型与必经阶段,提前 EOF 或错误消息不会被当作正常就绪

付出的代价

  • 握手增加进程间通信和状态文件 I/O,父子进程必须遵守同一协议;初始化总成本主要来自系统调用、挂载、限制与钩子,不能用常数条消息代表总耗时
  • saveState 的这段实现没有 fsync,因此 rename 提供文件替换的原子可见性,不应据此宣称任意掉电后都保证状态持久存在
  • 该顺序减少管理进程退出造成的清理缺口,但不是跨进程事务或自动回滚保证;各阶段失败仍需独立的终止与资源清理路径

可比较的选择

先放行子进程,再异步写状态启动等待可能更短,但父进程提前退出时可能留下缺乏完整管理状态的初始化进程。

轮询共享状态文件判断就绪减少显式消息类型,却引入轮询延迟和文件状态解释;仍必须明确谁写入、何时完整以及失败如何表达。

版本与适用范围#

固定 runc v1.2.3 的新容器标准初始化路径;不把 exec 加入已有容器的 setns 路径混为一谈。procRun 允许子进程继续初始化,不等于用户工作负载已经运行:后面仍有安全设置、关闭同步通道以及等待 exec FIFO。协议是明确的阶段握手,不是形式化验证过的全局状态机。

参考来源#

源码、官方文档与原始论文
01
runc · Init · 在后续安全设置前同步

libcontainer/standard_init_linux.go · Init · 在后续安全设置前同步

源码v1.2.3
02
runc · syncParentReady · 请求并等待放行

libcontainer/init_linux.go · syncParentReady · 请求并等待放行

源码v1.2.3
03
runc · initProcess.start · 父进程处理 ready

libcontainer/process_linux.go · initProcess.start · 父进程处理 ready

源码v1.2.3
04
runc · initProcess.start · 保存状态后回复

libcontainer/process_linux.go · initProcess.start · 保存状态后回复

源码v1.2.3
05
runc · saveState · 写临时文件后 rename

libcontainer/container_linux.go · saveState · 写临时文件后 rename

源码v1.2.3
06
runc · initProcess.start · 缺少 ready 就报错

libcontainer/process_linux.go · initProcess.start · 缺少 ready 就报错

源码v1.2.3
07
runc · updateState / saveState · 完整状态保存

libcontainer/container_linux.go · updateState / saveState · 完整状态保存

源码v1.2.3
08
runc · readSync · 消息类型与载荷校验

libcontainer/sync.go · readSync · 消息类型与载荷校验

源码v1.2.3
09
runc · Init · exec FIFO 与执行目标程序

libcontainer/standard_init_linux.go · Init · exec FIFO 与执行目标程序

源码v1.2.3

相关实现与比较#

4 个相关案例
12 个匹配案例