WhatAKitty Daily

A Programmer's Daily Record

Linux启动过程分析详解

WhatAKitty   阅读次数loading...

背景

一直想要自己做出一个操作系统,因此对于现有linux需要有个详细的了解,知道操作系统如何实现所有的过程。因此,在这篇文章中记录下linux的详细启动过程。

本篇打算从三个部分讲解Linux启动的过程:

  1. BIOS阶段
  2. BootLoader阶段
  3. 内核启动阶段

本文前提:非UEFI模式

BIOS阶段

当计算机启动电源,CPU需要执行第一条指令,这条指令又从何而来?

计算机的内存会将内存划分为多个区,每个区都有不同的功能,甚至直接映射到硬件。如下图为内存划分:

内存划分

上图中,0xF0000 ~ 0xFFFFF 为ROM区,直接映射为BIOS芯片存储区。当电脑启动的时候,相关的寄存器将会被初始化,这边重点关注段寄存器CS以及指令指针寄存器IP。CS将会重置为0xF000,IP将会重置为0x0000;CS:IP指向的就是第一条指令的地址:0xF0000。而从上图可以知晓,这个地址对应的就是ROM区。

CPU开始执行ROM区的指令,由于这块区域只有16字节的大小,所以不会放置太过复杂的程序,一般只有一条JMP指令;这条指令指向的是低地址空间,并将BIOS程序载入到RAM空间执行。之后开始执行初始化过程,包括中断向量表、中断服务(对于键盘、鼠标等中断信号的响应以及处理)以及硬件健康自检。

如下图描述了整个BIOS启动过程:

BIOS启动过程

上图中的流程需要提一嘴的是在按下【DEL】键的时候,会进行中断处理,解压Setup模块。这个模块是一个BIOS设置引导界面程序,在BIOS设置中可以设置启动介质以及介质的启动顺序、硬件相关的一些配置等等。而由于BIOS芯片是ROM芯片,不存在存储的能力,所以计算机专门准备了一个RAM介质用于存储配置,这个RAM名为CMOS。一般我们经常听到其他人说什么CMOS放电处理,其实就是为了让RAM能够长期保存配置,会有一个专门的电池用于对CMOS供电,这样即便是在计算机断点的情况下,BIOS的设置也不会丢失;而放电的操作,就是让CMOS断电,将配置回复到初始状态。

自此,BIOS阶段结束。

BootLoader阶段

在BIOS在执行完初始化工作后,最终的操作系统又是如何被发现的?

BIOS程序在完成硬件自检之后,就需要查询从启动介质中查询可引导分区了,而引导分区的作用就是用来载入最终的操作系统。

前一节已经讲过CMOS,也知道CMOS用于保存BIOS配置,其中就包括了启动介质的配置以及顺序设置。BIOS程序之后就会按照顺序遍历启动介质,知道发现可引导分区(MBR);而MBR如何发现呢?在载入每个介质第一个扇区(512字节)后,会将这段内容防止在内存上的0x00000-0x7C00区域,并判断这段内容是否以AA55这两个字节结束;如果是,则该介质为启动介质;否则,继续查看下一个介质。

可以看下下图的MBR的引导分区结构图:

MBR分区结构

MBR分区主要分为三个块:

  1. 代码块:446字节,主要用于存储可执行代码以及错误消息文本
  2. 分区表:64字节,每个分区占用16字节,所以MBR最多可以划分4个分区;而为了突破这个限制,MBR将最后一个分区定义为扩展分区,然后在扩展分区内可以划分逻辑分区
  3. 结束符:2字节

在找到MBR之后,会加载代码块的代码;这段代码就是BootLoader,即GRUB的最小可执行程序。为什么只装在最小可执行程序,主要原因还是由于MBR的可容纳空间过于小,这段程序会加载完整的GRUB程序。

GRUB

GRUB,GRand Unified Bootloader。它是一个多重操作系统启动管理器,用来引导不同系统,如windows,linux。

在启动BootLoader之后,GRUB最小执行程序(boot.img)会载入MBR分区后的内容(core.img),这些内容由以下四个部分组成:diskboot.img、lzma_decompress.img、kernel.img以及一些模组。如下图:

GRUB

diskboot.img会解压lzma_decompress.img程序,然后将控制权转交给它。lzma在解压缩kernel.img之前,会将计算机从实模式转化为保护模式(实模式下内存最大寻址为2^20,而保护模式下内存最大寻址为2^32),这样,可以在解压kernel.img后,将其载入至内存空间内执行。

而在切换为保护模式后,计算机会处理如下工作:

  1. 启用分段:段寄存器指向段描述符表,用于隔离不同进程
  2. 启动分页:将内存分页,方便程序使用以及申请
  3. 打开Gate A20开关:在实模式下只有20根地址线,可以访问1M内存空间;所以,在保护模式下,需要打开这个开关,用于访问超过1M空间的内存

之后,将会解析grub的配置文件(/boot/grub/grub.conf),并将其配置显示,用户可以选择其中一个操作系统载入。

在用户选择某个操作系统后,GRUB会检查linux内核头文件信息是否完整和正常,并加载linux内核镜像到内存;最终执行grub_command_execute(“boot”, 0,0)方法启动内核,将控制权交由给内核。

内核启动

加载内核镜像至内存后,linux如何做系统初始化以及启动第一个用户进程?

内核会处理如下事项:

  1. 初始化进程栈以及初始化0号进程(该进程是唯一没有fork或者通过kernel_thread产生的进程)
  2. 中断门初始化
  3. 内存管理初始化
  4. 调度系统初始化
  5. 虚拟文件系统初始化
  6. 1号进程初始化
  7. 2号进程初始化

着重对于1号进程以及2号进程的初始化做具体分析

1号进程初始化

在初始完成线程栈、中断门、MMU、VFS后会开始执行创建第二个进程,也就是1号进程;这个进程将会运行第一个用户进程。

它会尝试通过一个初始化文件来执行,不同linux版本可能运行的文件不同:

  1. /sbin/init
  2. /etc/init
  3. /bin/init
  4. /bin/sh

执行init文件的过程中,它会处理如下事项?

  1. 载入ELF(用于规定程序载入的内存格式)
  2. 启动用户线程

自此,1号进程作为所有用户态进程存在

2号进程初始化

在1号进程完成用户线程启动后,rest_init函数还创建了2号进程用于管理所有的内核态进程。

总结

linux启动过程在此告一段落,之后可能会对这个过程做更详细的分析以及说明。