关于 Linux 中的 1 号初始进程,RHEL 5 默认的是 SysV init,到了 RHEL 6 则是昙花一现的 upstart。而大包大揽、管天管地的 systemd,曾经因为不符合 Unix 小而美的设计风格而饱受争议。但可能确实好用吧(至少笔者是这么觉得的),到了 RHEL 7 时代,systemd 逐渐成为了主角。
初探
一个 service 在传统的 Linux 系统中通常被实现为 daemon,在 systemd 中则被抽象成了unit,SysV 中的 init script 在 systemd 中被替换成了 unit file,以大家常用的 sshd 为例,它大概长这样(省略号的部分将在后文展开讲述):

如果想修改这个文件的内容,需要拿个小本本才能记住 "/usr/lib/systemd/system" 这串长长的路径?其实不必,直接用 "systemctl edit" 命令试一下(自 systemd 的 218 版本后支持此项功能):

怎么打开的是一个空文件?因为这里默认的是 override 方式,即你在这个文件里写入的,会覆盖掉原来 unit file 中对应的部分,当该 unit 被加载时,systemd 会自动进行文件内容的 merge 操作。
这种将原生文件和修改相分离的做法还是蛮好的,不用在改动配置文件之前因为担心出错,先手动将原文件备份一下(比如命名为 "*.bak"),需要撤销改动又再手动恢复。如果还是比较习惯直接编辑原文件的方式,那就用 "edit --full"。
虽然在实现上勇于革新,但 systemd 还是提供了对 SysV init 的前向兼容性,init script 在 systemd 中依然可以运行。如果想将这些既有的 init script 转换为 systemd 的 unit files,请参考这篇文章 https://access.redhat.com/solutions/912263。
细究
接下来好好看看一个 uint file 中的三部分内容分别表示什么(还是以 sshd.service 为例):

"After" 限制了启动的顺序,因为 ssh 是网络服务的一种,所以它需要等到 "network" 起来之后才可以启动。至于 "Wants",则体现了 systemd 的一个特性:可以自动解决启动时的依赖问题。
如果试图手动启动服务 A,而服务 A 依赖于服务 B(比如 sshd.service 依赖于 sshd-keygen.service),那么 systemd 就会自动帮你服务 B 先启动起来。这种依赖关系除了可以通过服务 A 的unit-file看出来,也可以通过 "systemctl list-dependencies" 得知。

然后来到 "Type" 这一行,如果它的值是 "simple" 或者 "notify",表明由 "ExecStart" 创建的就是该 service 的主进程。

还有一种传统的 type 是 "forking",意思是由 "ExecStart" 创建父进程,父进程再 fork 一个子进程作为该 service 的主进程,所以此时通常需配合 "PIDFile" 来标识主进程对应的 pid 文件(参考 https://lwn.net/Articles/801319/)。

说到最后的这个 "WantedBy",先来回忆下 SysV init 中经典的 "runlevel" 的概念,它代表了 OS 不同的运行模式(用数字 0~6 表示)。

而在 systemd 中,"runlevel" 被精简成了对应命令行终端(CLI)的 "multi-user target",和对应图形界面的 "graphical target"。什么是 "target"?可以简单地理解为构成执行环境的一组 unit 的集合(这里 sshd 就是构成 "multi-user target" 的一员)。

那基于 target,怎样在命令行和 GUI 之间切换呢?熟悉 "runlevel" 概念的你,可能还是习惯性地会去 "/etc/inittab" 中寻找答案。
虽然这个文件在 systemd 里其实已经没用了,但它还是被留在那里,等待你打开的时候,贴心地告诉你:新的 systemd 中的这两个 "target" 和 "runlevel" 大致是怎样的一种对应关系,以及如何实现设置的切换。

当然,也可以使用基于 web 的 cockpit 来进行切换:

按需激活
除了上面介绍的 service,systemd要管的东西还多着呢,本着「分而治之」的原则,还有很多其他类型的unit,它们大都和 systemd 的按需启动和并行启动有关。这里以 socket unit 为例来进行说明。
作为一个像 sshd 这样的service,它需要监听特定的端口,以便处理来自 client 的连接请求,所以传统的做法是:你得先启动起来,即便之后一直没有 client 发出连接请求,你也得一直保持在线状态,守护进程嘛,这就是你的职责。
而 systemd 采用的做法是:你们这些 service 都先不用启动,需要的 listen socket 都由我来创建和监听,当有真正的连接到来时(比如 ssh client 的请求),那我再把对应的 sshd service 拉起来,然后把这个 socket 交给 sshd。这里,systemd 其实充当了一个 socket 代理的角色。

那在 sshd 得到 systemd 移交的这个 socket 之前,通过这个 socket 监听的消息岂不是收不到?不用担心,systemd 会把代管 socket 期间收到的消息也一并传给 sshd,不会让消息遗漏。
这种激活 service 的方式被称为 "socket-based activation"。除此之外,还有当设备真正挂载才激活的 "device-based activation",当目录或文件真正改变才激活的 "path-based activation" 等等,反正都是 on-demand,有需求再说,这也再次体现了 "lazy" 的设计思想。
也正是由于这种设计,systemd 才敢进行激进的并行启动,只要 listen socket 都准备好了,就不怕。




