当你执行 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()等系统调用
如果你没有 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 核心依赖关系
二、容器创建流程
runc 的 CLI 命令对应容器生命周期的不同阶段,每个命令触发不同的代码路径:
| 命令 | 容器状态 | 代码入口 | 主要操作 |
|---|---|---|---|
runc create | 不存在 → Created | create.go → Factory.Create() | 创建 Namespace、配置 Cgroup、挂载 rootfs、写入状态文件 |
runc start | Created → Running | start.go → Container.Execute() | 执行 runc init,运行用户进程 |
runc run | 不存在 → Running | run.go → Factory.Create() + Container.Run() | create + start 的组合,一步到位 |
runc exec | Running | exec.go → Container.Exec() | 在已有容器中执行新进程(进入 Namespace) |
runc kill | Running → Stopped | kill.go → Container.Signal() | 向容器 init 进程发送信号 |
runc delete | Stopped → 不存在 | 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 中运行,负责完成所有容器初始化工作,然后执行用户命令。
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 | 设置 Namespace | clone(CLONE_NEWNS|CLONE_NEWPID|…) | 容器无法隔离 |
| 2 | 配置 Cgroup | 写入 /sys/fs/cgroup/… | 资源限制不生效 |
| 3 | 挂载 rootfs | mount(), pivot_root() | 容器无法访问文件系统 |
| 4 | 设置 hostname | sethostname() | 容器内主机名不正确 |
| 5 | 配置网络 | netlink, veth pair | 容器网络不可用 |
| 6 | 设置 seccomp | prctl(PR_SET_SECCOMP) | 系统调用过滤不生效 |
| 7 | 设置 AppArmor | /proc/self/attr/apparmor/ | 安全策略不生效 |
| 8 | 设置 capabilities | capset() | 权限控制不正确 |
| 9 | 执行用户命令 | execve() | 容器进程无法启动 |
runc init 的各初始化步骤存在严格的先后依赖——顺序错误会导致容器创建失败:
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 的进程关系
八、动手实践
8.1 用 strace 追踪 runc 的系统调用
#!/bin/bash# 追踪 runc 的系统调用
# 1. 创建 OCI Bundlemkdir -p /tmp/strace-bundle/rootfscd /tmp/strace-bundledocker export $(docker create busybox) | tar -C rootfs -xvf -runc spec
# 2. 用 strace 追踪 runcsudo 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 -58.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 Manager | Cgroup 资源管理 | cgroups/fs2/manager.go |
| seccomp | 系统调用过滤 | seccomp/seccomp.go |
| Capabilities | 权限控制 | capabilities/capabilities.go |
runc 的代码虽然复杂,但核心逻辑是线性的:读取 config.json → 创建 Namespace → 配置 Cgroup → 挂载文件系统 → 设置安全策略 → 执行用户命令。理解这条主线,就能把握 runc 的全貌。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






