mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1498 字
4 分钟
containerd-shim
2026-05-06

一个令人困惑的问题:为什么 containerd 不直接调用 runc,而要在中间加一层 shim?答案涉及三个关键场景:

  1. 升级安全:containerd 升级重启时,运行中的容器不受影响
  2. 进程监督:容器进程有独立的父进程,不会因为 containerd 崩溃而变成孤儿
  3. runc 退出:runc 在创建容器后可以退出,不需要一直运行

shim 的诞生源于一个运维痛点。在早期 Docker 架构中,containerd 直接调用 runc 管理容器进程,runc 作为 containerd 的子进程持续运行。这意味着 containerd 升级重启时,所有容器进程的父进程关系会断裂——容器要么变成孤儿进程,要么跟着重启。2016 年,containerd 引入 shim 作为”中间人”:shim 作为容器进程的父进程独立运行,containerd 通过 gRPC 与 shim 通信。这样,containerd 升级重启不影响运行中的容器,runc 在创建容器后也可以退出。shim 用极小的代价(一个轻量级守护进程)换来了巨大的运维收益——这是容器运行时演进中”用架构换可靠性”的经典案例。

前置知识#

  • Ch07 containerd 架构:shim 是 containerd 的子组件,理解 containerd 的整体架构是前提
  • Ch06 runc 源码分析:shim 调用 runc 创建容器,理解 runc 的生命周期管理
  • Linux 进程关系:父进程、孤儿进程、僵尸进程、prctl(PR_SET_CHILD_SUBREAPER)
Note

shim 的设计思想——“用独立进程解耦父子关系”——在系统编程中并不罕见。systemd 的 socket activation、SSH 的代理跳转,都采用了类似的解耦模式。

一、为什么需要 Shim#

1.1 没有 Shim 的问题#

假设 containerd 直接调用 runc,不经过 shim:

graph TB subgraph 没有Shim["没有 Shim 的问题"] CTNRD1["containerd<br/>PID 1000"] RUNC1["runc<br/>PID 2000(持续运行)"] APP1["容器进程<br/>PID 3000"] CTNRD1 -->|"直接调用"| RUNC1 RUNC1 -->|"父进程"| APP1 PROBLEM1["containerd 重启 → runc 可能受影响"] PROBLEM2["runc 必须持续运行 → 资源浪费"] PROBLEM3["containerd 崩溃 → 容器变孤儿进程"] end style 没有Shim fill:#ffcdd2,stroke:#c62828

1.2 有 Shim 的解决方案#

graph TB subgraph 有Shim["有 Shim 的解决方案"] CTNRD2["containerd<br/>PID 1000"] SHIM2["containerd-shim<br/>PID 2000"] RUNC2["runc<br/>PID 3000(创建后退出)"] APP2["容器进程<br/>PID 4000"] CTNRD2 -->|"fork + exec"| SHIM2 SHIM2 -->|"exec runc create"| RUNC2 RUNC2 -.->|"创建后退出"| EXIT["runc 退出"] SHIM2 -->|"父进程"| APP2 BENEFIT1["containerd 重启 → shim 不受影响"] BENEFIT2["runc 创建后退出 → 不浪费资源"] BENEFIT3["shim 监督容器 → 不会变孤儿"] end style 有Shim fill:#c8e6c9,stroke:#2e7d32

1.3 Shim 的核心价值#

特性无 shim(旧版 Docker)shim v1shim v2 (shim-runc-v2)
daemon 重启影响所有容器退出容器继续运行容器继续运行
shim 进程模型每个容器一个 shim每个容器一个 shim
多容器支持N/A每个容器独立 shim同一 shim 管理多个容器
资源开销每容器 ~5MB每容器 ~2MB(共享)
Tip

containerd 1.6+ 默认使用 shim v2。在 Kubernetes 中运行大量短生命周期容器时,shim v2 的共享进程模型可以显著减少内存开销。

问题没有 Shim有 Shim
containerd 升级影响所有容器容器不受影响
containerd 崩溃容器变孤儿进程shim 继续监督
runc 生命周期必须持续运行创建后可退出
容器 stdout/stderr需要容器运行时处理shim 负责收集
容器退出状态需要容器运行时等待shim 负责收集

二、Shim 的进程关系#

2.1 完整的进程树#

# 查看容器相关的进程树
pstree -p $(pgrep containerd) | head -30
# 典型输出:
# containerd(1000)───containerd-shim(2000)───nginx(3000)
# └─nginx(3001)
# ───containerd-shim(4000)───redis(5000)
# ───containerd-shim(6000)───myapp(7000)

2.2 进程关系详解#

graph TB subgraph 宿主机进程树["宿主机进程树"] SYSTEMD["systemd<br/>PID 1"] CTNRD["containerd<br/>PID 1000"] SHIM1["containerd-shim<br/>PID 2000<br/>(容器 A)"] SHIM2["containerd-shim<br/>PID 4000<br/>(容器 B)"] APP1["nginx<br/>PID 3000"] APP2["redis<br/>PID 5000"] end SYSTEMD --> CTNRD CTNRD --> SHIM1 CTNRD --> SHIM2 SHIM1 --> APP1 SHIM2 --> APP2 subgraph 关键特性["关键特性"] F1["1. shim 是容器的父进程"] F2["2. containerd 通过 gRPC 与 shim 通信"] F3["3. containerd 重启后重新连接 shim"] F4["4. shim 收集容器退出状态"] end style 宿主机进程树 fill:#e8eaf6,stroke:#283593 style 关键特性 fill:#c8e6c9,stroke:#2e7d32

2.3 Shim 与 runc 的交互#

# shim 调用 runc 的时序:
# 1. shim 启动后,调用 runc create 创建容器
# 2. runc create 完成后,runc 进程退出
# 3. 容器进程由 shim 监督
# 4. 当需要启动容器时,shim 调用 runc start
# 5. runc start 完成后,runc 进程退出
# 查看 shim 的命令行参数
ps aux | grep containerd-shim
# containerd-shim -namespace default -id mynginx -address /run/containerd/containerd.sock

三、Shim API#

3.1 Task Service API#

shim 通过 gRPC 暴露 Task Service API,containerd 通过这个 API 管理容器:

方法功能对应 runc 命令
Create创建容器runc create
Start启动容器runc start
Kill发送信号runc kill
Delete删除容器runc delete
State查询状态runc state
Exec执行命令runc exec
Pids获取 PID
ResizePty调整终端大小
CloseIO关闭 IO
Checkpoint检查点runc checkpoint
Restore恢复runc restore
Update更新资源runc update
Wait等待退出
Stats获取统计

3.2 Shim 的 gRPC 通信#

// shim 的 gRPC 服务定义(简化)
service Task {
rpc Create(CreateTaskRequest) returns (CreateTaskResponse);
rpc Start(StartRequest) returns (StartResponse);
rpc Kill(KillRequest) returns (google.protobuf.Empty);
rpc Delete(DeleteRequest) returns (DeleteResponse);
rpc State(StateRequest) returns (StateResponse);
rpc Exec(ExecRequest) returns (ExecResponse);
rpc Pids(PidsRequest) returns (PidsResponse);
rpc Wait(WaitRequest) returns (WaitResponse);
rpc Stats(StatsRequest) returns (StatsResponse);
}
message CreateTaskRequest {
string id = 1;
string bundle = 2;
repeated Mount rootfs = 3;
bool terminal = 4;
string stdin = 5;
string stdout = 6;
string stderr = 7;
bool checkpoint = 8;
string parent_checkpoint = 9;
}

3.3 containerd 与 shim 的通信流程#

sequenceDiagram participant CTNRD as containerd participant SHIM as containerd-shim participant RUNC as runc participant APP as 容器进程 Note over CTNRD,APP: 容器创建 CTNRD->>SHIM: fork + exec shim SHIM-->>CTNRD: shim 启动成功 CTNRD->>SHIM: gRPC: Create(bundle, rootfs) SHIM->>RUNC: exec: runc create --bundle <path> <id> RUNC->>APP: clone(CLONE_NEWPID|...) + exec RUNC-->>SHIM: 容器已创建(runc 退出) SHIM-->>CTNRD: gRPC: CreateResponse{pid} Note over CTNRD,APP: 容器启动 CTNRD->>SHIM: gRPC: Start(id) SHIM->>RUNC: exec: runc start <id> RUNC-->>SHIM: 容器已启动(runc 退出) SHIM-->>CTNRD: gRPC: StartResponse{pid} Note over CTNRD,APP: 容器退出 APP-->>SHIM: 进程退出(wait 获取退出码) SHIM->>CTNRD: 事件: TaskExit{exit_status}

四、Shim 的升级安全机制#

4.1 containerd 重启后的恢复#

当 containerd 重启时,它需要重新连接所有运行中的 shim:

// containerd 重启后的 shim 恢复(简化)
func (m *TaskManager) LoadTasks(ctx context.Context) error {
// 1. 扫描 shim 的 socket 文件
shimSockets, err := filepath.Glob("/run/containerd/s/*.sock")
// 2. 逐个连接 shim
for _, socket := range shimSockets {
shim, err := shim.Connect(ctx, socket)
if err != nil {
continue // shim 可能已退出
}
// 3. 查询 shim 管理的容器状态
state, err := shim.State(ctx, &task.StateRequest{})
if err != nil {
continue
}
// 4. 恢复 Task 对象
m.tasks[state.ID] = &Task{
shim: shim,
id: state.ID,
pid: state.Pid,
status: state.Status,
}
}
return nil
}

4.2 Shim 的 Socket 文件#

每个 shim 在 /run/containerd/s/ 目录下创建一个 Unix socket 文件:

# 查看 shim 的 socket 文件
ls /run/containerd/s/
# 输出示例:
# 5a3b2c1d4e5f.sock
# 7f8e9d0c1b2a.sock
# socket 文件名与容器 ID 对应
# containerd 重启后通过这些 socket 重新连接 shim

4.3 升级安全的保证#

flowchart TB subgraph 升级前["升级前"] CTNRD1["containerd v1.7"] SHIM1A["shim (容器 A)"] SHIM1B["shim (容器 B)"] APP1A["nginx"] APP1B["redis"] CTNRD1 --> SHIM1A CTNRD1 --> SHIM1B SHIM1A --> APP1A SHIM1B --> APP1B end subgraph 升级中["升级中"] CTNRD_STOP["containerd 停止"] SHIM2A["shim (容器 A)<br/>继续运行"] SHIM2B["shim (容器 B)<br/>继续运行"] APP2A["nginx<br/>继续运行"] APP2B["redis<br/>继续运行"] SHIM2A --> APP2A SHIM2B --> APP2B end subgraph 升级后["升级后"] CTNRD3["containerd v1.8"] SHIM3A["shim (容器 A)<br/>重新连接"] SHIM3B["shim (容器 B)<br/>重新连接"] APP3A["nginx<br/>继续运行"] APP3B["redis<br/>继续运行"] CTNRD3 -->|"重新连接 socket"| SHIM3A CTNRD3 -->|"重新连接 socket"| SHIM3B SHIM3A --> APP3A SHIM3B --> APP3B end 升级前 --> 升级中 --> 升级后 style 升级前 fill:#bbdefb,stroke:#1565c0 style 升级中 fill:#fff3e0,stroke:#e65100 style 升级后 fill:#c8e6c9,stroke:#2e7d32

五、Shim v1 vs v2#

5.1 架构对比#

维度Shim v1Shim v2
进程模型每个容器一个 shim每个容器一个 shim
通信方式gRPC over Unix socketttrpc over Unix socket
二进制containerd-shim自定义 shim 二进制
运行时支持仅 runcrunc/gVisor/Kata/Wasm
配置方式命令行参数containerd 配置文件
IO 处理FIFO + 外部进程内置 FIFO 处理

5.2 Shim v2 的改进#

Shim v2 的最大改进是支持自定义运行时——每个运行时可以提供自己的 shim 二进制:

# containerd 配置:注册自定义运行时
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1" # gVisor shim
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
runtime_type = "io.containerd.kata.v2" # Kata shim

5.3 自定义 Shim 的接口#

// shim v2 的接口定义(简化)
type Shim interface {
// 启动 shim
Start(ctx context.Context, id string, opts StartOpts) (Shim, error)
// 容器操作
Create(ctx context.Context, task *TaskConfig) (Task, error)
Delete(ctx context.Context, id string) error
// 生命周期
Wait(ctx context.Context, id string) (uint32, error)
Kill(ctx context.Context, id string, signal syscall.Signal) error
// IO
ResizePty(ctx context.Context, id string, size ConsoleSize) error
CloseIO(ctx context.Context, id string) error
// 清理
Shutdown(ctx context.Context) error
}

六、Shim 的 IO 处理#

6.1 容器 IO 的路径#

flowchart LR subgraph 容器进程["容器进程"] STDIN["stdin"] STDOUT["stdout"] STDERR["stderr"] end subgraph Shim["containerd-shim"] FIFO_IN["FIFO: stdin"] FIFO_OUT["FIFO: stdout"] FIFO_ERR["FIFO: stderr"] IO_COPY["IO 转发"] end subgraph 客户端["客户端"] CLIENT_STDIN["stdin"] CLIENT_STDOUT["stdout"] CLIENT_STDERR["stderr"] end CLIENT_STDIN --> FIFO_IN --> STDIN STDOUT --> FIFO_OUT --> IO_COPY --> CLIENT_STDOUT STDERR --> FIFO_ERR --> IO_COPY --> CLIENT_STDERR style 容器进程 fill:#ffcdd2,stroke:#c62828 style Shim fill:#c8e6c9,stroke:#2e7d32 style 客户端 fill:#bbdefb,stroke:#1565c0

6.2 FIFO 文件的位置#

# 查看容器的 FIFO 文件
ls /run/containerd/fifo/
# 输出示例:
# 5a3b2c1d4e5f-stdin
# 5a3b2c1d4e5f-stdout
# 5a3b2c1d4e5f-stderr
# shim 负责打开这些 FIFO 并转发 IO
Warning

shim 进程以 root 权限运行,且每个容器对应一个 shim。如果攻击者通过容器逃逸获得了 shim 进程的控制权,可以利用 shim 的 gRPC 接口操作该容器(kill、exec 等)。在安全要求高的场景下,可以考虑使用 User Namespace 将 shim 也放入非特权上下文中运行。

七、动手实践#

7.1 观察 Shim 的行为#

#!/bin/bash
# 观察 containerd-shim 的行为
# 1. 启动一个容器
ctr run -d docker.io/library/nginx:alpine mynginx /bin/sh -c "while true; do echo hello; sleep 1; done"
# 2. 查看 shim 进程
ps aux | grep "containerd-shim.*mynginx"
# 3. 查看 shim 的 socket
ls -la /run/containerd/s/
# 4. 查看容器的 FIFO
ls -la /run/containerd/fifo/
# 5. 重启 containerd
sudo systemctl restart containerd
# 6. 验证容器仍在运行
ctr tasks list
# TASK PID STATUS
# mynginx 12345 RUNNING ← 容器未受影响
# 7. 查看 shim 进程(新的 PID)
ps aux | grep "containerd-shim.*mynginx"
# 8. 清理
ctr tasks kill mynginx
ctr containers delete mynginx

7.2 用 strace 追踪 Shim 与 runc 的交互#

#!/bin/bash
# 追踪 shim 调用 runc 的过程
# 1. 找到 shim 进程
SHIM_PID=$(pgrep -f "containerd-shim.*mynginx")
# 2. 追踪 shim 的子进程(runc)
sudo strace -f -p $SHIM_PID -e trace=execve,clone,wait4 -o /tmp/shim-strace.log &
# 3. 通过 containerd 操作容器
ctr tasks kill mynginx SIGUSR1
# 4. 查看追踪结果
grep "execve" /tmp/shim-strace.log
# 应该能看到 shim 调用 runc kill 的记录

八、本章小结#

上一章了解了containerd 的架构设计。

特性说明
解耦shim 让 containerd 与容器进程解耦
升级安全containerd 升级重启不影响运行中的容器
进程监督shim 是容器进程的父进程,负责收集退出状态
IO 转发shim 通过 FIFO 转发容器的 stdin/stdout/stderr
自定义运行时shim v2 支持自定义 shim 二进制(gVisor/Kata/Wasm)
通信方式shim 通过 gRPC/ttrpc 与 containerd 通信
Note

shim 是 containerd 架构中最容易被忽视但最重要的组件。没有 shim,containerd 的升级安全就无法保证,容器的 IO 处理会变得复杂,自定义运行时的支持也会受限。


扩展阅读#


参考#

支持与分享

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

containerd-shim
https://blog.souloss.com/posts/container-runtime/containerd-shim/
作者
Souloss
发布于
2026-05-06
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
containerd 架构
容器运行时 containerd 是工业级容器运行时管理器,是 Docker 和 Kubernetes 的核心依赖。从零讲透 containerd 的架构设计——gRPC API 服务、镜像管理(pull/unpack/mount)、任务管理(create/start/kill)、插件体系、事件系统,以及 containerd 如何在 runc 之上构建完整的容器生命周期管理能力。
2
容器在 Kubernetes 中
容器运行时 深入容器在 Kubernetes 中的运行机制——CRI 接口、Pod Sandbox 创建、多容器模式、Init Container、Sidecar 注入,以及 kubelet 如何通过 CRI 与容器运行时交互。
3
系列导读
容器运行时 本系列从 Linux 内核的 Namespace、Cgroup、OverlayFS 出发,深入 OCI 规范、runc 源码、containerd 架构,再到容器安全、沙箱运行时、网络、存储、镜像构建、Wasm 容器,最后综合实战构建一个迷你容器运行时——从「会用 Docker」到「理解容器运行时的每一行代码」,每章配有可运行的代码示例与架构图,让你从容器用户进阶到容器运行时工程师。
4
Wasm 容器:WasmEdge/WASI
容器运行时 WebAssembly(Wasm)正在从浏览器走向服务器——WASI(WebAssembly System Interface)定义了 Wasm 访问操作系统的标准接口,WasmEdge/runwasi 等运行时让 Wasm 模块可以作为容器运行。详细解读 Wasm 容器的架构、WASI 接口、与 OCI 的集成、与 Linux 容器的对比——从「Wasm 只能在浏览器运行」到「Wasm 是下一代容器运行时」。
5
综合实战:构建一个迷你容器运行时
容器运行时 综合实战——用 Go 从零构建一个迷你容器运行时——实现 Namespace 隔离(PID/Mount/UTS/IPC/Network)、Cgroup 资源限制(CPU/内存)、OverlayFS 分层文件系统、OCI Bundle 解析,最终实现一个能运行容器的 minirunc。将前 15 章的知识融会贯通,从「理解原理」到「动手实现」。