之前看到了 Linux 的启动流程的大概描述:
我想知道更具体的细节,所以搜寻了一些资料,做一个总结。
一、CPU Reset#
当我们按下电源按钮后,电源就开始给设备供电,一开始供电并不稳定,主板检测到后,就会一直给 CPU 发送RESET 信号,此时的 CPU 就会清除寄存器上的残留数据并进行寄存器的设置:
在这之中最重要的寄存器就是CS 和 EIP,CS 中隐式的 Base 会和 RIP 相加,即,存放在这个地址 (也称为重置向量) 的是一条跳转指令,其跳转的目的地址是BIOS 的入口地址。
为什么重置向量不直接设为 BIOS 入口地址,而是要跳转呢?
X86 芯片一开始是运行在实模式下的,实模式只有 20 位寻址空间(1M),而我们的重置向量是 fff0H,到 ffffH 只有很短的距离,根本放不下 BIOS 程序,所以要跳转。
至于为什么要把重置向量放在地址空间的高地址处,是为了给内存腾出更大的空间。
实模式下的内存布局
多核系统下的 CPU Reset#
多核计算机下,一开始要执行某个协议,系统将选取一个 CPU 来执行,其他 CPU 都处于等待状态,这个被选取的主 CPU 叫 Bootstrpping CPU(BSP),只有 BSP 继续执行,其它 CPU 等待 BSP 给它们发指令。
二、BIOS 执行#
BIOS 是固定在 EPROM 中的程序,一般由硬件厂商写死,其负责引导系统的启动。
POST 加电自检#
BIOS 首先执行的程序就是 POST,其用于计算机刚接通电源时对硬件部分的检测,如果自检中发现不是很严重的错误,系统会根据检测代码给出提示信息或鸣笛警告。
为什么选择鸣笛的方式来发出警报?
因为以前没有核显,显卡就一定是外设,POST 执行时,显卡还没有完成初始化,所以无法将错误信息显示在屏幕上,只能通过声音来报警。
检测的过程是逐一进行的,BIOS 厂商对每个设备都提供了一个POST CODE
,当对某个设备进行检测时,就会把这个 POST CODE 装到诊断的端口,如果没通过,这个 CODE 就会被保留,进行报警。
初始化设备#
POST 结束后,BIOS 还会调用各外设的 BIOS 进行自检和初始化,比如显卡的 BIOS。所有的设备都是这个时候初始化并启动的。
除了初始化设备,BIOS 这个时候还会初始化中断向量表。
启动 Bootloader#
在检测和初始化各种设备之后,BIOS 会查找用户自定义的启动顺序,默认是磁盘,当然也可以是光盘和 U 盘(以前重装操作系统时就会用到光盘和 U 盘):
根据顺序,BIOS 会把排在最前面的设备的MBR(主引导记录),即第 0 磁道第一个扇区的 512 字节,读到RAM 绝对地址 0x7C00 处,并跳转到这个地址。
MBR 格式
MBR 不属于任何一个操作系统,其 512 字节的内容如下:
- 启动代码 (446B):检测分区表的准确性,并将系统控制转移给硬盘上的 Bootloader 程序(比如 grub)
- 磁盘分区表 (16 X 4B):DPT,由 4 个分区表组成,每个 16B,说明磁盘分区情况。
- 结束标志 (2B)
UEFI
现在说 BIOS,主要指的是 UEFI,而不是传统的 BIOS。不管是传统 BIOS 还是 UEFI,都是经过 ROM→RAM→BOOT 的过程,主干是没有区别的,那么我们为什么需要 UEFI 呢?
可以参考这个回答:UEFI 引导与 BIOS 引导在原理上有什么区别?
三、Bootloader#
所谓的 Bootloader 程序就是用来加载操作系统内核文件到内存,这类程序的主要功能为:
- 从实模式到保护模式,从 16 位寻址空间到 32 位寻址空间,使能段机制。
- 从硬盘读取 ELF 格式的 Kernel(就是跟在 MBR 后面的扇区)并放到内存中的固定位置,这个过程一般分两步,最终是执行 boot 指令,即加载系统引导菜单 (
/boot/grub/menu.lst
或 grub.lst),内核 vmlinuz 和 RAM 磁盘 initrd。
我们这里以 GNU 的grub为例。grub 可用于选择操作系统分区上的不同内核,也可用于向这些内核传递启动参数。grub 被载入一般包括两个步骤: - 装载基本引导程序 - stage1,其主要功能是装载第二引导程序
- 装载第二引导程序 - stage2,这第二引导装载程序用于引出更高级的功能,以允许用户装载入一个特定的操作系统,在 grub 中,这步是给用户一个显示菜单或者让用户输入命令,该阶段引导的最终状态是执行 boot 命令,将操作系统内核加载进入内存中,进而将控制权转交给内核。
当内核加载完成后,内存被映射为:
四、Linux 内核设置和启动#
Linux 内核启动后,执行的一系列检测,检测完成后跳转到 start_kernel 函数,这个函数会依次初始化各个模块,比如页表、中断向量等,然后变成 0 号进程。
0 号进程会 fork 出 1 号 kernel_init 进程,kernel_init 会执行 init 程序,大致过程如下:
init 程序
- init 进程读取
/etc/inittab
文件,作用是设定 Linux 的运行等级,决定进程运行模式。- Linux 执行第一个用户层文件
/etc/rc.d/rc.sysinit
,该文件功能包括:设定 PATH 运行变量、设定网络配置、启动 swap 分区、设定 /proc、系统函数等.- 读取
/etc/modules.conf
及/etc/modules.d
目录下的文件来加载系统内核模块。- 启动一些服务,之后执行
/etc/rc.d/rc.local
文件。- 最后执行 /bin/login 程序,启动到登录界面,让用户输入用户名和密码。
完成用户的启动后,0 号进程进入 cpu_idle, 变成 idle 进程。到这 Linux 差不多就启动好了。