Linux 中服务的初始化和管理:过去、现在和未来

对于任何类 UNIX 操作系统而言,最关键的部分之一就是 init 守护进程。在 Linux 中,此进程由内核启动,并且是第一个生成的用户空间进程,也是关机期间最后一个终止的进程。

在 UNIX 和 Linux 的历史中,许多 init 系统都曾流行一时,然后逐渐衰落。在本文中,我将重点介绍 init 系统与 Linux 相关的历史,并讨论 init 在现代 Linux 系统中的作用。我还将介绍 System V Init (SysV) 方案的一些历史,该方案长期以来一直是许多 Linux 发行版的实际标准。然后,我将介绍几种更现代的系统初始化方法,例如 Upstart 和 systemd。最后,我将关注 systemd 的工作原理,因为对于几个最大的发行版而言,这似乎是目前流行的选择。

Init 的作用

Init 是 initializer(初始化器)的缩写,它既是 Linux 和其他 UNIX 系统的启动管理器,又是会话管理器。它是一个启动管理器,因为它在 Linux 的启动中起着至关重要的作用。它是创建或初始化用户空间,并最终创建所有用户空间进程的进程。它也可以被视为会话管理器,因为它负责用户空间及其进程的许多方面,一旦系统启动并运行。

实际上,启动此进程的调用已硬编码在 Linux 内核中。下载最新的内核源代码,并在 init/main.c 文件中查找名为 kernel_init 的函数。Linux 内核将尝试执行的文件包括 /sbin/init。如果 Linux 找不到这些进程之一,它将抛出内核恐慌并停止。

内核为 init 进程分配 ID 1 或 PID 1。所有其他用户空间进程都从 init fork 而来,因此,PID 1 声称对所有其他用户空间进程拥有祖先权利。PID 1 也会自动成为任何孤立用户空间进程的直接父进程。

一点历史

既然我已经为本文奠定了基础,并为您提供了对 init 是什么以及做什么的基本了解,我想稍微跑题,谈谈 UNIX 的历史。

随着时间的推移,类 UNIX 操作系统的初始化方案已经非常多样化。对不同 Linux 发行版的做事方式产生历史性影响的两个最重要的 init 方案是 4.4 BSD 中使用的 rc 方案和 SunOS 和 Solaris 中使用的 SysV 方案。

4.4 BSD init 系统非常简单且是单体式的。启动时,内核运行 /sbin/init,它会生成一个 shell 来运行 /etc/rc 脚本。/etc/rc 脚本包含用于检查硬盘驱动器完整性并挂载它们、启动其他进程以及启动网络子系统的命令。此方案完全包含在几个脚本中:即 /etc/rc、/etc/rc.local 和 /etc/netstart。此方案也没有特定的关机程序。Init 会收到 SIGTERM 信号,并向其子进程发送 SIGHUP 和/或 SIGTERM,并且在所有进程退出后,它将进入单用户模式并关机。

今天,继承了 rc 初始化方案的系统是 Free-BSD、Net-BSD 和 Slackware Linux 发行版。这些现代系统在原始 4.4 BSD 方案的基础上进行了相当大的改进,并且比原始方案更加模块化。

从历史上看,大多数其他 Linux 发行版都是 SysV 方案的拥护者,该方案最初在 AT&T UNIX 和派生系统(如 Solaris)中实现。

System V Init

实现 SysV 方案的 Linux 发行版可以处于许多不同的状态之一,在这些状态下,可以运行预定数量的进程。这些状态称为运行级别,进入某个运行级别意味着系统处于某个操作阶段。

每个运行级别的含义可能因您的 Linux 发行版而异。例如,有些发行版(如 Ubuntu)使用运行级别 2 表示启用网络的多用户图形模式。另一些发行版(如 Fedora)使用运行级别 5 表示相同的含义。

在 SysV Linux 机器中,内核像往常一样运行 /sbin/init,然后它将加载参数并执行在 /etc/inittab 中定义的指令。此文件定义了整个系统的默认运行级别,描述了按下 Ctrl-Alt-Del 时会发生什么,加载了键盘映射文件,定义了要为哪些终端生成 getty,生成了终端登录进程,运行了 /etc/init.d/rcS 脚本,并且它还影响其他运行级别脚本的执行顺序。

/etc/init.d/rcS 脚本会将系统置于单用户模式,以便完成硬件探测、磁盘挂载、设置主机名、设置网络等等。查看 Debian 7 系统中的 /etc/rcS.d/ 以了解所有细节。接下来,/sbin/init 会将自身切换到默认运行级别以启动所有系统服务。默认运行级别值在 /etc/inittab 的 initdefault 行中定义。

这实际上转化为调用带有运行级别值 2 的 /etc/init.d/rc 脚本。然后,rc 脚本将执行 /etc/rc2.d/ 目录中的所有 K*(表示 Kill)和 S*(表示 Start)脚本。这些实际上是指向 /etc/init.d/ 中真实脚本的链接。链接的名称遵循格式 S##<service-name>K##<service-name>,其中 ## 标记是用于确定脚本应运行顺序的两位数字。顺序是字母顺序,并且 Kill 脚本在 Start 脚本之前执行。最后要做的事情是运行 /etc/rc.local 脚本,您可以在其中添加要在启动时执行的自定义系统命令。

使用 SysV 方案的系统通常附带 service 程序,用于在系统运行时管理服务。您可以使用 service 实用程序分别检查服务或所有服务的状态,以及启动或停止服务。

  • $ service <service> status

  • $ service status -all

  • # service <service> start|stop

要管理服务到特定运行级别的分配,您可以使用名为 sysv-rc-conf 的工具,该工具管理各个 rc 目录中所有链接的设置。当您以特权用户身份使用命令 telinit 时,您也可以随时切换系统的运行级别。例如,telinit 6 将重新启动 SysV 系统。

SysV 方案今天仍然在 Debian 7 (Wheezy) 系统中使用。但是,Debian 开发人员将在版本 8 中将 init 系统更改为 systemd。我在下面更详细地介绍了 systemd,但首先,让我们看看为什么我们需要一个新的 init 系统。

System V Init 的问题

SysV 方案一直很棒,但当桌面上的 Linux 获得更多发展势头时,它开始显现出其局限性。当最初设计 SysV 方案时,计算机与今天截然不同。SysV 的设计并非旨在很好地处理某些事情

  • USB 设备。

  • 外部存储卷。

  • 蓝牙设备。

  • 云。

SysV 方案是为静态且移动缓慢的世界而设计的。此 init 方案最初仅负责在开机后将系统置于正常运行状态,或在关机前优雅地关闭服务。因此,该设计是严格同步的,会阻止未来的任务,直到当前任务完成。

这使得系统无法处理各种与系统启动或关机无关的事件。在 SysV init 的鼎盛时期,我们今天认为理所当然的事情真的很难优雅地处理

  • 没有真正的进程监管——例如,守护进程在崩溃时不会自动重启。

  • 没有真正的依赖项检查。脚本命名的顺序决定了它们的加载顺序。

  • 在机器运行时添加或删除 USB 驱动器和其他便携式存储/网络设备非常麻烦,并且通常需要重新启动。

  • 没有设施可以在不锁定系统的情况下发现和扫描新存储设备,尤其是在磁盘可能在扫描之前甚至没有通电的情况下。

  • 没有设施可以加载设备的固件,这可能需要在检测到设备之后但在设备可用之前发生。

不可避免地,在 2005/2006 年左右,一些替代方案试图修复 SysV 方案的所有问题。但在那段时间,看起来最有希望的努力是 Canonical 赞助的 Upstart init 项目。

Upstart

可以肯定的是,Upstart init 与 SysV init 方案没有任何代码共享,但它实际上是 SysV init 方案的超集,提供了良好的向后兼容性。与传统 SysV 方法的主要区别在于,Upstart 实现了事件驱动模型,该模型允许它在达到里程碑时异步响应。Upstart 还实现了作业的概念,这些作业由 /etc/init/*.conf 下的文件描述,其目的是执行一个脚本部分,该部分生成一个进程。因此,系统初始化可以表示为一系列连续的“当事件 Y 发生时生成进程 X”规则。

就像在 SysV 方案中一样,当 Linux 内核执行 /sbin/init 的 Upstart 实现时,它会将控制权交给 Upstart。此时,事情的工作方式可能因您的 Linux 发行版而异。对于 Red Hat Enterprise Linux (RHEL) 6 用户,您仍然会在 /etc/inittab 中找到一个文件,但此文件的唯一功能是设置系统的默认运行级别。如果您的发行版是 Ubuntu 衍生版本之一,则 /etc/inittab 甚至不再存在,默认运行级别而是设置在名为 /etc/init/rc-sysinit.conf 的文件中。

/sbin/init 的 Upstart 版本将发出一个名为 startup 的单事件,该事件触发系统初始化的其余部分。有一些作业指定 startup 事件作为其启动条件,其中最著名的是 mountall,它挂载所有文件系统。然后,mountall 作业会触发与磁盘和文件系统初始化相关的各种其他事件。这些事件反过来又触发 udev 内核设备管理器启动,并且它发出启动网络子系统的事件。

这是 Upstart 触发的最关键的作业之一。此作业称为 rc-sysinit,它对文件系统和 network-up 事件具有启动依赖性。此作业的作用是将系统置于其默认运行级别。它执行命令 telinit <runlevel> 来实现此目的。然后,telinit 命令发出运行级别事件,这会导致许多其他作业启动。这包括 /etc/init/rc.conf 作业,它为 SysV init 方案实现了兼容性层。它执行 /etc/init.d/rc <runlevel> 并确定当前运行级别是否存在 /etc/rc#.d/ 目录,并执行其中的所有脚本。

在基于 Upstart 的系统(如 Ubuntu 和 RHEL 6)中,您可以使用工具 sysv-rc-confchkconfig 分别管理不同服务的运行级别。您还可以通过 initctl 实用程序管理作业。您可以使用命令 initctl show-config 列出所有作业及其各自的启动和停止事件。您还可以分别使用以下命令检查作业状态、列出可用作业以及启动/停止作业

  • $ initctl status <job>

  • $ initctl list

  • # initctl start|stop <job>

Upstart 方案已在流行的 Linux 发行版中使用,例如 Fedora 版本 9 到 14、RHEL 6 系列以及 Ubuntu 版本 6.10 到现在。但是,尽管 Upstart init 为 Linux 带来了所有灵活性,但它在一些基本方面仍然不足

  • 它忽略事件之间的系统状态。例如,系统已插入电源线,然后系统在交流电源上运行一段时间,然后用户拔下电源线。Upstart 关注上面的每个事件,将其作为一个单独的离散且不相关的单元,而不是跟踪整个事件链。

  • 系统的事件驱动性质将其依赖项链颠倒了。当事件被触发时,它不是执行使系统进入工作状态所需的绝对最小量的工作,而是执行所有可能跟随它的作业。例如,仅仅因为网络已启动,并不意味着 NFS 也应该启动。事实上,相反才是正确的顺序:当用户请求访问 NFS 共享时,系统应验证网络也已启动并正在运行。

  • 依赖项链仍然存在。尽管 Upstart 中并行发生的事情更多,但用户必须将原始脚本序列从 SysV init 移植到 /etc/init 中 *.conf 文件中的一组事件触发操作规则。此外,由于事件系统的生成树结构,因此很难弄清楚为什么会发生某些事情以及是什么事件触发了它。

还有另一种 init 方案,其目的是解决上面列出的问题。

systemd

systemd 是通往 init 系统涅槃之路上的最新里程碑。systemd 的主要设计目标是,根据 systemd 的首席开发人员 Lennart Poettering 的说法,“启动更少,并行启动更多”。这意味着您只执行使系统进入运行状态绝对必要的操作,并尽可能多地同时运行。

为了实现这些目标,systemd 旨在对抗以前的 init 方案的两个主要麻烦点:shell 和并行性。systemd 的主要可执行文件 /lib/systemd/systemd 执行最初在脚本中存在的所有调用,从而消除了生成 shell 环境的需要。那么对 /sbin/init 的调用又如何呢?它以指向 /lib/systemd/systemd 的符号链接的形式仍然存在于 Linux 内核中。

为了解决并行性问题,您需要删除各种服务之间的依赖项链,或者至少使其成为次要关注点。如果您从最基本的层面看待这个问题,则各种服务之间的依赖关系归结为一件事:拥有一个套接字供进程之间相互通信。systemd 首先创建所有套接字,然后并行生成所有进程。例如,需要写入系统日志的服务需要等待 /dev/log 套接字可用,但是一旦可用,这些服务就可以启动。因此,如果 systemd 首先创建套接字 /dev/log,那么这将减少阻止其他服务的依赖项。即使套接字的另一端没有任何东西接收消息,此策略仍然有效。内核本身将管理套接字的缓冲区,并且一旦接收服务启动,它将刷新缓冲区并处理所有消息。上面的想法并非新鲜或革命性的。它们以前在诸如 xinetd 超级服务器和 OS X 中使用的 launchd init 方案之类的项目中尝试过。

systemd 确实引入了单元和目标的新概念。目标类似于以前方案中的运行级别,并且由多个单元组成。systemd 将执行单元以达到目标。每个单元的指令都位于 /lib/systemd/system/ 目录中。这些文件使用看起来像 Windows INI 文件的声明性格式。这些单元最常见的类型是服务单元,用于启动服务。来自 Arch Linux 的 sshd.service 文件如下所示


[Unit]
Description=OpenSSH Daemon
Wants=sshdgenkeys.service
After=sshdgenkeys.service
After=network.target

[Service]
ExecStart=/usr/bin/sshd -D
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=always

[Install]
WantedBy=multi-user.target

这种格式非常简单,并且在多个不同的发行版中真正可移植。还有其他类型的单元文件描述系统,它们是套接字、设备、挂载、自动挂载、交换、目标、路径、计时器、快照、切片和作用域。详细介绍所有这些内容超出了本文的范围;但是,我想提一件事:目标是一种特殊的单元文件类型,它将其他类型粘合在一起形成一个有凝聚力的整体。例如,以下是来自 Arch Linux 的 basic.target 的内容


[Unit]
Description=Basic System
Documentation=man:systemd.special(7)
Requires=sysinit.target
Wants=sockets.target timers.target paths.target 
 ↪slices.target
After=sysinit.target sockets.target timers.target 
 ↪paths.target slices.target
JobTimeoutSec=15min
JobTimeoutAction=poweroff-force

如果您查看 basic.target 需要和想要的内容,则可以跟踪依赖项链。这些是同一 /lib/systemd/system/ 目录中的实际单元文件。上面的 RequiresWants 指令是 systemd 如何定义单元之间依赖项链的方式。Requires 指令表示硬性要求,Wants 表示可选要求。还要记住,RequiresWants 并不意味着顺序。如果未指定 After 指令,systemd 将并行启动单元。

计时器单元也非常有趣。它们是包含 [Timer] 部分的单元文件,并定义 systemd 的 TimeDateD 子系统将如何激活未来事件。在这些计时器单元中,您可以创建两种类型的计时器:一种将在基于可变起点的时段(例如系统启动)之后激活,另一种将在固定间隔(如 cron 作业)激活。实际上,计时器单元是 cron 作业的替代方案。

关于 systemd 单元文件的最后一件事是,它们提供了轻松描述服务崩溃时该怎么做的手段。您可以通过在单元文件中使用指令 RestartRestartSec 来做到这一点。此功能允许 systemd 也承担进程监控器的角色。

systemd 指的是 init 守护进程可执行文件本身,即 /lib/systemd/systemd,但也指用于管理系统和服务的实用程序和程序集。这些实用程序中最主要的是用于管理服务的 systemctl 程序。您可以使用它来启用、启动和禁用服务,查找给定服务的状态,以及列出所有已加载的单元。例如

  • # systemctl enable sshd

  • # systemctl start sshd

  • # systemctl stop sshd

  • # systemctl status sshd

  • # systemctl list-units

某些 Linux 发行版(如 RHEL 7 和 CentOS 7)提供了兼容性层,该层将 SysV 和 Upstart 命令转换为 systemd 命令。如果您在 CentOS 7 中发出命令 service sshd status,您将获得以下输出


Redirecting to /bin/systemctl status  sshd.service
sshd.service - OpenSSH server daemon
   Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled)
   Active: active (running) since Mon 2014-12-08 02:01:53 PST; 
 ↪12h ago
  Process: 915 ExecStartPre=/usr/sbin/sshd-keygen (code=exited,
 ↪status=0/SUCCESS)
     Main PID: 937 (sshd)
       CGroup: /system.slice/sshd.service
              ...937 /usr/sbin/sshd -D

请注意上面控制台输出的第一行,以及它如何指示 SysV 样式的命令已重定向到 systemd 样式的命令。这允许用户轻松进入 systemd 的做事方式,同时仍然允许用户利用以前的技能集。

systemd 工具箱中另一个非常重要的程序是 journalctl 实用程序。它允许您查看和管理名为 journald 的 systemd 日志记录子系统。systemd 的日志文件是一个二进制文件,使用 journalctl 真的简化了用户体验。以下是一些有趣的示例

  • 显示完整日志:# journalctl --all

  • 跟踪日志:# journalctl -f

  • 按可执行文件过滤日志:# journalctl /lib/systemd/systemd

  • 显示自上次启动以来的日志:# journalctl -b

  • 显示上次启动以来的错误:# journalctl -b -p err

我敦促您查看此处介绍的不同方案的文档以了解更多信息。

争议

从我的角度来看,当涉及到 Linux 的 init 方案时,未来并非 100% 确定。正如我在 2014 年底撰写本文时,明显的领导者是 systemd。许多发行版正在采用它;最新的发行版是 RHEL 7 和 Debian 8。但是,systemd 的采用一直存在争议,这些发行版收到了来自各自社区的强烈反馈。值得注意的是 Debian 技术委员会在 Debian 邮件列表中的辩论以及 Linus Torvalds 本人在 Linux 内核邮件列表中的投诉。

systemd 不仅仅是一个 init 方案。它将与启动和管理系统服务相关的所有内容统一到一个集中且单体的整体中:用户登录、cron 作业、网络服务、虚拟 TTY 管理等等。使用 shell 脚本来控制系统启动的好处是提供了灵活性,并且社区的许多成员希望能够选择他们喜欢的 init 方案。这催生了 systemd 的一些分支,甚至 Linux 社区中有一个派别完全抵制 systemd。查看网站 http://boycottsystemd.org

结论

Linux 系统的用户空间初始化和管理有着丰富多样的历史。我希望本文为您提供了一个新的视角,了解我们今天如何走到 systemd 成为新标准的这一步。我已经介绍了不同方案的所有优缺点,以及这些因素如何随着时间的推移影响了该领域用户的选择。我希望本文将促进进一步的讨论,并非常鼓励您的反馈。

资源

各种 Linux 发行版的源代码,包括

  • Debian 7 和 8

  • CentOS 6.5 和 7

  • Slackware 14

  • Fedora 20

  • Ubuntu 12.4 和 14.4

  • Arch Linux

Lennart Poettering 的网站:http://0pointer.net/blog

systemd 文档:http://freedesktop.org/wiki/Software/systemd

Upstart 文档:http://upstart.ubuntu.com/cookbook

加载 Disqus 评论