For the complete documentation index, see llms.txt. This page is also available as Markdown.

把活干完:init 系统辩论再起

随着时间推移,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 移植到其他操作系统。

最后更新于