66 init系统0.9版发布:摆脱s6依赖并引入声明式事件系统

一个更独立的 init 系统诞生了
在 Linux init 系统与服务管理器的版本图谱中,66 是一个相对小众但设计考究的存在。它由 Obarun 项目维护,长期以来构建在 skalibs、s6 与 execline 这套由 Laurent Bercot 打造的底层工具链之上。而在 0.9.0.0 与 0.9.1.0 两个版本中,66 完成了一次重大的架构转向:它在构建阶段彻底摆脱了 skalibs、s6 和 execline 的依赖。
在 Linux 生态中,init 系统(即 PID 1 进程)的演化经历了几个关键阶段:最早的 SysVinit 采用串行化的 shell 脚本启动模型,简单但缓慢;Upstart(Ubuntu 主导)引入了事件驱动的并行启动;systemd(2010 年由 Lennart Poettering 发起)则以二进制单元文件、socket 激活、cgroup 追踪等特性成为大多数主流发行版的默认选择,但也因其架构膨胀和"做太多事"而引发持续争议。与此同时,一条更贴近 Unix 哲学的进程监督路线始终存在:daemontools → runit → s6 → 66,每一代都在前代基础上改进易用性或功能完备度。66 的定位是在 s6 的可靠性基础上提供更高层级的抽象——声明式服务定义、树状组织、用户级服务管理——使得非专家用户也能使用进程监督范式。
skalibs 是由法国开发者 Laurent Bercot 创建的一套 C 语言底层库集合,提供字符串处理、数据结构、网络通信、进程管理等基础设施,其设计哲学是极致的最小化与安全性——避免动态内存分配中的常见陷阱,所有接口都追求确定性行为。s6 则是构建在 skalibs 之上的进程监督套件(process supervision suite),它实现了 daemontools 范式的现代化版本,核心理念是每个服务由一个专属的监督进程看护,服务崩溃时自动重启,状态变更通过文件描述符和 fifodir 通知。execline 是一种非交互式脚本语言,用链式执行(chain-loading)取代传统 shell 的 fork-exec 模型,每条命令解析自己的参数后直接 exec 下一条命令,消除了 shell 解析器的复杂性与安全隐患。这三者共同构成了一个自洽的 Unix 服务管理生态,但其高度内聚也意味着任何依赖它们的上层项目都必须完整引入整条链。
execline 的链式执行模型与传统 shell 脚本的工作方式有本质区别。在 bash/sh 中,解释器读取整个脚本,维护变量状态,通过 fork+exec 启动子进程并 wait 其返回。而在 execline 中,每个命令是一个独立的可执行文件,它解析自己需要的参数后,直接通过 exec() 系统调用替换自身为下一个命令——整个过程没有常驻的解释器进程,没有变量作用域的状态机,内存占用极低。这种模型的安全优势在于:不存在 shell 注入的攻击面(因为根本没有字符串插值和命令替换),也不存在解释器本身的漏洞暴露窗口。代价是编写 execline 脚本需要理解完全不同的编程范式,可读性对新手而言较差。
这个决定其实早在 0.7.0.0 版本时就已经预告,如今终于落地。66 现在只依赖 oblibs 进行构建,过去所有"借用"自 s6 生态的程序,现在都变成了 66 自己的实现。oblibs 是 Obarun 项目自行维护的基础库,功能上覆盖了 66 所需的那部分 skalibs 能力(如字符串操作、环境变量处理、进程生命周期管理等),但归属于 66 的治理体系。构建期依赖与运行时依赖的区分在发行版打包中至关重要:构建期依赖决定了编译服务器上需要安装什么、版本锁定关系如何传递、ABI 兼容性窗口多大;而运行时依赖只影响最终用户系统。当 66 在构建期不再依赖 skalibs/s6 时,打包者无需跟踪上游三个独立项目的发布节奏与 ABI 变更,大幅降低了交叉编译和 CI 矩阵的复杂度。
在开源生态中,项目间的依赖关系是维护成本的主要来源之一。skalibs 作为一套追求极致正确性的底层库,其 ABI(应用二进制接口)稳定性策略相对保守——Laurent Bercot 明确表示 skalibs 的 ABI 可能在主版本间不兼容。这意味着每次 skalibs 发布新版本,所有依赖它的项目(s6、s6-rc、execline、66 等)都可能需要重新编译,且版本组合需要精确匹配。对于发行版打包者而言,这创造了一个 N×M 的兼容性矩阵。66 迁移到自维护的 oblibs 后,将这个矩阵降维为 66 自身的单一版本线,构建可重复性和 CI 效率都显著提升。

需要精确澄清一点措辞,因为这很关键:execline 仍然是运行时依赖——生成的服务脚本仍会链式加载它。真正消失的是构建期依赖,以及随之而来的整个 skalibs/s6 层。这意味着打包者和发行版维护者的构建链条大幅简化。
为什么要离开 s6 层?
作者在帖子结尾主动表示,愿意解答"为什么离开 s6 层值得这么折腾"这一设计选择。对于熟悉这套工具链的人来说,s6 与 execline 以极致的最小化和 Unix 哲学著称,但也带来了较陡的学习曲线和构建复杂度。66 走向独立,本质上是在追求更强的可控性与可维护性——所有代码归于一处,测试、调试和演进都不再受制于上游。
进程监督是一种服务管理哲学,起源于 Daniel J. Bernstein 的 daemontools(1997 年前后)。其核心思想是:每个长期运行的服务都由一个轻量级的监督进程负责看护,监督器通过 waitpid 等系统调用等待子进程退出,一旦检测到退出即按策略重启。这与传统的 SysVinit 脚本模型形成对比——后者启动服务后就"放手",服务崩溃需要外部 cron 或人工介入。s6、runit、daemontools-encore 都属于这一范式,systemd 虽然也实现了自动重启,但其架构远比纯监督系统复杂。66 继承的正是这套监督理念,但在上层提供了更高级的声明式服务定义和事件驱动能力。
全新的事件系统:声明式服务响应模型
本次更新最引人注目的功能,是 66 引入的事件系统(event system)。它让一个服务能够响应系统上别处发生的事件,并对自身运行一条 66 命令。整个机制是完全可选的——一个不声明任何事件的服务,行为与以往完全一致。
反应器与事件源的角色划分
事件系统定义了两种角色:
- 反应器(Reactor):一个普通的 classic、oneshot 或 module 服务,带有一个
[Event]段。当触发条件满足时,它会对自身运行一条 66 命令(Do),并/或抛出一个具名事件(Emit)。 - 事件源(Source):一种新的
Type = event服务。它不运行任何进程,也没有[Start]段,66 start和66 stop用来"武装"或"解除"它。
反应器可以基于多种条件触发:某个服务的状态或结果、信号、文件系统事件(inotify)、cron 表达式、定时器,或者通过新命令 66 emit 抛出的用户事件。
66 的事件系统在软件架构层面实现了发布-订阅(pub-sub)模式。这一模式在消息中间件(如 MQTT、AMQP、NATS)中已是标准实践:发布者向主题(topic)发送消息,订阅者声明自己关心哪些主题,中间件负责路由。将此模式引入 init 系统是一个创新:事件源相当于发布者,反应器相当于订阅者,66 的事件调度机制相当于消息代理。与 systemd 的 Wants/After 依赖图相比,发布-订阅模型的优势在于松耦合——添加新的事件消费者不需要修改事件生产者的配置,事件源甚至不知道有多少反应器在监听。这使得系统配置的可组合性大大增强。
systemd 提供 .path 单元监控文件系统变更、.timer 单元实现定时触发,但它们本质上是"触发器单元激活目标单元"的命令式模型——触发逻辑与被触发服务之间存在隐式耦合,且触发条件分散在多个单元文件中。66 的事件系统采用不同的设计哲学:反应器在自己的服务文件中声明"我关心什么事件"以及"事件发生后我要做什么",事件源则是纯粹的信号发射器,不知道也不关心谁在监听。这种发布-订阅式的解耦使得服务的行为完全自描述——读一个服务文件就能理解它的完整生命周期,而不需要在系统中搜索所有可能触发它的路径。
实用示例:配置文件变更自动重启服务
官方给出了一个"配置文件被编辑时自动重启服务"的例子。反应器服务定义如下:
[Main]
Type = oneshot
Description = "A simple proof of event concept"
[Start]
Execute = ( echo "I restart myself when /etc/my-daemon is edited" )
[Event]
EventType = inotify
From = ( my-config-watcher )
Do = restart
它所监听的事件源:
[Main]
Type = event
EventType = inotify
Watch = /etc/my-daemon
On = ( IN_CLOSE_WRITE )
inotify 是 Linux 内核自 2.6.13 起提供的文件系统事件通知接口,取代了早期的 dnotify。用户空间程序通过 inotify_init 创建实例、inotify_add_watch 添加监控目标(文件或目录),内核随后将 IN_CREATE、IN_MODIFY、IN_CLOSE_WRITE、IN_DELETE 等事件推送到文件描述符上。相比轮询(polling),inotify 的 CPU 开销接近零,且响应延迟在毫秒级。66 的事件源中 EventType = inotify 直接映射到这一内核机制,On 字段指定的 IN_CLOSE_WRITE 表示文件被写入后关闭——这是检测配置文件修改完成的惯用事件,因为编辑器通常在写入完毕后才关闭文件描述符。
一个事件源可以服务多个反应器。这套设计相比 systemd 的 path 单元与依赖机制,显得更加声明式且解耦——服务"等待"它需要的东西,而不是被外部命令强制指挥。
环境变量管理与新命令
66 env:解决环境变量冻结难题
0.9.0.0 还解决了一个长期痛点。scandir 会在会话(session)存在之前很早就启动,因此它的环境是被"冻结"的,像 DISPLAY 或 WAYLAND_DISPLAY 这样的变量永远无法抵达被监督的服务。
在 s6 和 66 的架构中,scandir(扫描目录)是监督器树的根:一个 s6-svscan 或等效进程持续扫描某个目录,为目录中每个子目录启动一个监督器。问题在于,scandir 进程通常在系统引导极早期就启动(甚至在显示服务器之前),此时进程环境中不存在 DISPLAY、WAYLAND_DISPLAY、DBUS_SESSION_BUS_ADDRESS 等图形会话变量。由于 Unix 的进程模型是子进程继承父进程环境的快照,后续启动的服务永远看不到这些迟到的变量。systemd 通过 systemctl import-environment 和 dbus-update-activation-environment 解决此问题;66 现在用 66 env import 实现同等能力,并且与事件系统联动,让服务可以声明"等待 DISPLAY 变量就绪后再启动"。
新命令 66 env import DISPLAY XAUTHORITY 可以将这些变量原样发布给 scandir 中的每个服务。每次发布都会抛出一个 env.<variable> 事件,于是服务可以直接等待它所需的值到位,而不是被动接受指令。这与事件系统形成了优雅的呼应。
新增命令一览
本次新增了多条命令:66 log(读取服务、系统或交错日志,支持 -f、-g 和时间边界)、66 emit、66 env、66 runstate、66 fdholder、66 suspend、66 hibernate。值得强调的是,没有任何旧命令被移除。
此外,整套工具现在全面支持长选项。过去只接受短选项,如今 --help、--verbosity、--tree、--timeout 等都可用,--opt=value 与紧凑的 -tfoo 形式都支持,短选项保持不变,脚本无需改动。
66 status 的输出也更丰富了:人类可读的时长、所有服务类型统一的输出格式、最近的显著结果,以及一个全新的 by <who> 字段,告诉你上次状态转换是由谁触发的(用户、启动、事件、关机或自身)。
稳定性与代码质量的显著提升
启动流程也做了简化:boot 不再需要被告知要启动哪棵 tree,它会原生启动已启用的 trees。如果你有 boot 服务手动做了这件事,需要移除,否则会运行两次。
质量方面的进步同样值得关注。开发过程中修复了一长串内存 bug,包括:
- 解析任何带 logger 的服务时的栈溢出(stack smash);
Nice值从未生效(每个声明了 Nice 的服务都以 nice 19 运行);CapsBound与CapsAmbient同时使用会杀死服务的问题。
更重要的是,测试套件从 4 个文件扩展到 36 个,并在 CI 中通过 ASan 和 UBSan 运行。AddressSanitizer(ASan)和 UndefinedBehaviorSanitizer(UBSan)是 LLVM/GCC 提供的编译期插桩工具。ASan 通过在每次内存访问前插入边界检查代码来检测堆溢出、栈溢出、use-after-free、double-free 等内存安全问题,运行时开销约 2 倍。UBSan 则捕获 C/C++ 标准中未定义行为——如有符号整数溢出、空指针解引用、类型双关违规等。在 CI 中启用这两个工具意味着每次提交都会在受保护环境下运行测试套件,任何内存越界或未定义行为都会导致构建失败。这是一个健康的工程信号,说明项目正在从"能用"走向"可靠"。
66-tools 0.2.0.1:会话管理与命名空间支持
工具侧讲的是同一个故事——skalibs 与 execline 被移除,改用 Meson 构建。Meson 是一个现代化的构建系统生成器(类似 CMake),由 Jussi Pakkanen 于 2013 年发起,使用 Python 实现,后端通常搭配 Ninja 执行实际编译。相比传统的 autotools(autoconf/automake/libtool)三件套,Meson 的构建定义文件更简洁、构建速度更快(Ninja 的并行调度效率极高)、交叉编译配置更直观。对于 66 这样需要支持多种架构(x86_64、aarch64、RISC-V 等)的基础设施软件,Meson 的交叉编译 cross-file 机制比 autotools 的 --host/--build 参数体系更易维护。
此外还带来了几个不依赖 D-Bus 的重要组件:
- 66-userd / 66-userctl / pam_userd:为 66 系统提供会话与用户追踪。一个 PAM 模块向守护进程报告登录会话的开启/关闭;在用户首次登录时挂载其运行时目录并启动其 66-scandir 与已启用的 trees,在最后一次登出时停止它们。若该用户的 scandir 已在运行,则会被"接管"而非重复启动。注意:PAM 模块虽被安装,但不会自动接入你的 PAM 栈,这一步需要用户自行配置,文档中有说明。
PAM(Pluggable Authentication Modules)是 Linux/Unix 系统中统一认证、账户管理、会话管理和密码管理的框架。登录管理器(如 login、sshd、gdm)通过 PAM 栈执行一系列模块来完成用户认证和会话初始化。pam_userd 是 66-tools 提供的 PAM 模块,它在用户登录时通知 66-userd 守护进程"某用户的第 N 个会话已开启",使得 66 能够在正确的时机挂载 /run/user/<uid> 并启动用户级服务树。之所以不自动接入 PAM 栈,是因为 PAM 配置错误可能导致所有用户无法登录——这是一个需要管理员明确决策的安全敏感操作。
- 66-ns:pid namespace 下的监督机制被重写。当请求 pid namespace 时,66-ns 作为 pid 1 运行,充当被监督守护进程的透明代理——转发可捕获的控制信号、跨越双重 fork 跟踪真实守护进程、将其退出码镜像回监督器,并在服务消失后拆除 namespace。新的
-p/--pidfile让监督更具权威性。
PID namespace 是 Linux 内核命名空间(namespace)机制的一部分,它为进程提供一个隔离的进程 ID 空间——namespace 内的第一个进程看到的 PID 为 1,与宿主机的 PID 1(通常是 init)完全独立。这在容器(如 Docker、LXC)中广泛使用,但也可以用于服务隔离:将一个服务及其子进程置于独立 PID namespace 中,防止它们向 namespace 外的进程发送信号。挑战在于 PID namespace 内的 PID 1 具有特殊语义——如果它退出,内核会向 namespace 内所有进程发送 SIGKILL。66-ns 充当这个 PID 1 的角色,透明地代理信号并追踪真实的服务进程,即使服务执行了传统守护进程的双重 fork(daemonize),66-ns 仍能通过 pidfile 或进程树追踪找到它。
传统的守护进程通过两次 fork 实现自我"脱离":第一次 fork 后父进程退出,子进程成为孤儿被 init 收养;子进程调用 setsid() 成为新会话的领导者;第二次 fork 后,最终的孙进程不再是会话领导者(因此无法意外获取控制终端),这个孙进程就是实际运行的守护进程。问题在于,监督器启动的是原始进程,两次 fork 后真正的服务进程 PID 已经完全不同,监督器对原始子进程执行 waitpid 只会立即得到"已退出"的返回。现代服务管理(包括 systemd 的 Type=forking 和 66-ns 的 --pidfile)通过读取服务写入的 PID 文件来重新定位到真实进程,从而恢复监督能力。
结语:小众但认真的 systemd 替代方案
对于运行 Obarun 或希望脱离 systemd 的用户来说,66 的这次更新意义重大。它不仅摆脱了外部构建依赖、简化了打包,还通过事件系统提供了一种声明式、解耦的服务响应模型,同时在稳定性上做了扎实的工程投入。
本次发布还配套了大量新的上手文档:入门指南、速查表、依赖说明、日志、故障排查,以及"从 systemd/OpenRC/runit 迁移"和"在容器中运行 66"等专题。对于评估替代 init 系统的读者,这是一个值得关注的成熟度里程碑。
相关推荐

Claude Code创建者建议:大改动别急着写代码,先对齐再动手
Claude Code创建者Boris分享AI编程协作最佳实践:面对大改动,先读仓库提问、确认方案再编码、写完立刻验证。掌握这套流程,避免AI沿错误方向返工,提升编程效率。

HydraNet-VSM架构解析:Mamba与注意力机制并行融合的推理新思路
深入解析HydraNet-VSM混合架构设计提案,探讨Mamba状态空间模型与Attention注意力机制并行融合方案,以及Verified Step Memory验证循环如何解决思维链推理不忠实问题。

Seed7编程语言:无GC实现内存安全的独特设计
深入解析Seed7编程语言如何在不依赖垃圾回收(GC)的情况下实现内存安全,探讨其AOT编译、可扩展语法、整数溢出检查等核心特性,以及与C++、Rust、Java等主流语言的对比。