Boot
本文介绍了 Linux 启动的整个过程。
Overview
总的来说我们可以把 Linux 系统启动的过程分为下面几个阶段:
- ROM Stage
- 在这个阶段没有内存,需要在 ROM 上运行代码。这时因为没有内存,没有 C 语言运行需要的栈空间,开始往往是汇编语言,直接在 ROM 空间上运行。在找到个临时空间(Cache 空间用作 RAM,Cache As Ram, CAR)后,C 语言终于可以粉墨登场了,后期用 C 语言初始化内存和为这个目的需要做的一切服务。
- RAM Stage
- 经过 ROM 阶段的困难情况后,我们终于有了可以大展拳脚的内存,很多额外需要大内存的东西可以开始运行了。在这时我们开始进行初始化芯片组、CPU、主板模块等等核心过程。
- Find Something To Boot Stage
- 终于要进入正题了,需要启动,我们找到启动设备。就要枚举设备,发现启动设备,并把启动设备之前需要依赖的节点统统打通。然后开始移交工作,Windows 或者 Linux 的时代开始。
具体来说,
- 在 ROM/RAM Stage,运行的代码我们一般称作固件,也就是 Firmware,最早是 BIOS,现在 UEFI 应用越来越广泛。
- 固件初始化执行完毕后,Firmware 会将控制权交给
boot loader,最常见的是 GRUB。 boot loader之后会将 OS 内核加载进内存,开始进入操作系统。- OS 一般通过一个 init 进程启动所有的进程,最开始用的是
SysVinit,现在Systemd应用越来越广泛。
Firmware
BIOS
BIOS, Basic Input-Output System
BIOS 是一组固化在计算机主板 ROM 里的程序代码,其主要功能是在计算机上电时对硬件进行初始化配置,并将硬件操作封装为 BIOS 中断服务。这样,各种硬件间的差异便由 BIOS 负责维护,程序直接调用 BIOS 中断服务即可实现对硬件的控制。
在系统上电后,CPU 运行于实模式工作环境中,数据位宽为 16 位,最大物理地址寻址范围是 0~1MB,其中的物理地址 0x0C0000~0x0FFFFF 保留给 BIOS 使用。开机后,CPU 硬件逻辑设计为在加电瞬间强行将 CS 值置为 0XF000,IP 为 0XFFF0,这样 CS:IP 就指向0XFFFF0这个位置,这个位置正是 BIOS 程序的入口地址。一般情况下,这里是一条跳转指令,CPU 通过执行此处的跳转指令跳转到真正的 BIOS 入口地址处执行,以下是 BIOS 的启动流程:
加电自检
BIOS 代码首先做的是POST(Power On Self Test,加电自检)操作,主要是检测关键设备是否正常工作,设备设置是否与 CMOS 中的设置一致。如果发现硬件错误,则通过喇叭报警;如果没有问题,屏幕就会显示出 CPU、内存等信息。
初始化设备
BIOS 的第二步动作就是枚举本地设备并初始化。
有一项对启动操作系统至关重要的工作,那就是BIOS 在内存中建立中断向量表和中断服务程序。
BIOS 程序在内存最开始的位置(0x00000)用 1KB 的内存空间(0x00000~0x003FF)构建中断向量表,在紧挨着它的位置用 256KB 的内存空间构建 BIOS 数据区(0x00400~0x004FF),并在大约 57KB 以后得位置(0x0e05b)加载了 8KB 左右的与中断向量表相应的若干中断服务程序。
中断向量表有 256 个中断向量,每个中断向量占 4 个字节,其中两个字节是 CS 值,两个字节是 IP 值。每个中断向量都指向一个具体的中断服务程序。
MBR
硬件自检完成后,BIOS 把控制权转交给下一阶段的启动程序。
这时,BIOS 需要知道「下一阶段的启动程序」具体存放在哪一个设备。也就是说,BIOS 需要有一个外部储存设备的排序,排在前面的设备就是优先转交控制权的设备。这种排序叫做"启动顺序"(Boot Sequence)。
打开 BIOS 的操作界面,里面有一项就是 设定启动顺序。
BIOS 按照"启动顺序",把控制权转交给排在第一位的储存设备。
{% note info %}
CMOS 又被称作互补金属氧化物半导体,电压控制的一种放大器件,是组成 CMOS 数字集成电路的基本单元。在计算机领域,CMOS 常指保存计算机基本启动信息(如日期、时间、启动设置等)的位于微机主板上的一块可读写 RAM 芯片。在今日,CMOS 制造工艺也被应用于制作数码影像器材的感光元件,尤其是片幅规格较大的单反数码相机。
CMOS 主要用来保存当前系统的硬件配置和操作人员对某些参数的设定,CMOS RAM 芯片由系统通过后备电池供电,在关机状态中,还是遇到系统掉电,CMOS 信息不会丢失。
{% endnote %}
这时,计算机读取该设备的第一个扇区,也就是读取最前面的 512 个字节。如果这 512 个字节的最后两个字节是 0x55 和 0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动,控制权于是被转交给"启动顺序"中的下一个设备。
这最前面的 512 个字节,就叫做"主引导记录"(Master boot record,缩写为 MBR)。
MBR 只有 512 字节,由以下三个部分组成:
- Bootstrap Code Area,前 446 字节,它里面就包含有可执行代码以及错误消息文本。
- Partition Table,接下来的 64 字节,其中包含有四个分区的各自的记录(一个分区占 16 字节)。
- Boot Signature,最后两个字节,0x55AA
简单来说,MBR 从 BIOS 获得控制权之后,主要做的事情就是寻找并加载 boot loader。MBR 的前 446 字节包含某个启动引导器,像 GRUB 、Syslinux 和 LILO 之类的第一启动阶段代码。
MBR 接管后,执行它之后的第二阶段代码,如果后者存在的话,它一般就是 Boot Loader。在这之后,我们就进入了 Boot Loader Phase。
UEFI
UEFI,Unified Extensible Firmware Interface,前身是 EFI 规范 1.10。UEFI 规范描述了操作系统和平台固件之间的接口,其目的是为操作系统和平台固件定义一种通信方法。
UEFI 规范仅提供操作系统引导过程所需的信息,旨在无需对平台或操作系统进行深入定制便可在处理器规范兼容的平台上运行操作系统。UEFI 规范还允许平台引入创新的特性和功能,在无需为 OS 引导程序重新编程的情况下增强平台功能。UEFI 规范适用于从移动系统到服务器的各种硬件平台,并允许原始设备制造商具有最大的扩展性和定制能力,以实现差异化。
UEFI 接口的表现形式是数据表,其中包括与平台相关的信息,以及操作系统加载器和操作系统可使用的引导服务和运行时服务。它们一起为启动操作系统提供了一个标准环境。UEFI 规范设计为纯接口规范。因此,UEFI 规范定义了平台固件必须实现的一组接口和结构。
以下是 UEFI 设计的基本要素:
- 重用现有接口表。为了让操作系统和固件中的代码可以在现有设计结构中持续使用。凡是兼容 UEFI 规范的处理器平台都必须遵照 UEFI 规范进行实现。
- 系统分区。系统分区定义了一个独立的、可共享的分区和文件系统,这个系统分区可允许多个供应商之间安全共享数据,即使这些供应商出于不同目的去访问系统分区。
- 引导服务。引导服务提供了在启动期间可以使用的设备和系统功能的接口。设备的访问是通过句柄(Handle)和协议(Protocol)抽象出来的。UEFI 通过将基础实现隔离在规范之外,以避免给设备的访问者带来负担,进而促进现有 BIOS 代码的重用。
- 运行时服务。运行时服务为操作系统提供了正常运行期间可以使用的基础平台硬件资源的接口。
UEFI 不仅能读取分区表,还能自动支持文件系统。所以它不像 BIOS,已经没有仅仅 440 字节可执行代码即 MBR 的限制了,它完全用不到 MBR。
不管第一块上有没有 MBR,UEFI 都不会执行它。相反,它依赖分区表上的一个特殊分区,叫 EFI 系统分区,里面有 UEFI 所要用到的一些文件。计算机供应商可以在 /EFI// 文件夹里放官方指定的文件,还能用固件或它的 shell,即 UEFI shell,来启动引导程序。EFI 系统分区一般被格式化成 FAT32,或比较非主流的 FAT16。
- 系统开机 - 上电自检(Power On Self Test 或 POST)。
- UEFI 固件被加载,并由它初始化启动要用的硬件。
- 固件读取其引导管理器以确定从何处(比如,从哪个硬盘及分区)加载哪个 UEFI 应用。
- 固件按照引导管理器中的启动项目,加载UEFI 应用。
- 已启动的 UEFI 应用还可以启动其他应用(对应于 UEFI shell 或 rEFInd 之类的引导管理器的情况)或者启动内核及initramfs(对应于GRUB之类引导器的情况),这取决于 UEFI 应用的配置。
Security Phase
验证阶段 (Security,SEC)
系统上电后,CPU 开始执行第一条指令,此时系统就进入 SEC 阶段。这个阶段的内存尚未被初始化,不可使用。所以,SEC 阶段最主要的工作是建立一些临时内存并将 CPU 切换到保护模式,这里提到的临时内存可以是处理器的缓存,亦或者系统的物理内存。
Pre-EFI Initailization Phase
EFI 环境预初始化阶段(Pre-EFI Initialization Environment,PEI)
PEI 阶段最主要的工作就是对内存、CPU 以及芯片组等关键设备进行初始化。由于这部分代码没有进行压缩,因此代码必须越精简越好。而且,在 PEI 阶段还要确定操作系统的引导路径,初始化 UEFI 驱动和固件需要的内存。
####Driver eXecution Environment Phase
驱动运行环境阶段(Driver Execution Environment,DXE)
DXE 是 EFI 最重要的阶段,大部分的驱动、固件加载工作都是在这个阶段完成的。
Boot Device Selection Phase
引导设备选择阶段(Boot Device Select,BDS)
BDS 阶段的主要工作是初始化控制台设备的环境变量,尝试加载环境变量列表中记录的驱动,并尝试从环境变量列表中记录的启动设备中启动。
Transient System Load Phase
临时系统运行阶段(Transient System Load,TSL)
这个阶段将进入 UEFI 的临时 Shell 系统环境。
Run Time Phase
运行时阶段(RunTime,RT)
当操作系统调用 EFI_BOOT_SERVICES.ExitBootServices 服务后,系统进入 RT 阶段。此时,DXE 与引导服务都将销毁,只有 EFI 运行时服务和 EFI 系统表可以继续使用。
After Life Phase
后世阶段(After Life,AL)
当操作系统调用 EFI_RUNTIME_SERVICES.ResetSystem 服务或者调用 ACPI Sleep State,系统进入 AL 阶段。触发异步事件(比如:SMI、NMI)亦可使系统进入 AL 阶段,这在服务器和工作站中比较常见。
UEFI VS BIOS
-
开发效率
- BIOS 开发一般采用汇编语言,代码大多与硬件控制相关。
- 在 UEFI 中,绝大部分代码采用 C 语言编写,UEFI 应用程序和驱动甚至可以使用 C++编写。UEFI 通过固件-操作系统接口(引导服务和运行时服务)为操作系统和操作系统加载器屏蔽了底层硬件细节,使得 UEFI 上层应用可以方便重用。
-
可扩展性
大部分硬件的初始化通过 UEFI 驱动实现。每个驱动是一个独立的模块,可以包含在固件中,也可以放在设备上,运行时根据需要动态加载。UEFI 中的每个表和协议(包括驱动)都有版本号,这使得系统升级过程更加简单、平滑。
UEFI 系统的可扩展性体现在两个方面
- 一是驱动的模块化设计
- 二是软硬件升级的兼容性
-
性能
相比 BIOS,UEFI 有了很大的性能提升,从启动到进入操作系统的时间大大缩短。性能的提高源于以下几个方面:
- UEFI 提供了异步操作。基于事件的异步操作,提高了 CPU 利用率,减少了总的等待时间。
- UEFI 舍弃了中断这种比较耗时的操作外部设备的方式,仅仅保留了时钟中断。外部设备的操作采用“事件+异步操作”完成。
- 可伸缩的设备遍历方式,启动时可以仅仅遍历启动所需的设备,进而加速系统启动。
-
安全性
UEFI 的一个重要突破就是其安全方面的考虑。当系统的安全启动功能被打开后,UEFI 在执行应用程序和驱动前会先检测程序和驱动的证书,仅当证书被信任时才会执行这个应用程序或驱动。UEFI 应用程序和驱动采用 PE/COFF 格式,其签名放在签名块中。
Bootloader
bootloader是 BIOS 或 UEFI 启动的第一个程序。它负责使用正确的 内核参数 加载内核, 并根据配置文件加载 初始化 RAM disk。对于 UEFI,内核本身可以由 UEFI 使用 EFI boot stub 直接启动,也可以使用单独的引导加载程序或引导管理器来在引导之前编辑内核参数。
在 Linux 中,GRUB 是最常用的一个 boot loader。在 /boot目录下,我们可以看到,除了内核的四个文件,另外就是 grub的相关文件。
|
|
在这里,内核的四个文件解释如下:
- vmlinuz
- vmlinuz 是可引导的、压缩的内核。“vm”代表“Virtual Memory”。
- vmlinuz 的建立有两种方式。
- zImage
- 编译内核时执行
make zImage - 然后通过:
cp /usr/src/linux-2.4/arch/i386/linux/boot/zImage /boot/vmlinuz产生 - zImage 适用于小内核的情况,它的存在是为了向后的兼容性。
- 编译内核时执行
- bzImage
- 内核编译时执行
make bzImage - 然后通过:
cp /usr/src/linux-2.4/arch/i386/linux/boot/bzImage /boot/vmlinuz产生 - bzImage 是压缩的内核映像,需要注意,bzImage 不是用 bzip2 压缩的,bzImage 中的 bz 容易引起误解,bz 表示
big zImage。
- 内核编译时执行
- zImage 和 bzImage 都是用 gzip 压缩的。它们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有 gzip 解压缩代码。所以你不能用 gunzip 或 gzip –dc 解包 vmlinuz。
- 内核文件中包含一个微型的 gzip 用于解压缩内核并引导它。两者的不同之处在于,老的 zImage 解压缩内核到低端内存(第一个 640K),bzImage 解压缩内核到高端内存(1M 以上)。如果内核比较小,那么可以采用 zImage 或 bzImage 之一,两种方式引导的系统运行时是相同的。大的内核采用 bzImage,不能采用 zImage。
- vmlinux 是未压缩的内核,vmlinuz 是 vmlinux 的压缩文件。
- zImage
- initrd.img
- initrd 是
initial ramdisk的简写,值得是一个临时文件系统,它在启动阶段被内核调用。 initrd和initramfs是实现的两种技术。- initrd 是 kernel 2.4 及更早的用法
- initramfs 是 kernel 2.6 的技术,现在看到的 initrd 文件基本上都是 initramfs 了。启动的时候加载内核和 initramfs 到内存执行,内核初始化之后,切换到用户态执行 initramfs 的程序/脚本,加载需要的驱动模块、必要配置等,然后加载 rootfs 切换到真正的 rootfs 上去执行后续的 init 过程。
- Initrd 是在实际根文件系统可用之前挂载到系统中的一个初始根文件系统。initrd 与内核绑定在一起,并作为内核引导过程的一部分进行加载。内核然后会将这个 initrd 文件作为其两阶段引导过程的一部分来加载模块,这样才能稍后使用真正的文件系统,并挂载实际的根文件系统。
- initrd 中包含了实现这个目标所需要的目录和可执行程序的最小集合,例如将内核模块加载到内核中所使用的 insmod 工具。在桌面或服务器 Linux 系统中,initrd 是一个临时的文件系统。其生存周期很短,只会用作到真实文件系统的一个桥梁。在没有存储设备的嵌入式系统中,initrd 是永久的根文件系统。
- initrd 是
- System.map
- System.map 内核符号映射表,顾名思义就是将内核中的符号(也就是内核中的函数)和它的地址能联系起来的一个列表。是所有符号及其对应地址的一个列表。之所以这样就使为了用户编程方便,直接使用函数符号就可以了,而不用去记要使用函数的地址。当你编译一个新内核时,原来的 System.map 中的符号信息就不正确了。随着每次内核的编译,就会产生一个新的 System.map 文件,并且需要用该文件取代原来的文件。System.map 是一个特定内核的内核符号表。它是你当前运行的内核的 System.map 的链接。
- config
- 内核编译时的配置选项。
下面是一个典型的 grub.cfg
# shutdown menu
menuentry "System shutdown" {
echo "System shutting down..."
halt
}
# restart menu
menuentry "System restart" {
echo "System rebooting..."
reboot
}
# UEFI shell
menuentry "UEFI Shell" {
insmod fat
insmod chain
search --no-floppy --set=root --file /shellx64.efi
chainloader /shellx64.efi
}
# linux boot menu
menuentry "Linux" {
set root=(hd0,1)
linux /boot/vmlinuz (add other options here as required)
initrd /boot/initrd.img (if the other kernel uses/needs one)
}
Kernel
Linux 内核处理所有操作系统进程,如内存管理、任务调度、I/O、进程间通信和系统总体控制。
在 bootloader加载 kernel 和可用的 initramfs 文件,并执行 kernel 之后,kernel 将 initramfs(初始 RAM 文件系统)压缩包解压缩到(然后清空)rootfs(初始根文件系统,特别是 ramfs 或 tmpfs)。首先提取的 initramfs 是在 kernel 构建过程中嵌入 kernel 二进制 Update translation.的 initramfs,然后提取可用的外部 initramfs 文件。因此,外部 initramfs 中的文件会覆盖嵌入式 initramfs 中具有相同名称的文件。然后, kernel 执行 /init (在 rootfs 中)作为第一个进程。early userspace开始。
initramfs 之所以存在,是为了帮系统访问真正的根文件系统(参见 Arch filesystem hierarchy (简体中文))。也就是说,那些硬件 IDE, SCSI, SATA, USB/FW 所要求的 kernel 模块,如果并没有内置在 kernel 里,就会被 initramfs 负责加载。一旦通过 udev (简体中文) 之类的程序或脚本加载好模块,启动流程才会继续下去。所以,initramfs 只要有能够让系统访问真实根文件系统的模块就可以了,不用尽可能地包含一切模块。当然,其它真正有用的模块之后会在 init 流程中被 udev 加载好。
Init Process
在「早期用户空间」的最终环节里,真正的根文件系统被挂载好后,就会替换掉原来的伪根文件系统。接着 /sbin/init 被执行,同样也替换掉原来的 /init 进程。
SysVinit
1983 年以来,System V 便是 Unix 和类 Unix (例如 Linux)系统中的经典启动过程。它包括小程序 init 用于 启动诸如 login (由 getty 启动)这样的基础程序,并运行着名为 rc 的脚本。该脚本,控制着一众附加脚本的 执行,而那些附加脚本便是实施系统初始化所需要的任务的脚本。
程序 init 由文件 /etc/inittab 控制着,并且被组织成用户能够运行的运行级别形式:
0 — 停止
1 — 单用户模式
2 — 多用户,无网络
3 — 完整的多用户模式
4 — 用户可定义
5 — 完整的多用户模式,附带显示管理
6 — 重启
常用的默认运行级为 3 或 5。
- 启动时间长,init 是串行启动,只有前一个进程启动完,才会启动下一个进程
- 启动脚本复杂,Init 进程只是执行启动脚本,不管其他事情,脚本需要自己处理各种情况,这往往使得脚本变得很长
- 由 Linux 内核加载运行,位于
/sbin/init,是系统中第一个进程,PID 永远为 1
Systemd
- 按需启动服务,减少系统资源消耗。
- 尽可能并行启动进程,减少系统启动等待时间
- 由 Linx 内核加载运行,位于
/usr/lib/systemd/systemd,是系统中第一个进程,PID 永远为 1
命令对比
| 动作 | SystemV | Systemd |
|---|---|---|
| 停止某服务 | service httpd stop | systemctl stop httpd |
| 重启某服务 | service httpd restart | systemctl restart httpd |
| 检查服务状态 | service httpd status | systemctl status httpd |
| 删除某服务 | chkconfig –del httpd | 停掉应用,删除其配置文件 |
| 使服务开机自启动 | chkconfig –level 5 httpd on | systemctl enable httpd |
| 使服务开机不自启动 | chkconfig –level 5 httpd off | systemctl disable httpd |
| 显示所有已启动的服务 | chkconfig –list | systemctl list-unit-files |
| 加入自定义服务 | chkconfig –add test | systemctl load test |
Login
一般来说,用户的登录方式有三种:
- 命令行登录
- ssh 登录
- 图形界面登录
这三种情况,都有自己的方式对用户进行认证。
- 命令行登录:init 进程调用 getty 程序(意为 get teletype),让用户输入用户名和密码。输入完成后,再调用 login 程序,核对密码(Debian 还会再多运行一个身份核对程序/etc/pam.d/login)。如果密码正确,就从文件 /etc/passwd 读取该用户指定的 shell,然后启动这个 shell。
- ssh 登录:这时系统调用 sshd 程序(Debian 还会再运行/etc/pam.d/ssh ),取代 getty 和 login,然后启动 shell。
- 图形界面登录:init 进程调用显示管理器,Gnome 图形界面对应的显示管理器为 gdm(GNOME Display Manager),然后用户输入用户名和密码。如果密码正确,就读取/etc/gdm3/Xsession,启动用户的会话。
Reference
-
No backlinks found.