mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1353 字
4 分钟
runc 源码分析
2026-04-27

当你执行 runc run mycontainer 时,runc 做了什么?它读取 config.json,创建 Namespace,配置 Cgroup,挂载 OverlayFS,设置 seccomp 规则,最终执行容器进程。这个过程涉及几十个系统调用和数百行内核交互代码。

runc 是 OCI Runtime Spec 的参考实现,也是 Docker 和 Kubernetes 默认使用的低层容器运行时。它的核心库 libcontainer 封装了所有与 Linux 内核的交互逻辑——从 clone() 创建 Namespace 到 mount() 挂载文件系统,从写入 Cgroup 文件到加载 seccomp BPF 程序。

runc 的前身是 libcontainer。2013 年,Docker 的创始人 Solomon Hykes 意识到 LXC(Linux Containers)的 API 不够稳定,决定用 Go 语言重写容器运行时,这就是 libcontainer。libcontainer 直接调用 Linux 系统调用(clone()mount()pivot_root() 等),不再依赖 LXC。2015 年 6 月,Docker 将 libcontainer 捐赠给新成立的 OCI(Open Container Initiative),并重命名为 runc。runc 成为 OCI Runtime Spec 的参考实现——它定义了”一个符合 OCI 规范的运行时应该怎么写”。此后,runc 的代码路径成为所有容器运行时的”参考答案”:gVisor、Kata Containers、甚至 containerd 的容器创建逻辑,都遵循 runc 建立的模式。理解 runc 的源码,就是理解容器运行时的”底层语法”。

前置知识#

  • Ch05 OCI 规范详解:runc 是 OCI Runtime Spec 的参考实现,理解 Runtime Spec 是阅读 runc 源码的前提
  • Go 语言基础:runc 用 Go 编写,需要理解 goroutine、interface、error handling 等 Go 特性
  • Linux 系统编程:clone()mount()pivot_root()seccomp() 等系统调用
Note

如果你没有 Go 语言基础,不必担心——runc 的核心逻辑集中在 libcontainer/ 目录,代码风格清晰,会逐行解读关键路径。

本章将深入 runc 的源码,追踪容器创建的完整代码路径。

一、runc 项目结构#

1.1 目录结构#

runc/
├── main.go # CLI 入口
├── create.go # runc create 命令
├── start.go # runc start 命令
├── run.go # runc run 命令
├── kill.go # runc kill 命令
├── delete.go # runc delete 命令
├── spec.go # runc spec 命令
├── libcontainer/ # 核心库
│ ├── container.go # Container 接口
│ ├── process.go # Process 定义
│ ├── factory.go # 容器工厂
│ ├── linux/
│ │ ├── container.go # Linux 容器实现
│ │ ├── process.go # Linux 进程实现
│ │ ├── init.go # 容器初始化
│ │ ├── standard_init.go # 标准初始化
│ │ └── nsenter/ # Namespace 进入
│ ├── configs/
│ │ ├── config.go # 容器配置
│ │ ├── mount.go # 挂载配置
│ │ └── ns.go # Namespace 配置
│ ├── cgroups/ # Cgroup 管理
│ ├── seccomp/ # seccomp 配置
│ ├── apparmor/ # AppArmor 配置
│ ├── system/ # 系统调用封装
│ └── user/ # 用户命名空间
├── contrib/ # 辅助工具
└── tests/ # 测试

1.2 核心依赖关系#

graph TB subgraph CLI层["CLI 层"] MAIN["main.go"] RUN["run.go"] CREATE["create.go"] START["start.go"] end subgraph 核心库["libcontainer"] FACTORY["Factory"] CONTAINER["Container"] PROCESS["Process"] INIT["Init (runc init)"] end subgraph 内核交互["内核交互层"] NS["Namespace<br/>clone/unshare/setns"] CG["Cgroup<br/>文件系统写入"] MNT["Mount<br/>pivot_root/mount"] SEC["Security<br/>seccomp/AppArmor/Cap"] end MAIN --> RUN MAIN --> CREATE MAIN --> START RUN --> FACTORY CREATE --> FACTORY START --> CONTAINER FACTORY --> CONTAINER CONTAINER --> PROCESS CONTAINER --> INIT INIT --> NS INIT --> CG INIT --> MNT INIT --> SEC style CLI层 fill:#bbdefb,stroke:#1565c0 style 核心库 fill:#c8e6c9,stroke:#2e7d32 style 内核交互层 fill:#fff3e0,stroke:#e65100

二、容器创建流程#

runc 的 CLI 命令对应容器生命周期的不同阶段,每个命令触发不同的代码路径:

命令容器状态代码入口主要操作
runc create不存在 → Createdcreate.go → Factory.Create()创建 Namespace、配置 Cgroup、挂载 rootfs、写入状态文件
runc startCreated → Runningstart.go → Container.Execute()执行 runc init,运行用户进程
runc run不存在 → Runningrun.go → Factory.Create() + Container.Run()create + start 的组合,一步到位
runc execRunningexec.go → Container.Exec()在已有容器中执行新进程(进入 Namespace)
runc killRunning → Stoppedkill.go → Container.Signal()向容器 init 进程发送信号
runc deleteStopped → 不存在delete.go → Container.Destroy()清理 Cgroup、卸载文件系统、删除状态文件

2.1 runc run 的代码路径#

// run.go(简化)
func runAction(ctx *cli.Context) error {
// 1. 解析命令行参数
spec, err := setupSpec(ctx)
// 2. 创建容器工厂
factory, err := libcontainer.New(
libcontainer.RootPath(rootfs),
)
// 3. 创建容器
container, err := factory.Create(ctx.String("id"), spec.Config)
// 4. 启动容器
process := &libcontainer.Process{
Args: spec.Process.Args,
Env: spec.Process.Env,
User: spec.Process.User.UID,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
err = container.Run(process)
// 5. 等待容器退出
status, err := process.Wait()
return nil
}

2.2 Factory.Create:创建容器#

// libcontainer/factory.go(简化)
func (f *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
// 1. 验证配置
if err := validateConfig(config); err != nil {
return nil, err
}
// 2. 创建容器状态目录
containerRoot := filepath.Join(f.Root, id)
if err := os.MkdirAll(containerRoot, 0711); err != nil {
return nil, err
}
// 3. 保存配置到状态目录
if err := writeConfig(containerRoot, config); err != nil {
return nil, err
}
// 4. 创建容器对象
c := &linuxContainer{
id: id,
root: containerRoot,
config: config,
initProcess: nil,
initStartTime: time.Now().UTC(),
}
return c, nil
}

2.3 Container.Run:启动容器#

// libcontainer/linux/container.go(简化)
func (c *linuxContainer) Run(process *Process) error {
// 1. 如果容器未启动,先创建
if c.initProcess == nil {
if err := c.start(process); err != nil {
return err
}
}
// 2. 执行用户进程
return process.Start()
}
func (c *linuxContainer) start(process *Process) error {
// 1. 创建父进程管道(用于与子进程通信)
parentPipe, childPipe, err := newPipe()
// 2. 准备命令(runc init 是容器初始化进程)
cmd := exec.Command("/proc/self/exe", "init")
cmd.Stdin = process.Stdin
cmd.Stdout = process.Stdout
cmd.Stderr = process.Stderr
// 3. 设置 Namespace clone 标志
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: c.getCloneFlags(),
}
// 4. 启动子进程(runc init)
if err := cmd.Start(); err != nil {
return err
}
// 5. 通过管道发送配置给子进程
if err := writeConfig(childPipe, c.config); err != nil {
return err
}
// 6. 等待子进程初始化完成
if err := readError(parentPipe); err != nil {
return err
}
c.initProcess = process
return nil
}

2.4 getCloneFlags:Namespace 配置#

func (c *linuxContainer) getCloneFlags() uintptr {
var flag uintptr
for _, ns := range c.config.Namespaces {
switch ns.Type {
case configs.NEWPID:
flag |= syscall.CLONE_NEWPID
case configs.NEWNS:
flag |= syscall.CLONE_NEWNS
case configs.NEWUTS:
flag |= syscall.CLONE_NEWUTS
case configs.NEWIPC:
flag |= syscall.CLONE_NEWIPC
case configs.NEWNET:
flag |= syscall.CLONE_NEWNET
case configs.NEWUSER:
flag |= syscall.CLONE_NEWUSER
case configs.NEWCGROUP:
flag |= syscall.CLONE_NEWCGROUP
}
}
return flag
}

三、runc init:容器初始化#

3.1 runc init 的角色#

runc init 是容器进程的第一个进程——它在新的 Namespace 中运行,负责完成所有容器初始化工作,然后执行用户命令。

sequenceDiagram participant Parent as runc (父进程) participant Child as runc init (子进程) participant Kernel as Linux 内核 Parent->>Kernel: clone(CLONE_NEWPID|CLONE_NEWNS|...) Kernel->>Child: 在新 Namespace 中启动 Note over Child: 此时还在宿主 rootfs Parent->>Child: 通过管道发送配置 Child->>Child: 1. 设置 User Namespace UID 映射 Child->>Child: 2. 设置 Cgroup Child->>Child: 3. 配置 mount namespace Child->>Child: 4. 挂载 proc/sys/dev Child->>Child: 5. pivot_root 切换根文件系统 Child->>Child: 6. 设置 hostname Child->>Child: 7. 配置网络 Child->>Child: 8. 设置 seccomp Child->>Child: 9. 设置 AppArmor Child->>Child: 10. 设置 Capabilities Child->>Parent: 通过管道报告初始化完成 Parent->>Child: 发送执行用户命令的信号 Child->>Child: exec 用户命令

3.2 standard_init_linux.go:核心初始化#

// libcontainer/linux/standard_init_linux.go(简化)
func (l *linuxStandardInit) Init() error {
// 1. 设置 User Namespace UID/GID 映射
if err := l.setupUser(); err != nil {
return err
}
// 2. 配置 Cgroup
if err := l.setupCgroup(); err != nil {
return err
}
// 3. 准备 rootfs
if err := l.prepareRootfs(); err != nil {
return err
}
// 4. 设置 hostname
if l.config.Hostname != "" {
if err := syscall.Sethostname([]byte(l.config.Hostname)); err != nil {
return err
}
}
// 5. 设置网络
if err := l.setupNetwork(); err != nil {
return err
}
// 6. 设置路由
if err := l.setupRoute(); err != nil {
return err
}
// 7. 设置 seccomp
if l.config.Seccomp != nil {
if err := seccomp.InitSeccomp(l.config.Seccomp); err != nil {
return err
}
}
// 8. 设置 AppArmor
if l.config.AppArmorProfile != "" {
if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil {
return err
}
}
// 9. 设置 Capabilities
if err := l.setupCapabilities(); err != nil {
return err
}
// 10. 切换到用户身份
if err := syscall.Setuid(l.config.UID); err != nil {
return err
}
// 11. 执行用户命令
return syscall.Exec(l.config.Args[0], l.config.Args, l.config.Env)
}

3.3 prepareRootfs:挂载文件系统#

func (l *linuxStandardInit) prepareRootfs() error {
// 1. 挂载所有配置的挂载点
for _, m := range l.config.Mounts {
if err := mount(m.Source, m.Destination, m.Type, m.Flags, m.Data); err != nil {
return err
}
}
// 2. pivot_root 切换根文件系统
if err := pivotRoot(l.config.Rootfs); err != nil {
return err
}
// 3. 重新挂载 /proc, /sys 等
if err := remountProc(); err != nil {
return err
}
return nil
}
func pivotRoot(rootfs string) error {
// 1. 绑定挂载 rootfs 到自身(确保是挂载点)
if err := syscall.Mount(rootfs, rootfs, "", syscall.MS_BIND, ""); err != nil {
return err
}
// 2. 创建 pivot_root 的 put_old 目录
putOld := filepath.Join(rootfs, ".pivot_root")
if err := os.MkdirAll(putOld, 0755); err != nil {
return err
}
// 3. 执行 pivot_root 系统调用
if err := syscall.PivotRoot(rootfs, putOld); err != nil {
return err
}
// 4. 修正工作目录
if err := syscall.Chdir("/"); err != nil {
return err
}
// 5. 卸载旧的根文件系统
if err := syscall.Unmount(".pivot_root", syscall.MNT_DETACH); err != nil {
return err
}
return os.Remove(".pivot_root")
}

四、runc init 初始化顺序与依赖#

runc init 进程的初始化按严格顺序执行,每一步都依赖前一步的完成:

步骤操作内核接口失败影响
1设置 Namespaceclone(CLONE_NEWNS|CLONE_NEWPID|…)容器无法隔离
2配置 Cgroup写入 /sys/fs/cgroup/…资源限制不生效
3挂载 rootfsmount(), pivot_root()容器无法访问文件系统
4设置 hostnamesethostname()容器内主机名不正确
5配置网络netlink, veth pair容器网络不可用
6设置 seccompprctl(PR_SET_SECCOMP)系统调用过滤不生效
7设置 AppArmor/proc/self/attr/apparmor/安全策略不生效
8设置 capabilitiescapset()权限控制不正确
9执行用户命令execve()容器进程无法启动

runc init 的各初始化步骤存在严格的先后依赖——顺序错误会导致容器创建失败:

flowchart TB START["runc init 启动"] --> USER["1. setupUser<br/>User Namespace UID/GID 映射"] USER --> CG["2. setupCgroup<br/>创建 cgroup 目录 + 写入限制"] CG --> ROOTFS["3. prepareRootfs<br/>挂载 proc/sys/dev + pivot_root"] ROOTFS --> HOSTNAME["4. sethostname"] HOSTNAME --> NET["5. setupNetwork<br/>配置网络设备"] NET --> ROUTE["6. setupRoute<br/>设置路由"] ROUTE --> SECCOMP["7. InitSeccomp<br/>加载 BPF 过滤规则"] SECCOMP --> APPARMOR["8. ApplyProfile<br/>设置 AppArmor"] APPARMOR --> CAP["9. setupCapabilities<br/>设置 Capabilities"] CAP --> UID["10. setuid/setgid<br/>切换到非 root 用户"] UID --> EXEC["11. execve<br/>执行用户命令"] USER -.->|"必须在其他 NS 操作之前<br/>因为 User NS 影响 Capability"| CAP ROOTFS -.->|"必须在 Cgroup 之后<br/>因为 pivot_root 后路径改变"| CG SECCOMP -.->|"必须在 execve 之前<br/>否则无法过滤用户进程"| EXEC style START fill:#bbdefb,stroke:#1565c0 style EXEC fill:#c8e6c9,stroke:#2e7d32
Warning

runc init 的初始化顺序不可随意调整。User Namespace 必须最先设置,因为它决定了后续操作的 Capability 集合;seccomp 必须在 execve 之前加载,否则用户进程不受过滤;pivot_root 必须在 mount 操作之后,否则挂载点路径不正确。修改 runc 源码时务必遵守这些依赖关系。

五、Cgroup 管理#

5.1 runc 的 Cgroup 实现#

// libcontainer/cgroups/fs2/manager.go(简化)
type Manager struct {
controllers map[string]controller
path string
}
func (m *Manager) Apply(pid int) error {
// 1. 创建 cgroup 目录
if err := os.MkdirAll(m.path, 0755); err != nil {
return err
}
// 2. 将进程加入 cgroup
if err := os.WriteFile(
filepath.Join(m.path, "cgroup.procs"),
[]byte(strconv.Itoa(pid)),
0644,
); err != nil {
return err
}
// 3. 应用各控制器配置
for _, c := range m.controllers {
if err := c.Apply(m.path); err != nil {
return err
}
}
return nil
}
// CPU 控制器
type cpuController struct{}
func (c *cpuController) Apply(path string) error {
// 写入 cpu.max
if err := os.WriteFile(filepath.Join(path, "cpu.max"), []byte("200000 100000"), 0644); err != nil {
return err
}
// 写入 cpu.weight
return os.WriteFile(filepath.Join(path, "cpu.weight"), []byte("100"), 0644)
}
// 内存控制器
type memoryController struct{}
func (c *memoryController) Apply(path string) error {
// 写入 memory.max
return os.WriteFile(filepath.Join(path, "memory.max"), []byte("536870912"), 0644)
}

六、安全配置#

6.1 Capabilities 设置#

func (l *linuxStandardInit) setupCapabilities() error {
// 1. 清除所有 Capabilities
if err := cap.Reset(); err != nil {
return err
}
// 2. 只添加 config.json 中指定的 Capabilities
for _, capName := range l.config.Capabilities.Bounding {
if err := cap.Set(capName); err != nil {
return err
}
}
return nil
}

6.2 seccomp 配置#

func InitSeccomp(config *configs.Seccomp) error {
// 1. 构建 BPF 程序
filter, err := buildSeccompFilter(config)
if err != nil {
return err
}
// 2. 加载 BPF 程序
prog := &syscall.SockFprog{
Len: uint16(len(filter)),
Filter: &filter[0],
}
// 3. 应用 seccomp 过滤
if _, _, err := syscall.Syscall(
syscall.SYS_PRCTL,
syscall.PR_SET_SECCOMP,
syscall.SECCOMP_MODE_FILTER,
uintptr(unsafe.Pointer(prog)),
); err != 0 {
return err
}
return nil
}

七、runc 与 containerd 的交互#

7.1 containerd 如何调用 runc#

containerd 通过 containerd-shim 调用 runc,shim 是 runc 的直接调用者:

// containerd-shim 调用 runc 的方式
func (s *service) Create(ctx context.Context, r *task.CreateRequest) (*task.CreateResponse, error) {
// 1. 准备 OCI Bundle
bundle := &Bundle{
ID: r.ID,
Path: r.Bundle,
Rootfs: r.Rootfs,
}
// 2. 构造 runc 命令
cmd := exec.Command("runc", "create",
"--bundle", bundle.Path,
"--pid-file", pidFile,
r.ID,
)
// 3. 执行 runc create
if err := cmd.Run(); err != nil {
return nil, err
}
// 4. 读取容器 PID
pid, _ := os.ReadFile(pidFile)
return &task.CreateResponse{Pid: uint32(pid)}, nil
}

7.2 runc 的进程关系#

graph TB CTNRD["containerd<br/>PID 1000"] SHIM["containerd-shim<br/>PID 2000"] RUNC_CREATE["runc create<br/>PID 3000(临时)"] RUNC_INIT["runc init<br/>PID 3001(容器 init 进程)"] APP["nginx<br/>PID 1(容器内)<br/>PID 3001(宿主)"] CTNRD -->|"fork"| SHIM SHIM -->|"exec"| RUNC_CREATE RUNC_CREATE -->|"clone + exec"| RUNC_INIT RUNC_INIT -->|"exec"| APP SHIM -->|"监督"| APP style CTNRD fill:#bbdefb,stroke:#1565c0 style SHIM fill:#c8e6c9,stroke:#2e7d32 style RUNC_CREATE fill:#fff3e0,stroke:#e65100 style RUNC_INIT fill:#e1bee7,stroke:#6a1b9a style APP fill:#ffcdd2,stroke:#c62828

八、动手实践#

8.1 用 strace 追踪 runc 的系统调用#

#!/bin/bash
# 追踪 runc 的系统调用
# 1. 创建 OCI Bundle
mkdir -p /tmp/strace-bundle/rootfs
cd /tmp/strace-bundle
docker export $(docker create busybox) | tar -C rootfs -xvf -
runc spec
# 2. 用 strace 追踪 runc
sudo strace -f -o /tmp/runc-strace.log \
-e trace=clone,unshare,mount,pivot_root,prctl,capset,write \
runc run strace-test
# 3. 分析系统调用
echo "=== clone 调用(创建 Namespace)==="
grep "clone" /tmp/runc-strace.log | head -5
echo "=== mount 调用(挂载文件系统)==="
grep "mount" /tmp/runc-strace.log | head -10
echo "=== pivot_root 调用(切换根文件系统)==="
grep "pivot_root" /tmp/runc-strace.log
echo "=== prctl 调用(seccomp/Capabilities)==="
grep "prctl" /tmp/runc-strace.log | head -5
echo "=== Cgroup 写入 ==="
grep "cgroup" /tmp/runc-strace.log | head -5

8.2 修改 runc 源码添加日志#

// 在 libcontainer/linux/standard_init_linux.go 的 Init 方法中添加日志
func (l *linuxStandardInit) Init() error {
logFile, _ := os.OpenFile("/tmp/runc-init.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
log := log.New(logFile, "[runc-init] ", log.Lmicroseconds)
log.Println("Starting container initialization")
log.Printf("Container ID: %s", l.config.ContainerID)
log.Printf("Namespaces: %v", l.config.Namespaces)
// ... 原有初始化代码 ...
log.Println("Setting up user namespace")
if err := l.setupUser(); err != nil {
log.Printf("setupUser failed: %v", err)
return err
}
log.Println("Setting up cgroup")
// ... 继续添加日志 ...
log.Println("Container initialization complete, executing user command")
return syscall.Exec(l.config.Args[0], l.config.Args, l.config.Env)
}

九、本章小结#

上一章深入探讨了OCI 规范体系。

组件职责关键代码
Factory创建容器对象factory.go
Container管理容器生命周期linux/container.go
runc init容器初始化进程standard_init_linux.go
Cgroup ManagerCgroup 资源管理cgroups/fs2/manager.go
seccomp系统调用过滤seccomp/seccomp.go
Capabilities权限控制capabilities/capabilities.go
Note

runc 的代码虽然复杂,但核心逻辑是线性的:读取 config.json → 创建 Namespace → 配置 Cgroup → 挂载文件系统 → 设置安全策略 → 执行用户命令。理解这条主线,就能把握 runc 的全貌。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

runc 源码分析
https://blog.souloss.com/posts/container-runtime/runc-source-code/
作者
Souloss
发布于
2026-04-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
系列导读
容器运行时 本系列从 Linux 内核的 Namespace、Cgroup、OverlayFS 出发,深入 OCI 规范、runc 源码、containerd 架构,再到容器安全、沙箱运行时、网络、存储、镜像构建、Wasm 容器,最后综合实战构建一个迷你容器运行时——从「会用 Docker」到「理解容器运行时的每一行代码」,每章配有可运行的代码示例与架构图,让你从容器用户进阶到容器运行时工程师。
2
容器完整流程:docker run 背后
容器运行时 当你执行 docker run nginx 时,背后发生了什么?本章完整追踪从 Docker CLI 到容器进程启动的每一步——镜像拉取、OCI Bundle 生成、shim 启动、runc 创建、Namespace/Cgroup/OverlayFS 配置、容器进程执行,让你对 docker run 的每一步都了如指掌。
3
综合实战:构建一个迷你容器运行时
容器运行时 综合实战——用 Go 从零构建一个迷你容器运行时——实现 Namespace 隔离(PID/Mount/UTS/IPC/Network)、Cgroup 资源限制(CPU/内存)、OverlayFS 分层文件系统、OCI Bundle 解析,最终实现一个能运行容器的 minirunc。将前 15 章的知识融会贯通,从「理解原理」到「动手实现」。
4
容器安全:seccomp/AppArmor/Capabilities
容器运行时 容器的安全边界在哪里?Namespace 提供视图隔离,但不阻止特权操作。从零讲透 Linux 安全模块在容器中的应用——Capabilities(权限细分)、seccomp(系统调用过滤)、AppArmor(文件访问控制),以及 rootless 容器的实现,让你理解容器的安全边界和加固方法。
5
镜像构建:BuildKit
容器运行时 BuildKit 是 Docker 的新一代镜像构建引擎,支持并行构建、缓存导入导出、多阶段构建优化、secret 挂载等高级特性。从零讲透 BuildKit 的架构设计、Dockerfile 指令的执行原理、多阶段构建、缓存策略、以及如何编写高效的 Dockerfile——从「会写 Dockerfile」到「理解每一条指令的构建机制」。