Runc 深度解析:从原理到实操
一、Runc 是什么?
1.1 定义
Runc 是 OCI(Open Container Initiative)运行时规范的参考实现 ,是容器技术栈中最底层的组件,负责真正创建和运行容器进程。
┌─────────────────────────────────────────────────────────────┐
│ 容器技术栈 │
│ │
│ 用户命令:docker run / ctr run / kubectl run │
│ ↓ │
│ 高级运行时:Docker Daemon / Containerd │
│ ↓ │
│ 底层运行时:Runc ← 你在这里 │
│ ↓ │
│ Linux 内核:namespace + cgroup + 文件系统 │
│ ↓ │
│ 容器进程 │
└─────────────────────────────────────────────────────────────┘
1.2 一句话理解
Runc 是"容器的创建者",它读取配置文件,调用 Linux 系统调用,创建出一个隔离的进程,这就是容器。
1.3 历史演变
┌─────────────────────────────────────────────────────────────┐
│ Runc 的诞生 │
│ │
│ 2013年 │
│ Docker 发布,使用 LXC 作为运行时 │
│ 问题:LXC 不是 Docker 自己的,依赖外部项目 │
│ │
│ 2014年 │
│ Docker 开发 libcontainer,自己的运行时库 │
│ libcontainer 直接调用 Linux 系统调用 │
│ │
│ 2015年 │
│ OCI(Open Container Initiative)成立 │
│ 目标:制定容器格式和运行时的行业标准 │
│ │
│ Docker 将 libcontainer 重构为 runc │
│ runc 成为 OCI 运行时规范的第一个参考实现 │
│ │
│ 现在 │
│ runc 是行业标准,所有主流容器运行时都基于 runc 或兼容其接口 │
└─────────────────────────────────────────────────────────────┘
二、OCI 运行时规范
2.1 什么是 OCI?
OCI(Open Container Initiative) 是 Linux 基金会下的项目,制定容器行业的两大标准:
┌─────────────────────────────────────────────────────────────┐
│ OCI 两大规范 │
│ │
│ 1. 镜像规范(Image Spec) │
│ 定义容器镜像的格式 │
│ ├── 镜像清单(manifest) │
│ ├── 镜像层(layers) │
│ └── 配置(config) │
│ │
│ 2. 运行时规范(Runtime Spec) │
│ 定义如何运行容器 │
│ ├── 容器配置格式(config.json) │
│ ├── 运行时接口(create、start、delete 等) │
│ └── 容器生命周期 │
└─────────────────────────────────────────────────────────────┘
2.2 运行时规范核心内容
OCI 运行时规范定义了:
| 规范内容 | 说明 |
|---|---|
| config.json | 容器配置文件格式 |
| 命令接口 | create、start、state、kill、delete 等 |
| 容器状态 | created、running、paused、stopped |
| 钩子机制 | prestart、poststart、poststop |
| 命名空间 | 使用哪些 Linux namespace |
| 资源限制 | cgroup 配置 |
2.3 Runc 与 OCI 的关系
┌─────────────────────────────────────────────────────────────┐
│ OCI 运行时规范 │
│ (一份文档,定义接口和格式) │
│ │
│ 实现者: │
│ ├── runc ← Docker 的实现,最主流 │
│ ├── crun ← C 语言实现,更快更小 │
│ ├── kata ← 基于虚拟机的实现 │
│ └── gvisor ← Google 的沙箱实现 │
│ │
│ 所有实现都遵循相同的接口,可以互相替换 │
└─────────────────────────────────────────────────────────────┘
三、Runc 的工作原理
3.1 核心流程
┌─────────────────────────────────────────────────────────────┐
│ runc create 容器流程 │
│ │
│ 1. 读取 config.json │
│ ↓ │
│ 2. 创建容器目录(bundle) │
│ ↓ │
│ 3. clone() 系统调用,创建新进程 │
│ ├── 设置 namespace(隔离) │
│ ├── 设置 cgroup(资源限制) │
│ ├── 设置 rootfs(文件系统) │
│ └── 设置 capabilities(权限) │
│ ↓ │
│ 4. 写入容器状态到 state.json │
│ ↓ │
│ 5. 容器进入 "created" 状态 │
│ │
│ runc start 容器流程 │
│ ↓ │
│ 6. 执行用户命令 │
│ ↓ │
│ 7. 容器进入 "running" 状态 │
└─────────────────────────────────────────────────────────────┘
3.2 涉及的 Linux 技术
| 技术 | 作用 | Runc 中的使用 |
|---|---|---|
| namespace | 资源隔离 | 隔离 PID、网络、挂载点等 |
| cgroup | 资源限制 | 限制 CPU、内存、IO 等 |
| pivot_root | 切换根文件系统 | 切换到容器的 rootfs |
| clone() | 创建新进程 | 创建容器进程 |
| execve() | 执行程序 | 执行容器内的命令 |
| capabilities | 权限控制 | 限制容器权限 |
| seccomp | 系统调用过滤 | 限制容器可用的系统调用 |
| AppArmor/SELinux | 强制访问控制 | 增强安全性 |
3.3 Namespace 隔离详解
bash
# Runc 创建容器时设置的 namespace
# PID namespace:容器有自己的进程树
# 容器内 PID 1 的进程,在宿主机可能是 PID 12345
# Network namespace:容器有自己的网络栈
# 容器有自己的网卡、路由表、iptables
# Mount namespace:容器有自己的文件系统视图
# 容器内的挂载不影响宿主机
# UTS namespace:容器有自己的主机名
# 容器可以设置自己的 hostname
# IPC namespace:容器有自己的 IPC 资源
# 容器内的信号量、消息队列隔离
# User namespace:容器有自己的用户映射
# 容器内 root 可以映射到宿主机普通用户
四、Runc 安装与验证
4.1 安装方式
方式一:包管理器安装
bash
# Ubuntu/Debian
apt-get update
apt-get install runc
# CentOS/RHEL
yum install runc
# 验证安装
runc --version
方式二:二进制安装
bash
# 下载最新版本
wget https://github.com/opencontainers/runc/releases/download/v1.1.10/runc.amd64
# 安装
install -m 755 runc.amd64 /usr/local/sbin/runc
# 验证
runc --version
# 预期输出:
# runc version 1.1.10
# spec: 1.0.2-dev
# go: go1.20.10
4.2 验证安装
bash
# 查看版本
runc --version
# 查看帮助
runc --help
# 查看支持的命令
runc help
五、实操:手动创建容器
5.1 准备工作:创建 Bundle
Bundle 是 OCI 运行时规范定义的容器包,包含:
┌─────────────────────────────────────────────────────────────┐
│ Bundle 结构 │
│ │
│ mycontainer/ │
│ ├── config.json ← 容器配置文件(必须) │
│ └── rootfs/ ← 容器根文件系统(必须) │
│ ├── bin/ │
│ ├── dev/ │
│ ├── etc/ │
│ └── ... │
└─────────────────────────────────────────────────────────────┘
5.2 步骤一:创建目录结构
bash
# 创建 bundle 目录
mkdir -p mycontainer/rootfs
# 进入目录
cd mycontainer
5.3 步骤二:准备 rootfs
方法一:使用 Docker 导出
bash
# 拉取最小镜像
docker pull alpine:latest
# 导出 rootfs
docker export $(docker create alpine) | tar -xf - -C rootfs/
# 查看 rootfs 内容
ls rootfs/
# 输出示例:
# bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
方法二:使用 Containerd 导出
bash
# 拉取镜像
ctr images pull docker.io/library/alpine:latest
# 导出镜像
ctr images export alpine.tar docker.io/library/alpine:latest
# 解压镜像(需要解压两层)
mkdir -p temp
tar -xf alpine.tar -C temp
# 找到 layer 并解压到 rootfs
# (具体步骤取决于镜像格式)
5.4 步骤三:生成配置文件
bash
# 使用 runc 生成默认配置
runc spec
# 查看生成的 config.json
cat config.json
5.5 步骤四:理解 config.json
config.json 是容器的"说明书",告诉 runc 如何创建容器:
json
{
"ociVersion": "1.0.2",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
"capabilities": {
"bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"effective": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"inheritable": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"permitted": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"]
}
},
"root": {
"path": "rootfs",
"readonly": false
},
"hostname": "runc",
"mounts": [
{
"destination": "/proc",
"type": "proc",
"source": "proc"
}
],
"linux": {
"namespaces": [
{
"type": "pid"
},
{
"type": "network"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
}
]
}
}
关键字段详解:
┌─────────────────────────────────────────────────────────────┐
│ config.json 关键字段 │
│ │
│ ociVersion OCI 规范版本 │
│ │
│ process 容器进程配置 │
│ ├── terminal 是否分配终端 │
│ ├── user 运行用户(uid/gid) │
│ ├── args 执行命令(["sh"] 表示运行 shell) │
│ ├── env 环境变量 │
│ ├── cwd 工作目录 │
│ └── capabilities Linux 权限能力 │
│ │
│ root 根文件系统配置 │
│ ├── path rootfs 路径 │
│ └── readonly 是否只读 │
│ │
│ hostname 容器主机名 │
│ │
│ mounts 挂载点列表 │
│ ├── destination 容器内挂载路径 │
│ ├── type 文件系统类型 │
│ └── source 宿主机源路径 │
│ │
│ linux Linux 特定配置 │
│ ├── namespaces 命名空间配置 │
│ ├── resources cgroup 资源限制 │
│ └── seccomp 系统调用过滤 │
└─────────────────────────────────────────────────────────────┘
5.6 步骤五:创建并启动容器
bash
# 确保在 bundle 目录下
cd mycontainer
# 创建容器(进入 created 状态)
runc create mycontainer
# 查看容器状态
runc state mycontainer
# 输出示例:
# {
# "ociVersion": "1.0.2",
# "id": "mycontainer",
# "status": "created",
# "bundle": "/root/mycontainer",
# "created": "2024-01-01T10:00:00.000000000Z"
# }
# 启动容器(进入 running 状态)
runc start mycontainer
# 此时容器运行 sh,你可以交互
# 输入命令测试:
# ls /
# hostname
# ps aux
# 退出容器
# exit
5.7 容器生命周期管理
bash
# ─────────────────────────────────────────
# 查看容器状态
# ─────────────────────────────────────────
runc state mycontainer
# ─────────────────────────────────────────
# 列出所有容器
# ─────────────────────────────────────────
runc list
# 输出示例:
# ID PID STATUS BUNDLE CREATED
# mycontainer 12345 running /root/mycontainer 2024-01-01T10:00:00Z
# ─────────────────────────────────────────
# 暂停容器
# ─────────────────────────────────────────
runc pause mycontainer
# 查看状态(变为 paused)
runc state mycontainer
# "status": "paused"
# 恢复容器
runc resume mycontainer
# ─────────────────────────────────────────
# 发送信号到容器
# ─────────────────────────────────────────
# 发送 SIGTERM
runc kill mycontainer
# 发送 SIGKILL(强制终止)
runc kill mycontainer SIGKILL
# ─────────────────────────────────────────
# 删除容器
# ─────────────────────────────────────────
# 容器停止后才能删除
runc delete mycontainer
# 确认删除
runc list
# (无输出)
5.8 完整实操流程图
┌─────────────────────────────────────────────────────────────┐
│ Runc 手动创建容器完整流程 │
│ │
│ 1. 创建 bundle 目录 │
│ mkdir -p mycontainer/rootfs │
│ cd mycontainer │
│ │
│ 2. 准备 rootfs │
│ docker export $(docker create alpine) | tar -xf - -C rootfs/│
│ │
│ 3. 生成配置文件 │
│ runc spec │
│ │
│ 4. 创建容器 │
│ runc create mycontainer │
│ 状态:created │
│ │
│ 5. 启动容器 │
│ runc start mycontainer │
│ 状态:running │
│ │
│ 6. 与容器交互 │
│ (容器内运行 sh,可输入命令) │
│ │
│ 7. 停止容器 │
│ runc kill mycontainer │
│ 状态:stopped │
│ │
│ 8. 删除容器 │
│ runc delete mycontainer │
└─────────────────────────────────────────────────────────────┘
六、Runc 命令详解
6.1 命令概览
bash
runc --help
# 可用命令:
# create 创建容器
# start 启动容器
# run 创建并启动容器(一步完成)
# delete 删除容器
# state 查看容器状态
# list 列出所有容器
# pause 暂停容器
# resume 恢复容器
# kill 发送信号到容器
# exec 在运行中的容器内执行命令
# spec 生成默认配置文件
# events 显示容器事件
# update 更新容器资源限制
# ps 显示容器内进程
6.2 create vs run
bash
# 方式一:分步执行(create + start)
runc create mycontainer
runc start mycontainer
# 方式二:一步完成(run)
runc run mycontainer
# 区别:
# create + start:可以在 created 状态做些操作(如设置网络)
# run:一步到位,适合简单场景
6.3 exec - 在容器内执行命令
bash
# 先启动一个后台容器
# 修改 config.json,设置 process.terminal = false
# process.args = ["sleep", "3600"]
runc run -d mycontainer
# 在容器内执行命令
runc exec mycontainer ls /
# 在容器内执行交互式命令
runc exec -t mycontainer sh
# 查看 exec 命令帮助
runc exec --help
6.4 update - 动态更新资源限制
bash
# 更新 CPU 限制
runc update --cpu-quota 50000 mycontainer
# 更新内存限制
runc update --memory 536870912 mycontainer # 512MB
# 通过 JSON 文件更新
cat > resources.json << EOF
{
"cpu": {
"quota": 50000,
"period": 100000
},
"memory": {
"limit": 536870912
}
}
EOF
runc update -r resources.json mycontainer
6.5 events - 监控容器事件
bash
# 实时监控容器事件
runc events mycontainer
# 输出示例:
# {"type":"stats","id":"mycontainer","data":{"cpu":{"usage":{"total":12345678}}}}
七、深入 config.json 配置
7.1 完整配置示例
json
{
"ociVersion": "1.0.2",
"process": {
"terminal": false,
"user": {
"uid": 0,
"gid": 0,
"additionalGids": [10, 100]
},
"args": [
"/bin/sh",
"-c",
"echo 'Hello from container' && sleep 3600"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm",
"HOSTNAME=runc-container",
"MY_VAR=custom_value"
],
"cwd": "/root",
"capabilities": {
"bounding": [
"CAP_AUDIT_WRITE",
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FOWNER",
"CAP_FSETID",
"CAP_KILL",
"CAP_MKNOD",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SETFCAP",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETUID",
"CAP_SYS_CHROOT"
],
"effective": [
"CAP_AUDIT_WRITE",
"CAP_CHOWN",
"CAP_DAC_OVERRIDE"
],
"inheritable": [],
"permitted": [
"CAP_AUDIT_WRITE",
"CAP_CHOWN"
]
},
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"noNewPrivileges": true
},
"root": {
"path": "rootfs",
"readonly": false
},
"hostname": "runc-container",
"mounts": [
{
"destination": "/proc",
"type": "proc",
"source": "proc",
"options": ["nosuid", "noexec", "nodev"]
},
{
"destination": "/dev",
"type": "tmpfs",
"source": "tmpfs",
"options": ["nosuid", "strictatime", "mode=755", "size=65536k"]
},
{
"destination": "/dev/pts",
"type": "devpts",
"source": "devpts",
"options": ["nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"]
},
{
"destination": "/dev/shm",
"type": "tmpfs",
"source": "shm",
"options": ["nosuid", "noexec", "nodev", "mode=1777", "size=65536k"]
},
{
"destination": "/dev/mqueue",
"type": "mqueue",
"source": "mqueue",
"options": ["nosuid", "noexec", "nodev"]
},
{
"destination": "/sys",
"type": "sysfs",
"source": "sysfs",
"options": ["nosuid", "noexec", "nodev", "ro"]
},
{
"destination": "/etc/resolv.conf",
"type": "bind",
"source": "/etc/resolv.conf",
"options": ["ro", "bind"]
},
{
"destination": "/etc/hosts",
"type": "bind",
"source": "/etc/hosts",
"options": ["ro", "bind"]
}
],
"hooks": {
"prestart": [
{
"path": "/usr/bin/setup-network",
"args": ["setup-network", "mycontainer"],
"env": ["LOG_LEVEL=debug"]
}
],
"poststart": [
{
"path": "/usr/bin/log-start",
"args": ["log-start", "mycontainer"]
}
],
"poststop": [
{
"path": "/usr/bin/cleanup",
"args": ["cleanup", "mycontainer"]
}
]
},
"linux": {
"uidMappings": [
{
"containerID": 0,
"hostID": 1000,
"size": 1
}
],
"gidMappings": [
{
"containerID": 0,
"hostID": 1000,
"size": 1
}
],
"resources": {
"cpu": {
"shares": 1024,
"quota": 100000,
"period": 100000,
"cpus": "0-1"
},
"memory": {
"limit": 536870912,
"reservation": 268435456,
"swap": 1073741824
},
"blockIO": {
"weight": 500
},
"pids": {
"limit": 1024
}
},
"namespaces": [
{
"type": "pid"
},
{
"type": "network"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
},
{
"type": "cgroup"
},
{
"type": "user"
}
],
"seccomp": {
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "exit"],
"action": "SCMP_ACT_ALLOW"
}
]
},
"rootfsPropagation": "private"
},
"annotations": {
"org.example.key": "value"
}
}
7.2 关键配置详解
7.2.1 Namespace 配置
json
"namespaces": [
{"type": "pid"}, // 进程隔离
{"type": "network"}, // 网络隔离
{"type": "ipc"}, // IPC 隔离
{"type": "uts"}, // 主机名隔离
{"type": "mount"}, // 挂载点隔离
{"type": "cgroup"}, // Cgroup 隔离
{"type": "user"} // 用户隔离
]
共享 namespace(加入已有 namespace):
json
{
"type": "network",
"path": "/proc/12345/ns/net" // 加入 PID 12345 的网络 namespace
}
7.2.2 资源限制配置
json
"resources": {
"cpu": {
"shares": 1024, // CPU 权重(相对值)
"quota": 100000, // CPU 配额(微秒)
"period": 100000, // CPU 周期(微秒)
"cpus": "0-1" // 绑定的 CPU 核心
},
"memory": {
"limit": 536870912, // 内存硬限制(512MB)
"reservation": 268435456, // 内存软限制(256MB)
"swap": 1073741824 // 内存+Swap 限制(1GB)
},
"blockIO": {
"weight": 500 // IO 权重(100-1000)
},
"pids": {
"limit": 1024 // 最大进程数
}
}
7.2.3 User Namespace 映射
json
"uidMappings": [
{
"containerID": 0, // 容器内 UID 0
"hostID": 1000, // 映射到宿主机 UID 1000
"size": 1 // 映射范围大小
}
],
"gidMappings": [
{
"containerID": 0, // 容器内 GID 0
"hostID": 1000, // 映射到宿主机 GID 1000
"size": 1
}
]
效果:容器内的 root(UID 0)实际上是宿主机的普通用户(UID 1000)
7.2.4 Hooks(钩子)
json
"hooks": {
"prestart": [ // 容器创建前执行
{
"path": "/usr/bin/setup-network",
"args": ["setup-network", "mycontainer"]
}
],
"poststart": [ // 容器启动后执行
{
"path": "/usr/bin/log-start",
"args": ["log-start", "mycontainer"]
}
],
"poststop": [ // 容器停止后执行
{
"path": "/usr/bin/cleanup",
"args": ["cleanup", "mycontainer"]
}
]
}
7.2.5 Seccomp(系统调用过滤)
json
"seccomp": {
"defaultAction": "SCMP_ACT_ERRNO", // 默认拒绝
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "open", "close", "exit"],
"action": "SCMP_ACT_ALLOW" // 允许这些系统调用
},
{
"names": ["reboot", "kexec_load"],
"action": "SCMP_ACT_KILL" // 禁止并杀死进程
}
]
}
八、Runc 与 Containerd 的关系
8.1 调用关系
┌─────────────────────────────────────────────────────────────┐
│ Containerd 调用 Runc 的流程 │
│ │
│ 1. Containerd 收到创建容器请求 │
│ ↓ │
│ 2. Containerd 准备 bundle │
│ ├── 解压镜像到 rootfs │
│ └── 生成 config.json │
│ ↓ │
│ 3. Containerd 调用 runc create │
│ runc create --bundle /var/run/containerd/xxx container-id│
│ ↓ │
│ 4. Runc 创建容器进程 │
│ ↓ │
│ 5. Containerd 调用 runc start │
│ runc start container-id │
│ ↓ │
│ 6. 容器运行 │
│ ↓ │
│ 7. Containerd 通过 shim 进程监控容器 │
└─────────────────────────────────────────────────────────────┘
8.2 Shim 进程的作用
┌─────────────────────────────────────────────────────────────┐
│ Shim 进程 │
│ │
│ 问题: │
│ 如果 containerd 重启,如何保持容器运行? │
│ │
│ 解决:Shim 进程 │
│ ├── 每个容器一个 shim 进程 │
│ ├── Shim 是 containerd 和 runc 的中间层 │
│ ├── Shim 持有容器的 stdin/stdout/stderr │
│ └── Containerd 重启后,通过 shim 重新连接容器 │
│ │
│ 架构: │
│ containerd → shim → runc → 容器进程 │
└─────────────────────────────────────────────────────────────┘
8.3 查看 Containerd 调用 Runc
bash
# 查看 containerd 创建的容器
ctr containers list
# 查看容器目录
ls /run/containerd/io.containerd.runtime.v2.task/k8s.io/<container-id>/
# 文件包括:
# config.json - runc 配置文件
# log.json - 容器日志
# init.pid - 容器进程 PID
# 可以直接用 runc 管理这个容器
cd /run/containerd/io.containerd.runtime.v2.task/k8s.io/<container-id>/
runc state <container-id>
九、高级实操
9.1 创建一个有网络能力的容器
bash
# 1. 创建 bundle
mkdir -p netcontainer/rootfs
cd netcontainer
docker export $(docker create alpine) | tar -xf - -C rootfs/
# 2. 生成配置
runc spec
# 3. 创建网络 namespace
ip netns add mynetns
# 4. 配置网络(创建 veth pair)
ip link add veth0 type veth peer name veth1
ip link set veth1 netns mynetns
ip netns exec mynetns ip link set veth1 up
ip netns exec mynetns ip addr add 10.0.0.2/24 dev veth1
# 5. 修改 config.json,加入网络 namespace
# 找到 namespaces 部分,修改 network 类型:
# {
# "type": "network",
# "path": "/var/run/netns/mynetns"
# }
# 6. 创建并启动容器
runc run mynetcontainer
# 7. 在容器内测试网络
# ip addr
# ping 10.0.0.1
9.2 创建一个资源受限的容器
bash
# 1. 创建 bundle
mkdir -p limitedcontainer/rootfs
cd limitedcontainer
docker export $(docker create alpine) | tar -xf - -C rootfs/
# 2. 生成配置
runc spec
# 3. 修改 config.json,添加资源限制
# 在 linux.resources 部分添加:
cat > resources_patch.json << 'EOF'
{
"linux": {
"resources": {
"cpu": {
"shares": 512,
"quota": 50000,
"period": 100000
},
"memory": {
"limit": 134217728
},
"pids": {
"limit": 100
}
}
}
}
EOF
# 手动合并到 config.json(或使用 jq)
# 限制说明:
# - CPU:最多使用 0.5 核
# - 内存:最多 128MB
# - 进程数:最多 100 个
# 4. 运行容器
runc run limitedcontainer
9.3 使用 User Namespace(安全容器)
bash
# 1. 创建 bundle
mkdir -p usercontainer/rootfs
cd usercontainer
docker export $(docker create alpine) | tar -xf - -C rootfs/
# 2. 生成配置
runc spec
# 3. 修改 config.json,添加 user namespace 映射
# 在 linux 部分添加:
# "uidMappings": [
# {"containerID": 0, "hostID": 1000, "size": 1}
# ],
# "gidMappings": [
# {"containerID": 0, "hostID": 1000, "size": 1}
# ],
# "namespaces": [
# {"type": "pid"},
# {"type": "network"},
# {"type": "ipc"},
# {"type": "uts"},
# {"type": "mount"},
# {"type": "cgroup"},
# {"type": "user"} // 添加 user namespace
# ]
# 4. 运行容器
runc run usercontainer
# 5. 在容器内验证
# id
# uid=0(root) gid=0(root)
# 在宿主机查看进程的真实 UID
ps aux | grep <容器进程PID>
# 会看到进程的 UID 是 1000,不是 0
9.4 调试 Runc
bash
# 启用调试日志
runc --debug run mycontainer
# 查看容器详细信息
runc state mycontainer
# 查看容器内所有进程
runc ps mycontainer
# 监控容器资源使用
runc events mycontainer
# 使用 strace 跟踪 runc 系统调用
strace -f -o runc.trace runc create mycontainer
十、常见问题排查
10.1 权限问题
bash
# 错误:permission denied
# 原因:需要 root 权限
# 解决
sudo runc run mycontainer
10.2 rootfs 不存在
bash
# 错误:rootfs does not exist
# 原因:config.json 中 root.path 指向的目录不存在
# 检查配置
cat config.json | grep -A 2 '"root"'
# 确保 rootfs 目录存在
ls rootfs/
10.3 容器立即退出
bash
# 错误:容器启动后立即退出
# 原因:process.args 中的命令执行完毕后退出了
# 解决:使用持久运行的命令
# 修改 config.json:
# "args": ["sleep", "3600"]
# 或
# "args": ["sh"] # 交互式
10.4 网络不通
bash
# 错误:容器内无法访问网络
# 原因:网络 namespace 配置不正确
# 检查 namespace 配置
cat config.json | grep -A 10 '"namespaces"'
# 确保有 network namespace
# 如果要共享宿主机网络,可以移除 network namespace
10.5 cgroup 挂载问题
bash
# 错误:cgroup filesystem not mounted
# 原因:系统 cgroup 未正确挂载
# 检查 cgroup 挂载
mount | grep cgroup
# 手动挂载(如果需要)
mount -t cgroup2 none /sys/fs/cgroup
十一、Runc 与其他运行时对比
11.1 对比表
| 运行时 | 语言 | 特点 | 适用场景 |
|---|---|---|---|
| runc | Go | OCI 标准实现,最成熟 | 通用场景 |
| crun | C | 更快、更小、内存占用低 | 性能敏感场景 |
| kata | Go | 基于虚拟机,强隔离 | 安全敏感场景 |
| gvisor | C++ | 用户态内核,沙箱隔离 | 安全敏感场景 |
| youki | Rust | 性能好,内存安全 | 新兴选择 |
11.2 性能对比
bash
# 创建 1000 个容器的时间对比(参考值)
# runc (Go)
# ~30 秒
# crun (C)
# ~10 秒
# kata (VM)
# ~300 秒(需要启动虚拟机)
十二、总结
12.1 核心要点
┌─────────────────────────────────────────────────────────────┐
│ Runc 核心知识点 │
│ │
│ 1. 定位 │
│ Runc 是 OCI 运行时规范的参考实现 │
│ 是容器技术栈的最底层 │
│ │
│ 2. 职责 │
│ 读取 config.json,调用 Linux 系统调用,创建容器进程 │
│ │
│ 3. 核心技术 │
│ namespace(隔离)+ cgroup(限制)+ rootfs(文件系统) │
│ │
│ 4. 使用方式 │
│ 直接使用:runc create/start/run/delete │
│ 间接使用:通过 containerd/docker 调用 │
│ │
│ 5. 配置文件 │
│ config.json 定义容器的所有配置 │
│ 包括 namespace、cgroup、mounts、process 等 │
└─────────────────────────────────────────────────────────────┘
12.2 学习路径
┌─────────────────────────────────────────────────────────────┐
│ Runc 学习路径 │
│ │
│ 入门 │
│ ├── 理解 OCI 规范 │
│ ├── 掌握 runc 基本命令 │
│ └── 手动创建一个简单容器 │
│ │
│ 进阶 │
│ ├── 深入理解 config.json │
│ ├── 配置 namespace 和 cgroup │
│ └── 使用 hooks 和 seccomp │
│ │
│ 高级 │
│ ├── 理解 runc 与 containerd 的关系 │
│ ├── 调试和排查问题 │
│ └── 对比其他运行时(crun、kata) │
└─────────────────────────────────────────────────────────────┘
12.3 一句话总结
Runc 是容器的"创建者",它把 config.json 变成一个真实的隔离进程。
十三、参考资源
- OCI 运行时规范:https://github.com/opencontainers/runtime-spec
- Runc GitHub:https://github.com/opencontainers/runc
- Linux Namespace 文档:https://man7.org/linux/man-pages/man7/namespaces.7.html
- Linux Cgroup 文档:https://man7.org/linux/man-pages/man7/cgroups.7.html
- Containerd 文档:https://containerd.io/docs/