探索 Linux 系统的启动过程

引言##

之所以想到写这些东西,那是因为我确实想让大家也和我一样,把 Linux 桌面系统打造成真真正正日常使用的工具,而不是安装之后试用几把再删掉。我是真的在日常生活和工作中都使用 Linux,比如在 Linux 下编程、写博客、写论文和做幻灯。当然,对于 LibreOffice 这样的软件使用起来都不会有什么困难,所以在我的博客中就基本没有提到,就像 Windows 下的程序员不会去写 MS Office 的使用指南一样。如果有人不能坚持使用 Linux,那一定是 Linux 中的某些困难打败了他。刚使用 Linux 时确实会碰到很多困难,比如界面不够美观啊、字体不够顺眼啊、输入法太难用啊,还有就是想做某个工作找不到工具、想改一个属性找不到入口、系统崩溃了无法恢复等等,甚至是刚安装一个软件包或者下载一个文件,却不知道放到文件系统的什么地方了。所以,我的这一个系列就是在向大家展示我的方法论,用什么软件不重要,怎么用好也不重要,而是碰到问题了,用什么思路去找答案,到哪里去找答案。在折腾 Linux 的旅途中,了解 Linux 系统的启动过程也是很重要的一环。

关于 Linux 系统的启动过程也是几经变革。首先是操作系统从磁盘载入的过程发生了变化,很老旧的操作系统需要自己写启动扇区,现在的操作系统早就将这个过程交给了专门的启动工具。最常用的操作系统加载程序是 Grub,这个名字大家肯定早就如雷灌耳了,而且 Grub 也已经从第 1 版发展到了第 2 版,我们称之为 Grub2。 Grub2 和 Grub1 有很大的差别,网络上有很多讲 Linux 的文章动不动就拿 Grub1 说事儿,比如让大家修改 menu.lst 文件这样的,都是很过时的内容了。关于 Grub2 的详细资料可以阅读文档info grub或者info grub2。Grub 的优点是支持多操作系统的启动,Windows、Linux、BSD 都不在话下,但是它们的启动协议是不一样的,启动 Windows 靠的是 chainloader,启动 Linux 直接使用 linux 命令,而 BSD 系统好像是使用的 multiboot,特别是 Grub 的 multiboot 规范对那些很想自己写操作系统的程序员很有用,因为再也不用从启动扇区的代码开始写了,可以直接进入保护模式用 C 语言写操作系统。Linux 系统在启动的时候还有 initrd 或 initramfs 机制,这个也需要 Linux 用户特别注意。initrd 或 initramfs 也经历过一次技术革新,最开始采用的是 RamDisk 技术,目前采用的 tmpfs 技术。最后,内核加载入内存后,会将系统初始化的控制权交给 init 程序,而 init 程序也不是一成不变的,最开始使用的是 SysVinit,后来又改成 Upstart,现在最流行的又变成了 systemd。现在仍然有很多书和网文讲的还是 SysVinit 时代的做法,当然这些文章只能以 RedHat EL 5 以及之前的发行版作为示例,而对于最新的发行版,这些文章中讲的方法又会给新入门的用户造成困难。

探索 Linux 系统的启动过程是每一个 Linux 用户进步到一定程度后必然要去做的一件事。有的人可能只是好奇,也有的人可能确实需要从启动过程中去解决某一个特定的问题。这种探索我进行过多次,也早就想写一写,但是总是写不好。这一次是借着 systemd 普及的东风,让这个探索过程成为我这一系列的里程碑。为什么说是借着 systemd 普及的东风呢?因为听说本月即将发布的 Ubuntu 15.04 版本将会从 upstart 更换为 systemd,目前正在紧张地测试中。当然,一下子迁移过来对开发人员来讲还是有点难度的,但是只要确定目标,即使 Ubuntu 15.04 不使用 systemd,到 15.10 也肯定是它了。因此,对于我这个喜欢折腾多个 Linux 发行版的人来说,就不用多线作战了,只需要学好 systemd 即可。

Linux 启动过程概述##

这里先简单列一下 Linux 操作系统启动的全过程:

  1. 按下电脑的电源键后,电脑通电,BIOS 启动;
  2. BIOS 读取硬盘的 MBR,运行启动扇区中的代码,旧系统往往需要自己写启动扇区,而新系统基本上由专用的启动软件接管了,在 Linux 世界中,目前都是用的 Grub2。由于启动扇区空间太小,放不下太复杂的代码逻辑,所以 Grub2 也使用了多阶段启动的策略;
  3. Grub2 负责将操作系统内核加载到内存,如果有必要,也会把 initramfs 文件加载到内存,然后将控制权交给内核;
  4. 内核进行初始化,内核的初始化过程结束后,就会把控制权交给/init程序,从此进入用户空间;
  5. 因为内核先是将 initramfs 文件挂在为根文件系统,所以刚开始运行的/init程序其实是 initramfs 文件中的,所以该文件需要的重要的初始化脚本、内核模块、配置文件等,都位于 initramfs 文件中,这也是为什么很多时候我们修改了某些配置文件后,需要先更新 initramfs 文件再重启操作系统才会生效;
  6. initramfs 文件中的/init程序负责挂载硬盘上的文件系统,然后再把根文件系统切换到硬盘上的根分区,再运行/sbin/init程序,这时所有程序、配置文件、脚本都是使用的硬盘上的了,当然,网络文件系统也是同理。可以看出 init 程序的运行也是一个分阶段的过程;
  7. /sbin/init程序负责系统的初始化、各种服务的运行、用户的登陆等等;
  8. 如果需要运行图形界面,则/sbin/init程序会运行 Display Manager,在 Fedora 中是 gdm,在 Ubuntu 中是 lightdm。然后 Display Manager 负责启动整个图形界面。

Grub 加载 Linux 内核和 initramfs##

研究 Linux 启动过程的第一站就是要研究 Grub。大部分时候我们不用考虑 Grub 的安装,因为安装 Linux 桌面系统的时候已经帮我们自动安装好了,我们要做的,最多就是改改 Grub 的配置文件。只有在一种情况下需要手动安装 Grub,那就是在系统崩溃后急救的时候,当然,需要先运行一个光盘版或 U 盘版的 Linux 系统,然后运行grub-install命令,这个命令需要指定将 Grub 安装到那个硬盘的 MBR,具体操作看文档即可。

Grub2 和 Grub 相比,有了很多改进,改进到 Grub2 都相当于一个小型的操作系统了,比如 Grub2 能识别各种格式的硬盘分区及文件系统,能识别各种格式的图像,甚至还能动态加载模块。其中最重要的改变是配置文件由 Grub1 时代的/boot/menu.lst文件变成 Grub2 时代的/boot/grub/grub.cfg文件了(注意,有的系统中是/boot/grub2/grub.cfg,比如 Fedora),配置文件的语法也变复杂了很多。虽然 Grub2 配置文件的语法比较复杂,但是我们一般不需要直接修改该文件,而是修改/etc/default/grub文件后,再使用update-grub命令自动生成配置文件。当然,要完成一些简单任务时对/boot/grub/grub.cfg文件进行直接修改也是可以的,只是一旦系统更新,则对该文件的修改有可能被覆盖。

Grub 最重要的任务就是加载 Linux 内核以及 initramfs 文件了,这几个命令是非常简单易懂的,在/boot/grub/grub.cfg文件中也非常容易找到,如下图:

探索 Linux 系统的启动过程

上图主要是展示 Grub 载入 Linux 内核并且给 Linux 内核传递参数的过程,同时展示载入 initramfs 文件的过程。

探索 initramfs 文件的方法##

Grub 将 Linux 内核和 initramfs 文件载入内存后,就把控制权交给内核,内核会将内存中的 initramfs 文件挂载为根文件系统,当内核初始化完成后需要进入用户空间时,会运行/init,其实这个/init是在 initramfs 文件中的,所以如果想知道系统启动后都干了些什么,研究一下 initramfs 文件中的内容是有必要的。

/boot目录下,一般都有一个/boot/initrd.img文件或一个/boot/initramfs.img文件,但是要看里面的内容还得有点技巧。先得学习一下历史。第一个问题就是为什么要有 initrd 或者 initramfs,答案是这样的,为了减小 Linux 内核的大小,有一些驱动并没有编译进内核,而是以模块的形式存在于文件系统中,但是有些文件系统呢又需要内核加载了相应的驱动模块才能读取,这样就形成了一个是先有鸡还是先有蛋这样的一个矛盾的问题,为了解决这个问题,就是把这些驱动模块放到 initrd 或 initramfs 文件中,由启动器将其载入内存,然后内核加载内存中的驱动模块,再驱动其它的文件系统,然后进行更多的工作。同时,对于很多嵌入式操作系统而言,可能载入内存的 initrd 或 initramfs 就是最终的文件系统。第二个问题就是 initrd 文件和 initramfs 文件是什么格式的,怎么创建和打开。这个问题有点复杂,且看我下面细细道来。

其实在 Linux 的历史中,内存中的文件系统使用的技术还不一样。在很老的系统中,是将某一块内存模拟成磁盘,称之为 RamDisk,这个技术效率不高,为什么不高呢,那是因为将内存模拟成磁盘后,依然需要像对待磁盘一样对待它,需要给它创建特定格式的文件系统,读取或写入文件的时候还是要用到内核的缓存系统,这样同一份数据就在内存中存在了两份,所以很浪费。老系统中使用的 initrd 文件其实就相当于是一个磁盘的镜像,所以要读取或写入它的内容,就需要将它挂在到系统中,然后进行读写。

现在我们用的都是新系统,initrd 已经被 initramfs 取代了,虽然很多系统中仍然用initrd.img作为文件名,其实使用的是 initramfs 技术。initramfs 技术不使用 RamDisk,而是使用 tempfs,也就是复用了内核中的缓存系统,所以 tempfs 中的内容在内存中只存在一份,那就是在内核的缓存中,不仅节约了内存开销,也提高了效率。那么 initramfs 文件是什么格式呢?它就是经过压缩的 CPIO 格式,只要会cpio,打开它就不是问题。

其实也不是完全没有问题,我在研究 Fedora 21 的 initramfs.img 文件时就碰到了困难。如果大家使用cat initramfs.img | cpio -imd将该 CPIO 文件解开,会发现只有少许几个文件,文件数量和文件大小都远远低于我们的预期,如下图:

探索 Linux 系统的启动过程

问题究竟出在哪里呢?这是因为 Fedora 21 采用了最新的 Early User Space 技术。这个问题在网上还暂时搜不到答案,我也是看到解压出的文件中有一个early_cpio,然后以 early 为关键词去看内核的代码,才知道的。但是关于该技术的具体信息,我也没搞懂。不过这不是重点,重点是如何正确地将该文件解包。首先,我找到了 Fedora 21 系统中用来构建 initramfs 文件的工具是 dracut,采用的方法论请看我该系列的第一篇,如下图:

探索 Linux 系统的启动过程

然后,我发现 dracut 软件包中提供的lsinitrd工具可以查看 initramfs 中的内容,而且该工具是一个脚本。所以我就把它打开看了一下,如下图:

探索 Linux 系统的启动过程

在这个脚本中,我发现它需要用到一个skipcpio程序,跳过 initramfs 文件的头部后,再将剩下的部分当成压缩的 CPIO 文件进行解包。看来,会读脚本程序有时也很重要,所以我的《Bash脚本编程语言中的美学与哲学》这一篇没有白写。最后,解包 initramfs 文件的命令是这样的:

sudo /usr/lib/dracut/skipcpio initramfs-$(uname -r).img | zcat | cpio -imd

如下图:

探索 Linux 系统的启动过程

接下来,我们就可以愉快地研究/init程序都干些什么了。

基于 systemd 的 init 系统##

其实 systemd 在开源社区是有争论的,有很多人反对 systemd,反对的理由是 systemd 太复杂了,它干的事太多了,它除了是一个 init 程序外,同时还负责管理硬件、日志、定时任务等,甚至连用户登录都接管了。很多人仍为 Unix 世界的哲学是只做一件事并做好,对于 systemd 这种什么都要管庞大软件包,很多人是不喜欢的。systemd 的另外一个违反 Unix 哲学的地方就是它的日志系统使用了二进制格式,而不是 Unix 中广泛使用的纯文本格式。

但是我们不要被这些反对意见误导。systemd 真的是一个非常优秀的初始化系统,至少我是在使用过后就喜欢上了它。systemd 对传统所做出的每一个改变都是有道理的,即使是二进制格式的日志,也是为了更好地管理和解析日志,同时也提供了非常完善的查看日志的工具。Linux 之父也说他对 systemd 没有看法,他认为所谓的“只做一件事并做好”的哲学早已过时,该哲学只适合于那些可以通过流水线将各程序组合起来的场景,而现实中有很多复杂的应用场景不能通过流水线解决。不管怎么样,systemd 的使用是越来越普遍,Fedora、RedHat EL 7均以采用 systemd,Ubuntu 的下一个版本也将采用 systemd。所以,学习 systemd 是必须的。

学习 systemd 的最好的途径是查看它的文档man systemd,然后根据该文档的提示查看其它相应的文档,比如man systemd.serviceman systemd.target等等。要完整地讲述 systemd 估计都可以写一本书,我下面只说说它的一些基本概念。

系统初始化要做很多工作,如挂在文件系统,启动sshd服务,配置交换分区等,systemd 把每一个工作作为一个 unit,每一个 unit 对应一个配置文件。systemd 又将 unit 分成不同的类型,每一种类型的 unit 其配置文件具有不同的扩展名,常见的 unit 类型有:

  • service 对应一个后台服务进程,如 httpd、mysqld 等;
  • soket 对应一个套接字,之后对应到一个service,类似于xinetd的功能;
  • device 对应一个用 udev 规则标记的设备;
  • mount 对应系统中的一个挂载点,systemd 据此进行自动挂载,为了与 SysVinit 兼容,目前 systemd 自动处理 /etc/fstab 并转化为 mount;
  • automount 自动挂载点;
  • swap 配置交换分区;
  • target 配置单元的逻辑分组,包含多个相关的配置单元,可以当成是 SysVinit 中的运行级;
  • timer 定时器,用来定时触发用户定义的操作,它可以用来取代传统的 atd,crond 等;

systemd 的配置文件存放于/usr/lib/systemd/system目录中,initramfs 中的配置文件自然是位于相对于其解压目录的usr/lib/systemd/system目录中了。initramfs 中的/init程序以及硬盘上的/sbin/init程序都只是/usr/lib/systemd/systemd程序的链接而已。systemd 启动后,先根据default.target配置文件中的规则初始化系统。其实default.target也是一个链接,如果它链接到multi-user.target,则启动字符界面,如果它链接到graphical.target,则启动图形界面。这个和传统的 SysVinit 中的运行级别有一定的相似性。systemd 中的 target 很多,它和传统的 SysVinit 中的运行级别对应关系如下:

SysVinit中的运行级别 systemd中的 target 意义
0 runlevel0.target, poweroff.target 关闭系统。
1, s, single runlevel1.target, rescue.target 单用户模式。
2, 4 runlevel2.target, runlevel4.target, multi-user.target 用户定义/域特定运行级别。默认等同于 3。
3 runlevel3.target, multi-user.target 多用户,非图形化。用户可以通过多个控制台或网络登录。
5 runlevel5.target, graphical.target 多用户,图形化。通常为所有运行级别 3 的服务外加图形化登录。
6 runlevel6.target, reboot.target 重启。
emergency emergency.target 紧急 Shell。

一个 target 会依赖其它的 unit,被依赖的 unit 又会依赖更多的 unit,系统启动时这些 unit 都会同时被运行,和拔出萝卜带出泥的原理是一样的,而且 systemd 是并行启动各个 unit,这样可以加快系统启动的速度。每一个 unit 的配置文件都是纯文本的,语法格式极其简单,可以打开直接阅读。这些配置的细节我就不讲了,文档里面都写得很清楚。对于用户来讲,只要会使用 systemd 的管理工具systemctl命令就可以解决大部分的问题。

我们经常和系统启动打交道的地方是设置某些服务在系统启动时自动运行。回忆一下 RedHat EL 5 之前的系统,要让某个服务在系统启动时运行,我们需要把一些脚本文件放到/etc/rc.d/init.d目录中,然后在相应的/etc/rc.d/rcX.d目录中建立符号链接,并且这些脚本的命名还有讲究。同时,RedHat EL 5 之前的系统还提供了几个工具来简化我们的工作,它们是service命令和chkconfig命令。在现在的新系统中,这几个命令就只能成为回忆了,systemctl命令接管了这些工作。如下表:

SysVinit 命令 Systemd 命令 意义
service foo start systemctl start foo.service 用来启动一个服务 (并不会重启现有的)。
service foo stop systemctl stop foo.service 用来停止一个服务 (并不会重启现有的)。
service foo restart systemctl restart foo.service 用来停止并启动一个服务。
service foo reload systemctl reload foo.service 当支持时,重新装载配置文件而不中断等待操作。
service foo condrestart systemctl condrestart foo.service 如果服务正在运行那么重启它。
service foo status systemctl status foo.service 汇报服务是否正在运行。
ls /etc/rc.d/init.d/ systemctl list-unit-files --type=service 用来列出可以启动或停止的服务列表。
chkconfig foo on systemctl enable foo.service 在下次启动时或满足其他触发条件时设置服务为启用。
chkconfig foo off systemctl disable foo.service 在下次启动时或满足其他触发条件时设置服务为禁用。
chkconfig foo systemctl is-enabled foo.service 用来检查一个服务在当前环境下被配置为启用还是禁用。
chkconfig –list systemctl list-unit-files --type=service 输出在各个运行级别下服务的启用和禁用情况。
chkconfig foo –list ls /etc/systemd/system/*.wants/foo.service 用来列出该服务在哪些运行级别下启用和禁用。
chkconfig foo –add systemctl daemon-reload 当您创建新服务文件或者变更设置时使用。
telinit 3 systemctl isolate multi-user.target 改变至多用户运行级别。

其实systemctl命令也只是把相应的*.service文件放到相应的目录中而已,这也说明,要使用 systemd,并不只是将 SysVinit 或者 upstart 替换成 systemd 就可以了的,所有的其它的和服务相关的软件包,都需要针对 systemd 进行修改。所以,Ubuntu 要走的路还有点长。

在使用了 systemd 的系统中,电源管理也是由 systemd 接管的。之所以提到电源管理,是因为我在笔记本上使用 Linux 时碰到的一个非常奇葩的问题,那就是在我的笔记本电脑上,进入桌面后,总是提示“无线网卡已通过硬件禁用”,只有先将系统挂起,然后再唤醒,无线网卡才能启用。systemd 电源管理的命令如下:

命令 操作
systemctl reboot 重启机器
systemctl poweroff 关机
systemctl suspend 挂起
systemctl hibernate 休眠
systemctl hybrid-sleep 混合休眠模式(同时休眠到硬盘并挂起)

最后补充一下,systemd 自带日志服务 journald,并且用二进制格式保存所有日志信息,用户使用journalctl命令来查看日志信息。

进入图形界面##

如果作为服务器运行,Linux 系统的启动就仅仅只包含以上过程。如果是使用 Linux 桌面系统,最后还需要进入图形界面。进入图形界面的工作是由显示管理器来完成的,在 Fedora 中,显示管理器程序是 gdm,在 Ubuntu中,是 lightdm。显示管理器负责启动 X Server 并显示登录界面,用户登录后,显示管理器启动 gnome-session 从而进入整个桌面环境。关于 X Window 方面的内容,可以看我写的这一篇《X Window的奥秘》。最后要说一下图形界面的新进展,那就是 Wayland 将要取代 Xorg 成为新的图形技术。关于 Wayland 的优点,请看这两篇《揭开Wayland的面纱(一):X Window的前生今世》《 揭开Wayland的面纱(二):Wayland应运而生》。这两篇文章发布于2010年,现在已经是2015年了,但是 Wayland 还是没有普及。不过在 Fedora 21 中,已经可以试用 Wayland 了,我试用了一下,貌似还不太稳定。

好了,关于 Linux 系统启动过程的东西就写这么多吧。从以上内容可以看到,Linux 中的技术也是一直不断在进步,我们需要拥抱变化,而不能墨守成规。很多时候,图书和网络教程并不能跟上最新的进展,这是就需要我们自己去思考和探索,因此,掌握正确的方法论尤其重要。

(京山游侠于2015-04-10发布于博客园,转载请注明出处。)

对于现在最新的 init 程序 systemd,很多人可能非常想了解 systemd 在系统启动时启动了哪些服务?这些服务的启动顺序如何?每个服务启动花费了多少时间?如果单纯去查看 systemd 的文档和各个服务的脚本,是非常难以搞清楚以上问题的。幸好 systemd 提供了一个非常好的分析工具 systemd-analyze,它可以帮我们分析各个复位启动的顺序、时间等。这样运行该程序

systemd-analyze plot > init.svg
`` 就可以以图形化的形式展示各服务启动的时间轴了,如下图:
![](http://images2015.cnblogs.com/blog/16576/201610/16576-20161010104459774-285353503.png) 当然,systemd-analyze 还有其它的输出方式,请自行查看它的文档。 (京山游侠于2016-10-10更新于博客园,转载请注明出处。)
上一篇:请不要重复犯我在学习Python和Linux系统上的错误


下一篇:Linux系统中有趣的命令(可以玩小游戏)