[译] 反思 1 号进程 / Rethinking PID 1

时间:2021-12-13 17:11:32

By Lennart Poettering
SReadFox
原文链接:http://0pointer.de/blog/projects/systemd.html

译注:笔者大约在 2011 年读到此文,近日从 Pocket 里面翻出来。那时有些东西并不十分明白,只感觉会有一场 PK,当时 Ubuntu 已经默认切换到了 Upstart,但没有想到在社区引起如此大的争议。如今 systemd 已经占据了主流的发行版,尽管它被批判为不符合 Unix 设计哲学,但它的隐式依赖管理及事件驱动模式或许值得思考一下。想像一下当流量到来的时候我们可以快速的按需启动依赖的服务,或许基于轻量级的容器,又或者想像一下 FaaS 呢?

如果你信息通畅并且擅于阅读的话,你可能已经看出来这篇文章要写什么了。尽管如此,你也许还是会觉得这是个有趣的故事。所以端起咖啡,坐下来,继续读下去。

文章很长,所以尽管我极力推荐通读整篇长文,还是给出一句话概括:我们正在试验一种新的、有趣的 init 系统。

代码请点 这里。现在开始我们的故事。

1 号进程

在所有 Unix 系统中都有一个进程号为 1 的特殊进程。这个进程在其它所有进程启动之前由内核启动,并且作为所有孤儿进程的父进程。因此它可以做许多其它进程不能做的事情。同时,它还负责一些其它进程不负责的事情,比如在系统启动时拉起并维护用户空间 (userspace)。

历史上 Linux 平台的 init 程序由 sysvinit 这个包提供,尽管它早就显得很古老了。有许多替换掉 sysvinit 的建议,其中只有一个真正付诸实施,它就是 Upstart。目前 Upstart 已经找到了进入主流发行版的路径。

如上文提到的,init 系统的核心职责是拉起用户空间,并且一个优秀的 init 系统需要尽可能快地完成这个任务。遗憾的是,传统的 SysV init 系统并不是很快。

要做到快速、高效地启动,有两个关键因素:

  • 更少的启动。
  • 更多的并行。

什么意思呢?『更少的启动』意味着启动更少的服务,或者将服务的启动延迟到真正需要它们的时候。我们知道有些服务早晚总是需要的(syslog, D-Bus 系统总线等),但其它很多服务并不是这样的。比如,bluetoothd 并不需要运行,除非一个蓝牙适配器被插入,或者某个应用程序需要访问它的 D-Bus 接口。同样的,对于打印系统来说,除非机器连接到一个打印机或者某个应用程序需要打印东西,否则的话根本没有必要启动一个像 CUPS 这样的守护进程。对 Avahi 来说,如果机器没有连接到网络就没有必要运行 Avahi,除非有应用程序想要访问它的 API。甚至于对 SSH 来说,只要没有人想要连接到你的机器上,它就没必要运行;等到有人要连接的时候,系统就在第一个连接时启动它。(承认吧,大多数一直运行 sshd 的机器,每个月可能只有一个人连接上去。)

『更多的并行』意味着如果我们必须要运行一些东西,我们不应该像 sysvinit 那样串行启动它们,而应该同时运行它们,以便最大化利用可用的 CPU 资源以及磁盘 I/O 带宽,使得整体启动时间最短。

硬件和软件是动态变化的

现代系统(尤其是通用操作系统)不管在配置还是使用上都是高度动态化的 :它们是移动的, 不同的应用程序被启停,不同的硬件被接入和移除。负责维护系统服务的 init 系统需要监听硬件和软件的变化。它需要动态地启动(或者停止)某些服务,以便运行某个程序或者使得某些硬件变得可用。

目前大多数尝试并行启动的系统仍然需要同步启动很多后台服务 (daemon):由于 Avahi 依赖 D-Bus,因此 D-Bus 先启动,只有到 D-Bus 发信号告知它已经启动完成之后,Avahi 才开始启动。其它服务也存在类似的情况:libvirtd 和 X11 依赖 HAL(这里用 Fedora 13 的服务举例,请忽略 HAL 已经被淘汰了),因此 HAL 需要在 libvirtd 和 X11 启动之前先被启动。同时,libvirtd 还依赖 Avahi,所以它也要等待 Avahi 启动。然后上述所有所有服务都依赖 syslog,所以它们在 syslog 完全启动并初始化之前必须等待。还有很多诸如此类的情况。

并行化套接字 (Socket) 服务

上述这种启动过程中的同步行为导致整个启动过程中的一大部分都是串行的。如果我们能够省略掉同步和串行的开销的话是不是就太好了?实际上我们可以做到。首先,我们需要搞清楚后台服务彼此依赖的到底是什么东西,为什么它们的启动被推迟了。对于传统的 Unix 守护进程,可以给出一个答案:它们在等待其它守护进程提供服务的套接字准备好接受连接。通常是文件系统上的一个 AF_UNIX 域套接字,但也有可能是 AF_INET 套接字。比如,D-Bus 的客户端等待 /var/run/dbus/system_bus_socket 可以被连接,syslog 的客户端等待 /dev/log,CUPS 的客户端等待 /var/run/cups/cups.sock,NFS 的挂载等待 /var/run/rpcbind.sock 和 portmapper 的 IP 端口,诸如此类。仔细想想,这其实是它们唯一等待的东西!

如果这就是它们唯一等待的东西,并且我们能够做到让这些套接字更早的接受连接,而且只等待套接字而不是其它守护进程的完全启动,我们就能加速整个启动过程,并且并行地启动更多进程。那么,我们怎么做呢?实际上在类 Unix (Unix-like) 系统中是很容易的:我们可以在实际启动某个守护进程之前创建监听套接字,然后在执行 exec() 的时候将套接字传递给它。这样,我们可以在 init 系统中第一步就创建好所有守护进程所需的所有套接字,然后在第二步中一次性启动所有守护进程。如果某个守护进程依赖另一个还没有完全启动的守护进程,这是完全没问题的:连接被服务提供者排队,客户端可能会阻塞在这单个请求上。但只有这一个客户端会阻塞,并且只有这一个请求会阻塞。另一方面,不必再为了并行启动去配置服务单的依赖关系:如果我们一次性启动所有套接字并且某个服务依赖另一个服务,那么它肯定可以连接到它需要的套接字。

由于上述内容是后续内容的核心,让我用不同的语言和例子再讲述一遍:如果你同时启动 syslog 和许多不同的 syslog 客户端,在上述模式下,来自客户端的消息会添加到 /dev/log 的套接字缓冲区中。只要缓冲区没有满,客户端就不需要任何等待,并且可以马上启动。一旦 syslog 自身完成了启动,它将会消费所有消息并且处理它们。另一个例子:同时启动 D-Bus 和多个客户端。如果某个客户端发送了一个同步请求(因此需要等待回复),那这个客户端会被阻塞,但只有这一个客户端会被阻塞,而且只会阻塞到 D-Bus 服务启动完成(并且处理掉这个请求)。

本质上为说,内核套接字缓冲区帮助我们将并行最大化了,内核处理了排序和同步问题,不需要用户空间关心!如果在守护进程启动前所有套接字都已经处于可用状态,那么依赖管理就不再需要了(至少不是最重要的):如果一个守护进程依赖另一个,那它直接连接到对应的套接字就行了。如果被依赖的这个守护进程已经启动了,那这个过程会立即成功。如果被依赖的进程正在启动过程中,那么前一个守护进程并不需要等待,除非它发起了一个同步请求。就算被启动的进程根本没有在运行,它也可以被自动启动。在前一个守护进程看来,这几种情况没有什么区别,因此依赖管理基本上不再需要或者至少不是最重要的,所有这些都是最大化并行的,或者按需加载的。除此之外,这种模式也更为健壮,因为套接字始终保持可用,而对应的实际守护进程有可能临时不可用(比如可能发生了 crash)。实际上,你可以很容易编写一个这样的后台程序,它开始运行,然后退出(或者崩溃),然后重新运行,再次退出(如此循环往复),而它的客户端不会注意到这些,也不会丢失任何请求。

是时候休息一下了,去加满咖啡,后面的内容更精彩。

但首先让我们搞清楚一些事情:这是一个全新的想法吗?不,当然不是。最著名的以这种模式工作的系统是 Apple 的 launchd 系统:在 MacOS 上所有套接字的监听从守护进程中剥离出来,统一由 launchd 处理。而服务自身则可以并行启动,并且不需要为它们配置依赖关系。这是一个非常精巧的设计,也是 MacOS 能够做到难以置信的启动时间的主要原因。我强烈推荐 这个视频,视频中 launchd 的开发者介绍了他们正在进行的工作。遗憾的是这个想法从来没有在 Apple 阵营以外真正被实施过。

这个想法本身甚至比 launchd 还要早。在 lanuchd 之前,古老的 inetd 以一种与此相似的方式工作:套接字由一个守护进程创建,然后这个守护进程会启动其它实际的服务进程,并且在 exec() 的时候将套接字的文件描述符 (file descriptior) 传递给它。但 inetd 的关注点显然不是本地服务,而是网络服务(尽管后来也实现了对 AF_UNIX 套接字的支持)。它也不是用作并行启动和处理隐式依赖关系的工具。

在 TCP 套接字场景下 inetd 主要以如下方式被使用:每个连接到来时创建一个新的守护进程实例。这意味着每个连接都会导致一个进程被创建和初始化,这并不是高性能服务器的做法。但实际上从一开始 inetd 就支持另外一种模式:在第一个连接到来的时候创建单个守护进程实例,然后这个实例将常驻并且接收后续连接(这就是 inetd.conf 配置文件中 wait/nowait 选项的意思,不幸的是这个选项的文档说明很糟糕)。每个连接启动一个守护进程实例的做法给 inetd 带来了运行速度慢的坏名声。但那并不完全公正。

并行化总线 (Bus) 服务

现代 Linux 上的守护进程倾向于通过 D-Bus 而不是 AF_UNIX 套接字提供服务。现在的问题是,对这些服务,我们能使用与传统套接字服务相同的并行启动逻辑吗?没错,我们可以,D-Bus 已经具备达到这个目标所需的所有钩子 (hooks) :使用总线激活 (bus activation) 能力,服务可以在它第一次被访问的时候被启动。总线激活最小化了同时启动 D-Bus 服务提供者和消费者所需的同步代价:如果我们同时启动 Avahi 和 CUPS(备注:CUPS 通过 Avahi 来查找 mDNS/DNS-SD 打印机),那我们就简单的同时运行它们,如果 CUPS 比 Avahi 启动得更快,通过总线激活逻辑我们可以让 D-Bus 缓冲这些请求直到 Avahi 成功启动。

所以,总的来说:基于套接字的服务激活和基于总线的服务激活使得我们可以并行启动所有守护进程,而不需要同步操作。同时这种激活能力也允许我们实现服务的懒加载 (lazy-loading):如果某个服务很少被使用,我们可以在有人第一次访问它的套接字或总线服务名的时候再加载它,而不是在系统启动时启动它。

如果这还不美妙的话,我不知道什么才叫美妙!

并行化文件系统作业 (File System Jobs)

如果你观察当前发行版 系统启动过程中的串行化,你会发现除了守护进程启动以外还有更多的更多的同步点。主要是文件系统相关的作业:挂载、一致性检查、配额检查 (mounting, fscking, quota)。目前,系统启动过程中的不少时间都在等待 /etc/fstab 中列出的设备展现在设备树 (device tree) 中,然后进行一致性检查、挂载以及配额检查(如果开启这个选项)。只有当这些完全结束后才能继续启动实际的服务。

我们能改进这种情况吗?事实证明可以。Harald Hoyer 提出了使用 autofs 来解决这个问题的想法。

就像 connect() 调用表明某个服务依赖另一个服务一样,open() 调用(或者其它类似调用)表明某个服务依赖一个文件或者文件系统。所以为了尽可能并行我们让应用只在依赖的文件系统尚未挂载或者不能立即可用时才进入等待:我们建立一个 autofs 自动挂载点,等到启动过程中文件系统完成一致性检查及配额检查之后,将自动挂载点替换为真正的文件系统挂载。在文件系统不可用时,对文件系统的访问将被内核排队,进程将会阻塞,但只有当前进程和当前访问会阻塞。这样,我们在所有文件系统完全可用前就可以启动守护进程,并且不会丢失文件,达到最大化并行。

并行化文件系统作业和服务作业对于根文件系统 / 来说是没有意义的,毕竟服务的二进制执行文件存储在其之上。不过,对于像 /home 这样通常容量更大、甚至加密的或者远程的(通常很少在守护进程启动过程中访问) 的文件系统而言,可以极大的改善启动时间。可能没必要提,不过虚拟文件系统,如 procfs 和 sysfs 是不能使用 autofs 挂载的。

如果有读者觉得在 init 系统中集成 autofs 显得有点脆弱、奇怪甚或是疯狂的话,我并不会感到惊讶。不过,在做了大量相关工作之后,我可以说实际上这个方案给人的感觉很好。在这个场景下使用 autofs 只是意味着我们可以创建一个挂载点,但不必立刻准备好相应的文件系统。事实上它只是延迟了访问。如果某个应用尝试访问一个 autofs 文件系统,但我们花了很长时间才用真实的文件系统来替换它,那么应用将处于可中断的睡眠状态,这意味着你可以安全地取消它,比如使用 Ctrl-C。同时注意在任何情况下,如果文件系统最终不能被挂载(可能由于 fsck 失败),我们可以告知 autofs 返回一个明确的错误码(比如 -ENOENT)。所以我想说的是,尽管在 init 系统中集成 autofs 初看起来有点冒险,但我们的试验代码表明这个想法工作得超乎寻常的好——只要出于正当的原因并且以正确的方法去做的话。

另外,要注意这些挂载点都是直接的 autofs 挂载点 (译注:direct autofs mounts,与 indirect autofs mount 相对),这意味着从应用的视角来看,基于 autofs 的挂载点和传统的挂载点几乎没有区别。

让第一个用户进程的 PID 更小

我们可以从 MacOS 的启动逻辑中学到另一件事:shell 脚本是有害的。Shell 既快速又耗时。写起来快,但执行起来耗时。传统的 sysvinit 启动逻辑就是构建在 shell 脚本的基础上。无论是 /bin/bash 还是其它的 shell(为了让 shell 脚本更快而开发),这种方案都注定是耗时的。在我的系统上,/etc/init.d 下面的脚本调用了 77 次 grep,92 次 awk,23 次 cut 以及 74 次 sed。每一次这些命令(或者别的命令)被调用,都会创建一个进程,查找共享库,做一些类似 i18n 之类的初始化工作,还有诸如此类的更多工作。然后这些进程往往就做了一个字符串操作就退出了。这样当然就很耗时了。除了 shell 之外没有别的语言是这样做的。除此之外,shell 脚本也非常脆弱,它们的行为根据环境变量之类的因素的不同而差异巨大,而这些因素往往很难控制。

所以,让我们在系统启动阶段抛弃 shell 脚本吧!在此之前我们来看一下 shell 脚本目前都被用来做些什么事:嗯,总的来说绝大多数时候,它们做的事情很乏味。大多数脚本都在做简单的服务启停,这些应该使用 C 语言重写,不管是编写一个独立的可执行文件,还是在后台服务内部实现,或者干脆做在 init 系统里面。

要立刻在系统启动过程中完全抛弃 shell 脚本是不太可能的。用 C 语言重写需要时间,很多情况下这样做也没有实际的意义,而且有时候 shell 脚本真是太方便了。但我们至少可以让 shell 脚本不再那么重要。

一个很好的度量系统启动过程中 shell 脚本泛滥的指标,是系统完全启动以后你启动的第一个进程的 PID。启动系统,登录,打开终端,然后输入 echo $$。在 Linux 上试一下,然后与 MacOS 上的结果对比!(提示,通常结果是这样的:在 Linux 上 PID 是 1823;MacOS 上是 154,在我们的测试系统上进行的测量。)

保持对进程的跟踪

一个负责启动并维护服务的系统的一个重要部分是进行进程看护:它应该监控服务。如果服务关闭了就重启它。如果服务崩溃了,它应该收集相关信息,存储下来以备管理员查看,并且关联到 crash dump 系统(比如 abrt)、日志系统(比如 syslog)或者审计系统中的可用信息。

除此之外,它还应该能够彻底关闭一个服务。这听起来很容易,但实际上远比你想像的困难。传统的 Unix 进程如果经过两次 fork (double-forking),就会脱离父进程的监管,老的父进程将无法知晓新进程与它实际启动的那个进程之间的关系。比如:在目前的情形下,一个不正当的 CGI 程序如果做了两次 fork,那么当你关闭 Apache 的时候它将不会被终止。更进一步,你甚至不能得出它与 Apache 之间的关系,除非你能从它的名字和行为判断出来。

那么,我们要怎样保持对进程的跟踪,以便它们不能逃脱看管,并且我们可以将其当作一个整体来管理,哪怕它们做了无数次 fork?

不同的人对此提出了不同的解决方案。这里我不想赘述,但需要说明的是,有人基于 ptrace 和 netlink (一个内核接口,允许你在每一次有进程 fork()exit() 的时候收到一个 netlink 消息) 的实现,已经被批评为丑陋且不可扩展的。

那么我们怎么解决这个问题呢?实际上,内核已经提供了 Control Groups (aka "cgroups"")。简单来说 cgroups 允许你创建进程组的层次结构。这个层次结构直接通过虚拟文件系统暴露出来,因此很容易访问。group 的名字就是文件的系统上对应目录的名字。如果一个进程属于某个 cgroup,那么它的子进程也会成为这个 cgroup 的成员。除非它拥有特权并且能够访问 cgroup 文件系统,否则它无法脱离当前 cgroup。最初,cgroups 是为了实现容器而被引入内核的:不同的内核子系统可以针对不同的 cgroup 添加资源限制 ,比如限制 CPU 和内存的使用。传统的资源限制(使用 setrlimit() 实现)基本上都是进程级别的。cgroups 使得你可以在整个进程组上施加资源限制。除了容器这个场景外,cgroups 的资源限制在其它场景下也很有用。比如你可以用它来限制 Apache 和它所子进程可以使用的内存或 CPU 总量。这样,一个行为不正当的 CGI 脚本就不能通过多次 fork() 来逃脱你使用 setrlimit() 施加的资源控制。

除了容器和资源限制以外,cgroups 在跟踪守护进程方面很有用武之地:cgroup 成员关系可以安全的被子进程继承,并且它们无法脱离。同时,还提供了一种通知机制,使得当前 cgroup 为空时管理进程可以收到通知。你可以查看 /proc/$PID/cgroup 文件来获知当前进程所属的 cgroup。因此,在进程看护场景下,cgroups 是一个很好的选择。

控制进程的执行环境

一个好的进程看护者不仅应该监控守护进程的启动、结束或崩溃,同时它还应该为其设置好一个最小可用的、良好、安全的工作环境。

这意味着需要设置明确的进程参数,比如 setrlimit() 资源限制,用户/组 ID,或者环境变量,但并不止于此。内核给用户和管理员提供了进程上的许多控制能力(其中有一些目前很少用到)。你可以为每一个进程调用 CPU 和 IO 调度器控制选项,Capability 权限组,CPU 亲缘性或者 cgroup 环境中的其它更多的限制。

比如,使用 IOPRIO_CLASS_IDLE 选项设置 ioprio_set() 是一个最小化 locate 的 updatedb 对系统交互性影响的好方法。

除此之外,一些更高层次的控制也非常有用,比如基于只读的 bind 挂载构建一个只读的文件系统 overlay 层。通过这种方式可以让所有(或者部分)文件系统对某些守护进程暴露出只读视图,它们的每一个写请求都会返回 -EROFS 错误。这样就可以类似 SELinux 一样,限制守护进程能做什么(这当然不能不能替代 SELinux,请不要有什么不好的想法)。

最后,日志也是运行服务的一个重要部分:理想情况下服务输出的所有东西都应该被记录下来。因此,init 系统应该在守护进程最初启动的时候就提供好日志能力,将标准输出和标准错误输出连接到 syslog,甚至可以连接到 /dev/kmsg,这在一些场景下可以作为 syslog 的一个很好的替换(嵌入式的同学们,说你们呢!),特别是当内核的日志缓冲区大小默认被配置的很大的时候。

关于 Upstart

在开始之前,我必须强调一下,事实上我非常喜欢 Upstart 的代码,有良好的注释和文档,很容易学习和跟进。很明显其它项目(包括我自己的)都应该学习这一点。

话虽如此,我对 Upstart 的解决方案并不赞同。首先,让我们对 Upstart 了解更多一些。

Upstart 不与 sysvinit 共享代码,它的功能是 sysvinit 的超集,并且它对 sysvinit 的脚本提供了一定程度的支持。它的主要功能是它基于事件的方案:进程的启动和停止与系统中发生的事件绑定在一起,而『事件』可以是很多不同的东西,比如网卡变得可用或者某个软件启动了。

Upstart 通过事件来处理服务的串行化。如果 syslog-started 事件被触发,那么这标志着 D-Bus 可以启动,因为现在它可以使用 syslog 了。然后 dbus-started 被触发,NetworkManager 被启动,因为它现在可以使用 D-Bus 了。诸如此类。

你可以说这种方式将管理员或开发者知晓的的逻辑依赖树翻译和编码成了事件和动作规则 :每一条管理员/开发者知晓的『A 依赖 B』规则都变成了『当 B 启动以后启动 A』以及『当 B 停止以后停止 A』。在某些场景下这确实是一种简化:特别是在 Upstart 代码内部。但是我得指出这种简化实际上是有害的。首先,这些逻辑依赖并没有消失,编写 Upstart 文件的人现在必须将依赖人工翻译成事件/动作规则(事实上每个依赖都需要两条规则)。所以,用户必须要人工把依赖关系翻译成简单的事件/动作规则,而不是让系统基于依赖关系来得出应该做什么。另一方面,由于依赖关系没有被编码,因此这个信息在运行时是不可用的,这实际上意味着管理员难以了解某些事件为什么会发生,比如为什么 B 启动的时候 A 被启动了。

此外,事件逻辑将依赖关系从头到脚翻转了一遍。它使得运维过程中的工作最大化了,而不是将其最小化(如本文开头所述,一个优秀的 init 系统正应该专注于此)。或者换句话说,原本应该设立一个清晰的目标,然后只做达成目标所需的事情,而它去先做了一步操作,等它完成以后,接着做了在此之后所有可能发生的操作。

或者简单的说:用户启动 D-Bus 无论怎样都不表示 NetworkManager 应该被启动(但这就是 Upstart 所做的)。实际上正好应该是反过来的:当用户要求启动 NetworkManager 的时候绝对意味着 D-Bus 应该被启动(这肯定是大多数用户期望的,对吧?)。

一个优秀的 init 系统只应该按需启动需要的服务,无论是懒加载还是提前并行启动。而不应该启动不需要的服务,何况是已安装的所有可以使用某个服务的程序。

最后,我并没有看到事件逻辑的实际用处。 在我看来 Upstart 暴露的大多数事件在本质上不是瞬时的,而是有一个持续时间:某个服务启动,正在运行,停止。某个设备插入了,处理可用状态,然后又被拔除。某个挂载点正在被挂载,已完成挂载,或者正在被卸载。电源被插入了,系统工作在外接电源模式下,电源被拔除。应该由 init 系统或者进程监控程序处理的事件只有少量是瞬时的,大部分是启动、条件、停止的元组。而这些信息 Upstart 又不能提供,因为它只关注单个事件,而忽略了持久的依赖。

目前,我知道上文指出一些问题在 Upstart 最近的更新中已经得到了缓解,尤其是基于条件的语法,比如 Upstart 文件中的 start on (local-filesystems and net-device-up IFACE=lo)。但是,对我来说这只是在尝试修补一个设计上有缺陷的系统。

除此之外,Upstart 在进程看护方面做得挺好,尽管有些决择也许未必准确(见上文),并且错过了很多机会(亦见上文)。

除了 sysvinit, Upstart 和 lanuchd 以外还有些别的 init 系统。它们绝大多数都没有什么比 sysvinit 和 Upstart 更值得一提的。其它竞争者里面最有意思的一个是 Solaris SMF,它支持服务之间的正确依赖。但是,它在很多方面都过于复杂了,并且有一点『学院派』,比如过度使用 XML 以及为大家熟悉的东西创造新的术语。而且它和 Solaris 的某些功能强绑定,比如 contract 系统。

集大成者:systemd

好了,又到了休息一下的时候了,因为在我充满希望的描述了我认为一个优秀的 1 号进程应该做什么以及目前广泛使用的系统是怎么做的以后,我们马上就要到重点了。所以,再次去加满你的咖啡,肯定会值得的。

你也许已经猜到了:在这里我要家里面,我在上文中所建议的一个理想的 init 系统应该具有的功能现在已经可用了,包含在一个叫作 systemd 的 init 系统中(目前还处于试验阶段)。再提一下,代码请点这里。下面是它的一个功能概览,以及背后的实现原理。

systemd 启动以及监控整个系统(所以它才叫这个名字......)。它实现了上文中所说的所有功能,并且还包含更多的功能。它基于 unit 的概念构建。Unit 拥有名字和类型。由于它们的配置文件通常是从文件系统直接加载的,unit 的名称实际上就是文件名。比如,一个名叫 avahi.service 的 unit 从同名的文件中加载配置,而且显然这是一个封装 Avahi 守护进程的 unit。存在如下类型的 unit:

  • service。这是最显而易见的 unit 类型:守护进程可以被启动、停止、重启和重新加载。为了兼容 SysV,我们不仅支持我们自己的配置文件,也支持 SysV init 脚本,特别的,我们也会解析 LSB 头部,如果存在的话。因此 /etc/init.d 只不过是另一个配置来源而已。
  • socket。此类 unit 封装了一个文件系统或者 Internet 上的 socket。目前我们支持 SOCK_STREAM,SOCK_DGRAM,SOCK_SEQPACKET 类型的 AF_INET,AF_INET6,AF_UNIX 套接字。我们也支持传统的 FIFO 管道作为传输通道。每一个 socket unit 与一个 service unit 对应,当第一个连接到达套接字和 FIFO 管道的时候,对应的 service 将被启动。比如,当 nscd.socket 有连接到来的时候 nscd.service 将启动。
  • device。此类 unit 封装了 Linux 设备树中的一个设备。如果某个设备通过 udev 规则指定,它将在 systemd 中暴露为一个 device unit。udev 属性可以作为设置置 device unit 依赖的一个配置来源。
  • mount。此类 unit 封装文件系统结构中的一个挂载点。systemd 监控所有挂载点的生命周期,也可以用来 mount 或者 umount 挂载点。/etc/fstab 被视为这些挂载点的额外配置来源,如同 SysV init 脚本被视作 service unit 的额外配置来源一样。
  • automount。此类 unit 封装了文件系统结构中的一个自动挂载点。每一个 automount unit 都有一个与之对应的 mount unit,当这个自动挂载目录被访问的时候,对应的挂载点就会被启动(也就是被挂载 )。
  • target。这种 unit 类型用作对 unit 进行逻辑分组,它实际上并不做任何事情,只是简单的引用其它 unit,使得这些 unit 可以作为一个整体进行控制。比如,multi-user.target 就是办演 SysV 系统中 run-level 5 的角色,又或者像 bluetooth.target 只是在有蓝牙设备可用时启动相关的服务,比如 bluetoothd 或者 obexd 之类。
  • snapshot。与 target 类型类似,这类 unit 自己并不做任何事情,只是引用其它 unit。Snapshot 可以用作保存/回滚 init 系统的所有服务和 unit 状态。它主要有两个用作:其一,允许用户暂时停止当前所有服务,然后进入一种特定的状态(比如『Emergency Shell』),同时它提供了一种简单的方式回到之前的状态——将之前停止掉的服务重新拉起来。其二,用来支持系统挂起:目前仍然有不少服务不能正确处理系统挂起事件,通常在挂起之前把它们停止掉、在唤醒时再把它们启动起来是一个好方法。

所有这些 unit 彼此之间都有依赖(无论是正向或者反向的,比如『需要某个服务』或者『与某个服务冲突』)。一个设备可能依赖某个服务,这意味着一旦这个设备可用对应的服务就应该被启动。挂载点对它要挂载的设备有一个隐式的依赖。同时,挂载点还对路径是它前缀的挂载点有隐式依赖(比如 /home/lennart 依赖于 /home),还有很多诸如此类的例子。

下面是其它功能的一个简短列表:

  1. 针对每一个由 systemd 启动的进程,你都可以进行如下方面的控制:运行环境、资源限制、工作和根目录、umask、OOM Killer 调整、nice 值、IO 类别和优先级、CPU 策略和优先级、CPU 亲缘性、timer slack、用户 ID、用户组 ID、附加用户组 ID (supplementary group ids)、可读/可写/不可访问的目录、共享/私有/从属挂载标志(shared/private/slave mount flags)、权能边界集(capabilities/bounding set)、安全位(secure bits)、fork() 调用时的 CPU 调度器重置、私有的 /tmp 命名空间、各类子系统的 cgroup 控制。另外,你也可以很容易的将服务的标准输入/标准输出/标准错误输出连接到 syslog,/dev/kmsg 或者任意的 TTY 设备。如果将标准输入连接到某个 TTY 设备,systemd 将通过等待或者强制执行来保证进程独占此设备。
  2. 每个进程都有它自己的 cgroup(目前默认在 debug 子系统下,因为这个子系统不会被其它人使用,而且除了基本的进程分组之外不会做别的事情 ),而且很容易通过配置将服务置于外部已配置好的 cgroup 下,比如通过 libcgroups 工具配置的 cgroup。
  3. 配置文件语法遵循大家熟悉的 .desktop 文件的语法。这是一种简单的语法,而且在很多软件框架里面都有现存的解析器。同时,这也允许我们在进行服务描述时依赖现有的 i18n 工具或者其它类似的工具。管理员和开发者也不需要学习一种新的语法。
  4. 如前文所述,我们提供了对 SysV init 脚本的兼容。我们会优先使用 LSB 或者 Red Hat chkconfig 文件头,如果它们存在的话。如果文件头不存在,我们会尽量使用其它可用信息,比如 /etc/rc.d 中的启动优先级。这些 init 脚本只是简单的被视作一个不同的配置来源,因此可以比较容易的升级到 systemd 服务。作为一个可选项,我们可以读取服务的 PID 文件来识别守护进程主要的 pid。值得注意的是,我们会使用 LSB 文件头中的依赖信息,并将其转换为原生的 systemd 依赖关系。Upstart 则不能处理和利用这些信息。因此在一个主要由 LSB SysV init 脚本构成的系统上,如果使用 Upstart,那么启动过程不是并行的,而使用 systemd 则可以并行。实际上,对于 Upstart 来说,所有的 SysV init 脚本整个被作为一个任务来执行,并不是每一个脚本单独处理的。而对于 systemd 来说,SysV init 脚本只是另一个配置来源,每一个脚本都是被单独处理和控制的,就像其它原生的 systemd 服务一样。
  5. 类似的,我们可以读取 /etc/fstab 并视作另一个配置来源。使用 comment=fstab 选项你甚至可以将 /etc/fstab 中的配置项变成 systemd 管理的自动挂载点。
  6. 如果同一个 unit 出现在多个不同的配置源中(比如 /etc/systemd/system/avahi.service/etc/init.d/avahi 同时存在),那么原生配置优先生效,老的配置格式被忽略。这使得升级更为容易,并且允许软件包在一段时间内同时提供 SysV init 脚本和 systemd 服务文件。
  7. 我们支持简单的模板/实例化机制。比如,如果有 6 个 tty 设备,我们并不会写 6 个配置文件,而是只有一个 getty@.service 文件,然后通过 getty@tty2.service 这样的文件来实例化它。接口部分甚至可以从依赖表达式中继承而来,比如很容易使得 dhcpcd@eth0.serviceavahi-autoipd@eth0.service 中被拉起,只要将字符串 eth0 用通配符替代即可。
  8. 对于套接字激活机制我们支持与传统 inetd 完全兼容的模式,同时也支持一种非常简单的模仿 launchd 套接字激活的模式,新服务推荐使用后者。inetd 模式下只能传递一个套接字给启动的进程,而原生模式下则可以传递任意多个文件描述符。我们支持一个链接一个实例的模式,也支持单个实例服务所有连接的模式。对于前者,我们将根据连接参数来命名进程运行的 cgroup,并且在命名时使用前文提到的模板逻辑。比如,sshd.socket 可能创建出 sshd@192.168.0.1-4711-192.168.0.2-22.service,对应的 cgroup 名字为 sshd@.service/192.168.0.1-4711-192.168.0.2-22(也就是在实例名中包含了 IP 和端口号。对于 AF_UNIX 套接字我们使用连接客户端的用户 ID 和 PID)。这为管理员提供了识别守护进程各种实例的好方法,从而可以独立控制这些实例的运行环境。原生的套接字传递模式在应用程序中是非常容易实现的:如果 $LISTEN_FDS 变量存在,则变量中存储的就是传递给守护进程的套接字。这些套接字是按 .service 文件中列出的顺序排序的,文件描述符从 3 开始(一个严谨的守护进程在收到多个套接字时应该使用 fstat()getsockname() 来进行识别)。除此之外,我们通过 $LISTEN_PID 指定了接收套接字的守护进程 PID,因为环境变量会被子进程继承从而可能在进程链上导致混乱。尽管套接字传递逻辑在守护进程中很容易实现,我们还是会提供一个基于 BSD 许可证的参考实现来示范应该如何处理。我们已经将若干现存的服务迁移到了这种新模式。
  9. 我们在一定程度上提供了对 /dev/initctl 的兼容。这种兼容性实际上是通过 FIFO 激活服务来实现的,它只是将旧格式的请求转换为 D-Bus 请求。这意味着 Upstart 和 sysvinit 中老的 shutdown, poweroff 等类似命令在 systemd 中仍然可以继续工作。
  10. 我们也兼容 utmp 和 wtmp。尽管 utmp 和 wtmp 如此繁琐,我们还是很大程度上保持了兼容。
  11. systemd 支持 unit 之间的多种依赖类型。After/Before 可以用来调整 unit 被激活的顺序,这与 RequiresWants 互不影响,后者用来表达强制或可选的正向依赖。Conflicts 则用来表达反向依赖。除此之外,还提供了另外三种不常用的依赖类型。
  12. systemd 有一个最小化的事务系统。这意味着如果某个 unit 需要被启动或停止,我们会把它以及它的所有依赖添加到一个临时事务里面。然后,我们检测这个事务是否是一致的(也就是说,检查通过 After/Before 指定的 unit 顺序是否是无环的)。如果不一致,systemd 会尝试修复它,通过将非必须的任务从事务中移除,来尝试解除事务中存在的环。同时,systemd 也会禁止掉会造成某个当前运行服务停止的非必须任务。非必须任务是指那些不包含在最初的请求中,而是通过 Wants 依赖引入的任务。最后,检查当前事务是否与已排队的任务有冲突,如果有冲突则事务终止。如果事务一致并且其影响已经最小化,将会与现存的未完成任务合并,然后添加到运行队列中。这意味着在执行一个请求操作前,我们会校验它是否能正常工作,如果需要的话尝试修复它,只有在它无法工作的时候才会失败。
  13. 我们会记录创建并管控的进程的 PID、启动和退出时间,以及进程的退出状态。这些数据可以将守护进程与它们在 abrtd,auditd 和 syslog 中的数据关联起来。想像一下有一个 UI 界面可以高亮显示崩溃的守护进程,并且允许你很容易的导航到对应的 UI 界面中查看其某次运行过程中产生的 abrtd,auditd 和 syslog 数据。
  14. 我们支持在任意时刻重新执行 init 进程。守护进程的状态将在 init 进程重新执行前被序列化,然后在重新执行之后反序列化。通过这种简单的方式我们可以升级 init 系统,也更容易将控制从 initrd 转交给 systemd。那些打开的套接字和 autofs 挂载点会被妥善的序列化,以保证它们总是可被连接的,客户端甚至感受不到 init 进程的重新执行。实际上服务的大部分状态都保存在 cgroup 虚拟文件系统中,这甚至允许我们不用访问序列化的数据就能恢复执行。重新执行的代码路径与重新加载 init 系统的配置几乎相同,这保证了重新执行(很少触发)与重新加载(更加频繁)一样得到良好的测试。
  15. 将 shell 脚本从系统启动过程移除的工作开始以后,我们已经将系统启动的一些基础部分用 C 重写了,并且将这些功能直接移动到 systemd 中。其中包括 API 文件系统(也就是像 /proc, /sys, /dev 这样的虚拟文件系统)挂载以及主机名设置。
  16. 服务器状态可以通过 D-Bus 探知和控制。这一块已经做了大量工作,即将完成。
  17. 尽管我们重点推荐基于套接字和总线名字的服务激活(因而我们支持套接字和服务的依赖关系),但我们也支持传统的服务之间的依赖关系。我们提供了多种方法来让一个服务表达它已处于就绪状态:创建启动进程,然后退出(也就是与传统的 daemonize() 行为一样);或者观测总线直到配置的服务名出现。
  18. 我们提供了一个交互模式,在此模式下 systemd 每次创建一个进程前都会进行询问并等待确认。你可以通过内核命令行参数 systemd.confirm_spawn=1 来打开这个功能。
  19. 使用 systemd.default= 内核命令行参数可以指定 systemd 在系统启动的时候运行哪个 unit。通常你应该指定 multi-user.target 之类的 target,但你也可以指定一个单独的服务而不是一个 target。比如我们内置了一个 emergency.service,它的功能与 init=/bin/bash 类似,但它是实实在在启动了 init 系统的,这使得在 emergency shell 中可以启动整个系统。
  20. 我们提供了一个最小化的 UI 界面可以用来启动/停止/探测服务。目前离完成还有一段很长的距离,但作为一个调试工具还是很有用的。它是使用 Vala 编写的,被命名为 systemadm。

应该注意的是 systemd 使用了很多 Linux 特有的功能,并不仅限于 POSIX。因此,与那些被设计为在操作系统间具备可移植性的系统比起来,它具有更多的功能特性。

项目状态

所有上述列出的功能都已经实现了。现在 systemd 可以直接替换 Upstart 和 sysvinit(至少在没有太多原生 Upstart 服务的情况下。幸运的是大多数发行版都还没有太多的原生 Upstart 服务)。

但是,测试还远远不够充分,我们的版本号目前还是 0。如果你在目前的状态使用它的话,可能会发生意外的损坏。即便如此,总体来说它已经很稳定了,我们有一些人已经用 systemd 来启动他们的开发系统了(而不仅仅是在 VM 中使用)。但是你的情况也许会有所不同,尤其是你在我们的开发者没有使用的发行版中尝试它的话。

后续如何发展?

上述功能集已经很完整了,但我们还有更多一些事情要做。我不喜欢说太多关于大的规划的东西,但还是做一个简短的概述来说明我们要向什么方向推进。

我们想要至少再加入两种 unit 类型:swap 类型用来控制 swap 设备,就像我们控制挂载点一样,自动依赖设备树中激活它们的那些设备或者其它东西。timer 类型提供类似 cron 的功能,也就是说基于定时事件启动服务,同时支持单调时间 (monotonic time) 和日历时间 (wall-clock/calendar) 事件(比如 『距离上次运行 5 小时后启动服务』或者『每个周一早上 5 点启动服务』)。

更重要的是,我们准备试验将 systemd 作为一个理想的会话 (session) 管理器(而不仅是用来优化系统启动时间),用来替换(或者增强)gnome-session, kdeinit 之类的守护进程。会话管理器的问题集合与 init 系统是非常相似的:快速启动是必须的,并且专注于进程看护。这两者使用同一份代码是可行的。Apple 认识到了这个问题并且在 launchd 中这是这么做的。我们也应该这么做:会话服务和系统服务都能从基于套接字和总线的激活以及并行中得到同等收益 。

上述三个功能在当前的代码中已经部分可用了,但还没有完成。比如,你可以用普通用户的身份来运行 systemd,它会检测到这一点并且在相应的模式下运行。对这个模式的支持从一开始就可用了,是 systemd 非常核心的一部分。(这对于调试也特别有用!就算不使用 systemd 启动系统它也能工作得很好。)

但是,在结束这项工作之前,我们还有一些问题需要在内核或者其它地方进行修复。我们需要来自内核的 swap 状态改变的通知,就像我们已经订阅的挂载点变化信息一样;我们期望在 CLOCK_REALTIME 相对于 CLOCK_MONOTONIC 发生跳变时收到通知;我们想 让普通进程具有一个类似 init 进程的能力;我们也需要 一个明确定义的地方来存放用户套接字。这些问题对 systemd 来说都不是必须的,但肯定会事情有所提升。

想要实战看一看?

目前,还没有提供压缩包发布文件,但从 我们代码仓库 中检出代码是很容易的。另外,为了能够启动一些东西,这里提供了一个 unit 配置文件的压缩包,允许未经修改的 Fedora 13 可以与 systemd 一起工作。目前我们没有 RPM 包可以提供给你。

一种更简单的方法是下载 这个 Fedora 13 的 qemu 镜像,这个镜像是为 systemd 准备的。在 grub 启动菜单上你可以选择通过 Upstart 或者 systemd 启动系统。请注意这个系统只做了最小化的修改。服务信息只会从 SysV init 脚本中读取。因此我们不能完全利用前文所述的基于套接字和总线的激活,但它会解析 LSB 文件头中的并行指示信息,因此比 Upstart 启动得更快,Upstart 目前在 Fedora 中还没有多少并行性可言。这个镜像配置为将调试信息输出到串口控制台,同时在调试信息写入内核日志缓冲区(你可以通过 dmesg 访问)。我可能需要运行一个配置了串口终端的 qemu 进程。所有的密码都被设置为 systemd。

比下载并启动 qemu 镜像更简单的方式是看一下这些漂亮的截图。由于 init 系统通常都隐藏在用户界面后面,因此需要运行 systemadmps 命令:

[译] 反思 1 号进程 / Rethinking PID 1

上图中 systemadm 展示了所有已加载的 unit,并且展示了其中一个 getty 实例的详细信息。

[译] 反思 1 号进程 / Rethinking PID 1

这是 ps xaf -eo pid,user,args,cgroup 命令输出的一个摘要,显示了进程按服务所属的 cgroup 排列的多么整齐。(第 4 列是 cgroup,显示 debug: 前缀是因为 systemd 使用 debug cgroup 控制器,如前文所述。这只是暂时的。)

要注意的是这些截图都只展示了经过最小化修改的 Fedora 13 Live CD 安装,服务信息只从 SysV init 脚本中读取。因此,对于现存的所有服务都没有使用基于套接字或总线的激活方式。

请原谅目前还没有关于启动时间的 bootchart 图表或者可靠数据。等我们将默认安装的 Fedora 上的所有服务并行化以后,会尽快给出相关数据。也欢迎大家来评测 systemd,我们也会给出自己的评测数据。

大概所有人都会一直来询问我,所以这里我给出两个数字。但它们是完全不科学的,因为它们是在一台 VM(只有一个 CPU)中使用我的秒表测量的。Fedora 13 使用 Upstart 启动耗费 27s,使用 systemd 时达到 24s(从 grub 到 gdm,同一个系统,同样的设置,取连续两次启动中的较小值)。需要注意的是,这仅仅展示了通过解析 init 脚本的 LSB 文件头中的依赖信息用来进行并行化的加速效果。基于套接字和总线的激活方式完全没有使用,因此这些数据不能用来评估前文提出的方案。另外,systemd 在一个串口控制台上开启了 debug 级别的日志。所以这个评测数据没有什么价值。

编写守护进程

一个使用 systemd 的理想的守护进程比起传统守护进程来需要做一些不同的事情。后面我们会发布一份详情的指南来解释和建议如何编写使用 systemd 的守护进程。总的来说,对于守护进程编写者来讲事情变得更简单了:

  • 我们要求守护进程编写者在进程中不要 fork 或者 double fork,而是直接在 systemd 启动的原始进程中直接执行他们的事件循环。另外,也不要调用 setsid()
  • 不要在守护进程中丢弃用户权限,把这个问题留给 systemd 管理,在 systemd 的 service 配置文件中进行配置。(这里会有一些例外,比如,对于某些守护进程来说,在一个需要较高权限的初始化阶段结束后,在守护进程代码中放弃权限是合理的。)
  • 不要写 PID 文件。
  • 在总线上注册一个名称。
  • 你可以依赖 systemd 来进行日志管理,把所有需要输出的日志输出到标准错误输出。
  • 让 systemd 为你创建和监控套接字,以便使用套接字激活功能。因此,需要如前文所述那样解析 $LISTEN_FDS$LISTEN_PID 变量。
  • 使用 SIGTERM 信号来停止守护进程。

上述列表与 Apple 推荐的与 launchd 兼容的守护进程 非常相似。应该很容易扩展已经支持 launchd 激活方式的守护进程以支持 systemd。

要注意的是,出于兼容原因,systemd 完美支持不是以上述方式编写的守护进程(launchd 只提供有限的支持)。如前文提到的,甚至可以支持那些 inetd 守护进程,它们可以使用 systemd 的套接字激活方式而无需任何修改。

所以,没错,一旦 systemd 在我们的试验中得到验证并且被发行版采用,至少重写这些以默认方式启动的服务以使用套接字或者总线激活方式是有意义的。我们已经编写了一些概念验证补丁,这些移植被证明是非常容易的。在一定程度上我们也可以利用一些已经为 launchd 做过的工作。此外,给服务添加套接字激活方法并不会导致它与非 systemd 系统不兼容。

FAQs

  • 项目成员有哪些?
    目前的代码主要是我的工作,Lennart Poettering (Red Hat)。但设计和所有的细节是 Kay Sievers (Novell) 和我紧密合作的结果。其他参与的人包括 Harald Hoyer (Red Hat), Dhaval Giani (Formerly IBM) 和来自其他公司(比如 Intel, SUSE 和 Nokia)的一些人。
  • 这是一个 Red Hat 的项目吗?
    不是,这是我的个人项目。同时,让我强调一下:本文中的观点只代表我个人,不代表我的雇主、或者 Ronald McDonald 或者其他任何人的看法。
  • 这会进入到 Fedora 吗?
    如果我们的实验证明这个方案是可行的,并且在 Fedora 社区的讨论中得到支持,那么是的,我们当然会尝试进入到 Fedora。
  • 会进入到 OpenSUSE 吗?
    Kay 正在推进这件事情,因此针对 Fedora 的答复也适用于这里。
  • 会进入到 Debian/Gentoo/Mandriva/MeeGo/Ubuntu/[请在这儿填入你喜爱的发行版]?
    这取决于他们。如果他们有兴趣的话我们当然非常欢迎,而且会帮助完成集成工作。
  • 为什么你不将这个方案添加到 Upstart 中,而是发明一个新东西?
    前文中关于 Upstart 的部分已经说过,在我看来 Upstart 的核心设计是有缺陷的。如果现存的方案在核心设计上存在缺陷的话,从头再来一遍是可行的。但要说明的是,我们从 Upstart 的代码中得到了不少灵感。
  • 如果你这么喜欢 Apple 的 launchd,为什么不直接使用它?
    launchd 是一个很好的发明,但我不认为它能很好的适用于 Linux,也不认为它能适应像 Linux 这样具有很强扩展性和灵活性的通用系统。
  • 这是一个 NIH 项目吗?
    我希望在前文中我已经解决清楚了为什么我们要提出一个新东西,而不是构建在 Upstart 或者 launchd 上。我们提出 systemd 是出于技术原因,而不是政治因素。别忘了 Upstart 包含了 一个叫作 NIH 的库 (实际上是 glib 的一种实现)—— systemd 可没有这么做!
  • 可以运行在 [请在这里填入非 Linux 操作系统] 上吗?
    不太可能。前文已经说明,systemd 使用了很多 Linux 特有的 API(比如 epoll, signalfd, libudev, cgroups,还有很多别的),移植到其它操作系统对我们来说没有太大意义。而且对于可能存在的移植代码,我们(项目组成员)也不太可能合并它,因为我们不愿意接受由此引入的诸多限制。尽管如此,万一有人真的想要进行移植,git 很好的支持了分支和 rebase。
    实际上,可移植性上的限制不仅仅只是针对其它操作系统的:我们要求最近版本的内核、glibc、libcgroup 和 libudev。不支持旧版本的系统,不好意思。
    如果有人想要为其它操作系统实现类似的东西,建议的合作模式是我们帮助你识别哪些接口可以与你的系统共享,从而让守护进程的编写者更容易同时支持 systemd 和你的 systemd 副本。我们的关注点应该放在接口的共享上,而不是代码的共享。
  • 我听说 [填入下列其中一个:Gentoo boot system, initng, Solaris SMF, runit, uxlaunch, ...] 是一个非常优秀的 init 系统,而且也支持并行启动,为什么不使用它呢?
    我们在开始这个项目之前仔细调研过各种系统,没有一个与我们脑海中为 systemd 设计的所做的一样(当然了,launchd 除外)。如果你没有看出来这一点,请把我前文所写的内容重新读一遍。

贡献代码

我们非常乐于接受补丁和帮助。每一个*软件项目只有从广大的外部贡献中才能获益,这应该是共识了。对于操作系统的核心部分来说尤其如此,比如 init 系统。我们非常珍视你的贡献,因此不要求版权让渡(与 Canonical/Upstart 非常不同!)。最后,我们使用 git,每个人最爱的版本控制系统!
我们特别愿意接受帮助以让 systemd 工作在其它发行版上,而不只是 Fedora 和 OpenSUSE(嘿,有 Debian, Gentoo, Mandriva, MeeGo 的人在找事情做吗?)除此之外,我们渴望吸引每一个层次的贡献者:我们欢迎 C 开发者、包管理者以及那些乐于编写文档或者贡献一个 logo 的人们。

社区

目前我们只有 源码仓库 和 IRC 频道(#systemd on Freenode),还没有邮件列表、网站或者缺陷跟踪系统。我们也许很快会在 freedesktop.org 上把这些建立起来。如果你有任何问题或者有其它事情想要联系我们,我们邀请你加入 IRC!

更新:我们的 GIT 仓库换地方了。

同步发布:https://hellogc.net/archives/134