迷茫的时候,就朝着热情的方向走。
BIOS 的引导
当按下开机键,你的主板开始加电,在刚加电的时候电脑会做一些初始化寄存器的工作,比如将 CS代码段寄存器 设置为 0xFFFF ,将 IP指令寄存器 设置为 0x0000,这样我们第一条指令就会指向 0xFFFF0 ,于是 CPU 执行这条指令,这条指令的内容很简单,就是跳转到指定位置去进行 BIOS 的初始化工作。
那么,所谓的指定位置究竟是在哪呢?这就要从 实模式 开始讲了。
说完实模式的由来,我们继续来看看第一段所说的指定位置是什么意思。现在我们拥有了 1MB 的内存空间,那么我们该如何使用呢?首先我们要启动 BIOS 吧,它是一个程序肯定要占用内存的,那么我们就给 BIOS 分配内存。所以我们内存就划出一块 ROM 区域来映射 BIOS 的 ROM 。
然后我们开始运行 BIOS 程序,首先 BIOS 先检查身边的硬件是否有故障(即 POST 加电自检)。如果你曾经进入过 BIOS 界面,你就会发现你可以获取像内存、处理器、硬盘等硬件数据。
然后进行分配中断、IO端口、DMA资源等,这个时候会建立一个中断向量表和中断服务程序主要用于用户进行键盘和鼠标操作。
比如我们在重装系统需要进入 BIOS 界面的时候需要在开机那几秒中敲击键盘的某一个键,这中间的几秒中其实就是 BIOS 在进行加电自检(有些主板可以设置 POST 延迟),如果经过这几秒你没有选择进入 BIOS ,那么 BIOS 就会进行默认启动项的加载,这也是 BIOS 最后的任务了,即选择启动设备运行其 引导程序boot 。
boot引导程序的工作
上面提到了 boot引导程序 只有512字节(除去末尾标识其实也就510字节),这么大能干什么?好吧干不了什么。你可以理解为加载操作系统就是一次火箭升空,需要一级一级的助推器,而 boot 就是其中的一级。
所谓助推器就是通过它再加载其他引导程序,就拿 GRUB (一种操作系统启动管理器) 举例,510字节是无法装载全部的 GRUB 的,而在这510字节所干的事情就是 装载第二引导装载程序 。
而在这个时候就又要考虑一个问题了,假设你现在是 boot 程序的开发者,你想使用 boot 去加载 loader 程序(也就是第二引导装载程序),那么一种办法是将 loader 也像 boot 一样放在磁盘的指定物理位置,然后将这个物理位置硬编码在 boot 程序中。毫无疑问,这肯定会带来诸多问题,比如说硬编码的方式比较 死板不灵活 ,没有文件系统支持就必须保证之后的程序在磁盘上是连续的。当然这也是一种办法,像早期普遍使用的 LILO 引导程序就是这么干的。
与其以后需要一直承受这种硬编码的折磨和痛苦,那倒不如长痛不如短痛,我可以直接在 boot 程序中去 创建一个简单文件系统 ,而 GRUB 就是这么做的。甚至我们可以理解为 GRUB 是一个小型的操作系统。
总之 boot 的主要任务就是创建了简易文件系统并且去装载第二引导程序了。
Loader引导加载程序
随着 boot 这个一级助推器脱离火箭,处理器的控制权就移交给了 Loader 程序。而这个 Loader 主要干了三件事情。
检测硬件信息
模式切换
向内核传递数据,启动内核
首先就是通过 BIOS 的中断服务去获取硬件信息,这是为了第三步给内核传输数据做准备。
抛开了 1MB 的窄小空间,这个时候我们就可以真正有所作为了。刚刚我们第一步获取的硬件信息就有用了,还有就是设置一些内核启动参数,然后去将信息输入到 内核启动程序 。伴随着 Loader 程序最后一条指令的完成,内核就开始启动了。
内核头程序
不是说现在内核开始启动了么,这个 内核头程序 又是什么呢?在内核启动前还需要进行 全局段描述表(GDT)、中断描述表(IDT) 以及 页表 的结构初始化,为后面内核进行 中断处理 和 内存管理 的初始化奠定基础。而内核头程序就是来干这个的,它是一段特殊的汇编代码,必须在内核程序执行之前得到执行。
内核初始化
当进入内核初始化之后,整个主程序就会调用一系列的初始化函数,第一个当然是创建0号进程了,它是唯一一个没有经过 fork() 和 kernel_thread() 产生的进程。
回想一下 BIOS ,在进行 POST加电自检 后立马就进行了中断处理的初始化,因为在一个程序进行执行的时候我们必须首先要考虑到异常情况,所以内核初始化过程中在进程的初始化完成之后首要的就是 进行中断处理的初始化 。依托于内核头程序完成的 IDT 以及 GDT ,我们在进行一些中断策略等初始化操作就完成了中断处理的初始化了。
中断初始化完成之后就是 内存管理初始化 ,对于内存管理的初始化,主要就是 如何获取物理内存信息,如何进行物理内存的分配管理以及如何设计可用内存和可用物理页的分配与回收 等等。
内存管理初始化初始化完成之后就需要进行 进程管理的初始化 了。而对于进程管理我们可以分为两个步骤,如何设计我们的 进程控制结构(PCB) 以及如何设计 进程间的调度策略 。
等到文件系统初始化完成之后我们就需要 创建1号进程 ,也就是我们的 第一个用户进程 。在虚拟文件系统初始化完成之后就需要进入用户态以完成真正的用户文件系统的创建过程。如果说 grub 中有配置 initrd 的话就会首先执行再 ramdisk 中的 init ,这个 init 主要干的事情就是先根据存储系统的类型 加载驱动 ,有了驱动之后我们就可以通过 ramdisk 去设置真正的根文件系统了,然后我们就可以访问根文件系统中的 init 程序做一些用户态的初始化了。
当用户进程有了祖先之后就会创建内核进程的祖先,也就是 2号进程 。它的作用很简单,就是 管理调度其它的内核线程 。它会一直循环运行 kthreadd 函数,这个函数主要就是 负责所有内核态的线程的调度和管理 。
等到内核态也有了管理者,整个内核的初始化流程也就结束了。用户就可以开始自己创建进程,运行各种软件了。
本文使用 mdnice 排版