前言
在上一篇文章中,我们完成了 Linux 结构体「中篇」的拆解,深入探讨了命名空间与身份映射、内核参数调优、文件系统掩蔽以及平台执行域等核心概念。
正如文末预告,本篇将是 OCI Spec 数据结构解析系列的最后一篇。我们将聚焦于整个 config.go 中最庞大、最复杂的两个子系统:
LinuxResources—— Linux Cgroups v2 资源控制器的完整数据映射LinuxSeccomp—— Linux 系统调用过滤机制的精密配置结构
这两个子系统是生产环境容器隔离能力的根基,理解它们的数据结构设计是理解容器资源保障与安全加固的前提。
1. 资源控制总览:LinuxResources
在 vendor/github.com/opencontainers/runtime-spec/specs-go/config.go 中,LinuxResources 定义的全文如下:
type LinuxResources struct {
// 设备白名单控制器
Devices []LinuxDeviceCgroup `json:"devices,omitempty"`
// 内存限制
Memory *LinuxMemory `json:"memory,omitempty"`
// CPU 资源限制
CPU *LinuxCPU `json:"cpu,omitempty"`
// PID 数量限制
Pids *LinuxPids `json:"pids,omitempty"`
// Block I/O 限制
BlockIO *LinuxBlockIO `json:"blockIO,omitempty"`
// 巨页(Hugepage)限制
HugepageLimits []LinuxHugepageLimit `json:"hugepageLimits,omitempty"`
// 网络优先级与流量整形
Network *LinuxNetwork `json:"network,omitempty"`
// RDMA 资源限制(键值对形式,键为设备名)
Rdma map[string]LinuxRdma `json:"rdma,omitempty"`
// 通用统一层级资源(cgroups v2 unified tree)
Unified map[string]string `json:"unified,omitempty"`
}从结构可以看出,OCI 规范对 Linux 资源控制器的覆盖是非常全面的。接下来我们逐个拆解。
2. 内存子系统:LinuxMemory
type LinuxMemory struct {
Limit *int64 `json:"limit,omitempty"` // 内存限制(字节)
Reservation *int64 `json:"reservation,omitempty"` // 内存软限制(字节)
Swap *int64 `json:"swap,omitempty"` // 内存+Swap 总限制(字节)
Kernel *int64 `json:"kernel,omitempty"` // 内核内存限制(已废弃)
KernelTCP *int64 `json:"kernelTCP,omitempty"` // TCP 内核内存限制(字节)
Swappiness *uint64 `json:"swappiness,omitempty"` // Swap 倾向(0-100)
DisableOOMKiller *bool `json:"disableOOMKiller,omitempty"` // 禁用 OOM Killer
UseHierarchy *bool `json:"useHierarchy,omitempty"` // 启用层级内存 accounting
CheckBeforeUpdate *bool `json:"checkBeforeUpdate,omitempty"` // 更新前检查限制是否低于当前使用量
}核心字段解读
Limit 是最常用的字段,直接对应对 cgroups v2 的 memory.max 文件。设置后,容器进程组的内存使用量被严格限制在指定字节数以内,超出部分触发 OOM(Out of Memory)——要么被 OOM Killer 杀掉,要么在启用了 memory.swap.max 的情况下被 swap out。
Reservation(软限制)对应对 cgroups v2 的 memory.low。它不硬性阻止内存分配,但当系统内存紧张时,内核会优先回收超过软限制的 cgroup 的内存。与硬限制的「一刀切」不同,软限制是一种协作式的资源保障机制,适合有服务质量要求的多租户场景。
Swap 对应 memory.swap.max。需要特别注意的是,在 cgroups v2 中,Swap 限制与内存限制是独立设置的——即使 memory.limit 是 1GB,也可以把 memory.swap.max 设为 2GB,让容器最多使用 1GB 物理内存 + 1GB Swap。这在容器化环境中对于控制意外内存膨胀非常有用。
DisableOOMKiller 是一个安全开关。当设为 true 时,即使容器耗尽内存,也不会触发 Linux OOM Killer 去杀掉容器内的进程——代价是后续的内存分配请求会直接返回 ENOMEM 错误(返回 NULL 或 -1),应用程序需要自行处理这种情况。在某些追求高可用的金融或计算密集型场景下,显式禁用 OOM Killer + 应用程序自行处理 ENOMEM 是一种更优雅的降级策略。
⚠️ 废弃字段 Kernel:kernel-memory 限制在 cgroups v1 时代曾经用于限制内核态内存使用(例如 slab allocator),但从 Linux kernel 5.4 起已被移除,runc 运行时也不会实际应用这个字段。规范的注释中明确标注了废弃原因和内核 commit 引用,这是 OCI 规范与内核演进保持同步的一个例证。
3. CPU 子系统:LinuxCPU
type LinuxCPU struct {
Shares *uint64 `json:"shares,omitempty"` // CPU 权重(相对比例)
Quota *int64 `json:"quota,omitempty"` // 每周期可用 CPU 时间(微秒)
Burst *uint64 `json:"burst,omitempty"` // 突发可用 CPU 时间(微秒)
Period *uint64 `json:"period,omitempty"` // CPU 周期(微秒,默认 100000 = 100ms)
RealtimeRuntime *int64 `json:"realtimeRuntime,omitempty"` // 实时调度最大可用时间(微秒)
RealtimePeriod *uint64 `json:"realtimePeriod,omitempty"` // 实时调度周期(微秒)
Cpus string `json:"cpus,omitempty"` // 可用 CPU 核心列表(cpuset)
Mems string `json:"mems,omitempty"` // 可用内存节点列表(nodelist)
Idle *int64 `json:"idle,omitempty"` // 调度空闲权重(0=普通, 1=SCHED_IDLE)
}CPU 权重 vs CPU 上限
Linux CPU 调度器在处理资源竞争时有两套完全不同的机制:相对权重和绝对上限。Shares 属于前者,Quota+Period 属于后者。
Shares(对应 cgroups v2 的 cpu.weight)是一个相对权重,默认值 1024。当宿主机上只有容器 A 和容器 B 两个负载,且 Shares 分别为 1024 和 2048 时,它们获得的 CPU 时间的比例是 1:2——但这不意味着 B 能用满 2 个 CPU,只是说在竞争发生时,B 能分到 2/3 的 CPU 周期。如果只有一个容器在运行,它仍然可以使用全部 CPU。Shares 是「按比例分配剩余资源」。
Quota + Period 则提供绝对上限。quota 是每 period 微秒内容器可以使用的 CPU 时间上限。如果设置 quota=50000, period=100000,容器在每个 100ms 周期内最多只能使用 50ms 的 CPU 时间,即最多占 50% 的单核。这个机制常用于:
- 云服务多租户隔离:确保每个租户的容器不会超过约定的 CPU 配额
- 延迟敏感型服务:保证每个容器有确定的 CPU 时间预算
Burst(cgroups v2 cpu.burst)是一个较新的字段,用于支持突发资源使用。在上一篇文章的例子中,quota=50000, period=100000 意味着 50% 的稳态上限。但实际应用中,很多服务有明显的波峰波谷——比如 Web 服务在突发流量时需要更多 CPU。Burst 允许容器在短时间内(累积不超过 Burst 值)突破 Quota 的限制,获得更高的 CPU 吞吐量。这比直接调高 Quota 更安全,因为 Burst 累积后会自然回落,不会持续挤占其他容器。
Cpus / Mems 对应 cgroups v2 的 cpuset.cpus 和 cpuset.mems,用于将容器绑定到特定的 CPU 核心和 NUMA 内存节点。在高性能计算场景下,将容器绑定到同一 NUMA 节点的核心可以显著降低内存访问延迟(避免跨 NUMA 访问)。
Idle scheduler 是一个特殊选项。当设为 1 时,容器的进程使用 SCHED_IDLE 调度策略——只有在系统完全空闲时才会被调度,适用于非关键的后台批处理任务,避免影响交互式用户任务。
4. Block I/O 子系统:LinuxBlockIO
type LinuxBlockIO struct {
Weight *uint16 `json:"weight,omitempty"` // cgroup 权重(10-1000,默认 100)
LeafWeight *uint16 `json:"leafWeight,omitempty"` // CFQ 调度器下的叶子节点权重
WeightDevice []LinuxWeightDevice `json:"weightDevice,omitempty"` // 特定设备的权重(可覆盖 Weight)
ThrottleReadBpsDevice []LinuxThrottleDevice `json:"throttleReadBpsDevice,omitempty"` // 读吞吐量上限(bytes/s)
ThrottleWriteBpsDevice []LinuxThrottleDevice `json:"throttleWriteBpsDevice,omitempty"` // 写吞吐量上限(bytes/s)
ThrottleReadIOPSDevice []LinuxThrottleDevice `json:"throttleReadIOPSDevice,omitempty"` // 读 IOPS 上限
ThrottleWriteIOPSDevice []LinuxThrottleDevice `json:"throttleWriteIOPSDevice,omitempty"` // 写 IOPS 上限
}Block I/O 控制器允许对容器的磁盘 I/O 进行细粒度控制。与 CPU 的「时间分片」不同,磁盘 I/O 的问题是磁盘是物理旋转设备,并发访问会大幅降低吞吐量。因此 cgroups 提供了两种控制策略:
权重控制(Weight / WeightDevice):通过 iosched.weight(cgroups v2)设置相对权重,调度器在有多于一个进程竞争时按比例分配 I/O 带宽。注意权重只在调度队列排队时起作用,如果磁盘本身没有 I/O 积压,权重不产生实际限制。
上限控制(Throttle* 系列):这是更直接的控制方式,通过 io.max(cgroups v2)为特定设备设置绝对上限。ThrottleReadBpsDevice 设置每秒读取字节数上限,ThrottleReadIOPSDevice 设置每秒 I/O 操作次数上限。这种方式常用于数据库等对 I/O 延迟敏感的服务,防止它们占满磁盘带宽影响其他容器。
IOPS vs Bps 的区别:对于 SSD 这种无寻道延迟的设备,限制 IOPS 比限制 Bps 更合理,因为大块顺序读和小块随机读的 Bps 可能差异巨大,但 IOPS 相对稳定。对于机械硬盘,限制 Bps 更能反映实际磁盘负载。
5. PID 子系统:LinuxPids
type LinuxPids struct {
Limit *int64 `json:"limit,omitempty"` // 容器内最大 PID 数量
}这是最简单的子系统,对应 cgroups v2 的 pids.max。用于防止容器内的进程 fork 炸弹(fork bomb)耗尽系统的 PID 资源。默认值是「无限制」,设置一个合理的值(如 1024 或 4096)可以显著提高系统鲁棒性。
一个值得注意的细节是:这个限制是容器内所有进程树的 PID 总数,而不是独立进程数。如果你有一个 Python 进程产生了 10 个线程,每个线程在 Linux 中都是一个独立的 PID(通过 clone(2) 的 CLONE_THREAD 标志),它们都算在同一个 pids.max 计数内。
6. 网络子系统:LinuxNetwork
type LinuxNetwork struct {
ClassID *uint32 `json:"classID,omitempty"` // 网络流量分类 ID(用于 tc qdisc)
Priorities []LinuxInterfacePriority `json:"priorities,omitempty"` // 各接口的流量优先级
}type LinuxInterfacePriority struct {
Name string `json:"name"` // 网络接口名(如 eth0)
Priority uint32 `json:"priority"` // 优先级(0-15 或更高)
}ClassID 是一个精巧的设计。对应 cgroups v2 的 net_cls.classid,它允许将容器产生的网络流量打上特定的 tc(traffic control)标记,从而在宿主机的 tc 层 进行细粒度的流量整形和限速。这是一种将容器层和网络层的隔离解耦的设计——OCI 规范只负责「打标记」,具体的流量整形策略由运维人员在宿主机层面通过 tc 命令配置。
Priorities 则直接在接口层面设置 VLAN 优先级(IEEE 802.1p),用于容器与宿主机之间的网络 QoS。
7. RDMA 子系统:LinuxRdma
type LinuxRdma struct {
HcaHandles *uint32 `json:"hcaHandles,omitempty"` // 可打开的 HCA(Host Channel Adapter)句柄数上限
HcaObjects *uint32 `json:"hcaObjects,omitempty"` // 可创建的 RDMA 对象数上限
}RDMA(Remote Direct Memory Access)是高性能计算(HPC)和低延迟网络(RDMA over Converged Ethernet, RoCE)的核心技术。Linux 的 RDMA 资源通过 /sys/class/infiniband/ 暴露,LinuxRdma 结构允许为容器限制 InfiniBand 或 RoCE 网卡的资源使用,防止容器过度消耗 RDMA 队列对(Queue Pair)、内存注册区域等稀缺硬件资源。
在 Kubernetes 环境中,RDMA 设备通常通过 GPUDirect RDMA 或 RoCE 网卡为 AI/ML 训练任务提供高性能数据传输通道,此时 RDMA 资源限制对保障多租户公平性至关重要。
8. 通用统一层级:Unified
Unified map[string]string `json:"unified,omitempty"`这是一个键值对字典,直接对应 cgroups v2 unified cgroup 树中 /sys/fs/cgroup/ 下的任意配置文件。这是一种向前兼容的设计——当内核引入了新的 cgroup 控制器或参数,而 OCI 规范尚未为其定义专门的数据结构时,用户可以通过 Unified 字段直接传递内核接口,无需等待规范更新。
常见的用法包括设置 pids.max、cpu.stat(只读)、io.stat(只读)以及 cgroups v2 较新引入的各种控制器配置。
9. 设备白名单:LinuxDeviceCgroup
type LinuxDeviceCgroup struct {
Allow bool `json:"allow"` // 允许或拒绝
Type string `json:"type,omitempty"` // 设备类型:block, char, fifo
Major *int64 `json:"major,omitempty"` // 主设备号
Minor *int64 `json:"minor,omitempty"` // 次设备号
Access string `json:"access,omitempty"` // 访问权限:r, w, m(mknod)
}这是容器「设备可见性」控制的核心。docker run --device 和 runc 的设备管理背后都是这个结构。
Access 字段的 m 是一个容易忽略的安全点:mknod 权限允许容器在 /dev 下创建设备节点文件。结合 Major/Minor 为 nil 的通配符规则(如 {"allow": true, "type": "block", "access": "rwm"}),可以精确控制容器能看到哪些设备——默认情况下,容器只能看到 runc spec 生成的默认设备列表(null、zero、random、urandom、tty、console),无法访问宿主机的其他块设备或字符设备。
10. 系统调用过滤:LinuxSeccomp
终于到了容器安全领域最强大的武器之一——Seccomp(Secure Computing Mode)。
type LinuxSeccomp struct {
DefaultAction LinuxSeccompAction `json:"defaultAction"` // 默认动作
DefaultErrnoRet *uint `json:"defaultErrnoRet,omitempty"` // 默认 errno 返回值
Architectures []Arch `json:"architectures,omitempty"` // 目标架构列表
Flags []LinuxSeccompFlag `json:"flags,omitempty"` // Seccomp 过滤标志
ListenerPath string `json:"listenerPath,omitempty"` // seccomp NOTIFY_FD 路径
ListenerMetadata string `json:"listenerMetadata,omitempty"` // 元数据(供 listener 使用)
Syscalls []LinuxSyscall `json:"syscalls,omitempty"` // 系统调用规则
}默认动作与 syscall 匹配
DefaultAction 是整个 Seccomp 配置的默认策略——当没有规则匹配时采取的动作。常见选项包括:
| 动作 | 行为 |
|---|---|
SCMP_ACT_KILL | 杀死触发系统调用的进程(SIGSYS) |
SCMP_ACT_KILL_PROCESS | 杀死整个进程组 |
SCMP_ACT_KILL_THREAD | 只杀死触发调用的线程 |
SCMP_ACT_TRAP | 发送 SIGSYS 并陷入内核,进程收到信号后可处理 |
SCMP_ACT_ERRNO | 返回指定 errno(通过 DefaultErrnoRet 设置)给用户态 |
SCMP_ACT_TRACE | 触发 ptrace(PTRACE_SECCOMP),将控制权交给 tracer 进程 |
SCMP_ACT_LOG | 记录日志但允许调用(用于审计模式) |
SCMP_ACT_NOTIFY | 触发 seccomp NOTIFY 机制,将决策权交给用户态 listener 进程 |
生产环境最佳实践:先设置 DefaultAction: SCMP_ACT_LOG,观察哪些系统调用被触发(/proc/sys/kernel/seccomp/actions_logged),然后将允许的调用设为 SCMP_ACT_ALLOW,其余保持 SCMP_ACT_KILL 或 SCMP_ACT_ERRNO。直接设 SCMP_ACT_KILL 而不做审计,可能导致应用程序因使用了未预期系统调用而崩溃。
DefaultErrnoRet 允许将默认动作从「杀死」改为「返回特定错误码」。例如设为 1(EPERM),可以让被阻止的系统调用对应用程序表现得像「权限不足」,从而使应用程序能够优雅降级而不是直接崩溃。
架构支持:Architectures
const (
ArchX86 Arch = "SCMP_ARCH_X86"
ArchX86_64 Arch = "SCMP_ARCH_X86_64"
ArchX32 Arch = "SCMP_ARCH_X32"
ArchARM Arch = "SCMP_ARCH_ARM"
ArchAARCH64 Arch = "SCMP_ARCH_AARCH64"
ArchMIPS Arch = "SCMP_ARCH_MIPS"
ArchMIPS64 Arch = "SCMP_ARCH_MIPS64"
ArchPPC Arch = "SCMP_ARCH_PPC"
ArchPPC64 Arch = "SCMP_ARCH_PPC64"
ArchPPC64LE Arch = "SCMP_ARCH_PPC64LE"
ArchS390 Arch = "SCMP_ARCH_S390"
ArchS390X Arch = "SCMP_ARCH_S390X"
ArchRISCV64 Arch = "SCMP_ARCH_RISCV64"
ArchLOONGARCH64 Arch = "SCMP_ARCH_LOONGARCH64"
// ...
)这个字段用于指定规则适用的 CPU 架构。在支持多架构的容器中(如在 x86_64 宿主机上运行 i386 容器),可以为每种架构分别设置不同的系统调用过滤规则。默认情况下,只有宿主机的原生架构被允许执行。
Seccomp 标志:Flags
const (
// SECCOMP_FILTER_FLAG_LOG: 所有非 ALLOW 动作都记录到 actions_logged(Linux 4.14+)
LinuxSeccompFlagLog LinuxSeccompFlag = "SECCOMP_FILTER_FLAG_LOG"
// SECCOMP_FILTER_FLAG_SPEC_ALLOW: 禁用 Speculative Store Bypass(Linux 4.17+)
LinuxSeccompFlagSpecAllow LinuxSeccompFlag = "SECCOMP_FILTER_FLAG_SPEC_ALLOW"
// SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV: 等待信号可杀(Linux 5.19+)
LinuxSeccompFlagWaitKillableRecv LinuxSeccompFlag = "SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV"
)SECCOMP_FILTER_FLAG_LOG 是生产环境非常有用的一个标志——它允许在不阻断系统调用的情况下记录所有被 Seccomp 过滤的调用,这正是前文提到的「审计模式」的底层实现。
SECCOMP_FILTER_FLAG_SPEC_ALLOW 与 CPU 硬件安全漏洞(Speculative Store Bypass, SSB)相关。启用后允许应用程序使用可能被恶意推测执行的系统调用,但同时禁用了硬件层面的 SSB 缓解——这是一种性能与安全的权衡,在容器环境中,高安全要求场景(如多租户 SaaS)应该启用此标志。
核心规则定义:Syscalls
type LinuxSyscall struct {
Names []string `json:"names"` // 系统调用名列表(同一规则可匹配多个)
Action LinuxSeccompAction `json:"action"` // 匹配后执行的动作
ErrnoRet *uint `json:"errnoRet,omitempty"` // 覆盖默认 errno
Args []LinuxSeccompArg `json:"args,omitempty"` // 参数匹配条件
}每条规则的核心是系统调用名列表 + 匹配后的动作。通过 Names 可以将多条相关的系统调用放在同一规则里(例如 ["open", "openat", "openat2"]),让它们共享相同的安全策略。
Args 数组允许对系统调用的特定参数进行精细过滤,这是 Seccomp 最强大的能力之一:
type LinuxSeccompArg struct {
Index uint `json:"index"` // 参数下标(0=第一个参数)
Value uint64 `json:"value"` // 期望值
ValueTwo uint64 `json:"valueTwo,omitempty"` // 掩码比较时的第二个值
Op LinuxSeccompOperator `json:"op"` // 比较操作符
}支持的比较操作符:
| 操作符 | 含义 | 典型用例 |
|---|---|---|
SCMP_CMP_EQ | 等于 | 只允许 open("/path", O_RDONLY) |
SCMP_CMP_NE | 不等于 | 阻止写入特定 fd |
SCMP_CMP_LT | 小于 | 限制 setpriority 的 nice 值范围 |
SCMP_CMP_LE | 小于等于 | — |
SCMP_CMP_GE | 大于等于 | — |
SCMP_CMP_GT | 大于 | — |
SCMP_CMP_MASKED_EQ | 掩码相等 | arg[0] & O_ACCMODE == O_RDONLY(检查只读标志) |
一个实用例子:只允许容器进程使用 O_RDONLY(只读)标志打开文件,可以这样配置:
{
"names": ["open", "openat"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1,
"args": [
{
"index": 1,
"value": 0,
"valueTwo": 3,
"op": "SCMP_CMP_MASKED_EQ"
}
]
}这通过 O_ACCMODE = 3(二进制 11)掩码与 O_RDONLY = 0 的对比,确保第二个参数(flags)的低两位为 00——即强制只读。这是防止容器内恶意写入文件系统的一道强屏障。
Seccomp NOTIFY:ListenerPath
这是 Linux 5.0 引入的最现代化的一种 Seccomp 协作模式。传统 Seccomp 的限制是:一旦加载了 BPF 过滤器,内核态就开始工作,无法动态修改规则或与用户态程序交互。
ListenerPath 允许将一个用户态进程(通过 user_notification_addfd)注册为 Seccomp 事件的处理中心。当容器内的进程执行被过滤的系统调用时,内核会通过预先打开的 fd 发送一个通知消息,用户态 listener 可以:
- 检查调用参数
- 做出允许或拒绝的决策
- 甚至修改返回值
这种机制是容器运行时(如 containerd-shim)与安全策略引擎(如 Falco、Sysdig)协作的底层基础——它们可以在运行时动态决定是否允许某个「危险」系统调用(如 mount、ptrace),而不是在启动时静态决定。
结语
从 LinuxResources 到 LinuxSeccomp,我们看到了 OCI 规范是如何将 Linux 内核提供的两大隔离机制——资源控制(Cgroups)和系统调用过滤(Seccomp)——翻译为清晰的结构化数据。
在整个 OCI Spec 数据结构解析系列中,我们从顶层的 Spec 一路深入到 Seccomp 的参数级过滤规则。这条知识链条揭示了一个核心设计哲学:OCI 规范并不是简单地将内核接口直接暴露,而是通过 Go 数据结构提供语义化抽象,让容器配置既精确又具有可移植性。
理解这些数据结构的细节,对于排查容器性能问题、设计多租户隔离策略、以及加固容器安全边界,都有着不可替代的价值。这也正是阅读源码的意义所在——在纷繁的系统调用和内核参数背后,找到那条清晰的抽象主线。