在 Linux 系统的初始化与服务管理领域,systemd 已经成为现代发行版的事实标准。作为 PID 1 进程,它不仅承担着系统启动的重任,更通过其精心设计的架构实现了进程管理、资源控制、服务依赖等核心功能。本文从源码架构、cgroup 管理模型、unit 文件设计三个维度,剖析 systemd 的工程实现细节。

源码目录结构与核心组件

systemd 项目托管于 GitHub,其源码目录结构体现了清晰的职责分离设计理念。根目录下的 src/ 包含了所有守护进程、库和命令行工具的实现。根据代码共享程度的不同,源码被组织为多个层次化的目录:

基础层 包括 src/fundamental/src/basic/ 两个目录。前者遵循更严格的编译规则,可被树内所有代码使用但不能依赖任何外部代码,主要用于 EFI 和用户空间代码;后者则仅供用户空间代码使用,可依赖 src/fundamental/。这两个目录提供了字符串处理、哈希表、文件操作等基础原语,是整个项目的技术底座。

共享库层 包含 src/libsystemd/src/shared/ 两个目录。前者实现 libsystemd.so 共享库,提供与 systemd 守护进程通信的客户端接口;后者则提供各组件之间共享的工具代码,编译为 libsystemd-shared-<nnn>.so。这种分层设计确保了代码复用与模块边界的平衡。

核心服务层 位于 src/core/ 目录,实现了 systemd 系统和服务管理器的核心逻辑。PID 1 进程正是基于此目录的代码构建,它负责加载配置、管理 unit 生命周期、处理依赖关系以及与内核交互。此外,src/core/bpf/ 中包含了用于 PID 1 的 BPF 程序,用于追踪和资源监控。

工具与测试层 分别对应 src/test/test/ 目录。前者实现单元测试,覆盖 src/basic/src/shared/ 中的模块;后者则进行系统级集成测试,验证各组件在真实环境中的协作能力。

从架构图中可以看出,当启动一个 unit 需要 fork 新进程时,配置信息会通过 memfd 传递给 systemd-executor 二进制进程,由后者在 execve 之前完成沙箱化、资源限制、环境变量等配置的应用。这种设计避免了 fork 与 exec 之间的过度处理,遵循了 glibc 的最佳实践。

cgroup 层级模型与单写者原则

systemd 将 cgroup 作为进程分组和资源管理的核心机制。在 cgroup v2(统一层级)架构下,系统所有支持的控制器(cpu、io、memory、pids 等)都通过单一挂载点 /sys/fs/cgroup/ 暴露。systemd 在此基础上构建了与 unit 树一一对应的控制组层级。

在统一层级中,根切片 -.slice 位于树的顶端,其下分为 system.slice(系统服务)、user.slice(用户会话)、machine.slice(容器和虚拟机)等顶级切片。每个 *.service*.scope unit 对应一个叶子控制组,进程运行于其中。这种结构使得 systemd-cglssystemctl status 等工具能够清晰展示进程与资源的对应关系。

systemd 在 cgroup 管理中遵循严格的单写者原则。由于内核要求每个 cgroup 子树只能有一个用户空间所有者,systemd PID 1 声明对主层级的独占所有权,其他软件不得直接操作 systemd 管理的 cgroup 目录。若需在子树内进行资源管理,必须通过 systemd 提供的 API 或使用委托机制。

委托机制(Delegation)是 systemd 与其他管理器(如容器运行时或嵌套 systemd 实例)共享控制组子树的关键手段。Delegate= 属性仅在 servicescope unit 上可用,设置为 yes 时,systemd 保留对 unit 自身控制组的管理权,但将子控制组的控制权交给被委托程序。被委托程序需要在收到信号就绪之前,将自身进程移动到更深层的子控制组中,以避免违反 "内部节点不得包含进程" 的 cgroup v2 规则。

unit 文件设计与资源控制映射

unit 文件是 systemd 的声明式配置前端,采用 INI 风格语法,包含 [Unit][Service][Scope][Slice] 等段落。对于资源控制而言,大多数配置项位于 [Service][Scope][Slice] 段落,直接映射到 cgroup 控制器的相应参数。

常见的资源控制配置项包括:CPUWeight=CPUQuota= 用于设置 CPU 权重和配额限制;MemoryMax=MemoryHigh= 用于设定内存上限;IOWeight=IO 相关参数用于 I/O 资源管理;TasksMax= 用于限制可创建的线程或任务数量。这些配置在 unit 解析阶段被转换为内部数据结构,运行时由 systemd 转化为 cgroup 文件系统的相应操作。

unit 文件的解析主要发生在 src/core/load-fragment.c 中。配置项的元数据由 src/core/load-fragment-gperf.gperf.in 生成,确保每个设置项都能正确映射到内部字段和 cgroup 属性。新的 unit 设置需要同时支持三种输入方式:文本 unit 文件、D-Bus 消息,以及 systemctl set-propertysystemd-run 命令行工具。

systemd 提供了多层次的配置渠道:位于 /usr/lib/systemd/system/ 的供应商配置、/etc/systemd/system/ 的本地覆盖、以及 .d/ 目录下的 drop-in 文件。systemctl set-property 命令则将配置片段写入 /etc/systemd/system.control/ 目录,实现对运行中 unit 的资源控制参数调整。

工程实践启示

从 systemd 的架构设计中,可以提炼出若干系统级工程的实践原则。首先是分层与依赖管理:通过 src/fundamental/src/basic/src/libsystemd/src/shared/ 的层次划分,明确了代码的复用边界和依赖关系,避免了单体架构中常见的循环依赖问题。

其次是单点控制与委托分离:cgroup 的单写者原则确保了资源管理的一致性,避免多个组件同时修改控制组导致的不确定行为。委托机制则在保持主控权的前提下,实现了与外部管理器的协作。

第三是声明式配置与运行时转换:unit 文件提供了简洁的声明式接口,将用户意图与底层实现解耦。解析器将配置转换为内部模型,再由运行时转化为具体的系统调用和文件系统操作,这种设计模式在现代云原生工具中同样常见。

最后是测试覆盖与模糊测试:项目对 fuzzing 测试的重视(test/fuzz/ 目录和 OSS-Fuzz 集成)体现了对配置解析器等关键组件安全性的关注,这对于处理外部输入的系统组件尤为重要。


参考资料