第 A 章 Centos 6 开机流程(BIOS+MBR+SysV)

作者: Brinnatt 分类: ARM64 Linux 补充知识 发布时间: 2022-01-21 20:40

国内 ARM64 生态圈发展比较晚,所以一开始使用的就是新形态,UEFI + GPT 方式引导操作系统;而 ARM64 架构使用的操作系统从 Centos 7+ 开始支持。这里之所以介绍传统的操作系统引导方式,是因为 BIOS+MBR+SysV 这套方式在历史上存在了很长一段时间,几乎无处不在。当下国内正处于从 X86 到 ARM64 的过度期,必须要了解一下历史。

A1、BIOS 加电自检

计算机启动分为内核加载前、加载时和加载后 3 个大阶段;按下电源,计算机开始通电,最重要的是要接通 cpu 的电路,然后通过 cpu 的针脚让 cpu 运行起来,只有 cpu 运行起来才能执行相关代码跳到 bios。

bios 是按下开机键后第一个运行的程序,它会读取 CMOS 中的信息,以了解部分硬件的信息,比如硬件自检(post)、硬件上的时间、硬盘大小和型号等。

其实,手动进入 bios 界面看到的信息,都是在这一阶段获取到的,如下图。对本文来说,最重要的还是获取到了启动设备以及它们的启动顺序(顺序从上到下)信息。

PhoenixBIOS

当硬件检测和信息获取完毕,开始初始化硬件,最后从排在第一位的启动设备中读取 MBR,如果第一个启动设备中没有找到合理的 MBR,则继续从第二个启动设备中查找,直到找到正确的 MBR。

A2、MBR bootloader 阶段

MBR 是主引导记录,位于磁盘的第一个扇区,和分区无关,和操作系统无关,Bios 一定会读取 MBR 中的记录。

在 MBR 中存储了 bootloader/分区表/BRID。

  • bootloader 占用 446 个字节,用于引导加载;
  • 分区表占用 64 个字节,每个主分区或扩展分区占用 16 个字节,如果 16 个字节中的第一个字节为 0x80,则表示该分区为激活的分区(活动分区),且只允许有一个激活的分区;
  • 最后 2 个字节是 BRID(boot record ID),它固定为 0x55AA,用于标识该存储设备的 MBR 是否是合理有效的 MBR,如果 bios 读取 MBR 发现最后两个字节不是 0x55AA,就会读取下一个启动设备。

A2.1、boot loader

MBR 中的 bootloader 只占用 446 字节,所以可存储的代码有限,能加载引导的东西也有限,所以在磁盘的不同位置上设计了多种 boot loader。下面将说明各种情况。

在创建文件系统时,是否还记得有些分区的第一个 block 是 boot sector?这个启动扇区中也放了 boot loader,大小也很有限。

如果是主分区上的 boot sector,则该段 boot loader 所在扇区称为 VBR(volumn boot record),如果是逻辑分区上的 boot sector,则该段 boot loader 所在扇区称为 EBR(Extended boot sector)。

但很不幸,这两种方式的 boot loader 都很少被使用上了,因为它们很不方便,加上后面出现了启动管理器(LILO 和 GRUB),它们就被遗忘了。但即使如此,在分区中还是存在 boot sector。

A2.2、分区表

硬盘分区的好处之一就是可以在不同的分区中安装不同的操作系统,但 boot loader 必须要知道每个操作系统具体是在哪个分区。

分区表的长度只有 64 个字节,里面又分成四项,每项 16 个字节。所以,一个硬盘最多只能分四个主分区。

每个主分区表项的 16 个字节,都由 6 个部分组成:

  1. 第 1 个字节:只能为 0 或者 0x80。0x80 表示该主分区是激活分区,0 表示非激活分区。单磁盘只能有一个主分区是激活的。
  2. 第 2-4 个字节:主分区第一个扇区的物理位置(柱面、磁头、扇区号等等)。
  3. 第 5 个字节:主分区类型。
  4. 第 6-8 个字节:主分区最后一个扇区的物理位置。
  5. 第 9-12 字节:该主分区第一个扇区的逻辑地址。
  6. 第 13-16 字节:主分区的扇区总数。

最后的四个字节 主分区的扇区总数,决定了这个主分区的长度。也就是说,一个主分区的扇区总数最多不超过 2 的 32 次方。如果每个扇区为 512 个字节,就意味着单个分区最大不超过 2TB。

A2.3、VBR/EBR 方式引导操作系统

当 bios 读取到 MBR 中的 boot loader 后,会继续读取分区表。分两种情况:

  1. 如果查找分区表时发现某个主分区表的第一个字节是 0x80,也就是激活的分区,那么说明操作系统装在了该主分区,然后执行已载入的 MBR 中的 boot loader 代码,加载该激活主分区的 VBR 中的 boot loader,至此,控制权就交给了 VBR 的 boot loader 了;
  2. 如果操作系统不是装在主分区,那么肯定是装在逻辑分区中,所以查找完主分区表后会继续查找扩展分区表,直到找到 EBR 所在的分区,然后 MBR 中的 boot loader 将控制权交给该 EBR 的 boot loader。

也就是说,如果一块硬盘上装了多个操作系统,那么 boot loader 会分布在多个地方,可能是 VBR,也可能是 EBR,但 MBR 是一定有的,这是被 bios 给绑定了的。

在装 LINUX 操作系统时,其中有一个步骤就是询问你 MBR 装在哪里的,但这个 MBR 并非一定真的是 MBR,可能是 MBR,也可能是 VBR,还可能是 EBR,并且想要单磁盘多系统共存,则 MBR 一定不能被覆盖(此处不考虑grub)。

如下图,是测试单磁盘装 3 个操作系统时的分区结构。其中 /dev/sda{1,2,3} 是第一个 CentOS 6 系统,/dev/sda{5,6,7} 是第二个 CentOS 7 系统,/dev/sda{8,9,10} 是第三个 CentOS 6 系统,每一个操作系统的分区序号从前向后都是 /boot 分区、根分区、swap 分区。

three system

再看下图,是装第三个操作系统时的询问 boot loader 安装位置的步骤。

where boot loader

装第一个操作系统时,boot loader 可以装在 /dev/sda 上,也可以选择装在 /dev/sda1 上,这时装的是 MBR 和 VBR,任选一个都会将另一个也装上,从第二个操作系统开始,装的是 EBR 而非 MBR,且应该指定 boot loader 位置(如 /dev/sda5 和 /dev/sda8),否则默认选项是装在 /dev/sda 上,但这会覆盖原有的 MBR。

另外,在指定 boot loader 安装路径的下方,还有一个方框是操作系统列表,这就是操作系统菜单,其中可以指定默认的操作系统,这里的默认指的是 MBR 默认跳转到哪个 VBR 或 EBR 上。

所以,MBR/VBR 和 EBR 之间的跳转关系如下图。

Boot sector

使用这种方式的菜单管理操作系统启动,无需什么 stage1,stage1.5 和 stage2 的概念,只要跳转到了分区上的 VBR 或 EBR,那么直接就可以加载引导该分区上的操作系统。

但是,这种管理操作系统启动的菜单已经没有意义了,现在都是使用 grub 来管理,所以装第二个操作系统或第 n 个操作系统时不手动指定 boot loader 安装位置,覆盖掉 MBR 也无所谓,想要实现单磁盘多系统共存所需要做的,仅仅只是修改 grub 的配置文件而已。

使用 grub 管理引导菜单时,VBR/EBR 就毫无用处了,具体的见下文。

A3、grub 阶段

使用 grub 管理启动,则 MBR 中的 boot loader 是由 grub 程序安装的,此外还会安装其他的 boot loader。CentOS 6 使用的是传统的 grub,而 CentOS 7 使用的是 grub2。

如果使用的是传统的 grub,则安装的 boot loader 为 stage1、stage1_5 和 stage2;如果使用的是 grub2,则安装的是 boot.img 和 core.img。传统 grub 和 grub2 的区别还是挺大的,所以下面分开解释。

A3.1、使用 grub2 时的启动过程

grub2 程序安装 grub 后,会在 /boot/grub2/i386-pc/ 目录下生成 boot.img 和 core.img 文件,另外还有一些模块文件,其中包括文件系统类的模块。

[root@mysql ~]# find /boot/grub2/i386-pc/ -name "*.img" -o -name "*fs.mod" -o -name "*ext[0-9].mod"
/boot/grub2/i386-pc/affs.mod
/boot/grub2/i386-pc/afs.mod
/boot/grub2/i386-pc/bfs.mod
/boot/grub2/i386-pc/btrfs.mod
/boot/grub2/i386-pc/cbfs.mod
/boot/grub2/i386-pc/hfs.mod
/boot/grub2/i386-pc/jfs.mod
/boot/grub2/i386-pc/ext2.mod        # ext2、ext3和ext4都使用该模块
/boot/grub2/i386-pc/procfs.mod
/boot/grub2/i386-pc/ntfs.mod
/boot/grub2/i386-pc/reiserfs.mod
/boot/grub2/i386-pc/romfs.mod
/boot/grub2/i386-pc/sfs.mod
/boot/grub2/i386-pc/xfs.mod
/boot/grub2/i386-pc/zfs.mod
/boot/grub2/i386-pc/core.img        # 在这里
/boot/grub2/i386-pc/boot.img        # 在这里
[root@mysql ~]#

其中 boot.img 就是安装在 MBR 中的 boot loader。当然,它们的内容是不一样的,安装 boot loader 时 grub2-install 会将 boot.img 转换为合适的汇编代码写入 MBR 中的 boot loader 部分。

core.img 是第二段 Boot loader 段,grub2-install 会将 core.img 转换为合适的汇编代码写入到紧跟在 MBR 后面的空间,这段空间是 MBR 之后、第一个分区之前的空闲空间,被称为 MBR gap,这段空间最小 31KB,但一般都会是 1MB 左右。

实际上,core.img 是多个 img 文件的结合体。它们的关系如下图:

boot-core

这张图解释了开机过程中 grub2 阶段的所有过程,boot.img 段的 boot loader 只有一个作用,就是跳转到 core.img 对应的 boot loader 的第一个扇区,对于从硬盘启动的系统来说,该扇区是 diskboot.img 的内容, diskboot.img 的作用是加载 core.img 中剩余的内容。

由于 diskboot.img 所在的位置是以硬编码的方式写入到 boot.img 中的,所以 boot.img 总能找到 core.img 中 diskboot.img 的位置并跳转到它身上,随后控制权交给 diskboot.img。

随后 diskboot.img 加载压缩后的 kernel.img(注意,是 grub 的 kernel 不是操作系统的 kernel) 以初始化 grub 运行时的各种环境,控制权交给 kernel.img。

但直到目前为止,core.img 都还不识别 /boot 所在分区的文件系统,所以 kernel.img 初始化 grub 环境的过程就包括了加载模块,严格地说不是加载,因为在安装 grub 时,文件系统类的模块已经嵌入到了 core.img 中,例如 ext 类的文件系统模块 ext2.mod。

加载了模块后,kernel.img 就能识别 /boot 分区的文件系统,也就能找到 grub 的配置文件 /boot/grub2/grub.cfg,有了 grub.cfg 就能显示启动菜单,我们就能自由的选择要启动的操作系统。

当选择某个菜单项后,kernel.img 会根据 grub.cfg 中的配置加载对应的操作系统内核(/boot 目录下 vmlinuz 开头的文件),并向操作系统内核传递启动时参数,包括根文件系统所在的分区,init ramdisk(即 initrd 或 initramfs)的路径。例如下面是某个菜单项的配置:

menuentry 'CentOS Linux (3.10.0-862.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-862.el7.x86_64-advanced-b9d8b495-fdfb-445c-977b-2ffca891468e' {
        load_video
        set gfxpayload=keep
        insmod gzio
        insmod part_msdos
        insmod xfs
        set root='hd0,msdos1'
        if [ x$feature_platform_search_hint = xy ]; then
          search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint='hd0,msdos1'  88ef58d5-5609-4a4f-a20e-df6a57479dfa
        else
          search --no-floppy --fs-uuid --set=root 88ef58d5-5609-4a4f-a20e-df6a57479dfa
        fi
        linux16 /vmlinuz-3.10.0-862.el7.x86_64 root=UUID=b9d8b495-fdfb-445c-977b-2ffca891468e ro rhgb quiet LANG=en_US.UTF-8
        initrd16 /initramfs-3.10.0-862.el7.x86_64.img
}

加载完操作系统内核后 grub2 就将控制权交给操作系统内核。

总结下,从 MBR 开始后的过程是这样的:

  1. 执行 MBR 中的 boot loader(即 boot.img) 跳转到 diskboot.img。
  2. 执行 diskboot.img,加载 core.img 剩余的部分,并跳转到 kernel.img。
  3. kernel.img 读取 /boot/grub2/grub2.cfg,并显示启动管理菜单。
  4. 选中某菜单后,kernel.img 加载该菜单项配置的操作系统内核 /boot/vmlinux-XXX,并传递内核启动参数,包括根文件系统所在分区和 init ramdisk 的路径。
  5. 控制权交给操作系统内核。

A3.2、使用传统 grub 时的启动过程

传统 grub 对应的 boot loader 是 stage1 和 stage2,从 stage1 跳转到 stage2 大多数情况下还会用到 stage1_5 对应的 boot loader。

与 grub2 相比,stage1 和 boot.img 的作用是类似的,都在 MBR 中。当该段 boot loader 执行后,它的目的是跳转到 stage1_5 的第一个扇区上,然后由该扇区的代码加载剩余的内容,并跳转到 stage2 的第一个扇区上。

stage1_5 存在的理由是因为 stage2 功能较多,导致其文件体积较大(一般至少都有 100 多 K),所以并没有像 core.img 一样嵌入到磁盘上,而是简单地将其放在了 boot 分区上,但 stage1 并不识别 boot 分区的文件系统类型,所以借助中间的辅助 boot loader 即 stage1_5 来跳转。

stage1_5 的目的之一是识别文件系统,但文件系统的类型有很多,所以对应的 stage1_5 也有很多种。

[root@localhost ~]# ls -C /boot/grub/*stage1_5*
/boot/grub/e2fs_stage1_5     /boot/grub/jfs_stage1_5       /boot/grub/vstafs_stage1_5
/boot/grub/fat_stage1_5      /boot/grub/minix_stage1_5     /boot/grub/xfs_stage1_5
/boot/grub/ffs_stage1_5      /boot/grub/reiserfs_stage1_5
/boot/grub/iso9660_stage1_5  /boot/grub/ufs2_stage1_5
[root@localhost ~]#

虽然有很多种 stage1_5,但每个 boot 分区也只能对应一种 stage1_5。这个 stage1_5 对应的 boot loader 一般会被嵌入到 MBR 后、第一个分区前的中间那段空间(即 MBR gap)。

当执行了 stage1_5 对应的 boot loader 后 ,stage1_5 就能识别出 boot 所在的分区,并找到 stage2 文件的第一个扇区,然后跳转过去。

当控制权交给了 stage2,stage2 就能加载 grub 的配置文件 /boot/grub/grub.conf 并显示菜单并初始化 grub 的运行时环境,当选中操作系统后,stage2 将和 kernel.img 一样加载操作系统内核,传递内核启动参数,并将控制权交给操作系统内核。

所以,stage1、stage1_5 和 stage2 之间的关系如下图:

boot stage

虽然绝大多数都提供了 stage1_5,但它不是必须的,它的作用仅仅只是识别 boot 分区的文件系统类型,对于一个会编程的人来说,可以将固定 boot 分区的文件系统识别代码嵌入到 stage1 中,这样 stage1 自身就能识别 boot 分区,就不需要 stage1_5 了。

看看安装 grub 时,grub 到底做了些什么工作。

grub> setup (hd0)
 Checking if "/boot/grub/stage1" exists... yes
 Checking if "/boot/grub/stage2" exists... yes
 Checking if "/boot/grub/e2fs_stage1_5" exists... yes
 Running "embed /boot/grub/e2fs_stage1_5 (hd0)"...  15 sectors are embedded.
succeeded
 Running "install /boot/grub/stage1 (hd0) (hd0)1+15 p (hd0,0)/boot/grub/stage2 /boot/grub/menu.lst"... succeeded
Done.
  • 首先检测各 stage 文件是否存在于 /boot/grub 目录下,随后嵌入 stage1_5 到磁盘上,该文件系统类型的 stage1_5 占用了 15 个扇区,最后安装 stage1,并告知 stage1 stage1_5 的位置是第 1 到第 15 个扇区,之所以先嵌入 stage1_5 再嵌入 stage1 就是为了让 stage1 知道 stage1_5 的位置,最后还告知了 stage1 stage2 和配置文件 menu.lst(它是 grub.conf 的软链接)的路径。

A4、内核加载阶段

提前说明,下文所述均为 sysV init 系统启动风格,systemd 的启动管理方式大不相同,所以不要将 systemd 管理的启动方式与此做比较。

到目前为止,内核已经被加载到内存掌握了控制权,且收到了 boot loader 最后传递的内核启动参数以及 init ramdisk 的路径。

所有的内核都是以 bzImage 方式压缩过的,压缩后 CentOS 6 的内核大小大约为 4M,CentOS 7 的内核大小大约为 5M。内核要能正常运作下去,它需要进行解压释放。

解压释放之后,将创建 pid 为 0 的 idle 进程,该进程非常重要,后续内核所有的进程都是通过 fork 它创建的,且很多 cpu 降温工具就是强制执行 idle 进程来实现的。

然后创建 pid=1 和 pid=2 的内核进程。pid=1 的进程也就是 init 进程,pid=2 的进程是 kthread 内核线程,它的作用是在真正调用 init 程序之前完成内核环境初始化和设置工作,例如根据 grub 传递的内核启动参数找到 init ramdisk 并加载。

所谓的救援模式就是刚加载完内核,init 进程接收到控制权的那一阶段,因为没有进行任何操作系统初始化过程,所以可以修复和操作系统相关的很多问题。

另外,安装镜像中也有内核,可以通过安装镜像进入救援模式,这种进入救援模式的方式几乎可修复任何操作系统启动相关的问题,即使是 /boot 目录下内核镜像缺失都可以重装。(还有一种单用户模式,它是运行级别为 1 的环境,所以已经初始化完运行级别,见后文)

A4.1、加载 init ramdisk

在前面,已经创建了 pid=1 的 init 进程和 pid=2 的 kthread 进程,但注意,它们都是内核线程,全称应该是 kernel_init 和 kernel_kthread,而真正能被 ps 捕获到的 pid=1 的 init 进程是由 kernel_init 调用 init 程序后形成的。

要加载 /sbin/init 程序,首先要找到根分区,根分区是有文件系统的,所以内核需要先识别文件系统并加载文件系统的驱动,但文件系统的驱动又是放在根分区的,这就出现了先有鸡还是先有蛋的矛盾。

  • 解决的方法之一是像 grub2 识别 boot 分区的文件系统一样,将根文件系统驱动模块嵌入到内核中,但文件系统的种类太多,而且会升级,这样就导致内核不断的嵌入新的文件系统驱动模块,内核不断增大,这显然是不合适的。

  • 解决方法之二则像传统 grub 借助中间过渡引导段 stage1_5 一样,将根文件系统的驱动模块放入一个中间过渡文件,在加载根文件系统之前先加载这个过渡文件,再由过渡文件跳转到根文件系统。

    • 方法二正是现在采用的,其采用的中间过渡文件称为 init ramdisk,它是在安装完操作系统时生成的,这样它会收集到当前操作系统的根文件系统是什么类型的文件系统,也就能只嵌入一个对应的文件系统驱动模块使其变得足够小。

    initramdisk

在 CentOS 5 上采用的 init ramdisk 称为 initrd,而 CentOS 6 和 CentOS 7 采用的则是 initramfs,它们的目的是一样的,但在实现上却大有不同。但它们都存放在 /boot 目录下。

[root@localhost ~]# ll -h /boot/init*
-rw-r--r--. 1 root root 15M Jul 29 05:28 /boot/initramfs-2.6.32-358.el6.x86_64.img
[root@localhost ~]#

可以看到,它们的大小有十多兆,由此也可知道 init ramdisk 的作用肯定不仅仅只是找到根文件系统,它还会做其他工作。具体还做什么工作,请继续阅读下文。

A4.2、initrd

initrd 其实是一个镜像文件系统,是在内存中划分一片区域模拟磁盘分区,在该文件中包含了找到根文件系统的脚本和驱动。

既然是文件系统,那么内核也必须要带有对应文件系统的驱动,另外文件系统要使用就必须有根 /,这个根是内存中的 虚根

由于内核加载到这里已经初始化一些运行环境了,所以内核的运行状态等参数也要保存下来,保存的位置就是内存中虚根下的 /proc 和 /sys,此外还有收集到的硬件设备信息以及设备的运行环境也要保存下来,保存的位置是 /dev。到此为止,pid=2 的内核线程 kernel_kthread 就完成了基本工作,开始转到 kernel_init 进程上了。

再之后就是 kernel_init 挂载真正的根文件系统并从虚根切换到实根,最后 kernel_init 将调用 init 程序,也就是真正的 pid=1 的 init 进程,然后将控制权交给 init,所以从现在开始,将切换到用户空间,后续剩余的事情都将由用户空间的程序完成。

以下是 CentOS 5.8 中 initrd 文件的解压过程和解包后的目录结构。

$ cp /boot/initrd-2.6.18-308.el5.img /tmp/initrd.gz
$ gunzip initrd.gz
$ cpio -id < initrd  
$ ls 
bin  dev  etc  init  initrd  lib  proc  sbin  sys  sysroot

A4.3、initramfs

initramfs 比 initrd 又先进了一些,initrd 必须是一个文件系统,是在内存中模拟出磁盘分区的,所以内核必须要带有它的文件系统驱动,而 initramfs 则仅仅只是一个镜像压缩文件而非文件系统,所以它不需要带文件系统驱动,在加载时,内核会将其解压的内容装入到一个 tmpfs 中。

initramfs 和 initrd 最大的区别在于 init 进程的区别对待。initramfs 为了尽早进入用户空间,它将 init 程序集成到了 initramfs 镜像文件中,这样就可以在 initramfs 装入 tmpfs 时直接运行 init 进程,而不用去找根文件系统下的 /sbin/init,由此挂载根文件系统的工作将由 init 来完成,而不再是内核线程 kernel_init 完成。最后从虚根切换到实根。

那根分区下的 /sbin/init 是干嘛的呢?可以认为是 init ramdisk 中 init 的一个备份,如果 ramdisk 中找不到 init 就会去找 /sbin/init。另外,在正常运行的操作系统环境下,/sbin/init 还经常用来完成其他工作,如发送信号。

其实 initramfs 完成了很多工作,解开它的镜像文件就能发现它的目录结构和真实环境下的目录结构类似。以下是 CentOS 7 上 initramfs-3.10.0-327.el7.x86_64 解包过程和解包后的目录结构。

[root@localhost ~]# cp /boot/initramfs-3.10.0-693.el7.x86_64.img /tmp/initramfs.gz
[root@localhost ~]# cd /tmp/;gunzip /tmp/initramfs.gz 
[root@localhost tmp]# ls
initramfs
[root@localhost tmp]# cpio -id < initramfs 
85469 blocks
[root@localhost tmp]# ls -l
total 42744
lrwxrwxrwx.  1 root root        7 Sep  2 05:44 bin -> usr/bin
drwxr-xr-x.  2 root root       45 Sep  2 05:44 dev
drwxr-xr-x. 12 root root     4096 Sep  2 05:44 etc
lrwxrwxrwx.  1 root root       23 Sep  2 05:44 init -> usr/lib/systemd/systemd
-rw-------.  1 root root 43760128 Sep  2 05:44 initramfs
lrwxrwxrwx.  1 root root        7 Sep  2 05:44 lib -> usr/lib
lrwxrwxrwx.  1 root root        9 Sep  2 05:44 lib64 -> usr/lib64
drwxr-xr-x.  2 root root        6 Sep  2 05:44 proc
drwxr-xr-x.  2 root root        6 Sep  2 05:44 root
drwxr-xr-x.  2 root root        6 Sep  2 05:44 run
lrwxrwxrwx.  1 root root        8 Sep  2 05:44 sbin -> usr/sbin
-rwxr-xr-x.  1 root root     3117 Sep  2 05:44 shutdown
drwxr-xr-x.  2 root root        6 Sep  2 05:44 sys
drwxr-xr-x.  2 root root        6 Sep  2 05:44 sysroot
drwxr-xr-x.  2 root root        6 Sep  2 05:44 tmp
drwxr-xr-x.  7 root root       66 Sep  2 05:44 usr
drwxr-xr-x.  2 root root       29 Sep  2 05:44 var
[root@localhost tmp]#

另外,还可以在其 sbin 目录下发现 init 程序。

[root@localhost tmp]# ll sbin/init
lrwxrwxrwx. 1 root root 22 Sep  2 05:44 sbin/init -> ../lib/systemd/systemd
[root@localhost tmp]#

A5、操作系统初始化

下文解释的是 sysV 风格的系统环境,与 systemd 初始化大不相同。

当 init 进程掌握控制权后,意味着已经进入了用户空间,后续的事情也将以用户空间为主导来完成。

init 的名称是 initialize 的缩写,是初始化的意思,所以它的作用也就是初始化的作用。在内核加载阶段,也有初始化动作,初始化的环境是内核的环境,是由 kernel_init、kernel_thread 等内核线程完成的。而 init 掌握控制权后,已经可以和用户空间交互,意味着真正的开始进入操作系统,所以它初始化的是操作系统的环境。

操作系统初始化涉及了不少过程,大致如下:读取运行级别;初始化系统类的环境;根据运行级别初始化用户类的环境;执行 rc.local 文件完成用户自定义开机要执行的命令;加载终端;

A5.1、运行级别

在 sysV 风格的系统下,使用了运行级别的概念,不同运行级别初始化不同的系统类环境,你可以认为 windows 的安全模式也是使用运行级别的一种产物。

在 Linux 系统中定义了 7 个运行级别,使用 0-6 的数字表示。

0:halt,即关机
1:单用户模式
2:不带NFS的多用户模式
3:完整多用户模式
4:保留未使用的级别
5:X11,即图形界面模式
6:reboot,即重启

实际上,执行关机或重启命令的本质就是向 init 进程传递 0 或 6 这两个运行级别。

sysV 的 init 程序读取 /etc/inittab 文件来获取默认的运行级别,并根据此文件所指定的配置执行默认运行级别对应的操作。注意,systemd 管理的系统是没有 /etc/inittab 文件的,即使有也仅仅只是出于提醒的目的,因为 systemd 没有了运行级别的概念。

CentOS 6.6 上该文件内容如下:

[root@localhost ~]# cat /etc/inittab 
# inittab is only used by upstart for the default runlevel.
#
# ADDING OTHER CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM.
#
# System initialization is started by /etc/init/rcS.conf
#
# Individual runlevels are started by /etc/init/rc.conf
#
# Ctrl-Alt-Delete is handled by /etc/init/control-alt-delete.conf
#
# Terminal gettys are handled by /etc/init/tty.conf and /etc/init/serial.conf,
# with configuration in /etc/sysconfig/init.
#
# For information on how to write upstart event handlers, or how
# upstart works, see init(5), init(8), and initctl(8).
#
# Default runlevel. The runlevels used are:
#   0 - halt (Do NOT set initdefault to this)
#   1 - Single user mode
#   2 - Multiuser, without NFS (The same as 3, if you do not have networking)
#   3 - Full multiuser mode
#   4 - unused
#   5 - X11
#   6 - reboot (Do NOT set initdefault to this)
# 
id:3:initdefault:
[root@localhost ~]#

该文件告诉我们,系统初始化过程由 /etc/init/rcS.conf 完成,运行级别类的初始化过程由 /etc/init.conf 来完成,按下 CTRL+ALT+DEL 键要执行的过程由 /etc/init/control-alt-delete.conf 来完成,终端加载的过程由 /etc/init/tty.conf 和 /etc/init/serial.conf 读取配置文件 /etc/sysconfig/init 来完成。文件最后,还有一行 id:3:initdefault,表示默认的运行级别为 3,即完整的多用户模式。

确认了要进入的运行级别后,init 将先读取 /etc/init/rcS.conf 来完成系统环境类初始化动作,再读取 /etc/init/rc.conf 来完成运行级别类动作。

A5.2、系统环境初始化

先看看 /etc/init/rcS.conf 文件的内容。

[root@localhost ~]# cat /etc/init/rcS.conf 
# rcS - runlevel compatibility
#
# This task runs the old sysv-rc startup scripts.

start on startup

stop on runlevel

task

# Note: there can be no previous runlevel here, if we have one it's bad
# information (we enter rc1 not rcS for maintenance).  Run /etc/rc.d/rc
# without information so that it defaults to previous=N runlevel=S.
console output
pre-start script
    for t in $(cat /proc/cmdline); do
        case $t in
            emergency)
                start rcS-emergency
                break
            ;;
        esac
    done
end script
exec /etc/rc.d/rc.sysinit
post-stop script
    if [ "$UPSTART_EVENTS" = "startup" ]; then
        [ -f /etc/inittab ] && runlevel=$(/bin/awk -F ':' '$3 == "initdefault" && $1 !~ "^#" { print $2 }' /etc/inittab)
        [ -z "$runlevel" ] && runlevel="3"
        for t in $(cat /proc/cmdline); do
            case $t in
                -s|single|S|s) runlevel="S" ;;
                [1-9])       runlevel="$t" ;;
            esac
        done
        exec telinit $runlevel
    fi
end script
[root@localhost ~]#

其中 exec /etc/rc.d/rc.sysinit 这一行就表示要执行 /etc/rc.d/rc.sysinit 文件,该文件定义了系统初始化(system initialization) 的内容,包括:

(1).确认主机名。
(2).挂载/proc和/sys等特殊文件系统,使得内核参数和状态可与人进行交互。是否还记得在内核加载阶段时的/proc和/sys?
(3).启动udev,也就是启动类似windows中的设备管理器。
(4)初始化硬件参数,如加载某些驱动,设置时钟等。
(5).设置主机名。
(6).执行fsck检测磁盘是否健康。
(7).挂载/etc/fstab中除/proc和NFS的文件系统。
(8).激活swap。
(9).将所有执行的操作写入到/var/log/dmesg文件中。

A5.3、运行级别环境初始化

执行完系统初始化后,接下来就是执行运行级别的初始化。先看看 /etc/init/rc.conf 的内容。

[root@localhost ~]# cat /etc/init/rc.conf 
# rc - System V runlevel compatibility
#
# This task runs the old sysv-rc runlevel scripts.  It
# is usually started by the telinit compatibility wrapper.

start on runlevel [0123456]

stop on runlevel [!$RUNLEVEL]

task

export RUNLEVEL
console output
exec /etc/rc.d/rc $RUNLEVEL
[root@localhost ~]#

最后一行 exec /etc/rc.d/rc $RUNLEVEL 说明调用 /etc/rc.d/rc 这个脚本来初始化指定运行级别的环境。Linux 采用了将各运行级别初始化内容分开管理的方式,将 0-6 这 7 个运行级别要执行的初始化脚本分别放入 rc[0-6].d 这 7 个目录中。

[root@localhost ~]# ls -l /etc/rc.d/
total 60
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 init.d
-rwxr-xr-x. 1 root root  2617 Feb 22  2013 rc
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 rc0.d
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 rc1.d
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 rc2.d
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 rc3.d
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 rc4.d
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 rc5.d
drwxr-xr-x. 2 root root  4096 Jul 29 06:18 rc6.d
-rwxr-xr-x. 1 root root   220 Feb 22  2013 rc.local
-rwxr-xr-x. 1 root root 19472 Feb 22  2013 rc.sysinit
[root@localhost ~]#

实际上 /etc/init.d/ 下的脚本才是真正的脚本,放入 rcN.d 目录中的文件只不过是 /etc/init.d/ 目录下脚本的软链接。注意,/etc/init.d 是 Linux 耍的一个小把戏,它是 /etc/rc.d/init.d 的一个符号链接,在有些类 unix 系统中是没有 /etc/init.d 的,都是直接使用 /etc/rc.d/init.d。

以 /etc/rc.d/rc3.d 为例。

[root@localhost ~]# ll /etc/rc.d/rc3.d/ | head
total 0
lrwxrwxrwx. 1 root root 19 Jul 29 05:28 K10saslauthd -> ../init.d/saslauthd
lrwxrwxrwx. 1 root root 20 Jul 29 05:27 K50netconsole -> ../init.d/netconsole
lrwxrwxrwx. 1 root root 21 Jul 29 05:27 K87restorecond -> ../init.d/restorecond
lrwxrwxrwx. 1 root root 15 Jul 29 05:27 K89rdisc -> ../init.d/rdisc
lrwxrwxrwx. 1 root root 22 Jul 29 05:28 S02lvm2-monitor -> ../init.d/lvm2-monitor
lrwxrwxrwx. 1 root root 19 Jul 29 05:28 S08ip6tables -> ../init.d/ip6tables
lrwxrwxrwx. 1 root root 18 Jul 29 05:27 S08iptables -> ../init.d/iptables
lrwxrwxrwx. 1 root root 17 Jul 29 05:27 S10network -> ../init.d/network
lrwxrwxrwx. 1 root root 16 Jul 29 05:28 S11auditd -> ../init.d/auditd
[root@localhost ~]#

可见,rcN.d 中的文件都以 K 或 S 加一个数字开头,其后才是脚本名称,且它们都是 /etc/rc.d/init.d 中文件的链接。S 开头表示进入该运行级别时要运行的程序,S 字母后的数值表示启动顺序,数字越大,启动的越晚;K 开头的表示退出该运行级别时要杀掉的程序,数值表示关闭的顺序。

所有这些文件都是由 /etc/rc.d/rc 这个程序调用的,K 开头的则传给 rc 一个 stop 参数,S 开头的则传给 rc 一个 start 参数。

打开 rc0.d 和 rc6.d 这两个目录,你会发现在这两个目录中除了 S00killallS01reboot,其余都是 K 开头的文件。

而在 rc[2-5].d 这几个目录中,都有一个 S99local 文件,且它们都是指向 /etc/rc.d/rc.local 的软链接。S99 表示最后启动的一个程序,所以 rc.local 中的程序是 2345 这 4 个运行级别初始化过程中最后运行的一个脚本。这是 Linux 提供给我们定义自己想要在开机时(严格地说是进入运行级别) 就执行的命令的文件。

当初始化完运行级别环境后,将要准备登录系统了。

所谓的单用户模式(runlevel=1),就是初始化完运行级别 1 对应的环境。因为已经初始化了操作系统和运行级别,所以单用户模式所处的层次要比救援模式高的多,能修复的问题也就只有它后面还未初始化的过程:终端初始化和用户登录问题。

A6、终端初始化和登录系统

Linux 是多任务多用户的操作系统,它允许多人同时在线工作。但每个人都必须要输入用户名和密码才能验证身份并最终登录。但登陆时是以图形界面的方式给用户使用,还是以纯命令行模式给用户使用呢?这是终端决定的,也就是说在登录前需要先加载终端。

A6.1、终端初始化

在 Linux 上,每次开机都必然会开启所有支持的虚拟终端,如下图。

centos7tty

这些虚拟终端是由 getty 命令(get tty) 来完成的,getty 命令有很多变种,有 mingetty、agetty、rungettty 等,在 CentOS 5 和 CentOS 6 都使用 mingetty,在 CentOS 7 上使用 agetty。getty 命令的作用之一是调用登录程序 /bin/login。

例如,在 CentOS 6 下,捕获 tty 终端情况。

[root@localhost ~]# ps -elf | grep tt[y]
4 S root       1299      1  0  80   0 -  1015 n_tty_ 05:15 tty2     00:00:00 /sbin/mingetty /dev/tty2
4 S root       1301      1  0  80   0 -  1015 n_tty_ 05:15 tty3     00:00:00 /sbin/mingetty /dev/tty3
4 S root       1306      1  0  80   0 -  1015 n_tty_ 05:15 tty4     00:00:00 /sbin/mingetty /dev/tty4
4 S root       1309      1  0  80   0 -  1015 n_tty_ 05:15 tty5     00:00:00 /sbin/mingetty /dev/tty5
4 S root       1311      1  0  80   0 -  1015 n_tty_ 05:15 tty6     00:00:00 /sbin/mingetty /dev/tty6
4 S root       1315   1297  0  80   0 - 27074 n_tty_ 05:15 tty1     00:00:00 -bash
[root@localhost ~]#

在 CentOS 7 下,捕获 tty 终端情况。

[root@localhost ~]# ps -elf | grep tt[y]
4 S root      18475      1  0  80   0 - 27511 n_tty_ 06:44 tty1     00:00:00 /sbin/agetty --noclear tty1 linux
4 S root      18525  18521  0  80   0 - 28848 n_tty_ 06:49 pts/1    00:00:00 -bash
[root@localhost ~]#

细心一点会发现,有的 tty 终端仍然以 /sbin/mingetty 进程或 /sbin/agetty 进程显示,有些却以 bash 进程显示。这是因为 getty 进程在调用 /bin/login 后,如果输入用户名和密码成功登录了某个虚拟终端,那么 gettty 程序会融合到 bash(假设 bash 是默认的 shell) 进程,这样 getty 进程就不会再显示了。

虽然 getty 不显示了,但并不代表它消失了,它仍以特殊的方式存在着。是否还记得 /etc/inittab 文件?此文件中提示了终端加载的过程由 /etc/init/tty.conf 读取配置文件 /etc/sysconfig/init 来完成。

[root@localhost ~]# grep tty -A 1 /etc/inittab 
# Terminal gettys are handled by /etc/init/tty.conf and /etc/init/serial.conf,
# with configuration in /etc/sysconfig/init.
[root@localhost ~]#

那么就看看 /etc/init/tty.conf 文件。

[root@localhost ~]# cat /etc/init/tty.conf 
# tty - getty
#
# This service maintains a getty on the specified device.

stop on runlevel [S016]

respawn
instance $TTY
exec /sbin/mingetty $TTY
usage 'tty TTY=/dev/ttyX  - where X is console id'
[root@localhost ~]#

此文件中的 respawn 表示进程由 init 进程监视,即使被杀掉了也会由 init 来重启它。所以,只要 getty 进程一结束,init 会立即监视到而重启该进程。因此,用户登录成功后 getty 只是融合到了 bash 进程中,并非退出,否则 init 会立即重启它,而它会调用 login 程序让你再次输入用户和密码。

再看看 /etc/sysconfig/init 文件。

[root@localhost ~]# cat /etc/sysconfig/init 
# color => new RH6.0 bootup
# verbose => old-style bootup
# anything else => new style bootup without ANSI colors or positioning
BOOTUP=color
# column to start "[  OK  ]" label in 
RES_COL=60
# terminal sequence to move to that column. You could change this
# to something like "tput hpa ${RES_COL}" if your terminal supports it
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
# terminal sequence to set color to a 'success' color (currently: green)
SETCOLOR_SUCCESS="echo -en \\033[0;32m"
# terminal sequence to set color to a 'failure' color (currently: red)
SETCOLOR_FAILURE="echo -en \\033[0;31m"
# terminal sequence to set color to a 'warning' color (currently: yellow)
SETCOLOR_WARNING="echo -en \\033[0;33m"
# terminal sequence to reset to the default color.
SETCOLOR_NORMAL="echo -en \\033[0;39m"
# Set to anything other than 'no' to allow hotkey interactive startup...
PROMPT=yes
# Set to 'yes' to allow probing for devices with swap signatures
AUTOSWAP=no
# What ttys should gettys be started on?
ACTIVE_CONSOLES=/dev/tty[1-6]
# Set to '/sbin/sulogin' to prompt for password on single-user mode
# Set to '/sbin/sushell' otherwise
SINGLE=/sbin/sushell
[root@localhost ~]#

其中 ACTIVE_CONSOLES 指令决定了要开启哪些虚拟终端。SINGLE 决定了在单用户模式下要调用哪个 login 程序和哪个 shell。

A6.2、登录过程

如果不在虚拟终端登录,而是通过为 ssh 分配的伪终端登录,那么到创建完 getty 进程那一步其实开机流程已经完成了。但不管在哪种终端下登录,登录过程也可以算作开机流程的一部分,所以也简单说明下。

getty 进程启用虚拟终端后将调用 login 进程提示用户输入用户名或密码(或伪终端的连接程序如 ssh 提示输入用户名和密码),当用户输入完成后,将验证输入的用户名是否合法,密码是否正确,用户名是否是明确被禁止登陆的,PAM 模块对此用户的限制是如何的等等,还要将登录过程记录到各个日志文件中。如果登录成功,将加载该用户的 bash,加载 bash 过程需要读取各种配置文件,初始化各种环境等等。但不管怎么说,只要登录成功就表示开机流程全部完成了。

标签云