Linux启动过程简述

BIOS加载bootloader阶段

计算机接电之后第一个启动的是BIOS,因为BIOS是存储在主板上的一个小程序,空间有限代码量少因此功能受限。要经过一步步的控制权转移,最后转移给功能更加强大的操作系统。

BIOS对系统做一些简单的初始化工作,然后把控制权交给MBR。约定MBR(Main Boot Record)存在于整个硬盘最开始的扇区。每个扇区都是512字节,MBR引导扇区的内容是:

446字节的引导程序及参数

64字节的分区表(每个分区表项16字节,因此只能有4个主分区)

2字节的结束标志0x55和0xaa

0x55和0xaa是MBR结束标志,0x7c00是MBR被加载在内存中的位置。

BIOS结束自己的工作以后,需要把控制权交给MBR,过程是:BIOS找到MBR并且把MBR加载在内存中,跳转到该位置。

为了方便BIOS寻找,约定好0盘0道1扇区(第一个扇区)用来存储MBR。MBR内容占510个字节,剩余两个字节用作结束标志。BIOS不用遍历这个扇区,只要检测到有这个标志就认为这个扇区有可以运行的MBR存在,否则BIOS会报错。这两个结束标志是0x55和0xaa。

关于0x7c00,这是一个历史遗留问题。MBR的名称是主引导记录,MBR主要工作是:建立分区表、引导其他更复杂的程序。8086cpu要求0~30k位置用来存储中断向量表,DOS1.0要求的最小内存是32k,因此MBR必须存储在钱32k中,中断向量表长度不一定,MBR希望自己有足够的空间运行(MBR也是程序,程序运行需要用到栈,MBR预估自己需要用到512字节的自身存储和512字节的堆栈空间),同时尽可能多的留给向量表足够的空间。综上,MBR存储在32k的最后1k空间,也就是从0x7c00开始。

MBR的主要工作是什么呢?

MBR加载bootloader

执行完了自己的工作,需要把控制权交出去。MBR遍历分区表中的4个分区,查找操作系统加载器,找到后把CPU交给加载器。MBR怎么知道加载器存在哪个分区呢?分区表项到 第一个字节就是活动分区标志,如果该分区存储了加载器,该标志被置为0x80,否则是0.MBR只需要遍历分区表就能找到下一棒选手应该是谁。这里为了方便MBR找到活动分区上的(因为程序跳转是需要指明跳转地址的),约定好加载器就存储在各分区的开始扇区,这个扇区被称为操作系统引导扇区也称为,这个OBR就是我们常见的x86平台上的grub或者arm平台上的uboot,这个程序负责的主要工作是加载kernel image 并解压缩。

bootloader加载kernel

CRUB会根据需求显示一个可用的内核列表(定义在/etc/grub.con,以及/etc/grub/menu.lst和/etc/grub.conf的软连接)。你可以选中一个内核,并且可以用附加的内核参数改进它。另外,你还能通过shell终端命令行的方式手动控制整个启动过程。

Bootloader完成CPU、RAM、网卡等信息的初始化,建立内存的映射关系。在外存上找到kernel image,启动IO读入内存。一般kernel image使用zlib压缩为zImage或者bzImage格式,读取了image以后,在内核镜像的头部,有一个小型的routine,做一些简单的硬件设置,解压缩至高端内存(输出解压缩提示信息:Decompressing Linux...),加载initrd(注意:这个在后面会作为临时根文件系统存在)进内存,执行kernel通过<code>./arch/i386/boot/head/<code>。

kernel初始化

Bootloader在把控制权交给kernel时候,会传递一些参数。这个参数可以是用户通过bootloader设置的(例如:grub的配置文件grub.conf),或者是bootloader自己检测到的硬件信息(例如:根设备标识、页面大小、内核需要的命令信息)。

内核先是设置系统的状态(此部分通过汇编程序head.S完成):查看CPU类型检查kernel是否支持此种类型,查找体系类型,初始化寄存器,创建页表,开启MMU,建立IDT、GDT、LDT,跳转到入口函数start_kernel(main.c中,是第一个C函数)处。

在main开始运行的时,PC处于一个实模式,CPU执行一些必要操作后跳转到保护模式。这里,有两个很重要的问题:中断和内存。在实模式下,处理器的中断向量表始终位于存储地址0(最开始的位置),而在保护模式下,中断向量表的位置存储在称为IDTR的CPU寄存器中。同是逻辑地址到线性地址转换实模式和保护模式是不一样的,保护模式需要一个称为GDTR的寄存器来加载内存的全局描述符表的地址。因此,<code>go_to_protected_mode/<code>调用<code>setup_idt/<code>和<code>setup_gdt/<code>来安装临时中断描述符表和全局描述符表。

现在我们准备好了跳转到保护模式了,调用<code>protected_mode_jump/<code>,这个函数设置CR0寄存器的PE位,打开A19地址线等等操作跳转到保护模式。注意此时分页还是被禁用的,因为暂时还不需要分页。现在,内存终于可以寻址到4GB,调用32位内核入口点<code>startup_32/<code>。

<code>startup_32/<code>调用<code>decompress_kernel/<code>解压缩真正的内核,并在显示器上打印信息Decompress Linux...。解压缩过的kernel此时会覆盖解压缩之前的内核镜像。解压缩结束以后,清楚BSS段设置最终的GDT和IDT,构建页表,打开分页初始化堆栈,最后跳转到<code>start_kernel/<code>。 上面的过程如下所示:

start_kernel

执行各种初始化函数:初始化IRQ、时钟、CPU、内存、VFS、中断向量表、内核子系统等等。最后调用

rest_init

,这几乎是start_kernel的全部工作,

rest_init

创建一个内核线程,执行

kernel_init

rest_init

调用

schedule

启动任务调度,调用

cpu_idle

cpu_idle

永远运行,只有当进程就绪列表中有就绪进程时候,cpu_idle才会被从CPU上拉下来,当没有进程时候,这个空闲进程又被拉起来执行,这就是进程0。进程0漫长的工作终于结束了,从BIOS、MBR、Bootloader最后到kernel。虽然进程0从启动的舞台上退出了,这并没有意味着kernel启动完成了。

进程0刚刚创建了一个内核线程执行

kernel_init

,还记得吗?我们现在看看这个线程的运行。

kernel_init

负责初始化启动剩下的CPU,从启动分析到现在,我们只有一个CPU在运行注意到了吗?这个CPU我们成为引导CPU,剩下的应用CPU初始化依然需要从实模式开始。最后

kernel_init

调用

init_post

,这个函数尝试执行一下用户进程:

/sbin/init

/etc/init

/bin/init

/bin/sh

,如果全部失败,产生kernel panic。如果上述程序存在,创建进程1运行(这个进程是所有用户进程的父进程)。过程如下图所示:

init读取的第一个文件是

/etc/inittab

,从这里init决定了我们的Linux操作系统的运行级别。Linux系统有7个运行级别(runlevel):

level 0:系统停机状态

level 1:单用户工作状态

level 2:多用户状态

level 3:完全多用户状态

level 4:保留

level 5:X11

level 6:reboot

Init从/etc/fstab

文件中查找分区表信息,用真正的根文件系统替换initrd。Init然后启动默认运行级别的/etc/init.d目录中指定的所有服务或者脚本。这是所有服务由init逐个初始化的步骤。在这个过程中,一次一个服务由init启动,所有守护程序在后台运行,init继续管理它们。

至此,内核启动基本结束了。