Init系统历代记-SysV

把时间往回拨到20世纪末,1983年,AT&T公司发行出了第一个成功的商业版UNIX,即System V Unix。该系统在各方面都给人留下了深刻的印象,init系统方面也不例外。

1992年,受那个年代的自由软件潮流和Linux发行版崛起的影响,Miquel van Smoorenburg编写了开源自由的复刻版sysvinit,它最初是在Minix平台上进行开发的,但在年末的2.0版本中迅速放弃了Minix支持,转而专注于Linux平台。从1993年第一个Linux发行版Slackware开始,SysV init系统在Linux发行版社区占据了数年的话语权,事实上在那时,这也是人们的唯一选择。

SysV的init系统分为一些散装的程序:

  • PID1:init以及它的软链接telinit
  • 单用户模式登录程序:sulogin(后来,该工具被剥离,并入Util-Linux)。
  • 关机程序:poweroffshutdownhaltreboot
  • 初始化日志获取辅助守护进程:bootlogd

和一个关键的文件:

  • 初始化过程定义文件/etc/inittab

init内部硬编码的逻辑在本质上只有在启动时读取/etc/inittab,并按照其中用户设定的方式拉起用户设定的进程。它的这种逻辑使得inittab中包含了大部分用户空间的初始化行为,而这些行为完全是用户(或者说,系统的开发者)自拟的。inittab中每一行的基本格式是:

1
id:runlevel:action:process

即,使用:分隔的四个部分,每一部分的含义分别是:

  • 标识符(ID),唯一的标识符,用于utmp记录。
  • 运行等级(Runlevel),执行该操作的运行等级。
  • 操作(Action),指定执行的操作,即启动进程的方式。最常见的操作无非就几种:
    • initdefault:设置默认运行等级。
    • sysinit:和wait类似,但是在系统可用之前的基本初始化阶段执行程序。该阶段不存在运行等级概念,因此该Action会导致无视运行等级。
    • once:在指定的运行等级下,只尝试启动程序一次,如果在之后程序停止了,也不会再次尝试启动。
    • wait:在指定的运行等级下,启动程序,并且等待程序退出后才继续执行。
    • respawn:在指定的运行等级下,程序必须处于运行状态,如果进程终止了,那么init会将其重新启动。
  • 程序(Process)。指定操作的程序及其参数。

根据inittab的格式我们不难看出运行等级(Runlevel)的概念在整个SysV init中处于核心地位。大部分Linux发行版都遵循System V Unix原本的用法,即:

  • sysinit阶段执行基本文件系统挂载等操作。
  • 使用运行等级0代表系统停机(Halt、Poweroff),这也是shutdown命令及其链接poweroffhalt的内部实现方式。
  • 使用运行等级1代表单用户维护模式(Single),该运行等级应当启动。
  • 运行等级2、3、4通常没有区别,均代表标准的多用户模式,尽管在严格定义上存在一些并不重要的细节差异
  • 相当一部分Linux发行版会使用5代表包含X图形环境的状态,虽然在最初的SysV init系统中,实际上是不存在运行等级4和5的。
  • 使用运行等级6代表系统重新启动(Reboot),这也是reboot命令的内部实现方式。

然而受到具体设计的影响,不同Linux发行版在细节上总是存在不同,例如Debian的Runlevel 1实际上是Runlevel S的链接,而Fedora/RHEL干脆就没有使用Runlevel S。这种不一致性是因为运行等级作为一个定义不清晰的概念,从一开始就不存在强限制,用户可以遵循约定俗成的规范,但是如果想的话也完全可以自己创造7,8,9甚至是更夸张的运行等级。

我们面对着如此多的运行等级,而每个运行等级又要执行如此多的程序,而所有这些信息都必须包含在单个inittab文件中(后来的sysvinit提供了inittab.d的支持,但是没人关心),如果将每个程序都写到inittab里面无疑是不可管理的。

针对这一问题,sysvinit的开发者的思路是:在一个运行等级中启动一个进程、或是启动多个进程,这完全是一回事,我们没有必要关心到底启动了多少,也不应该关心大部分启动的进程(及其子进程,当然)的状态,而应该让它们“自己照顾好自己”,因此他们的做法是使用一个(或多个)rc脚本,该脚本与大批开发者编写的Init脚本协同工作,功能无非就是执行/etc/rcX.d/目录下的Init脚本(如果以S开头,则位置参数为start;如果以K开头,则位置参数为stop)。如此一来,在一个运行等级中所做的事就被压缩成了一件:执行这个运行等级的rc脚本,并等待它执行对应运行等级目录下(/etc/rcX.d/)的所有的Init脚本。

1
l2:2:wait:/etc/init.d/rc 2

转过头来讨论Init脚本:sysvinit从来没有对Init脚本的内容格式进行过明确和严格的定义,它仅仅只有一些模糊的、约定俗成的“形式限制”,例如:

  • Init脚本必须是短生命周期(究竟什么是短,也并没有定义)的,不应该长期阻塞,否则会影响初始化过程。
  • Init脚本应当启动一个脱离终端的“后台”进程。
  • Init脚本应当接受startstop作为位置参数。

这种模糊性导致不同的Linux发行版之间,不同的程序之间的Init脚本实现五花八门:我是应该在程序内部实现Double fork,还是应该在脚本里面实现Double fork?我是应该用发行版特定的工具,还是应该自己重新造个轮子?

如果以上这些问题都不够让人难受的话,它还留下来一个最恶劣的问题:我怎么保证我的程序在启动之前能确保它需要的所有进程已经准备好,而不是在启动时当场崩溃?sysvinit对此的回答是:我帮不了你。这个任务要么放在开发者身上:在程序内部实现某种检查策略,在检查到需要的条件不满足时优雅地退出;要么放在系统管理员身上:艰难地在/etc/rcX.d/目录下为每一个脚本精心地命名,确保其在字典序上遵循特定的顺序(通常是加上编号,例如S02),以确保启动流程顺利进行。

2001年推出的LSB标准提供了一种解决这些问题的尝试:在Init脚本中附上一些特殊的注释头,以提供一些元信息,一些最有意义的元信息包括:

  • Provides::这个Init脚本启动后提供的启动设施。
  • Default-Start::这个Init脚本应当启动(以start位置参数执行)的运行等级。
  • Default-Stop::这个Init脚本应当停止(以stop位置参数执行)的运行等级。
  • Required-Start::这个Init脚本启动时必须依赖的启动设施。
  • Required-Stop::这个Init脚本停止时必须依赖的启动设施。

令人遗憾的是,LSB标准并没有引起广泛关注,直到2005年,SUSE的开发者和原sysvinit工作组合作开发了名为insserv的LSB头解析程序,它能够解析所有Init脚本中包含的LSB头,据此在/etc/rcX.d/目录下创建合适编号的软链接,同时还会预计算依赖树,并保存到文件中。这一切只需要一条命令:

1
insserv Init脚本名

这使得开发者只需要编写良好的LSB元信息即可构建出一个合理的启动顺序。不仅如此,真正诱人的是:在确定了依赖关系后,相互无关的Init脚本即可以并行方式启动——与insserv配套的startpar程序正是做这个事情的。

顺应当时的趋势,Debian也专门成立了initscripts-ng工作组,负责Init脚本的LSB标准化和insserv工具的打包,在2008年的6.0版本中实现了insserv的引入,在2009年的7.0版本才实现了通过startpar的并行化启动。相对的,RHEL/CentOS则由于其更新周期,完全错过了这一阶段,在RHEL/CentOS 5-6的更新中,直接切换到了Upstart,这是我们之后要讲到的。

SysV init的问题的本质在于它在设计上就缺乏对“依赖”的理解,全盘交给使用者维护。在后期出现的LSB头和insserv的支持,也只不过是将依赖关系抹平为顺序关系,这一关系并不是Init系统理解和维护的,而是用户(或者说,文件系统)理解和维护的,Init系统自始至终都不了解它启动的Init脚本的内容和Init脚本之间的关系。

除此以外,它本身的性质,以及大部分用户的用法还意味着一些额外的问题:

  1. 安全性:给Shell脚本做注入比喝水还简单,但是排查又比登天还难。Init脚本可能是一个守护进程的启动脚本,也可能是一个执行某些批处理操作的胶水脚本,甚至还可能是两者的结合体。而且,使用Shell脚本在关机时进行进程的终止也是不优雅的,可能会导致数据的丢失。
  2. 性能:Shell脚本的存在本身就是对性能的浪费,每执行一个脚本系统都要启动几十个甚至几百个进程,在一个标准的使用SysV init的Linux发行版上,启动后的进程号在刚启动完成时就已经刷到了1300多。
  3. 功能薄弱:rc脚本是通过wait方式启动的,而它调用的Init脚本启动的子进程会进行Double fork以成为孤立的守护进程。然而(参考PEP 3143)守护进程不是服务,或者说它至少不完全是服务,因为服务要求存在能和服务进程交流的另一个进程。这直接导致了以daemontools为首的一大批独立Supervisor(进程监视器)的诞生,它们的管理进程直接分叉出服务进程,这使得用户可以通过客户端工具和管理进程通信,以控制服务进程。

SysV init的功能松散,单一(用另一种说法,简单),人们最初使用它一方面是因为别无选择,另一方面也是因为当时的系统功能单一。但是操作系统提供的服务越是复杂多变,SysV init的负面效果就越是暴露无遗,因此它被时代抛弃也是必然的。