前言
在前序的七篇文章中,我们系统性地通读了 runc/libcontainer/SPEC.md,从理论层面理解了 Linux 容器所依赖的隔离、限制与安全机制。
理论需要落地为工程。在 OCI (Open Container Initiative) 标准中,上层容器引擎(如 Docker、containerd)与底层运行时(runc)之间的唯一通信协议,就是一个名为 config.json 的配置文件。
在 runc 源码中,这个 JSON 对应的 Go 语言数据结构位于第三方依赖包 github.com/opencontainers/runtime-spec/specs-go/config.go 中。整个文件长达 1000 多行。为了清晰起见,我们将分几篇文章对其进行深度拆解。本文是第一篇,主要解读容器的顶层基础结构(Spec)以及至关重要的进程环境(Process)。
1. 顶层结构:Spec
Spec 结构体是整个 config.json 的入口,它勾勒出了容器的全貌。
type Spec struct {
// 遵循的 OCI Runtime Specification 版本 (如 "1.0.2")
Version string `json:"ociVersion"`
// Process 配置了容器的主进程(如何运行)
Process *Process `json:"process,omitempty"`
// Root 配置了容器的根文件系统(在哪运行)
Root *Root `json:"root,omitempty"`
// Hostname 配置容器的主机名(对应 UTS Namespace)
Hostname string `json:"hostname,omitempty"`
// Domainname 配置容器的域名
Domainname string `json:"domainname,omitempty"`
// Mounts 配置在 Root 之上额外的挂载点(对应 Mount Namespace)
Mounts []Mount `json:"mounts,omitempty"`
// Hooks 配置容器生命周期事件的回调钩子
Hooks *Hooks `json:"hooks,omitempty" platform:"linux,solaris,zos"`
// Annotations 包含容器的任意元数据(键值对)
Annotations map[string]string `json:"annotations,omitempty"`
// Linux 是基于 Linux 容器的平台专属配置 (隔离机制的核心)
Linux *Linux `json:"linux,omitempty" platform:"linux"`
// ... 还有 Windows, Solaris, VM 等平台的专属配置,此处省略
}解析:
- 跨平台设计:你可以看到
Spec在顶层抽象了所有容器共有的属性(进程、根目录、挂载点、主机名),而将底层的系统级特性(如 Cgroups、Namespaces、Seccomp)封装在特定平台的字段中(如Linux、Windows)。 - Hooks(生命周期钩子):这非常关键。它允许外部程序在容器启动前(PreStart)、创建后(CreateRuntime)、启动后(PostStart)以及停止后(PostStop)介入执行自定义逻辑(例如,为主机分配一块虚拟网卡并塞进容器的网络命名空间里)。
2. 根文件系统:Root
Root 描述了容器进程眼中的 /(根目录)长什么样。
type Root struct {
// Path 是包含容器根文件系统的宿主机目录路径(绝对路径,或相对于 bundle 目录的相对路径)
Path string `json:"path"`
// Readonly 决定根文件系统是否以只读模式挂载
Readonly bool `json:"readonly,omitempty"`
}结合我们之前在 SPEC 中读到的知识,runc 会利用这个 Path,配合 pivot_root 系统调用,将容器进程“囚禁”在这个路径下。如果配置了 Readonly = true,则能有效防止恶意进程篡改基础系统文件。
3. 核心执行单元:Process
如果说 Root 和 Linux 定义了舞台和围墙,那么 Process 就是在舞台上跳舞的演员。它定义了容器主进程的完整上下文。
type Process struct {
// Terminal 决定是否为进程分配一个伪终端 (PTY)
Terminal bool `json:"terminal,omitempty"`
// ConsoleSize 指定控制台的初始大小(宽和高)
ConsoleSize *Box `json:"consoleSize,omitempty"`
// User 描述了进程应该以哪个系统用户/组的身份运行
User User `json:"user"`
// Args 指定可执行文件及其参数 (如 ["sh", "-c", "echo hello"])
Args []string `json:"args,omitempty"`
// Env 指定环境变量列表 (格式为 "KEY=VALUE")
Env []string `json:"env,omitempty"`
// Cwd 是容器内部的初始当前工作目录
Cwd string `json:"cwd"`
// Capabilities 定义了赋予进程的细粒度 Linux 权限
Capabilities *LinuxCapabilities `json:"capabilities,omitempty" platform:"linux"`
// Rlimits 定义了进程级别的资源限制 (如打开文件描述符的上限)
Rlimits []POSIXRlimit `json:"rlimits,omitempty" platform:"linux,solaris"`
// NoNewPrivileges 控制进程是否可以通过 setuid/setgid 获取额外的权限
NoNewPrivileges bool `json:"noNewPrivileges,omitempty" platform:"linux"`
// ApparmorProfile, SelinuxLabel 控制进程的 MAC 安全策略
ApparmorProfile string `json:"apparmorProfile,omitempty" platform:"linux"`
SelinuxLabel string `json:"selinuxLabel,omitempty" platform:"linux"`
// OOMScoreAdj 调整进程在 OOM (内存溢出) 发生时被内核杀死的优先级
OOMScoreAdj *int `json:"oomScoreAdj,omitempty" platform:"linux"`
}Process 字段深度剖析
User(用户身份)gotype User struct { UID uint32 `json:"uid" platform:"linux,solaris"` GID uint32 `json:"gid" platform:"linux,solaris"` AdditionalGids []uint32 `json:"additionalGids,omitempty" platform:"linux,solaris"` // ... }这里的
UID和GID是容器内部的身份标识。如果启用了 User Namespace(用户命名空间),底层的Linux配置中会有一张映射表,将容器内的 UID(如 0/root)映射为宿主机上的一个普通非特权 UID(如 100000),这也是 Rootless 容器的安全基石。Capabilities(能力控制)gotype LinuxCapabilities struct { Bounding []string `json:"bounding,omitempty"` Effective []string `json:"effective,omitempty"` Inheritable []string `json:"inheritable,omitempty"` Permitted []string `json:"permitted,omitempty"` Ambient []string `json:"ambient,omitempty"` }还记得在 Security 篇我们看到的默认 Capabilities 列表吗(如
CAP_NET_BIND_SERVICE)? Linux 的 Capabilities 被划分为 5 个集合:Bounding:能力上限,进程无论如何都无法超越的边界。Effective:进程当前实际生效的、起作用的能力。Permitted:进程被允许拥有(可以随时激活到 Effective 中)的能力。 这三个集合精确地勾勒出了黑客即便攻破了进程,所能造成的最大破坏范围。
NoNewPrivileges(禁止新权限) 这是 Linuxprctl(PR_SET_NO_NEW_PRIVS)系统调用的映射。如果设为true,容器内的进程即使执行了一个带有suid标志的二进制文件(如sudo命令),内核也会拒绝赋予它更高的权限。在多租户集群中,这是必开的安全选项。Rlimits(进程级资源软硬限) 不要把它和 Cgroups 搞混。Cgroups 是限制一组进程的 CPU 和内存,而Rlimits(通过setrlimit调用)是限制单个进程的系统级配额,比如著名的RLIMIT_NOFILE(限制最大打开文件数)或RLIMIT_NPROC(限制最大子进程数)。
结语
通过 config.go 中的 Spec 和 Process 结构体,我们可以清晰地看到 OCI 规范是如何通过 JSON 数据结构,将容器的运行指令传递给 runc 的。
在下一篇文章中,我们将进入 config.go 最硬核的部分——Linux 结构体,详细拆解 Namespaces 隔离、Cgroups 资源组绑定以及 Seccomp 系统调用过滤的配置定义。