avatar🌌
TangentTangent Blog

技术博客 | Go & Kubernetes & Linux

技术博客 | Go & Kubernetes & Linux

runc 源码解读(七):OCI Spec 核心数据结构解析 (上) —— 基础配置与 Process

前言 ​

在前序的七篇文章中,我们系统性地通读了 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 的入口,它勾勒出了容器的全貌。

go
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 描述了容器进程眼中的 /(根目录)长什么样。

go
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 就是在舞台上跳舞的演员。它定义了容器主进程的完整上下文。

go
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 字段深度剖析 ​

  1. User (用户身份)

    go
    type 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 容器的安全基石。

  2. Capabilities (能力控制)

    go
    type 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 中)的能力。 这三个集合精确地勾勒出了黑客即便攻破了进程,所能造成的最大破坏范围。
  3. NoNewPrivileges (禁止新权限) 这是 Linux prctl(PR_SET_NO_NEW_PRIVS) 系统调用的映射。如果设为 true,容器内的进程即使执行了一个带有 suid 标志的二进制文件(如 sudo 命令),内核也会拒绝赋予它更高的权限。在多租户集群中,这是必开的安全选项。

  4. Rlimits (进程级资源软硬限) 不要把它和 Cgroups 搞混。Cgroups 是限制一组进程的 CPU 和内存,而 Rlimits(通过 setrlimit 调用)是限制单个进程的系统级配额,比如著名的 RLIMIT_NOFILE(限制最大打开文件数)或 RLIMIT_NPROC(限制最大子进程数)。

结语 ​

通过 config.go 中的 Spec 和 Process 结构体,我们可以清晰地看到 OCI 规范是如何通过 JSON 数据结构,将容器的运行指令传递给 runc 的。

在下一篇文章中,我们将进入 config.go 最硬核的部分——Linux 结构体,详细拆解 Namespaces 隔离、Cgroups 资源组绑定以及 Seccomp 系统调用过滤的配置定义。

runc 源码解读(六):从 SPEC.md 看容器运行时、Init 进程与生命周期 (Actions)
runc 源码解读(五):从 SPEC.md 看容器安全与权限控制
由 Valaxy v1.2.2 驱动|主题-Yunv1.2.2