Linux系统启动时

之前看到了 Linux 的启动流程的大概描述：

我想知道更具体的细节，所以搜寻了一些资料，做一个总结。

一、CPU Reset

当我们按下电源按钮后，电源就开始给设备供电，一开始供电并不稳定，主板检测到后，就会一直给 CPU 发送RESET 信号，此时的 CPU 就会清除寄存器上的残留数据并进行寄存器的设置：

在这之中最重要的寄存器就是CS 和 EIP，CS 中隐式的 Base 会和 RIP 相加，即ffff 0000 H+fff 0 H=fffffff 0 H ffff 0000H + fff0H = ffff fff0 Hffff0000H+fff0H=fffffff0H，存放在这个地址 (也称为重置向量) 的是一条跳转指令，其跳转的目的地址是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 差不多就启动好了。