深入理解Linux启动过程
本文详细分析了Linux桌面操作系统的启动过程,涉及到BIOS系统、LILO 和GRUB引导装载程序,以及bootsect、setup、vmlinux等映像文件,并结合引导、启动原理和具体的代码实现机制由浅入深地进行了分析。
初学者刚接触Linux桌面系统会感觉系统启动速度较慢,那么,为什么它的启动速度慢呢?本文就桌面系统的引导和启动过程展开分析,以期对初学者熟悉Linux有所帮助。
一、Linux系统的引导过程
简单地说,系统的引导和启动过程就是计算机加电以后所要发生的事情, 比如,加电自检、引导程序的拷贝和执行、内核的拷贝和执行及用户程序的执行等。这个过程就是常说的bootstrap,我们把这些归纳为5个过程, 下面来逐一分析。
1.BIOS执行阶段
现代计算机系统的存储机制是“挥发”性的,一旦关机断电, 存储在内存中的信息。连同操作系统本身的映射就丢失了。所以,必须把操作系统(内核) 的映像存储在某些不“挥发” 的介质中,使得开机加电时由一个不“挥发”介质加载操作系统,并转入运行的过程。这就是引导,也称自举。这些不“挥发” 介质通常是指硬盘或软盘, 也可以是EPROM 或F1ash存储器,还可以是网络中别的节点。要想在开机时从不“挥发” 介质装入操作系统的映像,系统就要CPU在开机时能执行一段程序,这段程序本身必须存储在作为系统内存一部分的EPROM 或Flash等存储器中, 而且它们知道怎样才能从不“挥发” 介质装入操作系统的映像。事实上,各种CPU 被设计成一个加电后就从某个特殊的地址开始执行指令,所以这些不挥发存储器就被安置在这个位置上。比如在i386CPU系统中,计算机在加电的那一刻,RAM 芯片中所包含的是随机数据,还没有操作系统,在此刻有一个特殊硬件电路在加电时会在C P U 的一个引脚上产生一个RESET逻辑值,硬件电路设置RESET逻辑值以后,代码寄存器CS的内容为0xffff,而指令寄存器的内容为0。也就是说,CPU要从线性地址0xffff0开始处取第一条指令。硬件电路再把这个物理地址映射到RAM 芯片中,BIOS就存放在这里,这时候处理器就开始执行BIOS代码了。我们都知道BIOS中包含了几个中断驱动的低级程序,可以使用它们来初始化一些硬件设备,但它们是在实模式下工作的。其中实模式地址是由一个seg段和一个off偏移量组成的,相应的物理地址可以使用“seg*16+off” 来计算。
接下来BIOS要做的就是执行一系列的测试,看看到底系统中有什么设备,以及这些设备是否正常工作。在执行这个过程时,会显示一些如BIOS系统的版本号等信息。当检测到可用的设备后就进行一些初始化工作,比如初始化PCI设备以避免I RQ线与I/O端口的冲突,最后显示系统中安装的所有PCI设备的一个列表。
在早期的计算机系统中, 类似于BIOS功能的程序非常小,并且不同时期这段程序的设计也不相同。在PC发展早期, 由于当时存储芯片大小的限制,使得该段程序的目的和功能都很单一 再说,如此小的一段程序很难依靠自身的力量把庞大的操作系统的映像从磁盘里读进来。于足,人们又提出了引导扇区的概念,使得存储在引导扇区中的程序来协助BIOS完成操作系统的引导工作。但是,引导扇区的大小也不过5l2个字节, 能够容纳的信息和代码也很有限,所以说,操作系统的引导代码是一个循序渐进的过程,它分布在不同的角落。当BlOS根据设置将相应的启动设备的第一个扇区的内容拷贝到RAM 中时, 这些内容被放在物理地址0x0O007c00开始的地方。此后,系统就开始跳到这个地址,并开始执行相应的代码。
2.Boot Loader阶段
如此小的引导记录要完成这么大的任务,压力是不小的,所以引导扇区的程序及辅助程序必须很简练,它们都采用汇编语言编写,这些源代码都存放在arch/ 下具体CPU名下的boot目录中,如bootsect.S、setup.S和video.S。其中bootsect.S是Linux引导扇区的源代码。这样,经过编译、汇编和连接以后,形成了3个组成部分,即引导扇区的映像bootsect、辅助程序setup及内核映像本身(通常是vmlinux,有时也用uImage)。严格地说,bootsect和setup并不是内核的一部分。
引导装载程序就是由BIOS来把操作系统的内核映像装入到RAM 中所调用的一个程序。这里我们选择用硬盘启动来说明引导装载程序的执行过程。说起硬盘,大家都知道它是由许许多多的扇区和柱面组成,其中把第一个扇区称为主引导记录(Master Boot Record,MBR),在该扇区中包含了分区信息和一个小程序,这个小程序用来装载被启动的操作系统所在分区的第一个扇区。说到这里我们就要注意,这一段Windows系统和Linux系统是有区别的:Windows系统使用分区表中所包含的一个active标志来标识这个分区,当然这个分区也可以使用FDISK之类的程序进行设置,但有一个条件就是只有那些内核映像存放在活动分区的操作系统才可以启动。Linux系统的处理方法要更灵活些,它使用GRUB或是LILO程序把这个包含在MBR 中的不完善引导装载程序给替掉。装入程序在启动过程中被执行时,用户可以选择装入哪个操作系统。但LILO和GRUB的工作原理又不尽相同,关于它们的详细介绍可以查阅相关资料。LILO 引导装入程序被分为两部分,MBR 或分区引导扇区包括一个小的引导装入程序,由BIOS把这个小程序装入从地址0xO0007c00开始的RAM 中,这个小程序又把自己移到地址0x0009a000, 然后建立实模式栈。接着把LILO 的第二部分装入到从地址0x0009b000开始的RAM 中, 第二部分又读取可用操作系统的映射表,并给用户一个提示符号。这个时候用户可以从中选择一个操作系统进行启动,引导装入程序就可以把相应分区的引导扇区拷贝到RAM 中并执行,或者是直接把内核映像拷贝到RAM 中。在拷贝内核的过程中,首先是把内核映像所集成的引导装入程序拷贝到地址0xO009000,然后把setup()代码拷贝到地址0x00090200,最后把内核映像的其余部分拷贝到地址0x00010000或0x00100000, 最终系统执行跳到setup()代码上。
3.Setup函数执行阶段
Setup()是汇编语言函数代码,它在内核的编译链接过程中被放到内核的引导装入程序之后,也就是内核映像文件的偏移量0x200地址处,实际物理地址0x00090200开始的RAM 中。因为内核不依赖于BIOS, 虽然BIOS已经初始化了大部分硬件设备,但Linux系统还要以自己的方式重新初始化设备,以增加可移植性和健壮性。还要注意的是,内核是工作在保护模式下的。总的来说,setup()函数的作用就是初始化计算机中的硬件设备,并为内核程序的执行建立环境。比如,检查系统中可用的RAM 数量、设置键盘重复延时速率、显卡等其他设备的检查,以及初始化和切换实模式到保护模式等。最后,系统执行跳到startup_ 32汇编函数上。
二、Linux系统的启动过程
当内核映像被装载到RAM芯片后,就开始执行内核的代码,这意味着引导完成,开始进入Linux系统的启动过程。
1. Startup_32函数的执行阶段
在系统的启动过程中有两个startup_320()函数,即位于arch/i386/boot/compressed/head.S文件中实现的。就是在setup()函数结束以后,该函数就被移动到物理地址0X00100000或0x00001000处,这取决于内核映像是被装到RAM 的高位还是底位。因为内核映像文件在编译连接时所产生的大小不同, 如zImage和bzImage大小相差很大,在装载解压时所使用的缓冲区也不同,所以他们所处的物理地址是不同的。不过解压后的映像最终都处在物理地址0x00100000开始的位置。然后跳转到这个地址处执行解压后的映像中的另一个startup_32()函数,这个函数为第一个Linux进程(进程0)建立执行环境,该函数初始化段寄存器、为进程0建立内核态堆栈等一系列活动。最后识别处理器模式,并跳转到start_kernel()函数。将Linux内核的映像装入内存,并且setup()函数做了一些必要的准备,就该startup_32函数开始干活了。CPU通过一条长程转移指令转到映像代码段开头的入口startup_32处,对于SMP结构的系统来说,这个时候运行的只是其中的一个处理器,就是所谓的主CPU。其他的次CPU 处于停机状态, 等待主CPU 的启动。次CPU在受到启动进入内核时,同样也要从startup_32开始执行,所以从startup_32开始的代码是公共的。但有些操作仅由主CPU来执行,另一些操作由次CPU执行, 这并不意味着主CPU 和次CPU 并发地执行这段程序。实际上,主CPU 是开路先锋,首先执行这段程序,完成以后逐个启动次CPU执行,并且等待其完成。所以,在同一时间系统中最多只有一个处理器在执行这段程序。不管是主CPU还是次CPU,进入startup_32时都运行在保护模式下的段式寻址方式,等到第二个startup_32函数执行到最后时, 就开始执行start_kernel函数。
2. Start_kernel函数执行阶段
到了这个阶段才是真正的内核初始化阶段,几乎内核每个部分的初始化工作都是由这个函数来完成,如页表的初始化、系统日期和时间的初始化等。从某种意义上说,函数Start_kernel就好像一般可执行程序中的主函数main(),系统在进入这个函数之前已经进行了一些最底限度的初始化,为这个函数的执行建立起了一个环境,创造了必要的条件。当然,这个函数还要继续进行内核的初始化,甚至可以说内核的初始化在这里才真正开始,但它是较高层次的初始化。这个函数的代码在init/main.C中,从现在开始初始化流程不与CPU 类型和系统启动;方式相关了。此时系统运行在CPU的特权级,也就是我们常说的内
核模式下。start_ kernel函数主要完成一些数据结构的初始化,主要包括
如下:
printk(linux_banner) 输出
Linux版本信息;
Setup_arch()(arch/i386/kernel/traps.C)执行与体系结构相关的设置,如内存分析分配内
核页表, 处理启动命令行等;
Trap_init() 设置各种人口地址,如异常事件处理程序入口, 系统调用人口,
IniLIRQ() 初始化IRQ 中断处理机制;
Sched_init() 设置并启动第一个进程ini_task0 l
Softirq_init() 对软中断子系统进行初始化;
Time_initO 读取实时时间,重新设置时钟中断irq0的中断服务程序入口等;
Console__init() 初始化控制台和显示器;
Init_modules() 初始化
kernel__m odule l
Kmem_cache_init0 对内存的slab分配机制初始化{
Mem_init() 虚拟内存计算以及初始化;
Kmem_cache_size_jnit() 初始化slab分配器中的内部cashe和全局cashel
Fork_init() 定义了系统的最大进程数目。此外,还有一些对其他支持的初始化。
随后,进入reset—init0函数调用kernel__thread()函数为进程1创建init内核线程,这个内核线程又会创建其他的内核线程程序,并执行/sbin/init程序。此后start_kernel进入一个空闲等待循环(cpu_idle()), 使用系统初始化后CPU 的空闲时间片,init内核线程首先要锁定内核,然后调用do_basic_setup()来初始化外部设备及加载驱动程序。在do_basic_setup()函数调用完之后,init()函数会释放初始化函数所用的内存,并且打开/dev/console设备重新定向控制台,使用系统调用execve来执行用户态程序/sbin/init。
到目前为止,Linux内核的初始化工作完成,此时系统中已经存在5个运行实体:init线程、kflushd核心线程、kupdate核心线程、kswapd核心线程和keventd核心线程。本身所在的执行体其实就是一个线程,不过是由手工创建的。它在创建了init0线程以后就进入cpu_idle循环, 不会在进程列表中出现。如果使用pstree命令,则不能列出该线程。
最后,init程序会根据inittab文件中的设置信息启动相应的用户程序。当init得到控制并启动mingetty显示登录界面及提示后,系统启动完成。
三、小结
从加电自检开始, 引导过程要经历数十个回合来拷贝执行,使用不同的引导装载程序所使角的流程也不同。当把内核映像拷贝到RAM中展开后, 内核开始掌管主权,开始了自己的 “事业”。内核线程init()的任务仍然还是初始化,当然是进一步的、更高层次上的初始化。
事实上,从引导结束、CPU转入内核映像开始,一共有三个阶段的初始化:第一阶段是从进入startup_32()开始, 到进入start_kernel()或start_ secondary()。这个阶段主要是对CPU 自身的初始化,主CPU和次CPU 都要经历这种初始化,但是主CPU要多一些贡献。第二阶段是从进入start_kernel()开始,到进入cpu_idle()。这个阶段主要是对系统的宝贵资源的初始化, 仅由主CPU进行。第三阶段是init()的执行,这是对系统接近用户层的初始化, 这个时候表面上看已经没:有主CPU和次CPU之分,但谁执行init()取决于竞争调度的结果。事实上,由于主CPU预先留了一手, 实际上还是由它来执行。