mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
920 字
3 分钟
综合实战:构建一个迷你容器运行时
2026-06-17

这是本系列的最后一章。在前 15 章中,从容器全景出发,深入了 NamespaceCgroupOverlayFSOCI 规范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 overlayCh04
OCI Bundle 解析读取 config.jsonCh05
容器生命周期create/start/kill/deleteCh05
安全配置Capabilities + seccompCh10

1.2 架构设计#

graph TB subgraph CLI["CLI 层"] MAIN["main.go"] CMD_CREATE["create 命令"] CMD_START["start 命令"] CMD_RUN["run 命令"] CMD_KILL["kill 命令"] CMD_DELETE["delete 命令"] end subgraph 核心库["核心库"] BUNDLE["bundle.go<br/>OCI Bundle 解析"] NS["namespace.go<br/>Namespace 管理"] CG["cgroup.go<br/>Cgroup 管理"] OVL["overlay.go<br/>OverlayFS 管理"] SEC["security.go<br/>安全配置"] INIT["init.go<br/>容器初始化"] end subgraph 内核["Linux 内核"] CLONE["clone()"] MOUNT["mount()"] PIVOT["pivot_root()"] CGROUP_FS["cgroup 文件系统"] end MAIN --> CMD_CREATE MAIN --> CMD_START MAIN --> CMD_RUN MAIN --> CMD_KILL MAIN --> CMD_DELETE CMD_CREATE --> BUNDLE CMD_CREATE --> NS CMD_CREATE --> CG CMD_CREATE --> OVL CMD_START --> INIT INIT --> CLONE INIT --> MOUNT INIT --> PIVOT INIT --> CGROUP_FS INIT --> SEC style CLI fill:#bbdefb,stroke:#1565c0 style 核心库 fill:#c8e6c9,stroke:#2e7d32 style 内核 fill:#fff3e0,stroke:#e65100

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 main
import (
"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 main
import (
"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 minirunc
go mod init minirunc
go get github.com/opencontainers/runtime-spec/specs-go
go build -o minirunc .
mkdir -p /tmp/test-bundle/rootfs
docker 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
exit

9.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/RestoreCRIU 集成
多容器管理(类似 containerd)参见 Ch07 containerd
shim 进程监督参见 Ch08 shim

10.2 minirunc vs runc 对比#

功能miniruncrunc
Namespace5 种8 种
Cgroupv2 基础v1 + v2 完整
OverlayFS基础挂载完整管理
安全seccomp + AppArmor + Capabilities
网络完整支持
代码量~500 行~100,000 行
flowchart TB CLI["minirunc CLI"] --> PARSE["解析 OCI Bundle"] --> SETUP["设置 Namespace"] SETUP --> CG2["配置 Cgroup"] --> MOUNT["挂载 OverlayFS"] --> EXEC2["执行容器进程"] style CLI fill:#bbdefb,stroke:#1565c0 style EXEC2 fill:#c8e6c9,stroke:#2e7d32
graph TB subgraph 容器隔离 PID["PID Namespace<br/>进程隔离"] MNT["Mount Namespace<br/>文件系统隔离"] UTS["UTS Namespace<br/>主机名隔离"] IPC["IPC Namespace<br/>通信隔离"] NET2["Network Namespace<br/>网络隔离"] end style PID fill:#bbdefb,stroke:#1565c0 style MNT fill:#c8e6c9,stroke:#2e7d32
flowchart LR LOWER["Lower Layer<br/>只读基础层"] --> OVERLAY["OverlayFS<br/>合并视图"] UPPER["Upper Layer<br/>可写层"] --> OVERLAY OVERLAY --> CONTAINER["容器视图<br/>完整文件系统"] style OVERLAY fill:#fff9c4,stroke:#f9a825 style CONTAINER fill:#c8e6c9,stroke:#2e7d32
Warning

本实战项目仅用于学习容器运行时原理。minirunc 缺少安全加固(seccomp/AppArmor/Capabilities 限制),切勿用于生产环境。生产级容器运行时需要 runc/gVisor/Kata Containers 等经过安全审计的实现。

十一、本章小结#

上一章了解了容器在 Kubernetes 中的运行。

通过构建 minirunc,前 15 章的知识融会贯通:

主题核心要点关键词
Namespaceclone()CLONE_NEW* 标志创建隔离的进程视图clone, CLONE_NEW*
Cgroup通过写入 cgroup 文件系统设置资源限制cgroup v2, 资源限制
OverlayFSmount() 系统调用挂载分层文件系统mount, lowerdir/upperdir
OCI Bundle解析 config.json 获取容器配置config.json, rootfs
pivot_root切换容器的根文件系统pivot_root, chroot
生命周期实现 create → start → kill → delete 的标准流程create, start, delete
Note

minirunc 是一个教学项目,不适合生产使用。但它让你理解了容器运行时的核心逻辑——容器进程 = Linux 进程 + Namespace + Cgroup + OverlayFS。runc、containerd、gVisor、Kata 都是在这个基础上添加更多的功能和安全保障。


恭喜你完成了「容器运行时深入」系列!从这个系列的第一章到这里的综合实战,你已经从「会用 Docker」进阶到了「理解容器运行时的每一行代码」。无论你是要排查容器问题、优化容器性能、还是构建自定义运行时,这些知识都是你的坚实基础。

支持与分享

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

综合实战:构建一个迷你容器运行时
https://blog.souloss.com/posts/container-runtime/container-runtime-hands-on-practice/
作者
Souloss
发布于
2026-06-17
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
镜像构建:BuildKit
容器运行时 BuildKit 是 Docker 的新一代镜像构建引擎,支持并行构建、缓存导入导出、多阶段构建优化、secret 挂载等高级特性。从零讲透 BuildKit 的架构设计、Dockerfile 指令的执行原理、多阶段构建、缓存策略、以及如何编写高效的 Dockerfile——从「会写 Dockerfile」到「理解每一条指令的构建机制」。
2
沙箱运行时:gVisor/Kata
容器运行时 runc 的 Namespace+Cgroup 隔离依赖共享内核,内核漏洞可导致容器逃逸。gVisor 用用户态内核(Sentry)拦截所有系统调用,Kata Containers 用轻量级虚拟机实现硬件级隔离。深入探讨两种沙箱运行时的架构、实现原理、与 runc 的对比选型,以及它们在 Kubernetes 中的集成方式。
3
系列导读
容器运行时 本系列从 Linux 内核的 Namespace、Cgroup、OverlayFS 出发,深入 OCI 规范、runc 源码、containerd 架构,再到容器安全、沙箱运行时、网络、存储、镜像构建、Wasm 容器,最后综合实战构建一个迷你容器运行时——从「会用 Docker」到「理解容器运行时的每一行代码」,每章配有可运行的代码示例与架构图,让你从容器用户进阶到容器运行时工程师。
4
容器网络
容器运行时 容器网络的核心问题是——隔离的 Network Namespace 如何与外部通信?详细解读 veth pair(虚拟网卡对)、bridge(虚拟网桥)、iptables/NAT(地址转换)、CNI(容器网络接口)的完整链路,以及 Docker 的四种网络模式和 Kubernetes 的 Pod 网络模型——从「容器能 ping 通外网」到「理解每一条网络规则」。
5
Wasm 容器:WasmEdge/WASI
容器运行时 WebAssembly(Wasm)正在从浏览器走向服务器——WASI(WebAssembly System Interface)定义了 Wasm 访问操作系统的标准接口,WasmEdge/runwasi 等运行时让 Wasm 模块可以作为容器运行。详细解读 Wasm 容器的架构、WASI 接口、与 OCI 的集成、与 Linux 容器的对比——从「Wasm 只能在浏览器运行」到「Wasm 是下一代容器运行时」。