您可以将 Linux 系统启动过程(从启动电源到让系统完全运行)看作两个概念性阶段:
从设备引导,加载并初始化 Linux 内核 启动用户空间应用程序(包括服务器进程),挂载另外的文件系统,执行另外的内核配置与定制项,并提供对额外设备的访问权限
第一阶段中的基本步骤已经在名为 “深入理解 Linux 的启动过程” 的 developerWorks 文章中讲述过了。这些步骤多年来变化不大,包括使用 GRand Unified Bootloader 2 (GRUB 2),初始 RAM 磁盘的格式与使用方式的不同,以及对第一个启动的用户空间进程(通常是 init 进程)的改变。此阶段中的性能提升主要来自于硬件升级,比如速度更快的启动设备、更快的设备访问机制,以及速度更快的、更加强大的处理器。然而,第二阶段中的性能提升几乎完全是软件端的性能提升,可以继续使用大量不同的方法。
缩短阶段 2 系统启动时间有一种简便方法,即提高完成 Linux 启动过程第二阶段的机制性能。然而,改变启动次序本身就可以带来大幅性能提升。缩短阶段 2 系统启动时间的关键方面如下所示:
最小化:只启动最少的必要服务集合 线性:找出一个服务需要的其他一个或多个服务(也称为依赖性分析),并将这些需要的服务纳入启动过程 平行:尽可能地同时启动独立服务(或相关服务链)
纵观其历史,Linux 曾使用过各种各样的启动和关闭机制,尽力平衡线性与平行的问题,同时不会完全向后兼容性。下一节讨论了最常用的一些机制,以及如何在系统上监控它们的行为与性能,从而找出可以优化的地方。
了解和使用 SysVinit
Linux 系统上使用的传统系统启动与关闭机制叫作 SysVinit。顾名思义,SysVinit 机制的概念源自于 UNIX® 的 Sys V 版本,它的更恰当的名称应该是 UNIX System V,版本 4 (SVR4),发布于 1989 年。SysVinit init 程序将会读取 /etc/inittab 文件识别出系统到达默认状态(称为系统的默认运行级别)时可用的服务集合,以及为了到达该状态而应该执行的命令。使用 SysVinit 的系统通过 /etc/inittab 文件中的内容项定义这些信息,如 清单 1 中所示。
清单 1. /etc/inittab 中传统的启动相关命令
si::sysinit:/etc/init.d/rcSid:5:initdefault:l5:5:wait:/etc/init.d/rc 5
第一行确定初始化系统时系统应该执行的第一个脚本。第二行确定系统初始化后的默认运行级别。在这个例子中,默认的运行级别是 5,这通常意味着系统具有完整的网络与图形功能。第三行确定系统为了到达运行级别 5 而应该执行的命令。
在使用 SysVinit 时,/etc/init.d 目录包含用于启动和停止所有系统级进程的 shell 脚本。每种运行级别都有自己的目录,其中包含符号链接指向进入或离开相关运行级别时,应该启动或停止的、在 /etc/init.d 目录中选中的 shell 脚本。清单 1 中的第三行告诉 SysVinit 机制执行运行级别 5 相关目录中的脚本,根据您所运行的 Linux 版本,脚本通常为 /etc/rc5.d、/etc/init.d/rc5.d 或 /etc/rc.d/rc5.d。
运行级别目录中的符号链接以 S 或 K 字母开头,这表示它应该是在系统进入指定的运行级别时启动(S)相关系统进程,还是应该在系统离开该运行级别时终止(K)它。位于 S 或 K 之后的整数值定义了进入或离开运行级别时应该执行这些脚本的顺序。
在 /etc/init.d 目录中添加新脚本时,位于每个脚本开始部分的特定格式的注释会说明该脚本所依赖的其他所有脚本,同时说明与该脚本有关联的运行级别。这些脚本中应该出现的注释和其他信息被定义为 Linux Standard Base (LSB) 规范的组成部分,该规范由多个 Linux 发行方共同开发,以确保 SysVinit 脚本在不同 Linux 版本上的兼容性。清单 2 摘自 init 脚本中注释的 LSB 讨论,它给出了一个这样的例子。
清单 2. 传统 SysVinit 脚本中的注释块
### BEGIN INIT INFO# Provides: lsb-ourdb# Required-Start: $local_fs $network $remote_fs# Required-Stop: $local_fs $network $remote_fs# Default-Start: 2 3 4 5# Default-Stop: 0 1 6# Short-Description: start and stop OurDB# Description: OurDB is a very fast and reliable database# engine used for illustrating init scripts### END INIT INFO
表 1 对每一项的意义进行了说明。
表 1. SysVinit 脚本的 INIT INFO 区域中的注释
关键字 含义 Provides 这个初始化脚本提供的服务的逻辑名称。 Required-Start 成功启动此初始化脚本中定义的服务而必须运行的任何其他服务的逻辑名称。 Required-Stop 成功停止此初始化脚本中定义的服务而必须运行的任何其他服务的逻辑名称。 Default-Start 为了启动此初始化脚本定义的服务,执行该初始化脚本所需的运行级别。 Default-Stop 为了停止此初始化脚本定义的服务,执行该初始化脚本所需的运行级别。 Short-Description 与 Description 为了给与初始化脚本中命令相关联的服务提供简要说明。很多查询和概括 SysVinit 初始化脚本的系统实用工具都使用了这些关键字。
您可以使用 /sbin/service 命令来启动、停止与列出 SysVinit 脚本,也可以使用 /sbin/chkconfig 命令来列出或修改与脚本有关联的运行级别。
使用 shell 脚本启动系统可以很方便地给系统的启动过程增加新命令,或者修改启动或停止某项服务时发生的操作。也可以轻松在引导次序中的某个特定点增加新服务,只要创建到新服务的符号链接并在符号链接的名称中使用正确编号即可。
从性能的角度讲,SysVinit 按照指定的顺序执行多个 shell 脚本,从而到达指定的运行级别。执行 shell 脚本相对较慢,此外,因为无法利用可能的并行机制,这样的顺序启动机制原本需要的时间就很长。与目标运行级别相关联的每个 shell 脚本必须按顺序执行,而这种顺序是通过指向每个 shell 脚本的符号链接名称中的编号来指定的,其他服务只能等到当前服务完成后才能启动。因此,使用 SysVinit 的系统可以集成本文上一节(监控系统启动与脚本执行)中讨论的启动监控与配置工具,从而确定每个启动脚本所花费的具体时长。
了解和使用事件驱动的启动机制
尽管 SysVinit 易于使用和修改,因为它使用了 shell 脚本来启动与停止系统服务,但使用有序的 shell 脚本会让 SysVinit 变慢,并且不能并行启动不相关的服务。另一个问题是,启动与停止服务的脚本只在系统启动或关闭时才能执行,换句话说,系统运行级别的改变是它们惟一会响应的事件。很少有例外(比如由其他服务启动的服务),这意味着系统需要的所有服务必须一直运行,等待着可能永远不会来的请求。
无论从内存与处理器资源消耗方面,还是从首先启动它们所需的事件方面考虑,运行不使用的进程都是低效的。当今日益灵活的系统需要能够在各种环境中正确地、无缝地工作,比如网络连接从有线变为无线,从一个网络迁移到另一个网络,或者诸如增加和移除存储设备与其他外围设备这样的硬件变化。
事件驱动的机制正如其名所示,在系统发生特定事件时执行特定命令并启动相关服务。用于各种网络服务的 inetd 与 xinetd 守护进程对事件驱动的启动机制进行了很好地模拟,因为它们会等待某些事件发生(对它们所管理服务的网络连接请求),然后根据需要启动正确的服务。事件驱动的启动机制支持在响应事件时同时执行多个命令,并将同样的动态响应扩展到系统的运行时环境,从而最大程度地提高系统启动期间的并行化程度。事件实质上就是进程发送的字符串消息,用户可使用该消息作为对进程所监控状态出现变化的响应。一个进程发送一个事件消息。
最知名的事件驱动启动机制是 Upstart,它是很多 Linux 发行上使用的默认启动机制,包括 Ubuntu 9.10 及以上版本、RHEL6 及相关发行如 CentOS、Oracle Linux、Scientific Linux、Fedora 9-14 和其他众多 Linux 发行版本。Upstart 提供与 SysVinit 运行级别模型的向后兼容。
Upstart 使用任务配置(或 conf)文件来识别对事件的响应,比如启动、关闭、运行级别(运行级别变化)等。这些文件通常位于 /etc/init 目录中,但有些因为历史原因转移到了 /etc/event.d 目录中。您创建的任意新的系统级 conf 文件都应该位于 ./etc/init 目录中。Conf 文件的扩展名通常为 .conf。它们是文本文件,而且至少必须包含以下内容:
为了响应一个或多个事件,conf 文件应该执行某些操作。例如,start on startup 项说明在收到 startup 事件时应该执行一个任务文件,而 stop on runlevel 项说明在收到 runlevel 事件时应该停止一个任务文件,比如系统的运行级别改变时。
task 或 respawn 部分至少包含一个 exec 项,而 script 部分指定了响应触发此任务文件的事件时执行的命令。exec 项用于执行一个带有一组特定命令行参数的特定命令,通常为二进制命令。script 部分又叫作 stanza,它提供 shell 执行的命令,和其他 shell 脚本一样必须以 end script 语句结尾。Upstart 通过 task 和 respawn 关键字管理任务的两个概念性类:
任务必须完成,这意味着它们必须从停止状态
转变为启动状态,然后在完成之后返回到停止状态。 服务必须始终运行,因此只要从停止状态转变为启动状态。respawn 部分说明了如何启动和重启一项服务。
您还可以在 Upstart conf 文件的 task 部分使用其他关键字来识别输出设备,在执行主要的 exec 或 script 部分之前运行脚本,在完成主要的 exec 或 script 部分之后运行脚本,诸如此类。pre-start script 部分提供了一些命令来初始化 script 或 exec 命令所需的环境,而 post-stop script 部分则提供了一些命令,在 exec 命令或 script stanza 完成后进行清理或执行后处理。任务文件中还有其他许多可用的 Upstart 命令。
例如,清单 3 是一个 /etc/init/rcS.conf Upstart 任务文件,它模拟了 SysVinit 机制。它通过删除注释来提高代码可读性。
清单 3. /etc/init/rcS.conf Upstart 任务文件
start on runlevel [0123456]stop on runlevel [!$RUNLEVEL]taskexport RUNLEVELconsole outputexec /etc/rc.d/rc $RUNLEVEL
这个 conf 文件定义了一个任务,从与当前运行级别有关联的目录中运行所有现有的 SysVinit 脚本。
可以使用 /sbin/initctl 命令来启动、停止和列出 Upstart 任务。此命令可以显示当前正在运行的 Upstart 任务及其当前状态。在系统的初始化次序中添加一个新任务,只需要为该服务创建一个正确的 conf 文件,并在 /etc/init 目录中安装该文件。Upstart versions 1.3 及其更高版本甚至支持位于用户主目录的 .init 子目录中的用户特定的 conf 文件,但这个选项通常不受支持。
了解和使用 systemd
当 Upstart 似乎注定要在 Linux 系统中全面替代 SysVinit 的时候,另一种前途更耀眼、更闪耀和更高效的启动机制出现了:systemd (系统守护进程),最初由 Leonard Poettering 编写。systemd 系统启动机制通过了解各种服务需要和使用的基本资源,大大提高了并行化系统启动的性能。systemd 还使用了自从 2.6 内核以后的版本中都支持的控制组 (cgroups) 机制,让跟踪和管理相关进程的资源变得更容易。
大部分现代的 Linux 服务与相关客户端都使用了 UNIX 套接字来实现进程间通信,包括用于与一般硬件相关的、本地的应用间消息的 D-Bus 消息总线。当所需的套接字存在或 D-Bus 被激活时,就可以启动使用它们的任何服务,因此 systemd 首先创建与您想要在指定系统上启动的服务相关的所有套接字。使用这些资源的服务通常会受到阻塞,直到它们需要发送或接收消息为止,因此,要么并行地启动它们,要么根据传入的请求按需启动它们。
创建逻辑资源以启动可能并行使用这些资源的服务并不仅限于客户端与服务器。与随需文件系统访问机制(如 autofs)结合在一起之后,即使是系统启动过程中一般较慢的部分(比如文件系统一致性检查与挂载),也可以针对根文件系统(所有人都始终需要它)以外的文件系统使用这种模型。在结束文件系统挂载之后,就可以使用文件或文件系统更改事件来触发使用内核的 fanotify 与 fsnotify 机制的其他操作。
systemd 启动机制指的是作为单元 进行管理的所有内容。为了满足不同类型的初始化和启动需求,systemd 支持不同类型的单元,表 2 列出了其中最常用的一些单元。
表 2. systemd 的常用单元类型
单元类型 说明 automount 确定随需文件系统访问机制(如 autofs)使用的文件系统挂载点。每个 automount 单元都有一个相匹配的 mount 单元。 device 代表 udev 规则针对的物理设备。 mount 确定一个标准的文件系统挂载点。 service 定义一个可被启动、停止、重新启动、重新加载等的守护进程。因为这些是由 SysVinit 启动机制完成的传统类型的工作,systemd 可以自动解析 SysVinit 启动脚本中的 LSB 注释(在 了解和使用 SysVinit 一节中讨论过这一点),并正确使用这些信息。 socket 代表一个标准类型的文件系统或网络套接字,比如 AF_INET 或 AF_UNIX,这样通向这些套接字的传入连接就可以触发启动相关服务 target 定义一个用于对概念相关的其他单元进行分组的逻辑单元。例如,systemd 使用 graphical.target 单元来收集在图形设备变为可用时,应该在带有图形化控制台的系统上启动的所有应用程序。
与 systemd 支持的单元类型有关联的服务配置文件通常位于 /etc/systemd 的子目录中。清单 4 就是 systemd 服务配置文件的一个例子。
清单 4. 一个样例 systemd 服务配置文件
[Unit]Description=System Logging Service[Service]EnvironmentFile=-/etc/sysconfig/rsyslogExecStart=/sbin/rsyslogd -n $SYSLOGD_OPTIONSSockets=syslog.socketStandardOutput=null[Install]WantedBy=multi-user.targetAlias=syslog.service
这个文件是 Fedora 17 rsyslog.service 系统中系统日志的单元定义,位于 /etc/systemd/system/multi-user.target.wants 目录中。可以看出,这个文件的多处均描述了与它相关联的单元,说明如何启动服务以及它使用哪些资源,并说明在何种情况下调用单元—在这个例子中,是当系统的启动目标为 multi-user.target 时。
systemd 启动机制使用 systemctl 命令,在命令行中启动、停止与检查各类单元的状态。systemadm 命令功能相同,但具有图形化的界面,默认不安装。要添加此命令,必须安装 systemd-gtk 包。
systemd 启动机制可以带来实质性的性能提升,但如果需要在启动次序中的一个具体的确定点上添加命令,则需要更高的技巧。尽管 systemd 很值得一试,看它在特定系统上是否能提高启动性能,但并非所有人都喜欢它,因为 systemd 与以前的 Linux 启动机制有很大的不同。请参见 参考资料 中关于 systemd 的反面观点的链接。新方法固然有趣,但可能很快就变得很平常。
监控系统启动和脚本执行
如果要将系统变为可用的时间缩至最短,那么只是测量在按下电源后系统变为可用所花费的时间长短并不能提供很多有用的信息。您真正感兴趣的真实数据应该如下所示:
系统启动期间执行了哪些初始化命令或脚本? 这些命令或脚本按照什么顺序执行? 每个命令后脚本花费了多长时间?
如果要在系统的启动过程中添加一个脚本,您应该一开始就关注它是否确实得到了执行,以及它在什么点上被执行。确定这些信息的一种简便途径是在脚本中调用 /usr/bin/logger 命令。logger 命令使用指定的优先级将消息写入系统日志。例如,下面的命令使用与 alert 对应的数字优先级写入消息 New script executed,这种优先级的消息应该始终得到记录:
/usr/bin/logger -p 1 "New script executed"
验证新命令的执行是一个好习惯,但与分析所有启动脚本的相对性能和每个脚本执行时间的长短等相比,用处没那么大。作为一种极其方便的实用工具,Bootchart 以图形化的格式提供这类信息。
Bootchart 原本是一个 shell 脚本,但最新版本 Bootchart 2 使用 C 编程语言重新编写了它,所以它可以提供更高性能,并减少应用程序本身对其所收集的数据的影响。使用 Bootchart 2 最新版本的方法是,获取当前版本的源代码,编译并安装应用程序,然后将它集成到用于引导系统的引导装载程序命令中。要编译和安装 Bootchart 2,必须在系统上安装以下内容:git 源代码控制系统、make 应用程序编译工具和 gcc C 编译器。然后可以完成以下步骤:
使用 git 命令克隆 Bootchart 2 的源代码库:
git clone https://github.com/mmeeks/bootchart 将目录更改为在前一步骤中创建的 bootchart 目录,并执行 make 命令编译 Bootchart 2 的各个部分。 以 root 用户身份或通过 sudo 命令执行 make install 命令,在默认位置安装不同的 Bootchart 2 应用程序及其文档。
在目前正在使用的 bootloader 配置文件中 kernel 项的末尾处添加如下片段:
initcall_debug printk.time=y quiet init=/sbin/bootchartd
如果使用的是 GRUB bootloader,那么您很可能要在 /boot/grub/menu.lst 文件的 boot stanza 中添加前面的文本片段。如果使用的是 GRUB2 bootloader,那么您很可能要在 /boot/grub/grub.conf 文件的 boot stanza 中添加前面的文本片段。系统上的 GRUB 配置文件是使用命令自动生成的,比如 grub-mkconfig 或 grub2-mkconfig,您应该将前面的文本片段添加到 /etc/default/grub 文件中指定的默认内核引导选项,然后重新生成真正的 bootloader 配置文件。
下次重启系统时,Bootchart 2 会收集启动过程中每个阶段的相关数据,并采用 Portable Network Graphics (PNG) 格式,以图形化的方式对这些数据进行汇总。汇总数据保存在 /var/log/bootchart.tgz 文件中。这些数据的相关图形化表示保存在 /var/log/bootchart.png 文件中。图 1 是对启动过程的图形化表示的摘要。
图 1. Bootchart 的图形化启动表示摘要
在 Bootchart 2 的启动过程图形化视图上,顶部汇总了在系统上收集到的启动信息,包括启动过程所花费的总体时长。中部以图形化的方式展示所有启动进程(这里的 图 1 是一段摘要)。底部两个区域分别显示了进程的累计处理器使用情况和累计 I/O 使用情况。
每次重启系统时,所有以前叫这些名称的文件都将被覆盖。如果试验性地修改系统在启动期间执行各个脚本的顺序,或者尝试优化它们的内容,或者同时执行这两个操作,那么您很可能希望保留来自多次系统启动的摘要 Bootchart 2 数据。
Bootchart 2 在其配置文件(/etc/bootchartd.conf)中提供了 CUSTOM_POST_CMD 变量。这个变量的作用是在 Bootchart 2 创建了它的数据和相关图形文件后,指定到要执行的脚本或其他应用程序的完整路径。清单 5 是一个示例脚本,通过使用该脚本,可使用 YYYY-MM-DD-HH-MM-HOSTNAME 格式的名称重新命名输出文件,并将它们保存在 /var/log/bootchart 目录中。
清单 5. 对 Bootchart 输出文件进行惟一重命名的示例脚本
#!/bin/bashsource /etc/bootchartd.confHOST=$(hostname -s)if [ ! -d $(dirname $BOOTLOG_DEST)"/bootchart" ] ; then mkdir $(dirname $BOOTLOG_DEST)"/bootchart"fiDATE=$(date +%Y-%m-%d-%H-%M)filebase="$DATE-$HOST"mv $BOOTLOG_DEST $(dirname $BOOTLOG_DEST)"/bootchart/"${filebase}".tgz"if [ "x$AUTO_RENDER" = "xyes" ] ; then mv ${AUTO_RENDER_DIR}/bootchart.${AUTO_RENDER_FORMAT} \ $(dirname $BOOTLOG_DEST)"/bootchart/"${filebase}"."${AUTO_RENDER_FORMAT}fi
Bootchart 2 适用于本文中提到的所有 Linux 系统启动机制,而且它可以非常直观地展示系统启动过程中每个步骤所花费的时间、处理器和 I/O。这有助于找出启动过程中能够进行优化的地方,从而最大程度地缩短系统启动时间。
总结
Linux 提供多种启动机制,其中一些很容易修改,而其他一些是可扩展的,但主要是针对速度而设计的。本文总结了三种最流行的 Linux 启动机制,重点讲述了它们在目标与实现方面的差异。不同的 Linux 发行版本可开箱即用地使用不同的启动机制,安装它们并进行试验并不难,您最终可以找到在性能、灵活性和可用性最能满足自己要求的启动机制。