- 操作系统
- 云原生
- 容器运行时
【免费下载链接】os
Tiny Linux distro that runs the entire OS as Docker containers
导读
本文以当前仓库中随 RancherOS 一起 vendored 的 libcontainer SPEC v1 规范 为主线,系统讲解 v1 容器的标准配置:命名空间、标准文件系统布局、默认 Linux capabilities、cgroup 资源预留,以及容器创建后可执行的管理与检视动作。读完本文,你将理解一个符合 v1 规范的容器从内核隔离到资源控制、从安全加固到进程管理的完整技术骨架,并能对照 RancherOS 源码(如 pkg/init/switchroot/switchroot.go、pkg/init/env/env.go)与 libcontainer README 中的 Go API 示例,掌握在实际项目中构造与运维这类容器所需的核心知识。
libcontainer 提供了创建容器的原生 Go 实现——涉及命名空间、cgroups、capabilities 和文件系统访问控制,并允许在容器创建后执行生命周期管理操作。v1 配置档案(profile)的设计目标是:在强安全配置下,承载绝大多数应用程序。
一、v1 规范概述
Container Specification v1 是一份标准的容器配置规范,涵盖以下核心维度:
- 命名空间(Namespaces):进程级隔离;
- 标准文件系统(Filesystem):rootfs 布局与必需挂载;
- 默认 Linux capability 集合:安全与灵活性的平衡;
- 资源预留(Resource reservations):通过 cgroups 实现的 CPU、内存、设备访问控制;
- 进程运行环境(Environment):容器内进程被注入的环境设置。
除了描述容器如何创建,该规范还定义了容器创建后可用于管理和检视容器内进程的标准动作集(Actions)。
规范原文位于 vendor/github.com/opencontainers/runc/libcontainer/SPEC.md,属于 opencontainers/runc 项目中 libcontainer 子库的官方文档,被 RancherOS 项目以 vendor 方式引入,与项目的核心架构(“整个 OS 以 Docker 容器方式运行”)直接相关。
二、系统要求与兼容性(System Requirements and Compatibility)
运行 v1 规范容器的最低系统要求如下:
- 内核版本:推荐 3.10,最低 2.6.2x(需携带反向移植补丁 backported patches);
- cgroups 挂载:每个子系统(subsystem)必须挂载在**独立的层级(hierarchy)**中。
这一要求意味着宿主机内核必须提供完整的命名空间与 cgroup 支持,是运行本规范所描述容器类型的先决条件。
三、命名空间(Namespaces)
v1 容器默认启用以下六个命名空间,均通过clone系统调用创建:
| Flag | Enabled |
|---|---|
| CLONE_NEWPID | 1 |
| CLONE_NEWUTS | 1 |
| CLONE_NEWIPC | 1 |
| CLONE_NEWNET | 1 |
| CLONE_NEWNS | 1 |
| CLONE_NEWUSER | 1 |
CLONE_NEWPID:PID 命名空间,容器内进程拥有独立的 PID 视图(PID 1 为容器 init);CLONE_NEWUTS:UTS 命名空间,隔离主机名与域名(/etc/hostname相关);CLONE_NEWIPC:IPC 命名空间,隔离 System V IPC 与 POSIX 消息队列;CLONE_NEWNET:网络命名空间,隔离网络栈、接口、路由与防火墙规则;CLONE_NEWNS:挂载命名空间,隔离挂载点视图;CLONE_NEWUSER:用户命名空间,配合 uid/gid 映射实现非特权容器。
在 libcontainer README 的 Go 配置示例中,这六个命名空间对应configs.Namespaces数组:
Namespaces: configs.Namespaces([]configs.Namespace{ {Type: configs.NEWNS}, {Type: configs.NEWUTS}, {Type: configs.NEWIPC}, {Type: configs.NEWPID}, {Type: configs.NEWUSER}, {Type: configs.NEWNET}, }),与 RancherOS 的联系
RancherOS 的系统服务(system service)通过net: host、pid: host、uts: host、ipc: host等配置选择共享宿主机的某些命名空间,可见于 os-config.tpl.yml 中的console、network、docker等服务定义。这正是 v1 规范“命名空间可裁剪”特性在真实操作系统场景中的典型运用:系统级服务需要宿主机网络与 PID 视图,而用户容器则获得完整隔离。
四、文件系统(Filesystem)
4.1 rootfs 与隔离
容器必须被提供一个根文件系统(rootfs),用于“关押”(jail)并派生容器内进程,容器依赖的二进制与系统库都位于该目录内。任何要执行的二进制都必须位于 rootfs 之内。
一个重要的自动清理机制:容器内发生的挂载(mount)在容器退出时会自动被清理——因为挂载命名空间被销毁时,内核会卸载该命名空间内建立的所有挂载。
4.2 运行时必需的挂载
为了让容器正确执行,运行时必须在 rootfs 内挂载以下文件系统:
| Path | Type | Flags | Data |
|---|---|---|---|
| /proc | proc | MS_NOEXEC, MS_NOSUID, MS_NODEV | |
| /dev | tmpfs | MS_NOEXEC, MS_STRICTATIME | mode=755 |
| /dev/shm | tmpfs | MS_NOEXEC, MS_NOSUID, MS_NODEV | mode=1777, size=65536k |
| /dev/mqueue | mqueue | MS_NOEXEC, MS_NOSUID, MS_NODEV | |
| /dev/pts | devpts | MS_NOEXEC, MS_NOSUID | newinstance, ptmxmode=0666, mode=620, gid=5 |
| /sys | sysfs | MS_NOEXEC, MS_NOSUID, MS_NODEV, MS_RDONLY |
各挂载标志含义:
MS_NOEXEC:禁止在该文件系统上执行二进制;MS_NOSUID:忽略该文件系统上二进制文件的 setuid/setgid 位;MS_NODEV:禁止在该文件系统上访问设备节点;MS_RDONLY:只读挂载;MS_STRICTATIME:始终更新 atime(strict atime)。
上述配置在 libcontainer README 中对应configs.Mount数组,defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV,与规范表格逐一对应(如/dev使用MS_NOSUID | MS_STRICTATIME且Data: "mode=755",/dev/pts使用Data: "newinstance,ptmxmode=0666,mode=0620,gid=5")。
4.3 /dev 设备节点
在新建挂载命名空间内挂载完文件系统后,需要向/dev填充一组设备节点。规范明确:rootfs 内不需要预先指定任何/dev设备节点,容器运行时会按需创建执行进程所需的正确设备:
| Path | Mode | Access |
|---|---|---|
| /dev/null | 0666 | rwm |
| /dev/zero | 0666 | rwm |
| /dev/full | 0666 | rwm |
| /dev/tty | 0666 | rwm |
| /dev/random | 0666 | rwm |
| /dev/urandom | 0666 | rwm |
| /dev/fuse | 0666 | rwm |
4.4 ptmx 与伪终端(PTY)
/dev/ptmx必须是容器内指向宿主机/dev/ptmx的符号链接;- 伪 TTY 在容器内是可选的(容器应同时支持有无 PTY 两种情形);
- 若为容器提供伪终端,则
/dev/console需要在/dev填充并挂载于 tmpfs 之后,将控制台绑定(bind)进来:
| Source | Destination | UID GID | Mode | Type |
|---|---|---|---|---|
| *pty host path* | /dev/console | 0 0 | 0600 | bind |
4.5 标准 I/O 与符号链接
设置好/dev/null后,运行时需检查容器 I/O(STDIN、STDOUT、STDERR)与外部/dev/null之间是否存在链接:若容器的 I/O 指向容器外的/dev/null,则关闭它,并将容器 rootfs 内本地的/dev/null通过dup2复制到对应 fd。
/proc挂载完成后,需在/dev内为 I/O 建立标准符号链接:
| Source | Destination |
|---|---|
| /proc/self/fd | /dev/fd |
| /proc/self/fd/0 | /dev/stdin |
| /proc/self/fd/1 | /dev/stdout |
| /proc/self/fd/2 | /dev/stderr |
4.6 pivot_root 与 ramfs 特殊情况
pivot_root被用于改变进程的根,从而将进程有效地“关押”在 rootfs 内:
put_old = mkdir(...); pivot_root(rootfs, put_old); chdir("/"); unmount(put_old, MS_DETACH); rmdir(put_old);关键限制:若容器的 rootfs 位于ramfs中,则pivot_root不被支持,必须改用MS_MOVE加chroot的组合:
mount(rootfs, "/", NULL, MS_MOVE, NULL); chroot("."); chdir("/");这一“ramfs 备选方案”在 RancherOS 中有真实的对应实现:当从内存文件系统(initrd)启动时,pkg/init/env/env.go 会设置
DOCKER_RAMDISK=true(注释明确写道 “Magic setting to tell Docker to do switch_root and not pivot_root”);而 pkg/init/switchroot/switchroot.go 在切换根时执行的正是syscall.Mount(rootfs, "/", "", syscall.MS_MOVE, "")→syscall.Chroot(".")→syscall.Chdir("/")这一与规范完全一致的序列,并在成功后os.Unsetenv("DOCKER_RAMDISK")。这说明规范中的“备选路径”不是纸面设计,而是被实际操作系统引导流程采用的成熟方案。
文件系统设置完成后,umask被恢复为0022。
五、资源(Resources):cgroups 子系统
cgroups 负责容器的资源分配,涵盖系统资源如 CPU、内存与设备访问。v1 规范启用的子系统如下:
| Subsystem | Enabled |
|---|---|
| devices | 1 |
| memory | 1 |
| cpu | 1 |
| cpuacct | 1 |
| cpuset | 1 |
| blkio | 1 |
| perf_event | 1 |
| freezer | 1 |
| hugetlb | 1 |
| pids | 1 |
所有 cgroup 子系统都被加入(joined),以便从每个子系统收集统计信息。其中freezer不暴露任何统计信息,但它被加入的原因是为了支持容器的**暂停(pause)与恢复(resume)**操作。
时序上的关键保证:容器 init 的父进程必须在初始化开始前,将 init PID 放入正确的 cgroups——这样任何进程或线程都无法逃逸出 cgroups。这一同步通过一条管道(pipe)完成(详见下文“运行时与 init 进程”小节):容器 init 进程会阻塞等待父进程完成 cgroup 设置。
六、安全(Security)
6.1 默认 Linux Capabilities
容器内设置的标准 Linux capabilities 集合,为应用程序提供了兼顾安全与灵活性的良好默认值。默认启用的 capabilities:
| Capability | Enabled |
|---|---|
| CAP_NET_RAW | 1 |
| CAP_NET_BIND_SERVICE | 1 |
| CAP_AUDIT_READ | 1 |
| CAP_AUDIT_WRITE | 1 |
| CAP_DAC_OVERRIDE | 1 |
| CAP_SETFCAP | 1 |
| CAP_SETPCAP | 1 |
| CAP_SETGID | 1 |
| CAP_SETUID | 1 |
| CAP_MKNOD | 1 |
| CAP_CHOWN | 1 |
| CAP_FOWNER | 1 |
| CAP_FSETID | 1 |
| CAP_KILL | 1 |
| CAP_SYS_CHROOT | 1 |
默认禁用的 capabilities(共 22 项):
| Capability | Enabled |
|---|---|
| CAP_NET_BROADCAST | 0 |
| CAP_SYS_MODULE | 0 |
| CAP_SYS_RAWIO | 0 |
| CAP_SYS_PACCT | 0 |
| CAP_SYS_ADMIN | 0 |
| CAP_SYS_NICE | 0 |
| CAP_SYS_RESOURCE | 0 |
| CAP_SYS_TIME | 0 |
| CAP_SYS_TTY_CONFIG | 0 |
| CAP_AUDIT_CONTROL | 0 |
| CAP_MAC_OVERRIDE | 0 |
| CAP_MAC_ADMIN | 0 |
| CAP_NET_ADMIN | 0 |
| CAP_SYSLOG | 0 |
| CAP_DAC_READ_SEARCH | 0 |
| CAP_LINUX_IMMUTABLE | 0 |
| CAP_IPC_LOCK | 0 |
| CAP_IPC_OWNER | 0 |
| CAP_SYS_PTRACE | 0 |
| CAP_SYS_BOOT | 0 |
| CAP_LEASE | 0 |
| CAP_WAKE_ALARM | 0 |
| CAP_BLOCK_SUSPEND | 0 |
安全要点解读:
- 保留的是网络收发、文件属主、进程信号、chroot 等常规应用必需的能力;
- 禁用项集中在**内核模块加载(CAP_SYS_MODULE)、直接 I/O(CAP_SYS_RAWIO)、系统管理(CAP_SYS_ADMIN)、ptrace(CAP_SYS_PTRACE)、时间与时钟修改、网络管理(CAP_NET_ADMIN)**等高风险面,这正是“v1 档案以强安全配置承载大多数应用”的关键所在;
- 在 libcontainer README 的 Go 示例中,
Capabilities: []string{...}清单与本规范表格完全一致(14 项启用项)。
6.2 AppArmor 与 SELinux
容器还可叠加AppArmor与SELinux两层额外的安全机制。配置中若提供了 apparmor profile 或 selinux 进程/挂载标签(process and mount labels),容器应予以支持。
规范给出的标准 AppArmor profile:
#include <tunables/global> profile <profile_name> flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base> network, capability, file, umount, deny @{PROC}/sys/fs/** wklx, deny @{PROC}/sysrq-trigger rwklx, deny @{PROC}/mem rwklx, deny @{PROC}/kmem rwklx, deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx, deny @{PROC}/sys/kernel/*/** wklx, deny mount, deny /sys/[^f]*/** wklx, deny /sys/f[^s]*/** wklx, deny /sys/fs/[^c]*/** wklx, deny /sys/fs/c[^g]*/** wklx, deny /sys/fs/cg[^r]*/** wklx, deny /sys/firmware/efi/efivars/** rwklx, deny /sys/kernel/security/** rwklx, }该 profile 的策略要点:默认放行网络、capability、文件与 umount,但严格拒绝对/proc内核参数与内存(/proc/mem、/proc/kmem)、/sys下除 cgroup 之外的控制文件(含 EFI 变量、内核安全接口)的读写访问,并拒绝mount——这层“默认拒绝”为容器提供了纵深防御。
RancherOS 在 SELinux 侧的实际配置可见 assets/selinux/config(
SELINUX=permissive、SELINUXTYPE=ros),以及 cmd/control/selinux.go 与 pkg/selinux/selinux_linux.go 中的相关实现;配合 pkg/init/selinux/selinux.go 在启动阶段的加载,体现了规范“SELinux 可作为附加安全层”的设计在真实发行版中的落地。
规范同时标注了一项 TODO:seccomp 的默认配置仍在研究中(“seccomp work is being done to find a good default config”),即 v1 规范当时尚未给出默认 seccomp 过滤器。
七、运行时与 Init 进程(Runtime and Init Process)
7.1 管道同步机制
容器创建期间,父进程需要与容器 init 进程通信并完成同步。实现方式是创建一条传递给容器 init 的管道:
- init 进程首次派生(spawn)时,会阻塞在管道自己的那一端;
- 直到父进程关闭其管道端,init 才继续运行;
- 这为父进程留出时间,把新进程放入 cgroup 层级,以及/或者写入用户命名空间所需的 uid/gid 映射;
- 管道通过 FD 3 传递给 init 进程。
这一机制正是前文“没有任何进程或线程能逃逸 cgroups”的时序保证。
7.2 静态编译与无长驻 init
- 消费 libcontainer 的应用程序应静态编译;
- libcontainer不定义任何 init 进程,传入的参数直接用于在应用内部
exec目标进程; - 容器规范中不应存在长期运行的 init(“There should be no long running init within the container spec”)。
与之对应的启动模式(来自 libcontainer README):容器以两步过程派生,调用方可复用当前二进制/proc/self/exe作为容器 init,并以"init"参数进入“bootstrap”入口,例如:
func init() { if len(os.Args) > 1 && os.Args[1] == "init" { runtime.GOMAXPROCS(1) runtime.LockOSThread() factory, _ := libcontainer.New("") if err := factory.StartInitialization(); err != nil { logrus.Fatal(err) } panic("--this line should have never been executed, congratulations--") } }随后通过libcontainer.New(...)创建工厂、factory.Create(...)创建容器、container.Start(process)启动初始进程。
7.3 伪终端与控制台
若为容器提供伪终端(pseudo tty),运行时将打开控制台并dup2作为容器的 STDIN、STDOUT、STDERR,同时把控制台挂载为/dev/console。
7.4 额外运行时文件
容器会获得一组额外的挂载/文件,用于处理 rootfs 中“不可移植”的文件——这些文件通常由运行时创建并填充容器特定信息,以避免对宿主产生副作用:
/etc/hosts/etc/resolv.conf/etc/hostname/etc/localtime
这一设计在 RancherOS 的 os-config.tpl.yml 中同样可见:
system-volumes服务将/etc/hosts、/etc/resolv.conf等以卷方式注入系统容器,保证容器内网络与主机名信息的正确性。
7.5 默认值(Defaults)
以下默认值可由用户覆盖,但省略时对容器内进程生效:
| Type | Value |
|---|---|
| Parent Death Signal | SIGKILL |
| UID | 0 |
| GID | 0 |
| GROUPS | 0, NULL |
| CWD | "/" |
| $HOME | 当前用户的 home 目录,或 "/" |
| Readonly rootfs | false |
| Pseudo TTY | false |
- Parent Death Signal = SIGKILL:父进程死亡时,内核以 SIGKILL 终止容器进程,防止孤儿进程游离;
- UID/GID = 0:容器内默认以 root 运行,再通过 capabilities 收敛权限;
- Readonly rootfs = false:默认 rootfs 可写;
- Pseudo TTY = false:默认不分配伪终端。
八、容器动作(Actions):标准管理 API
容器创建完成后,存在一组标准动作作为容器的公共 API:
| Action | Description |
|---|---|
| Get processes | 返回容器内所有运行进程的 PID |
| Get Stats | 返回容器整体的资源统计 |
| Wait | 等待容器的 init 进程(PID 1) |
| Wait Process | 等待容器内任一进程,返回其退出状态 |
| Destroy | 杀死容器 init 进程并移除所有文件系统状态 |
| Signal | 向容器 init 进程发送信号 |
| Signal Process | 向容器内任一进程发送信号 |
| Pause | 暂停容器内所有进程 |
| Resume | 恢复所有被暂停的容器进程 |
| Exec | 在容器内执行新进程(需要 setns) |
| Set | 容器创建后设置其配置 |
在 libcontainer README 中,这些动作对应的 Go 调用为:
// 返回容器内所有进程的 pids processes, err := container.Processes() // 获取容器及其进程的详细 cpu、memory、io、network 统计 stats, err := container.Stats() // 暂停容器内所有进程 container.Pause() // 恢复所有被暂停的进程 container.Resume() // 向容器 init 进程发送信号 container.Signal(signal)九、在运行中的容器内执行新进程(Exec)
用户可以在运行中的容器内执行新进程,其语义要点如下:
- 二进制可达性:任何要执行的二进制必须位于容器 rootfs 内(与文件系统一节的原则一致);
- 持久性:新进程在容器 rootfs 内运行,其对容器文件系统所做的任何修改,在进程结束后依然保留;
- 命名空间共享:新进程将加入容器现有的全部命名空间;
- 暂停联动:容器被暂停时,新进程也随之暂停;容器恢复时进程恢复;
- 生命周期约束:新进程仅在容器主进程(PID 1)运行期间执行,容器重启时它不会被自动重启。
已规划的新增能力(Planned additions)
规范同时预告了 Exec 的后续增强方向:
- 新进程将拥有嵌套在容器 cgroups 内部的独立 cgroups,用于进程追踪与(可选)资源分配;
- 其中freezer cgroup 为必需,其余 cgroups 可选;
- 进程执行器必须在启动进程前把 PID 放入正确的 cgroups,确保任何子进程或线程都无法逃逸出 cgroups;
- 进程停止时,执行器会以 best-effort 方式尝试停止其所有子进程并移除子 cgroups。
结语
通过 libcontainer SPEC v1,我们可以看到一套完整、自洽的容器标准:六个命名空间定义隔离边界,rootfs 加六类必需挂载定义执行环境,十个 cgroup 子系统定义资源边界,15 项启用 / 22 项禁用的 capabilities 加 AppArmor、SELinux 定义安全边界,管道同步 + 无长驻 init 定义进程模型,十二种标准动作定义运维接口。而这些规范中的每一项,都能在 RancherOS 的源码与配置中找到对应实现——从 pkg/init/switchroot/switchroot.go 的MS_MOVE + chroot,到 pkg/init/env/env.go 的DOCKER_RAMDISK,再到 os-config.tpl.yml 中系统容器对命名空间的灵活取舍。对任何希望深入理解容器运行时内部机制、或基于 libcontainer 构建自有运行时的人来说,这份 v1 规范都是最值得精读的第一手材料。
- 操作系统
- 云原生
- 容器运行时
【免费下载链接】os
Tiny Linux distro that runs the entire OS as Docker containers
相关推荐
Flynn 中的 libcontainer 容器规范 v1 全解析:命名空间、cgroups、Intel RDT 与安全配置
Flynn 中的 libcontainer 容器规范 v1 全解析:命名空间、cgroups、Intel RDT 与安全配置 本文以开源仓库 Flynn(一个基
云原生微服务容器编排运维runc/libcontainer v1 容器规范深度解析:命名空间、根文件系统、cgroups、Intel RDT 与安全管理
runc/libcontainer v1 容器规范深度解析:命名空间、根文件系统、cgroups、Intel RDT 与安全管理 本文围绕开源仓库 runc(l
云原生容器运行时CLIDocker 底层容器引擎解析:libcontainer 的 Go 原生命名空间、cgroups 与生命周期管理实战
Docker 底层容器引擎解析:libcontainer 的 Go 原生命名空间、cgroups 与生命周期管理实战 导读 libcontainer 是 Doc
开发者工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考