SerenityOS 内核容器机制深度解析:VFS 根上下文、进程列表与 Hostname 隔离
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
本指南以 Documentation/Kernel/Containers.md 为核心脉络,系统讲解 SerenityOS 内核如何通过 VFS 根上下文(VFS Root Context)、作用域进程列表(Scoped Process List)与主机名上下文(Hostname Context)三类资源实现进程级隔离,并完整梳理unshare_open/unshare_create/unshare_enter三条系统调用及 Jail 安全机制的内核实现与用户态调用链。读完本文,你将掌握 SerenityOS 容器从资源创建、进程进入、exec 切换直到 jail 强制的完整生命周期,并能对照runc实用工具读懂真实容器配置文件的部署流程。
什么是容器:SerenityOS 中的“概念性建构”
在 SerenityOS 生态中,容器(Container)被定义为一种概念性建构(conceptual establishment):它并不对应某个单一内核对象,而是通过一组对外暴露的、不共享的资源(unshared resources)将用户程序彼此隔离。这些资源包括但不限于:
- PID 视图(process list)——进程只能看到与自己同属一个作用域的进程;
- 文件系统视图(filesystem view)——进程只能看到自己根上下文下的挂载树;
- 主机名(hostname)——一组进程共享一个独立的主机名命名空间。
这种“组合多种隔离维度、叠加为一个容器”的设计,与 Linux 中 namespace 的思想同源但实现独立,是 SerenityOS 内核在 Kernel/ 目录下自研的机制,而非对 Linux 代码的移植。
内核侧的三种隔离机制
当前内核(Kernel/)对外暴露三类可能的隔离机制,分别由独立的引用计数对象与全局链表管理:
VFS 根上下文(VFS Root Contexts)
VFS(虚拟文件系统)根上下文是每个进程持有的、用于“观察一棵文件系统树”的上下文。一个 VFS 根上下文持有两部分关键数据:
- 挂载表(mount table):该上下文内的全部挂载关系;
- 根目录 Custody:上下文的根目录句柄。
用户进程既可以持有所有默认程序共享的全局上下文,也可以持有特殊上下文来限制自身的文件系统视图。
从源码看,该机制实现在 Kernel/FileSystem/VFSRootContext.h:
- 通过
create_with_empty_ramfs(...)/create_with_filesystem(...)工厂方法创建,并可通过AddToGlobalContextList枚举控制是否加入全局上下文链表; - 每个上下文拥有独立的
IndexID(AK_TYPEDEF_DISTINCT_ORDERED_ID(u64, IndexID))作为全局索引; - 提供
root_custody()、add_new_mount()、unmount()等接口管理根目录与挂载点。
文档明确其生命周期规则:VFS 根上下文挂接在全局链表中,当其最后一个挂载(即根挂载)被卸载时,才会从全局链表中移除。这意味着只要根文件系统还挂着,即使没有进程引用该上下文,它依然存活在全局链表中,可供后续unshare_enter按 id 重新进入。
进程列表(Process lists)
进程列表分为两类:
- 全局进程列表:所有进程默认挂接其上;
- 作用域进程列表(scoped process list):由用户进程显式创建并持有引用。
当进程持有某个作用域进程列表的引用后,它只能看到位于同一列表上的其他进程,从而实现对 PID 视图的隔离。
实现见 Kernel/Tasks/ScopedProcessList.h:
- 基于
AtomicRefCounted的引用计数对象,内部以IntrusiveList维护attached_processes()集合; - 拥有独立
IndexID与全局链表节点m_list_node; - 通过
attach(Process&)/detach(Badge<Process>)维护成员关系,m_attach_count记录附着进程数。
其生命周期规则与主机名上下文一致:作用域进程列表挂接在全局链表上,当最后一个仍附着于该列表的进程与之分离时,列表才从全局链表中移除。
主机名上下文(Hostname contexts)
主机名上下文让一组用户进程共享一个自定义的主机名。任何一个持有该上下文引用的进程都可以修改主机名,且修改会立即反映给所有附着在该上下文上的其他进程,从而实现主机名维度的“命名空间”隔离。
实现见 Kernel/Tasks/HostnameContext.h:
- 同样基于
AtomicRefCounted,通过create_initial()创建内核初始上下文、create_with_name(StringView)创建命名上下文; - 主机名缓冲区为
FixedStringBuffer<UTSNAME_ENTRY_LEN - 1>(受 POSIXstruct utsname条目长度约束),并用RecursiveSpinlockProtected保护并发访问; - 拥有独立
IndexID、全局链表节点与m_attach_count引用计数。
生命周期规则:上下文挂接在全局链表中,当最后一个附着进程分离时才从链表移除。
内核与用户空间的三条接口
内核通过 3 条系统调用完成资源隔离的“创建—初始化—进入”三步操作,声明位于 Kernel/API/Syscall.h(第 195~197 行)并注册为NeedsBigProcessLock::No:
| 系统调用 | 参数结构体 | 职责 |
|---|---|---|
unshare_open | SC_unshare_open_params { int type; } | 为一种新的非共享资源创建文件描述符,返回可操作的 fd |
unshare_create | SC_unshare_create_params { int unshared_resource_fd; } | 真正创建非共享资源,返回资源的索引号(id) |
unshare_enter | SC_unshare_enter_params { int type; int id; int flags; } | 使当前进程按资源索引进入(附着到)指定资源 |
资源类型与进入标志
类型与标志定义于 Kernel/API/Unshare.h:
enum class UnshareType { ScopedProcessList = 1, VFSRootContext = 2, HostnameContext = 3, }; enum class UnshareEnterFlags : u32 { None = 0, CurrentProgram = 1 << 0, // 立即生效 AfterExec = 1 << 1, // 在下次 exec 时生效 };UnshareEnterFlags的两个标志值得重点说明:
CurrentProgram(立即生效):调用后当前进程立即替换/附着到新资源;AfterExec(exec 后生效):资源先被记录到进程的m_exec_*槽位,待下次exec时统一替换。内核实现中,Process维护了m_exec_vfs_root_context、m_exec_hostname_context、m_exec_scoped_process_list三个AfterExecResource槽位(见 Kernel/Tasks/Process.h),并在execve系统调用路径中通过replace_resource一次性应用(见 Kernel/Syscalls/execve.cpp 第 636~638 行)。这保证容器入口程序启动瞬间“原子”切换到隔离环境中。
内核实现细节
三条系统调用的完整实现位于 Kernel/Syscalls/unshare.cpp,其公共前置条件为:
- 调用进程必须持有
pledge("unshare")承诺(require_promise(Pledge::unshare)); - 调用进程必须为超级用户(
credentials->is_superuser()),否则返回EPERM; - 若进程已处于jail状态(
is_jailed()),直接返回EPERM——jail 与资源切换互斥。
unshare_open:根据params.type分派到ScopedProcessList/VFSRootContext/HostnameContext三种类型,创建对应的 Kernel/FileSystem/UnsharedResourceFile.h(一个不提供读写、仅作为资源句柄的特殊File子类),随后在进程 fd 表中分配新 fd 并设置FD_CLOEXEC。非法类型返回ENOTSUP,负数类型返回EINVAL。
unshare_create:通过 fd 找回对应的UnsharedResourceFile(校验is_unshared_resource_file(),否则EINVAL),然后调用其initialize_resource()。该方法(见 Kernel/FileSystem/UnsharedResourceFile.cpp)按类型完成资源实体创建并返回索引号:
ScopedProcessList:ScopedProcessList::create();VFSRootContext:若此前已通过 ioctl 附加了根文件系统,则VFSRootContext::create_with_filesystem(...),否则以空的 ramfs创建create_with_empty_ramfs(...),均加入全局上下文链表;HostnameContext:基于进程当前附着上下文的内容派生新上下文。
unshare_enter:要求flags != None(否则EINVAL),然后按params.type通过*_for_id(params.id)从全局链表查找资源,再根据CurrentProgram/AfterExec标志分别替换当前上下文或写入m_exec_*槽位。
用户态封装
LibCore为应用开发者提供了同名的 C++ 封装,见 Userland/Libraries/LibCore/System.cpp:
ErrorOr<unsigned> unshare_open(Kernel::UnshareType type); // 返回 fd ErrorOr<u32> unshare_create(unsigned fd); // 返回资源索引号 ErrorOr<void> unshare_enter(Kernel::UnshareType type, unsigned index, int flags);三个封装内部均通过syscall(SC_unshare_*, ¶ms)进入内核,并使用HANDLE_SYSCALL_RETURN_VALUE宏把负值错误码转换为Error。
Jail:将容器固化为安全沙箱
当用户进程被“直到退出”式 jail(jailed until exit)后,它不能再创建或附着到其他任何资源。这使得 jail 成为构建安全(沙箱化)容器的有效机制:用户程序及其全部后代进程,将始终使用容器创建时选定的那套资源,无法中途逃逸或换用别的隔离上下文。
内核侧的强制点
Process::is_jailed()(见 Kernel/Tasks/Process.h)判断依据是jailed_until_exit.was_set() || jailed_until_exec,即支持“直到退出”与“直到 exec”两种时限。jail 的强制力体现在多处:
- 资源切换被禁止:
unshare_open/unshare_create/unshare_enter在入口处即检查is_jailed()并返回EPERM(见 Kernel/Syscalls/unshare.cpp); - 设备访问受限:
Device::open检查is_jailed() && !is_openable_by_jailed_processes()时返回EPERM(见 Kernel/Devices/Device.cpp 第 115 行); - 内核配置只读化:SysFS 下的布尔/字符串内核配置项在写入路径上同样拒绝 jailed 进程(见 Kernel/FileSystem/SysFS/Subsystems/Kernel/Configuration/BooleanVariable.cpp 与 StringVariable.cpp),防止容器内进程篡改全局内核参数。
用户态入口
用户态通过prctl进入 jail 模式,LibCore封装见 Userland/Libraries/LibCore/System.cpp:
enter_jail_mode_until_exit():调用prctl(PR_SET_JAILED_UNTIL_EXIT, ...);- 另有
enter_jail_mode_until_exec()(PR_SET_JAILED_UNTIL_EXEC)对应“直到下次 exec 解除”的弱化模式。
实战:runc 工具如何组装一个容器
Userland/Utilities/runc/是 SerenityOS 上以用户态方式组装容器的参考实现,它把本文所述的三类资源与 jail 机制串成完整流程。
命令行用法
Userland/Utilities/runc/main.cpp 中serenity_main解析以下参数:
runc [-p|--pid-isolation] [-j|--enforce-jail] [-E|--preserve-env] \ [-f|--configuration <json-config>] [command...]-p/--pid-isolation:创建并进入新的作用域进程列表;-j/--enforce-jail:对容器强制 jail 限制;-E/--preserve-env:保留用户环境变量执行命令;-f/--configuration:使用 JSON 配置文件部署容器(与位置参数命令二选一);- 位置参数
command:要在容器内以提升权限运行的命令。
不使用配置文件时,要求至少指定--pid-isolation或--enforce-jail之一,否则报错Can't create a container with no attributes (jail/pid-isolation).。
JSON 配置文件格式
使用-f时,配置文件是 JSON 对象,必需/可选字段如下(解析逻辑见extract_values_from_file):
| 字段 | 类型 | 说明 |
|---|---|---|
jail | bool | 必需,是否启用 jail 强制 |
pid-isolation | bool | 必需,是否启用 PID 隔离(作用域进程列表) |
command | string | 必需,容器入口命令 |
layout | array | 必需,VFS 根上下文的挂载布局创建序列 |
hostname | string 或 null | 可选,但必须显式指定其一;为字符串时创建并进入新的主机名上下文 |
解析器对非法组合(如hostname同时为 null 与字符串)会拒绝加载。挂载布局由 Userland/Utilities/runc/LayoutParsing.cpp 与 Userland/Utilities/runc/VFSRootContextLayout.cpp 处理,支持“在临时目录中构造根挂载点 → 按序列创建后续挂载 → 复制挂载骨架到新的 VFS 根上下文”的流程。
部署序列:六步组装一个容器
deploy_container_based_on_config_file严格按以下顺序部署(源码注释亦明确此顺序不可颠倒):
- 创建 PID 隔离:
unshare_open(ScopedProcessList)→unshare_create(fd)→unshare_enter(ScopedProcessList, index, AfterExec); - 创建 VFS 根上下文:在临时目录构造布局并应用到新 VFS 根上下文;
- 附着 VFS 根上下文:
unshare_enter(VFSRootContext, id, AfterExec),随后卸载并清理临时目录; - 创建并附着主机名上下文:
unshare_open(HostnameContext)→unshare_create(fd)→unshare_enter(HostnameContext, id, CurrentProgram)→sethostname(hostname); - 强制 jail:
enter_jail_mode_until_exit(); - exec 入口命令:
exec_command(command, false),此时第 1、3 步中标记为AfterExec的资源随 exec 一并生效。
注意第 4 步使用CurrentProgram(立即生效)而非AfterExec,因为要在 exec 前先以新主机名调用sethostname,从而把容器主机名“烙进”新上下文。
最小权限原则:pledge 的逐步收紧
部署过程中pledge承诺被逐级收窄,体现最小权限原则:
- 读配置前:
pledge("stdio rpath wpath cpath proc mount unshare exec fattr chown"); - 构造完 VFS 布局后移除
fattr/chown:... mount unshare exec; - 附着完主机名上下文后移除
unshare:... mount exec; - jail 后移除
proc:... exec,最终以最少的权限执行容器命令。
关键源码索引
- 官方文档:Documentation/Kernel/Containers.md
- 系统调用实现:Kernel/Syscalls/unshare.cpp
- 资源类型与标志定义:Kernel/API/Unshare.h
- 系统调用号与参数结构:Kernel/API/Syscall.h
- 资源句柄文件:Kernel/FileSystem/UnsharedResourceFile.h、Kernel/FileSystem/UnsharedResourceFile.cpp
- 三类隔离对象:Kernel/FileSystem/VFSRootContext.h、Kernel/Tasks/ScopedProcessList.h、Kernel/Tasks/HostnameContext.h
- 进程侧资源槽位与 jail 状态:Kernel/Tasks/Process.h、Kernel/Syscalls/execve.cpp
- 用户态封装:Userland/Libraries/LibCore/System.cpp
- 容器部署工具:Userland/Utilities/runc/main.cpp
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考