3. 容器运行时基础:容器运行时架构与进程隔离机制

容器运行时是容器化技术的核心执行层,负责管理容器的创建、运行和销毁。理解其架构与隔离机制,是使用 eBPF 进行容器镜像异常定位的前提。本章将深入剖析 runc/containerd 的协作架构,以及 Namespace/Cgroup 如何实现进程隔离与资源管控。

3.1 容器运行时架构:runc 与 containerd

现代容器运行时采用分层架构,将高层编排逻辑与底层执行引擎解耦。以 Kubernetes 和 Docker 生态为例,containerd 作为行业标准的高层运行时,负责镜像管理、API 调用和容器生命周期管理;runc 作为底层运行时,直接与内核交互,通过 Namespace 和 Cgroup 创建容器进程。

关键概念: containerd 是 CRI(Container Runtime Interface)的标准实现,runc 是 OCI(Open Container Initiative)运行时规范的标准实现。两者通过 shim 进程桥接。

下图展示了 containerd 与 runc 在容器创建过程中的协作关系:

容器运行时架构:containerd 与 runc 协作流程 containerd 与 runc 协作架构 Kubernetes / Docker (CRI 调用) containerd (高层运行时) 镜像管理 · 容器生命周期 · gRPC API containerd-shim (进程桥接) runc (底层运行时) OCI 规范 · Namespace/Cgroup 创建 Linux 内核 (Namespace / Cgroup / eBPF) CRI 请求 (创建/启动/停止容器) OCI Bundle (配置 + 根文件系统) 容器进程 (隔离的执行环境)
eBPF 观测点: 在 containerd-shim 与 runc 之间,eBPF 可以 hook execve、clone 等系统调用,捕获容器进程的创建时刻,从而在进程启动瞬间注入观测逻辑,实现“零侵入”的异常定位。

3.2 容器进程隔离机制:Namespace

Namespace 是 Linux 内核提供的进程隔离机制,它让容器内的进程只能看到自己的“视图”,无法感知宿主机或其他容器的进程、网络、文件系统等资源。每个容器在创建时,runc 会为其申请一组独立的 Namespace。

Namespace 类型 隔离资源 系统调用参数 容器异常定位关注点
PID 进程编号 CLONE_NEWPID 容器内 PID 1 进程行为;僵尸进程排查
Network 网络设备、IP、端口 CLONE_NEWNET 网络连接异常;端口冲突;DNS 解析
Mount 文件系统挂载点 CLONE_NEWNS 镜像层文件缺失;挂载泄露;只读文件系统
UTS 主机名与域名 CLONE_NEWUTS 主机名配置错误
IPC 进程间通信资源 CLONE_NEWIPC 共享内存异常;信号量泄漏
User 用户与用户组 ID CLONE_NEWUSER 权限提升;用户映射错误
Cgroup Cgroup 根目录视图 CLONE_NEWCGROUP 资源统计隔离;cgroup 泄漏
常见陷阱: 默认情况下,容器不会启用 User Namespace,这意味着容器内 root 用户与宿主机 root 用户拥有相同 UID 0。若容器进程被突破,可能直接获得宿主机 root 权限。eBPF 可以监控 setuid、capset 等系统调用,检测非预期的权限变更。

3.3 容器资源管控机制:Cgroup

Cgroup(Control Groups)是 Linux 内核的资源管控机制,用于限制、记录和隔离进程组对 CPU、内存、磁盘 I/O、网络等资源的使用。runc 在创建容器时,会为容器进程创建独立的 Cgroup 子系统,确保容器不会耗尽宿主机资源。

下图展示了 Cgroup 的层级结构与资源控制路径:

Cgroup 层级结构与资源控制 Cgroup v2 层级结构与资源控制 /sys/fs/cgroup (根) system.slice docker-<id>.scope cpu.max CPU 配额 / 周期 memory.max 内存硬限制 io.max 磁盘读写带宽限制 eBPF 可读取 Cgroup 路径中的 memory.current、cpu.stat 等文件,实时关联容器资源使用与异常行为

3.4 容器进程创建流程:runc 如何应用 Namespace 与 Cgroup

当 containerd 收到创建容器的请求后,runc 执行以下关键步骤,将 Namespace 和 Cgroup 应用到容器进程:

  1. 解析 OCI Bundle: runc 读取 config.json 中的 Namespace 和 Cgroup 配置。
  2. 创建 Namespace: 通过 clone() 系统调用,传入 CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWNET | ... 等标志,创建新的隔离环境。
  3. 配置 Cgroup: 在 /sys/fs/cgroup 下创建容器对应的子目录,写入 CPU、内存、IO 等限制参数。
  4. 切换根文件系统: 使用 pivot_root() 或 chroot() 将进程的根目录切换到镜像层。
  5. 执行容器进程: 通过 execve() 运行容器入口程序(如 /bin/sh 或应用二进制)。
eBPF 介入时机: 在步骤 2(clone)和步骤 5(execve)挂载 eBPF 程序,可以捕获容器进程的 PID、命令行参数、环境变量、Cgroup 路径等关键信息,为异常定位提供第一手数据。

3.5 从 eBPF 视角理解容器进程标识

在宿主机上,容器进程本质上是受限的普通进程。eBPF 程序可以通过以下方式识别进程是否属于容器:

标识方法 数据来源 适用场景
Cgroup ID task_struct->cgroups 最可靠,直接关联容器 Cgroup 路径
Namespace 编号 /proc/<pid>/ns/* 的 inode 号 判断进程是否共享特定 Namespace
容器运行时标记 进程环境变量(如 container_uuid) 辅助识别,需解析 execve 参数
进程树关系 父进程为 containerd-shim 或 runc 快速过滤,但存在误判风险
最佳实践: 在 eBPF 程序中,优先使用 Cgroup ID 作为容器标识。通过 bpf_get_current_cgroup_id() 辅助函数,可以在 kprobe/tracepoint 中直接获取当前进程的 Cgroup ID,再与已知的容器 Cgroup 路径进行匹配,实现精确的容器级观测。