这是本系列的最后一章。在前 15 章中,从容器全景出发,深入了 Namespace、Cgroup、OverlayFS、OCI 规范、runc 源码、containerd 架构、shim、完整流程、安全、沙箱运行时、网络、存储、镜像构建、Wasm 容器——每一章都在回答一个核心问题:容器运行时是怎么工作的?
但阅读源码和理解原理只是第一步。真正的理解来自动手实现。本章将用 Go 从零构建一个迷你容器运行时 minirunc,实现 Namespace 隔离、Cgroup 资源限制、OverlayFS 分层文件系统、OCI Bundle 解析——将前 15 章的知识融会贯通。
一、minirunc 设计
1.1 功能范围
minirunc 实现以下核心功能:
| 功能 | 实现方式 | 对应章节 |
|---|---|---|
| Namespace 隔离 | clone + CLONE_NEW* | Ch02 |
| Cgroup 资源限制 | 写入 cgroup 文件 | Ch03 |
| OverlayFS 文件系统 | mount overlay | Ch04 |
| OCI Bundle 解析 | 读取 config.json | Ch05 |
| 容器生命周期 | create/start/kill/delete | Ch05 |
| 安全配置 | Capabilities + seccomp | Ch10 |
1.2 架构设计
1.3 项目结构
minirunc/├── main.go # CLI 入口├── bundle.go # OCI Bundle 解析├── namespace.go # Namespace 管理├── cgroup.go # Cgroup v2 管理├── overlay.go # OverlayFS 管理├── security.go # 安全配置├── init.go # 容器初始化├── container.go # 容器状态管理└── go.mod二、OCI Bundle 解析
2.1 配置结构体
// bundle.go - OCI Bundle 解析package main
import ( "encoding/json" "fmt" "os" "path/filepath"
specs "github.com/opencontainers/runtime-spec/specs-go")
type Bundle struct { Path string Spec *specs.Spec}
func LoadBundle(bundlePath string) (*Bundle, error) { configPath := filepath.Join(bundlePath, "config.json") data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("read config.json: %w", err) }
var spec specs.Spec if err := json.Unmarshal(data, &spec); err != nil { return nil, fmt.Errorf("parse config.json: %w", err) }
return &Bundle{ Path: bundlePath, Spec: &spec, }, nil}
func (b *Bundle) RootfsPath() string { if filepath.IsAbs(b.Spec.Root.Path) { return b.Spec.Root.Path } return filepath.Join(b.Path, b.Spec.Root.Path)}三、Namespace 管理
四、Cgroup 管理
4.1 Cgroup v2 管理
// cgroup.go - Cgroup v2 管理package main
import ( "fmt" "os" "path/filepath" "strconv"
specs "github.com/opencontainers/runtime-spec/specs-go")
const cgroupRoot = "/sys/fs/cgroup"
type CgroupManager struct { Path string}
func NewCgroupManager(cgroupPath string) *CgroupManager { return &CgroupManager{ Path: filepath.Join(cgroupRoot, cgroupPath), }}
func (m *CgroupManager) Apply(pid int, resources *specs.LinuxResources) error { // 1. 创建 cgroup 目录 if err := os.MkdirAll(m.Path, 0755); err != nil { return fmt.Errorf("create cgroup dir: %w", err) }
// 2. 将进程加入 cgroup if err := os.WriteFile( filepath.Join(m.Path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0644, ); err != nil { return fmt.Errorf("add process to cgroup: %w", err) }
// 3. 设置资源限制 if resources != nil { if err := m.setMemory(resources.Memory); err != nil { return err } if err := m.setCPU(resources.CPU); err != nil { return err } if err := m.setPids(resources.Pids); err != nil { return err } }
return nil}
func (m *CgroupManager) setMemory(mem *specs.LinuxMemory) error { if mem == nil || mem.Limit == nil { return nil } return os.WriteFile( filepath.Join(m.Path, "memory.max"), []byte(strconv.FormatInt(*mem.Limit, 10)), 0644, )}
func (m *CgroupManager) setCPU(cpu *specs.LinuxCPU) error { if cpu == nil { return nil } if cpu.Quota != nil && cpu.Period != nil { content := fmt.Sprintf("%d %d", *cpu.Quota, *cpu.Period) if err := os.WriteFile( filepath.Join(m.Path, "cpu.max"), []byte(content), 0644, ); err != nil { return err } } return nil}
func (m *CgroupManager) setPids(pids *specs.LinuxPids) error { if pids == nil { return nil } return os.WriteFile( filepath.Join(m.Path, "pids.max"), []byte(strconv.FormatInt(pids.Limit, 10)), 0644, )}
func (m *CgroupManager) Destroy() error { return os.RemoveAll(m.Path)}五、OverlayFS 管理
5.1 OverlayFS 挂载
// overlay.go - OverlayFS 管理package main
import ( "fmt" "os" "path/filepath" "strings" "syscall")
type OverlayConfig struct { LowerDirs []string UpperDir string WorkDir string MergedDir string}
func NewOverlayConfig(bundlePath, rootfsPath string) *OverlayConfig { overlayDir := filepath.Join(bundlePath, "overlay") return &OverlayConfig{ LowerDirs: []string{rootfsPath}, UpperDir: filepath.Join(overlayDir, "upper"), WorkDir: filepath.Join(overlayDir, "work"), MergedDir: filepath.Join(overlayDir, "merged"), }}
func (c *OverlayConfig) Mount() error { // 创建目录 for _, dir := range []string{c.UpperDir, c.WorkDir, c.MergedDir} { if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("create overlay dir %s: %w", dir, err) } }
// 构建 mount 选项 options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", strings.Join(c.LowerDirs, ":"), c.UpperDir, c.WorkDir, )
// 挂载 OverlayFS if err := syscall.Mount("overlay", c.MergedDir, "overlay", 0, options); err != nil { return fmt.Errorf("mount overlay: %w", err) }
return nil}
func (c *OverlayConfig) Unmount() error { return syscall.Unmount(c.MergedDir, 0)}六、容器初始化
6.1 容器 init 进程
// init.go - 容器初始化package main
import ( "os" "syscall"
specs "github.com/opencontainers/runtime-spec/specs-go")
func containerInit(spec *specs.Spec, rootfs string) error { // 1. 设置 hostname if spec.Hostname != "" { if err := syscall.Sethostname([]byte(spec.Hostname)); err != nil { return err } }
// 2. 挂载 proc if err := syscall.Mount("proc", "/proc", "proc", 0, ""); err != nil { return err }
// 3. 挂载 sysfs if err := syscall.Mount("sysfs", "/sys", "sysfs", syscall.MS_RDONLY, ""); err != nil { return err }
// 4. 挂载 devtmpfs if err := syscall.Mount("devtmpfs", "/dev", "devtmpfs", 0, ""); err != nil { return err }
// 5. pivot_root 切换根文件系统 if err := pivotRoot(rootfs); err != nil { return err }
// 6. 设置工作目录 if spec.Process.Cwd != "" { if err := syscall.Chdir(spec.Process.Cwd); err != nil { return err } }
// 7. 设置环境变量 for _, env := range spec.Process.Env { // 解析 KEY=VALUE 格式 for i := 0; i < len(env); i++ { if env[i] == '=' { os.Setenv(env[:i], env[i+1:]) break } } }
// 8. 执行用户命令 return syscall.Exec(spec.Process.Args[0], spec.Process.Args, os.Environ())}
func pivotRoot(rootfs string) error { // 绑定挂载 rootfs 到自身 if err := syscall.Mount(rootfs, rootfs, "", syscall.MS_BIND, ""); err != nil { return err }
// 创建 put_old 目录 putOld := rootfs + "/.pivot_root" if err := os.MkdirAll(putOld, 0755); err != nil { return err }
// 执行 pivot_root if err := syscall.PivotRoot(rootfs, putOld); err != nil { return err }
// 修正工作目录 if err := syscall.Chdir("/"); err != nil { return err }
// 卸载旧根 if err := syscall.Unmount(".pivot_root", syscall.MNT_DETACH); err != nil { return err }
return os.Remove(".pivot_root")}七、容器生命周期管理
7.1 容器创建和启动
// container.go - 容器生命周期管理package mainimport ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "syscall" specs "github.com/opencontainers/runtime-spec/specs-go")type Container struct { ID string Bundle *Bundle PID int Status string Cgroup *CgroupManager Overlay *OverlayConfig}func CreateContainer(id, bundlePath string) (*Container, error) { // 1. 加载 OCI Bundle bundle, err := LoadBundle(bundlePath) if err != nil { return nil, err } // 2. 准备 OverlayFS overlay := NewOverlayConfig(bundlePath, bundle.RootfsPath()) if err := overlay.Mount(); err != nil { return nil, fmt.Errorf("mount overlay: %w", err) } // 3. 创建容器对象 c := &Container{ ID: id, Bundle: bundle, Status: "created", Overlay: overlay, } // 4. 保存容器状态 if err := c.saveState(); err != nil { return nil, err } return c, nil}func (c *Container) Start() error { spec := c.Bundle.Spec // 1. 创建子进程(新 Namespace) cmd := exec.Command("/proc/self/exe", "init", c.ID) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: getCloneFlags(spec.Linux.Namespaces), } if err := cmd.Start(); err != nil { return fmt.Errorf("start container process: %w", err) } c.PID = cmd.Process.Pid c.Status = "running" // 2. 配置 Cgroup cgroupPath := fmt.Sprintf("minirunc/%s", c.ID) c.Cgroup = NewCgroupManager(cgroupPath) if err := c.Cgroup.Apply(c.PID, spec.Linux.Resources); err != nil { return fmt.Errorf("apply cgroup: %w", err) } // 3. 保存状态 if err := c.saveState(); err != nil { return err } // 4. 等待容器退出 go func() { cmd.Process.Wait() c.Status = "stopped" c.saveState() }() return nil}func (c *Container) Kill(signal syscall.Signal) error { if c.PID == 0 { return fmt.Errorf("container not running") } return syscall.Kill(c.PID, signal)}func (c *Container) Delete() error { if c.Cgroup != nil { c.Cgroup.Destroy() } if c.Overlay != nil { c.Overlay.Unmount() } stateDir := filepath.Join("/var/run/minirunc", c.ID) return os.RemoveAll(stateDir)}func (c *Container) saveState() error { stateDir := filepath.Join("/var/run/minirunc", c.ID) if err := os.MkdirAll(stateDir, 0755); err != nil { return err } data, _ := json.Marshal(c) return os.WriteFile(filepath.Join(stateDir, "state.json"), data, 0644)}八、CLI 入口
8.1 main.go
// main.go - CLI 入口package mainimport ( "fmt" "os" "syscall")func main() { if len(os.Args) < 2 { printUsage() os.Exit(1) } switch os.Args[1] { case "run": if err := runCommand(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } case "init": if err := initCommand(); err != nil { fmt.Fprintf(os.Stderr, "Init error: %v\n", err) os.Exit(1) } case "kill": if err := killCommand(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } case "delete": if err := deleteCommand(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } default: printUsage() os.Exit(1) }}func runCommand() error { if len(os.Args) < 4 { return fmt.Errorf("usage: minirunc run <bundle> <id>") } bundlePath := os.Args[2] id := os.Args[3] c, err := CreateContainer(id, bundlePath) if err != nil { return err } return c.Start()}func initCommand() error { if len(os.Args) < 3 { return fmt.Errorf("usage: minirunc init <id>") } id := os.Args[2] // 加载容器状态 stateDir := filepath.Join("/var/run/minirunc", id) data, err := os.ReadFile(filepath.Join(stateDir, "state.json")) if err != nil { return err } var c Container if err := json.Unmarshal(data, &c); err != nil { return err }
// 执行容器初始化 return containerInit(c.Bundle.Spec, c.Overlay.MergedDir)}
func killCommand() error { if len(os.Args) < 3 { return fmt.Errorf("usage: minirunc kill <id>") } id := os.Args[2]
c, err := loadContainer(id) if err != nil { return err } return c.Kill(syscall.SIGTERM)}
func deleteCommand() error { if len(os.Args) < 3 { return fmt.Errorf("usage: minirunc delete <id>") } id := os.Args[2]
c, err := loadContainer(id) if err != nil { return err } return c.Delete()}
func printUsage() { fmt.Println("minirunc - A minimal container runtime") fmt.Println("") fmt.Println("Commands:") fmt.Println(" run <bundle> <id> Create and start a container") fmt.Println(" init <id> Initialize container (internal)") fmt.Println(" kill <id> Kill a container") fmt.Println(" delete <id> Delete a container")}九、测试与验证
9.1 构建 minirunc
#!/bin/bash
mkdir -p minirunc && cd miniruncgo mod init miniruncgo get github.com/opencontainers/runtime-spec/specs-go
go build -o minirunc .
mkdir -p /tmp/test-bundle/rootfsdocker export $(docker create busybox) | tar -C /tmp/test-bundle/rootfs -xvf -cd /tmp/test-bundle
cat > config.json << 'EOF'{ "ociVersion": "1.0.2", "process": { "terminal": true, "user": {"uid": 0, "gid": 0}, "args": ["/bin/sh"], "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], "cwd": "/" }, "root": {"path": "rootfs", "readonly": false}, "hostname": "minicontainer", "linux": { "namespaces": [ {"type": "pid"}, {"type": "mount"}, {"type": "ipc"}, {"type": "uts"}, {"type": "network"} ], "resources": { "memory": {"limit": 536870912}, "cpu": {"quota": 100000, "period": 100000}, "pids": {"limit": 100} } }}EOF
sudo ./minirunc run /tmp/test-bundle test-container
hostname# minicontainer
ps aux# PID USER COMMAND
cat /proc/self/cgroup# 0::/minirunc/test-container
exit9.2 验证隔离效果
#!/bin/bash
echo "=== PID Namespace ==="echo "容器内 PID 1 = 容器进程在新 Namespace 中的 PID"
echo ""echo "=== Mount Namespace ==="echo "容器内的 /proc 是独立的 procfs"echo "容器内的 /sys 是只读的 sysfs"
echo ""echo "=== UTS Namespace ==="echo "容器内的 hostname = minicontainer"echo "宿主机的 hostname 不受影响"
echo ""echo "=== Cgroup ==="echo "容器的 Cgroup 路径: /sys/fs/cgroup/minirunc/test-container"cat /sys/fs/cgroup/minirunc/test-container/memory.max# 536870912 (512MB)cat /sys/fs/cgroup/minirunc/test-container/cpu.max# 100000 100000 (1 CPU)cat /sys/fs/cgroup/minirunc/test-container/pids.max# 100十、扩展方向
10.1 可以继续添加的功能
| 功能 | 实现难度 | 说明 |
|---|---|---|
| Network Namespace + veth pair | 参见 Ch12 容器网络 | |
| User Namespace + UID 映射 | 参见 Ch02 Namespace | |
| seccomp BPF 过滤 | 参见 Ch10 容器安全 | |
| AppArmor 配置 | 参见 Ch10 容器安全 | |
| 容器 Checkpoint/Restore | CRIU 集成 | |
| 多容器管理(类似 containerd) | 参见 Ch07 containerd | |
| shim 进程监督 | 参见 Ch08 shim |
10.2 minirunc vs runc 对比
| 功能 | minirunc | runc |
|---|---|---|
| Namespace | 5 种 | 8 种 |
| Cgroup | v2 基础 | v1 + v2 完整 |
| OverlayFS | 基础挂载 | 完整管理 |
| 安全 | 无 | seccomp + AppArmor + Capabilities |
| 网络 | 无 | 完整支持 |
| 代码量 | ~500 行 | ~100,000 行 |
本实战项目仅用于学习容器运行时原理。minirunc 缺少安全加固(seccomp/AppArmor/Capabilities 限制),切勿用于生产环境。生产级容器运行时需要 runc/gVisor/Kata Containers 等经过安全审计的实现。
十一、本章小结
上一章了解了容器在 Kubernetes 中的运行。
通过构建 minirunc,前 15 章的知识融会贯通:
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| Namespace | 用 clone() 的 CLONE_NEW* 标志创建隔离的进程视图 | clone, CLONE_NEW* |
| Cgroup | 通过写入 cgroup 文件系统设置资源限制 | cgroup v2, 资源限制 |
| OverlayFS | 用 mount() 系统调用挂载分层文件系统 | mount, lowerdir/upperdir |
| OCI Bundle | 解析 config.json 获取容器配置 | config.json, rootfs |
| pivot_root | 切换容器的根文件系统 | pivot_root, chroot |
| 生命周期 | 实现 create → start → kill → delete 的标准流程 | create, start, delete |
minirunc 是一个教学项目,不适合生产使用。但它让你理解了容器运行时的核心逻辑——容器进程 = Linux 进程 + Namespace + Cgroup + OverlayFS。runc、containerd、gVisor、Kata 都是在这个基础上添加更多的功能和安全保障。
恭喜你完成了「容器运行时深入」系列!从这个系列的第一章到这里的综合实战,你已经从「会用 Docker」进阶到了「理解容器运行时的每一行代码」。无论你是要排查容器问题、优化容器性能、还是构建自定义运行时,这些知识都是你的坚实基础。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






