> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/20160708-freebsd-yu-rtems/getting-the-job-done-the-init-system-debate-relaunched.md).

# 把活干完：init 系统辩论再起

* 原文：[Getting the Job Done: The init System Debate Relaunched](https://freebsdfoundation.org/wp-content/uploads/2016/08/Getting-the-Job-Done.pdf)
* 作者：**Mark Heily**

随着时间推移，Unix 操作系统演化出多种启动程序并影响其执行的机制。为便于理解，我们来快速浏览一下典型 FreeBSD 系统上运行的不同类型的程序。

计算机首次启动时，**rc(8)** 机制启动一系列称为“服务”的程序，这些程序可以执行一次性的系统初始化任务，也可以运行常驻后台的守护进程。这些服务在计算机运行期间也可以被停止和启动。

计算机每运行一分钟，**cron(8)** 守护进程就会唤醒并启动计划任务。cron 还支持在经过一定时间间隔后运行 **at(1)** 作业，也可以在系统负载降到某阈值以下时运行 **batch(1)** 作业。

还有许多其他启动程序的机制。**Inetd(8)** 在有入站套接字创建时启动程序。**nice(1)** 以修改后的优先级运行程序。**chroot(8)** 以替代的根目录启动程序。**service(8)** 用“监督”进程启动程序，在程序崩溃时可以重启它。**jail(8)** 等 Jail 管理器在 Jail 环境内启动程序。**iohyve(8)** 等虚拟机管理器启动执行 bhyve 虚拟机的程序。**cloudabi-run(1)** 命令以一组预分配的文件描述符启动程序。

上述每种机制都有自己的配置文件格式、独特的使用方式，基本上各自生活在自己的孤立世界里。如果我告诉你，这些不同的启动器背后有一个共同的主题呢？如果有可能用一个“启动一切”的机制，作为我们今天使用的许多操作系统工具乃至未来工具的共同基础呢？

我开始更详细地探讨这些问题，并有一些有趣的结果要分享。我实现了一个名为“the jobd job framework”（jobd 作业框架）的新系统，简称 jobd。

## 作业框架概览

jobd 是一种在单个操作系统实例中启动和监控“作业”（job）的机制。作业可以理解为：1）要完成的一定工作量，2）工作开始前所需的依赖和前置条件，3）观察并与执行工作的程序交互的多种方式，4）工作完成后执行的一组清理动作。

这番话有点长，让我们把它拆成小块。先从作业框架中的重要概念和常用术语说起。

作业由一个名为“manifest”的 JSON 配置文件定义。manifest 中包含程序、执行上下文、属性、依赖和资源的组合。这些术语有特定含义，下文逐一讨论。

作业中的“程序”就是可执行文件的路径和传给 **execl(3)** 系统调用的 ARGV 参数向量。在 jobd 调用 **fork(2)** 创建新进程后，这会启动新程序映像的执行。

作业的“执行上下文”代表 fork() 调用之后、exec() 调用之前对子进程的所有更改。例如：设置用户 ID 和组 ID、调用 **chroot(2)**、设置资源限制、设置 umask 和 nice 值等。

系统管理员需要灵活定制作业运行的各个方面。例如，他们可能想修改网络守护进程监听的端口号。作业“属性”就是允许这种控制的机制。

作业属性是简单的键值字符串，可以在 manifest 内部替换。属性也可以通过库调用或 **jobcfg(1)** 命令直接查询。更进一步，属性可以借助模板生成应用特定的配置文件，例如 Apache 使用的 httpd.conf 主配置文件。

作业可以有“依赖”，决定作业何时停止和启动。作业可以按需启动、按固定时间间隔启动、在特定日历日期/时间启动，或由系统管理员手动启用。作业还可以依赖其他作业的状态，从而按某种顺序启动和停止多个作业。

作业可以有“资源”，代表在作业启动前自动创建、在作业终止时自动销毁的外部事物。作业假定拥有这些资源。这与 Jail 结合使用最为有用，可让你动态创建 Jail 来运行作业。

我们已经讨论了作业框架中“作业”的主要构成。回想本文第一部分的启动器清单，jobd 提供的功能与任何现有机制都不是一一对应。jobd 与 **rc(8)** 非常相似，支持管理服务所需的一切，但它并不与 **init(1)** 和引导/关机过程绑定。它更像一个自动机，持续在后台运行，通过启动、停止或重启程序来响应事件和条件。

| 概念             | 含义             | 示例                                 |
| -------------- | -------------- | ---------------------------------- |
| 程序（Program）    | 运行什么           | **/usr/local/sbin/httpd**          |
| 上下文（Context）   | 如何运行程序         | **setuid(2)** 为 ‘httpd’            |
| 属性（Property）   | 告诉程序的信息        | 在 80 端口监听                          |
| 方法（Method）     | 用户可调用以与程序交互的命令 | 运行 `jobctl httpd status` 显示服务器状态信息 |
| 资源（Resource）   | 程序运行前需要的东西     | 从 Ports 树安装 ‘httpd’ 软件包            |
| 依赖（Dependency） | 程序何时运行         | 当有客户端连接到 80 端口时                    |

既然作业框架的范围已经清楚，有必要说明一些明确不在项目范围内的事项。与过去几十年出现的多数 init 系统替代方案不同，**jobd(8)** 并不想取代 **init(8)** 或夺取 pid #1 的角色。它不处理设置控制台和伪终端、挂载 **/etc/fstab** 中列出的文件系统等早期引导任务。如果你引导进入单用户模式，我预计这仍由 **init(8)** 和 **rc(8)** 处理，方式与现在基本相同。

尽管作业框架灵活，能覆盖大量用例，我仍希望它首先作为 **rc(8)** 和当前 FreeBSD init 系统的替代品。这一立场可能重新点燃社区内的激烈辩论，因为现状有热情的捍卫者。尽管如此，鉴于采用作业框架的潜在收益，这场辩论值得再次展开。

有人会问：“为什么要改？自 BSD 起源以来，shell 脚本一直运行良好。”我无法反驳这一说法的历史事实，但我要指出，现代计算的图景与 Unix 早期已大不相同。新问题需要新方案。

以下是我认为依靠一堆 shell 脚本驱动 init 系统会让我们裹足不前的理由：

* **rc(8)** 系统不是为后台运行、响应需要采取行动的事件和条件而设计的。它只在引导时运行一次，关机时运行一次。
* shell 变量是共享单一全局命名空间的简单字符串对。无法用简单的键值对构建复杂的层次化数据结构。不支持信息丰富的数据结构，**rc(8)** 系统就只能局限于简单问题域。
* shell 脚本把数据和代码混在同一个文件里。这使得无法以编程方式编辑 **rc(8)** 脚本，除非是更改变量名这类很琐碎的编辑。
* shell 脚本不够精确，除非以高度警惕的态度编写，否则运行脚本的人可能会污染其环境。
* 大规模管理大量服务器需要使用 Puppet 或 Chef 等配置管理系统。教这些系统如何配置各个服务非常困难，因为每个服务都有自己的配置文件格式和位置，且因平台而异。
* **rc(8)** 系统不可移植。每种 Unix 变种都对经典设计有自己的改版，不管是 System V 还是 BSD。独立软件供应商没有资源支持所有这些不同的 init 系统，因此他们会面向商业 Linux 发行版等最流行的平台。

作业框架试图解决上述所有问题，或者至少让我们朝着正确的方向前进。

## 历史与动机

jobd 源于一次实验，最初想克隆 macOS X 中的 **launchd(8)** 系统。它的命令行语法和故障处理能力深受 Solaris 的服务管理设施（SMF）系统启发。它并非这两个 Unix 工具的简单克隆或拼凑，而是从各自的最佳特性中大量借鉴，同时避开了一些不太理想的方面。

我写 jobd 的动机之一，是回应引发 Linux 社区重大分裂的 systemd 风波。systemd 的采用是我把个人计算机从 Linux 切换到 PC-BSD 的重要因素。切换之后，我开始寻找可以回馈 FreeBSD 项目的小项目。

创建新 init 系统的愿望一直存在，但需要一个火花来推动。巧合的是，在我切换到 PC-BSD 前后，NextBSD 项目问世，听了 Jordan Hubbard 几场关于 launchd 优势的演讲后，我准备好帮忙。

可惜，进一步审视后，我并不认同 NextBSD 移植 launchd 所用的技术路线——即决定编写 Mach 微内核的部分实现作为兼容层，本质上让 FreeBSD 假装自己是 macOS X 的半成品变种。

我此前花了好几年把 **kevent(2)** API 从 FreeBSD 移植到其他操作系统，深知创建兼容层让一个内核假装成另一个内核的痛苦。这段经历让我对为 launchd 创建 Mach 兼容层的决定高度怀疑。即便 Mach 兼容层在加入 FreeBSD 内核后工作完美，也会增加大量技术债，FreeBSD 项目未必能接受。它难以移植到其他类 Unix 操作系统，耗时费力，且不会有太大需求。

我希望在现有操作系统上用标准 POSIX API“立刻”工作的方案，于是我从头开始实现 launchd，命名为 relaunchd。

## 目前的进展：实现

我们创建并发布了 relaunchd 的一个相对稳定、功能完备的版本 0.6。它包含原版 launchd 的大部分功能。

在 iXsystems 的一次编程马拉松上，我与 Kris Moore 密切合作，尝试用 relaunchd 管理一个名为 SysAdm 的新 PC-BSD 服务。我们遇到了若干问题，暴露出原版 launchd 设计在 FreeBSD 软件打包方面的不足。macOS X 没有 Ports 树的概念，软件通常通过图形安装程序安装，由安装程序负责与 launchd 交互。结果发现，人工与 launchd 交互有不少不足。

launchd 也不支持用户自定义属性，也不支持向用户暴露属性和方法来操控服务的启动方式。从 **rc(8)** 背景过来的人，失去在 rc.conf 中定义变量 `$foo_flags` 来控制 foo 服务的能力会让他们不快，这是可以理解的。

launchd 设计的另一个弱点是缺乏故障管理设施。守护进程崩溃时，launchd 会在无限循环中尝试重启它，没有内置机制判断服务是否有故障并采取行动。我管理 Solaris 10 服务器多年，喜欢把行为异常的服务转入故障状态的理念。在 jobd 中，可以定义一个故障处理脚本，每当检测到作业故障时执行。这个故障处理器几乎可以做任何事：给管理员发邮件、在桌面上弹窗、向监控系统发警报等。

| 状态（STATUS） | 标签（LABEL）                     |
| ---------- | ----------------------------- |
| running    | com.example.proprietary-agent |
| offline    | org.freebsd.ports.apache24    |
| disabled   | org.freebsd.ports.mysql       |
| running    | org.freebsd.ports.postgresql  |
| running    | system.jail.my-jail-name      |
| waiting    | system.at.job-23              |
| done       | system.cron.job-4175          |
| disabled   | system.service.sshd           |
| running    | system.service.xorg           |
| running    | user.1001.kde-session         |

我开始担忧 relaunchd 想要实现的功能与原版 launchd 设计之间的不匹配。这让我退后一步问：“我们要解决的问题是什么，经典 launchd 设计是否是合适的方案？”通常情况下，现实抛给你的问题比最初预想的要多。

对 launchd 的这种日益增长的不满，让我开始思考把作业框架作为一种底层操作系统构造的概念。这个框架会提供一个库和配套的命令行工具，用于构建各种其他操作系统设施，比如服务管理器和 **cron(8)** 的替代品。我开始设想把 relaunchd 拆成两半：下层负责启动和停止作业，上层向外界呈现“服务管理器”的面孔。随着时间推移，可以加入其他领域特定的前端，它们共享同一个后端作业管理器。

巧合的是，编程马拉松上的其他开发者正在开发 iocage Jail 管理器工具和 iohyve 虚拟机管理器。我开始把初生的“relaunchd 下层”视为 iocage 和 iohyve 可复用的东西，因为 Jail 和虚拟机本质上就是进程。

在不同工具间共享代码的想法听起来可能疯狂，但仔细看看，rc、iocage 和 iohyve 有什么共同点？

* 它们启动守护进程，要么在系统引导时自动启动，要么由管理员手动请求启动。
* 它们允许停止、启动、启用和禁用这些守护进程。
* 它们允许系统管理员定制守护进程启动的某些方面，类似于“作业属性”的概念。
* 以 iocage 和 iohyve 为例，它们为每个进程创建虚拟网络接口。
* 它们可能需要修改防火墙规则，例如添加 NAT 规则或过滤规则。

除了支持更广泛的用例，我还想避免“功能蔓延”——让 relaunchd 慢慢承担新的角色和职责，最终变成一团乱麻。在分层设计中，各层之间明确分离关注点很重要，而 relaunchd 与构建在其上的工具之间应如何划界并不清楚。甚至“relaunchd”这个名字也给人留下它纯粹是 **launchd(8)** 风格 init 系统的印象。为了在更广泛的场景中有用，它需要彻底摆脱 init 系统的定位。

为避免功能蔓延问题，作业框架提供一组核心特性供栈中更高层使用。这些更高层负责处理作业特化和定制的细节，以适应特定的问题域。例如，如果 iocage 想提供“Jail 市场”让人下载预配置的 Jail，所有这些功能都可以在 iocage 内部实现，无需修改底层作业框架。

一旦作业框架的整体图景确定下来，项目决定更名为“jobd”，与过去做个干净的切割。现有的 **launchd(8)** 守护进程更名为 **jobd(8)**，而有问题的 **launchctl(8)** 工具将被淘汰，替换为一套新的命令行工具，为开发者、打包者和系统管理员提供更好的用户体验。

在设计新的命令行接口时，我并不想完全重新发明轮子，因此借鉴了 Solaris SMF 框架的一些概念，创建了三个新的 CLI 工具：**jobadm(1)**，用于操作作业数据库；**jobctl(1)**，控制单个作业，处理启动和停止作业等操作请求；**jobcfg(1)**，处理与获取和设置作业属性相关的一切。

由于设计简单，relaunchd 只有几千行 C 代码。随着 jobd 的新使命，我开始担心用标准 C 实现新作业框架功能的潜在挑战。原版 launchd API 足够简单，可以用 C 实现，但用 C 实现所有强大的新功能会很困难。这促使我决定用 C++ 重写项目，以利用更丰富的数据结构并转向更面向对象的设计。

转向 C++ 带来了一些初始挑战，尤其是我完全没有这门语言的实践经验。经过最初的不适期，C++ 的优势开始显现，实现标准 C 难以完成的强大功能变得更加容易。学习新语言也很有趣，能提升你做任何事情的挑战性。

## 目标

jobd 项目的总体目标如下：

* 探索操作系统层面的作业管理理念。包括向人们普及作业管理概念，并让开发者思考如何用作业解决问题。
* 成为市场上最好的作业管理框架；提供通用框架用于在单个操作系统实例中启动作业；处理作业生命周期的所有方面：设置、启动、监督、终止、清理。成为作业管理 API 的参考实现。
* 让简单的事情简单，让困难的事情可能；让以一致的方式启动进程变得轻而易举。困难的是让防火墙规则、Jail、数据集、网络接口、配置文件、软件包依赖等都协同工作；jobd 将让这一切通过单个配置文件关联起来成为可能。

***

**Mark Heily**

Mark Heily 是一名开源软件开发者，居住在宾夕法尼亚州匹兹堡。他多年来活跃于 FreeBSD 项目，是 jobd 作业框架的创建者，此前曾将 **kevent(2)** API 从 FreeBSD 移植到其他操作系统。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/20160708-freebsd-yu-rtems/getting-the-job-done-the-init-system-debate-relaunched.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
