惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
S
Schneier on Security
P
Privacy & Cybersecurity Law Blog
Cisco Talos Blog
Cisco Talos Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
NISL@THU
NISL@THU
L
LINUX DO - 最新话题
PCI Perspectives
PCI Perspectives
Cyberwarzone
Cyberwarzone
K
Kaspersky official blog
V
Vulnerabilities – Threatpost
G
GRAHAM CLULEY
Help Net Security
Help Net Security
Scott Helme
Scott Helme
V2EX - 技术
V2EX - 技术
Security Latest
Security Latest
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Hacker News: Ask HN
Hacker News: Ask HN
C
Cybersecurity and Infrastructure Security Agency CISA
O
OpenAI News
L
LINUX DO - 热门话题
T
Tor Project blog
M
MIT News - Artificial intelligence
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
Security Archives - TechRepublic
Security Archives - TechRepublic
量子位
I
InfoQ
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Application and Cybersecurity Blog
Application and Cybersecurity Blog
雷峰网
雷峰网
Cloudbric
Cloudbric
Microsoft Azure Blog
Microsoft Azure Blog
D
Docker
T
Threat Research - Cisco Blogs
Blog — PlanetScale
Blog — PlanetScale
H
Heimdal Security Blog
T
Tenable Blog
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
美团技术团队
S
Secure Thoughts
Know Your Adversary
Know Your Adversary
H
Hackread – Cybersecurity News, Data Breaches, AI and More

空鸣深语

CVE-2026-23086浅析 《Arcaea》双星纪念 KDE Plasma自动设置时区 CVE-2024-21626浅析 CVE-2022-0847浅析 CVE-2019-5736浅析 寓言:糊房子 有了工位之后:重新运转的日常 「瞬间」交织「记忆」:横滨站观后与圣地巡礼 我的音游心路历程 修正TUN下NetworkManager的网络状态 从「分歧路口」到「前途漫漫」:上海站观后 《少女乐队的呐喊》短评 恢复小爱课程表的「教务导入」 北航软件工程小记:用Rust开发「风行旅途」 写作即取舍:我的文字与版面 读书:《进化心理学:心理的新科学》 root仍在路上,现实却愈发收紧 浅谈选择困难症——以点外卖为例 必应索引失而复得小记 告别常驻进程:如何让微信使用FCM推送 《你想活出怎样的人生》影评——在留白中遨游 窥探镜中的自己:阿斯伯格综合征的诊断轨迹 首次踏入live house:躁动氛围与孤独心声 远程设置引发的Firefox Android搜索引擎缺失问题排查与恢复
OCI运行时:容器创建流程
深鸣 · 2026-04-20 · via 空鸣深语

最近,我一直在钻研OCI运行时规范,以及该规范采用Go语言编写的参考实现——runc。早在Kubernetes问世之初,我便开始接触容器技术。然而,在开放容器计划(Open Container Initiative,OCI)持续推进的岁月里,Linux容器于底层运行时架构上的诸多标准化演进,却大多处于我的视线之外。

这些操作均属抽象范畴,规范本身并未针对CLI运行时(如runc)强制规定具体的命令行API。尽管业界正积极探索如何界定命令行界面的合规标准,但这种极度的抽象性起初令我倍感困惑。

正如上图所示,通过OCI运行时运行容器化进程分为两个步骤:创建(Create)、启动(Start)。对于习惯驾驭Docker等高层容器运行时的开发者而言,他们往往会将这些OCI操作与docker container [create|start]等命令混为一谈。这种看法很容易产生误导,因为两者尽管语义相似,但并不等同。要理解其中的原因,我们必须观察当这两个操作在Linux宿主机上执行时,操作系统进程究竟发生了什么。

下面所有的实验都是针对名为mybundleOCI bundle进行的。它是从Docker Hub镜像docker.io/library/nginx生成的,其rootfs可以使用crane等OCI镜像处理工具导出。该bundle的目录结构如下所示,其中config.json是容器的OCI运行时配置文件,由runc spec或容器运行器/引擎生成。

1
2
3
4
5
6
7
8
9
10
11
12
mybundle
├── config.json
└── rootfs
├── bin
├── dev
├── docker-entrypoint.sh
├── etc
├── home
├── lib
├── ...
├── usr
└── var

runc需要根目录,还需要与容器ID对应的子目录(mycontainer),后者用于存储各操作间的状态。此类目录树通常交由containerd等容器管理器代为维护,但本文的实验已剥离了这层上级管控。

值得玩味的一个细节是,尽管目前状态为created而非running,但容器状态中竟然已经包含了进程ID19374)。这正是它与docker container create及其同类命令的首个主要区别——后者仅从指定镜像创建容器,而不会启动它。

在进一步深入探究之前,我们先试着解释一下刚刚观察到的行为。为此,我们将使用runc的高层命令之一:runc run。该命令并不直接映射到此前列举的任何一种OCI运行时操作,但可以粗略地将其描述为createstart的结合,尽管存在一些细微差别。

1
2
3
4
5
6
7
/docker-entrypoint.sh: Configuration complete; ready for start up
2023/08/17 19:12:34 [notice] 1#1: using the "epoll" event method
2023/08/17 19:12:34 [notice] 1#1: nginx/1.25.1
2023/08/17 19:12:34 [notice] 1#1: built by gcc 12.2.1 20220924 (Alpine 12.2.1)
2023/08/17 19:12:34 [notice] 1#1: OS: Linux 5.15.90.1-microsoft-standard-WSL2
2023/08/17 19:12:34 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1024:1024
2023/08/17 19:12:34 [notice] 1#1: start worker processes

读者不禁会产生疑问:既然在技术维度上容器完全能够一步到位启动,那么这种分两阶段的启动流程有何意义?OCI运行时规范并未阐述如此设计的初衷,但我相信,通过与UNIX创建进程的一些API(fork()exec())相类比,可以很好地回答这个问题。

这样一来,父进程便能在fork()exec()之间执行一些代码。这种机制对于像UNIX shell这样的程序至关重要,因为它需要支持I/O流重定向和其他进程操作等功能。

在本文的第一部分中,我们看到runc create最终产生了一个新进程,它并不是预期的容器应用程序,而是runc本身。深挖libcontainer底层的源码脉络不难发现,在这个被称为「bootstrap」(引导)的阶段中,系统会调用/proc/self/exe init命令(实质上是重新拉起runc二进制文件本身)来孵化出一个父进程。随后,该进程不仅会提前继承未来容器init进程(PID 1,即本文示例中的nginx)所需的Linux cgroup及命名空间上下文,还会从其源头父进程处接收待拉起容器的OCI运行时配置。此后,它将一直保持该状态,等待启动(Start)阶段的到来[1]

在创建(Create)阶段结束时,runc会将状态写入到state.json文件中,其存放于由--root命令行参数指定的目录下。该文件包含了诸如引导进程的进程ID等信息。随后所有的runc命令都会读取此文件,将其作为唯一依据,并根据其init进程的状态来确定容器的当前状态。

1
2
/run/runc/mycontainer
└── state.json
1
2
3
/run/runc/mycontainer
├── exec.fifo
└── state.json

正是这个命名管道,宿主机方能向引导进程传达启动容器的意图,并开启下一阶段:启动(Start)。该命名管道的写入端绑定至引导进程,而读取端则暂处于封闭状态。此时,引导进程会因尝试向exec.fifo执行写入调用而陷入阻塞态;直到外部执行runc start从中读取数据,扣下准许放行的「发令枪」后,阻塞方才解除。

在此阶段,调用者(例如containerd之类的容器管理器)在正式启动容器前,可以根据需要,执行任何其认为合适的额外步骤。在实际应用中,这通常意味着调用一系列CNI插件来配置容器的网络接口[2]。待此类操作完成后,调用者便可发起启动(Start)阶段。

而在容器启动的第二阶段(亦即最终收尾阶段),底层实际发生的动作核心,不过是一次exec()系统调用。而触发它的,正是前文提到的exec.fifo管道读取事件。由于引导进程在创建(Create)阶段就已经从父级runc进程接收了容器的运行时配置,因此无需再执行任何额外步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
libcontainer.parentProcess(*libcontainer.initProcess) *{
cmd: *os/exec.Cmd {
Path: "/proc/self/exe",
Args: []string len: 2, cap: 2, [
"/usr/local/bin/runc",
"init",
],
Env: []string len: 7, cap: 12, [
"GOMAXPROCS=",
"_LIBCONTAINER_INITPIPE=3",
"_LIBCONTAINER_STATEDIR=/run/runc/mycontainer",
"_LIBCONTAINER_LOGPIPE=4",
"_LIBCONTAINER_LOGLEVEL=4",
"_LIBCONTAINER_FIFOFD=5",
"_LIBCONTAINER_INITTYPE=standard",
],
Dir: "/home/acotten/mybundle/rootfs",
Stdin: io.Reader(*os.File) *{
file: *(*os.file)(0xc000082060),},
Stdout: io.Writer(*os.File) *{
file: *(*os.file)(0xc0000820c0),},
Stderr: io.Writer(*os.File) *{
file: *(*os.file)(0xc000082120),},
ExtraFiles: []*os.File len: 3, cap: 4, [
*(*os.File)(0xc000014bb8),
*(*os.File)(0xc000014bc8),
*(*os.File)(0xc000014be8),
],},
messageSockPair: libcontainer.filePair {
parent: *(*os.File)(0xc000014bb0),
child: *(*os.File)(0xc000014bb8),},
logFilePair: libcontainer.filePair {
parent: *(*os.File)(0xc000014bc0),
child: *(*os.File)(0xc000014bc8),},
config: *libcontainer.initConfig {
Args: []string len: 4, cap: 4, [
"/docker-entrypoint.sh",
"nginx",
"-g",
"daemon off;",
],
Env: []string len: 4, cap: 4, [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.25.1",
"PKG_RELEASE=1",
"NJS_VERSION=0.7.12",
],
Cwd: "/",
Capabilities: *(*"libcontainer/configs.Capabilities")(0xc0000bf200),
User: "0:0",
AdditionalGroups: []string len: 11, cap: 16, [
"0","1","2","3","4","6","10","11","20","26","27"],
Config: *(*"libcontainer/configs.Config")(0xc0000e21e0),
Networks: []*libcontainer.network len: 0, cap: 0, nil,
PassedFilesCount: 0,
ContainerId: "mycontainer",
Rlimits: []libcontainer/configs.Rlimit len: 1, cap: 1, [
(*"libcontainer/configs.Rlimit")(0xc00002d4d0),
],},
manager: libcontainer/cgroups.Manager(*libcontainer/cgroups/fs.manager) *{
cgroups: *libcontainer/configs.Cgroup {
Name: "",
Parent: "",
Path: "/default/1b260428b931d1c22fa618e251c7da66f84d18fe66f2ee67d...+15 more",
ScopePrefix: "",
Resources: *(*"libcontainer/configs.Resources")(0xc000002300),
Rootless: false,
OwnerUID: *int nil,},
paths: map[string]string [
"memory": "/sys/fs/cgroup/memory/default/1b260428b931d1c22fa618e2...+40 more",
"net_cls": "/sys/fs/cgroup/net_cls/default/1b260428b931d1c22fa618...+42 more",
"net_prio": "/sys/fs/cgroup/net_prio/default/1b260428b931d1c22fa6...+44 more",
"": "/sys/fs/cgroup/unified/default/1b260428b931d1c22fa618e251c7d...+35 more",
"cpuset": "/sys/fs/cgroup/cpuset/default/1b260428b931d1c22fa618e2...+40 more",
"rdma": "/sys/fs/cgroup/rdma/default/1b260428b931d1c22fa618e251c7...+36 more",
"devices": "/sys/fs/cgroup/devices/default/1b260428b931d1c22fa618...+42 more",
"cpu": "/sys/fs/cgroup/cpu/default/1b260428b931d1c22fa618e251c7da...+34 more",
"perf_event": "/sys/fs/cgroup/perf_event/default/1b260428b931d1c2...+48 more",
"misc": "/sys/fs/cgroup/misc/default/1b260428b931d1c22fa618e251c7...+36 more",
"cpuacct": "/sys/fs/cgroup/cpuacct/default/1b260428b931d1c22fa618...+42 more",
"pids": "/sys/fs/cgroup/pids/default/1b260428b931d1c22fa618e251c7...+36 more",
"blkio": "/sys/fs/cgroup/blkio/default/1b260428b931d1c22fa618e251...+38 more",
"hugetlb": "/sys/fs/cgroup/hugetlb/default/1b260428b931d1c22fa618...+42 more",
"freezer": "/sys/fs/cgroup/freezer/default/1b260428b931d1c22fa618...+42 more",
],}
container: *libcontainer.linuxContainer {
id: "mycontainer",
root: "/run/runc/mycontainer",
config: *(*"libcontainer/configs.Config")(0xc0000e21e0),
cgroupManager: libcontainer/cgroups.Manager(*libcontainer/cgroups/fs.manager) ...,
initPath: "/proc/self/exe",
initArgs: []string len: 2, cap: 2, [
"/usr/local/bin/runc",
"init",
],
initProcess: libcontainer.parentProcess(*libcontainer.initProcess) ...,
initProcessStartTime: 0,
state: libcontainer.containerState(*libcontainer.stoppedState) ...,
created: (*time.Time)(0xc0001d42b0),
fifo: *(*os.File)(0xc000014be8),},
fds: []string len: 0, cap: 0, nil,
process: *libcontainer.Process {
Args: []string len: 4, cap: 4, [
"/docker-entrypoint.sh",
"nginx",
"-g",
"daemon off;",
],
Env: []string len: 4, cap: 4, [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bi...+1 more",
"NGINX_VERSION=1.25.1",
"PKG_RELEASE=1",
"NJS_VERSION=0.7.12",
],
User: "0:0",
AdditionalGroups: []string len: 11, cap: 16, [
"0","1","2","3","4","6","10","11","20","26","27"],
Cwd: "/",
Stdin: io.Reader(*os.File) ...,
Stdout: io.Writer(*os.File) ...,
Stderr: io.Writer(*os.File) ...,
ExtraFiles: []*os.File len: 0, cap: 0, nil,
Capabilities: *(*"libcontainer/configs.Capabilities")(0xc0000bf200),
Rlimits: []libcontainer/configs.Rlimit len: 1, cap: 1, [
(*"libcontainer/configs.Rlimit")(0xc00002d4d0),
],
Init: true,
ops: libcontainer.processOperations nil,
LogLevel: "4",
SubCgroupPaths: map[string]string nil,},
bootstrapData: io.Reader(*bytes.Reader) *{
s: []uint8 len: 32, cap: 32, [
32,0,0,0,48,242,1,0,1,0,0,0,0,0,0,0,8,0,145,106,0,0,2,108,8,0,151,106,0,0,0,0],
i: 0,
prevRune: -1,},
}