Linux启动过程

来源:互联网 发布:mac 格式化硬盘 重装 编辑:程序博客网 时间:2024/05/13 10:01

 最近在学操作系统,随手翻了翻《深入理解Linux内核》,虽然有很多东西不是很理解,但是附录关于Linux的启动过程写的还是比较有意思的,遂手打下来分享一下。

系统启动

     本文介绍当用户打开计算机电源之后所发生的事情,也就是说,Linux内核映像是如何被拷贝进内存的,又是如何被执行的。简而言之,我们讨论内核,继而是整个系统。

    “启动(bootstrap)”这个术语的原意是一个热让你要穿上鞋子站起来。在操作系统中,这个术语专门表示把一部分操作系统装载到主存中并让处理器执行它,也就表示内核数据结构的初始化,一些用户进程的创建以及把控制权转移到其中某个进程。

    计算机启动时一个冗长乏味的任务,因为最开始时几乎每个硬件设备(包括RAM)都处于一种随机的、不可预知的状态。此外,启动过程在很大程度上都依赖于计算机的体系结构;和以前一样,我们在本文中特指80x86体系结构。

 

史前时代:BIOS

    计算机在加电的那一刻几乎是毫无用处的,因为RAM芯片中包含的随机数据,此时还没有操作系统在运行。在开始启动时,有一个特殊的硬件电路在CPU的一个引脚上产生一个RESET逻辑值。在RESET产生以后,就把处理器的一些寄存器(包括cs和eip)设置成固定的值,并执行在物理地址0xfffffff0处找到的代码。硬件把这个地址映射到某个只读。持久的存储芯片中,该芯片通常称为ROM(Read-Only Memory, 只读储存器)。ROM中所存放的程序集在80x86体系中叫做基本输入/输出系统(Basic Input/Output System, BIOS),因为它包括几个中断驱动的低级过程。所有操作系统在启动时,都要通过这些过程对计算机硬件设备初始化。一些操作系统,如微软的MS-DOS,依赖于BIOS实现大部分系统调用。

    Linux一旦进入保护模式,就不再使用BIOS,而是为计算机上的每个硬件设备提供各自的设备驱动程序。实际上,因为BIOS过程必须在实模式下运行,所以即使是有益,两者之间也不能共享函数。

    BIOS使用实模式的地址,因为在计算机加电启动时只有这些可以使用。一个实模式的地址由一个seg段和一个off偏移量组成。相应的物理地址可以这样计算:seg * 16 + off。所以CPU寻址电路根本就不需要全局描述符表、局部描述符表或者页表把逻辑地址转换成物理地址。显然,对GDT、LDT和页表进行初始化的代码必须在实模式下运行。

    Linux在启动阶段必须使用BIOS,此时Linux必须要从磁盘或者其他外部设备中获得内核映像。BIOS启动过程实际上执行了以下4个操作:

    1.对计算机硬件执行一系列的测试,用来检测现在都有什么设备以及这些设备是否正常工作。这个阶段通常称为POST(Power-On Self-Test, 上电自检)。在这个阶段中,会显示一些信息,例如BIOS版本号。

      如今的80x86、AMD64和Itanium计算机使用高级配置与开机界面(Advanced Configuration and Power Interface, ACPI)标准。在ACPI兼容的BIOS中,启动代码会建立几个表来描述当前系统中的硬件设备。这些表的格式独立于设备生产商,而且可由操作系统读取以获得如何调用这些设备的信息。

    2.初始化硬件设备。这个阶段在现代基于PCI的体系结构中相当重要,因为它可以保证所有的硬件设备操作不会引起IRQ线与I/O端口的冲突。在本阶段的最后,会显示系统中所安装的所有PCI设备的一个列表。

    3.搜索一个操作系统来启动。实际上,根据BIOS的设置,这个过程可能要试图访问(按照用户预定义的次序)系统中软盘、硬盘和CD-ROM的第一个扇区(引导扇区)。

    4.只要找到一个有效的设备,就把第一个扇区的内容拷贝到RAM中从物理地址0x00007c00开始的位置,然后跳转到这个地址处,开始执行刚才装载进来的代码。

 

远古时代:引导装入程序

    引导装入程序(boot loader)是由BIOS用来把操作系统的内核映像装载到RAM中所调用的一个程序。让我们简要地描绘一下引导装入程序在IBM的PC体系结构中是如何工作的。

    为了从软盘上启动,必须把第一个扇区中所存放的指令装载到RAM中并执行;这些指令再把包含内核映像的其他所有扇区都拷贝到RAM中。

    从硬盘启动的实现有所不同。硬盘的第一个扇区称为主引导记录“Master Boot Record, MBR”,该扇区中包括分区表和一个小程序,这个小程序用来装载被启动的操作系统所在的第一个扇区。诸如Microsoft Windows 98之类的操作系统使用分区表中所包含的一个活动(active)标志来标识这个分区。按照这种方法,只有那些内核映像存放在活动分区中的操作系统才可以被启动。正如我们将在后面看到的一样,Linux的处理方式更加灵活,因为Linux使用一个巧妙的引导装入程序取代这个MBR中不完善的程序,它允许用户来选择要启动的操作系统。

    Linux早期版本(一直到2.4系列)的内核映像,在第一个512字节有一个最小的引导装入程序,因此在第一个扇区拷贝一个内核映像就可以使软盘可启动。但是在Linux 2.6中就不再有这样的引导装入程序。而现在从软盘启动与从硬盘或CD-ROM启动时十分相似的。

 

从磁盘启动Linux

    从磁盘启动Linux内核需要一个两步的引导装入程序。在80x86体系中,众所周知的Linux引导装入程序叫做LInux LOader(LILO)。确实还有一些80x86体系的引导装入程序,如广泛使用的GRand Unified Bootloader(GRUB)。GRUB比LILO更先进,因为它可识别多个基于磁盘的操作系统,而且可以从文件中读入部分引导程序。当然,对于Linux支持的所有体系结构都有各自的专门引导装入程序、

    LILO或许被装在MBR上(代替那个装载活动引导扇区的小程序),或许被装在每个磁盘分区的引导扇区上。在这两种情况下,最终的结果是相同的:装入程序在启动过程中北执行时,用户都可以选择装入哪个操作系统。

    实际上,LILO引导装入程序被分为两部分,因为不划分的话,它就太大而无法装进单个扇区。MBR或者分区引导扇区包括一个小的引导装入程序,由BIOS把这个小程序装入从地址0x00007c00开始的RAM中。这个小程序又把自己移到地址0x00096a00,建立实模式栈(0x00098000 - 0x000969ff),并把LILO的第二部分装入到从地址0x00096c00开始的RAM中。

    第二部分又依次从磁盘读取可用操作系统的映射表,并提供给用户一个提示符,因此用户就可以从中选择一个操作系统。最后,用户选择了被装入的内核后(或经过一个延迟时间以使LILO选择一个缺省值),引导装入程序就可以把相应分区的引导扇区拷贝到RAM中并执行它,或直接把内核映像拷贝到RAM中。

    假定Linux内核映像必须被导入,LILO引导装入程序依赖于BIOS例程,主要执行如下步骤:

    1.调用一个BIOS过程显示“Loading”信息。

    2.调用一个BIOS过程从磁盘装入内核映像的初始部分,即将内核映像的第一个512字节从地址0x00090000开始存入RAM中,而将setup()函数的代码从地址0x00090200开始存入RAM中。

    3.调用一个BIOS过程从磁盘中装载其余的内核映像,并把内核映像放入葱低地址0x00010000(适用于使用make bzImage编译的小内核映像)或者从高地址0x00100000(适用于使用make bzImage编译的大内核映像)开始的RAM中。在以下的讨论中,我们分别称内核映像是“低装载”到RAM中或者“高装载”到RAM中。大内核映像的支持虽然本质与其他启动模式相同,但是它却把数据放到不同的物理内存地址,以避免遇到ISA黑洞问题。

    4.跳转到setup()代码。

 

中世纪:setup()函数

    setup()汇编语言函数的代码由链接程序放在内核映像文件的偏移量0x200处。引导装入程序因此就可以很容易地确定setup()代码的位置,并把它拷贝到从物理地址0x00090200开始的RAM中。

    setup()函数必须初始化计算机中的硬件设备,并为内核程序的执行建立环境。虽然BIOS已经初始化了大部分硬件设备,但是Linux并不依赖BIOS,而是以自己的方式重新初始化设备以增强可移植性和健壮性。setup()本质上执行以下操作:

    1.在ACPI兼容的系统中,它调用一个BIOS例程,以在RAM中建立系统物理内存布局表。在早期系统中,它调用BIOS例程,返回系统可用内存。

    2.设置键盘重复延时和速率(当用户一直按下一个键超过一定的时间,键盘设备就反复向CPU发送相应的键盘码)。

    3.初始化视频卡。

    4.重新初始化磁盘控制器并检测硬盘参数。

    5.检查IBM微通道总线(MCA)。

    6.检查PS/2指针设备(总线鼠标)。

    7.检查堆高级电源管理(APM)BIOS的支持。

    8.如果BIOS支持增强磁盘驱动服务(Enhanced Disk Drive Service, EDD),它就调用相应的BIOS过程在RAM中建立系统可用硬盘表(表中的信息可以通过sysfs特殊文件系统的firmware/edd目录查看)。

    9.如果内核映像被低装载到RAM中(在物理地址0x00010000处),就把它移动到物理地址0x00001000处。反之,如果内核映像被高装载到RAM中,就不用移动。这个步骤是必须的,因为为了能在软盘上储存内核映像并节省启动的时间,存放在磁盘上的内核映像都是压缩的,解压程序需要一些空闲空间作为临时缓冲区(在RAM中紧挨内核映像的地方)。

    10.置位8042键盘控制器的A20引脚。A20是在80286系统中引入的,为的是与古老的8088微处理器物理地址兼容。不幸的是,在切换到保护模式之前必须将A20引脚正确置位,否则,每个物理地址的第21位都会被CPU看做0.置位A20引脚是件讨厌的事。

    11.建立一个临时中断描述符表(IDT)和一个临时全局描述符表(GDT)。

    12.如果需要,重置浮点单元(FPU)。

    13.重新编写可编程中断控制器(Programmable Interrupt Controller, PIC),以屏蔽所有中断,但保留IRQ2,它是两个PIC之间的级联中断。

    14.通过设置cr0状态寄存器中的PE位,把CPU从实模式切换到保护模式。cr0状态寄存器中的PG位被清0,因此分页还没有启用。

    15.跳转到startup_32()汇编语言函数。

 

文艺复兴时期:startup_32()函数

    有两个不同的startup_32()函数,我们此处所指的是在arch/i386/boot/compressed/head.S文件中实现的那一个。在setup()结束之后,startup_32()就已经被移动到物理地址0x00100000处或者0x00001000处,这取决于内核映像是被高装载到RAM中还是低装载到RAM中。

    该函数执行以下操作:

    1.初始化段寄存器和一个临时堆栈。

    2.清零eflags寄存器的所有位。

    3.用0填充由_edata和_end符号标识的内核未初始化数据区。

    4.调用decompress_kernel()函数来解压内核映像。首先显示“Uncompromising Linux ...”信息。完成内核映像的解压之后,显示“OK, booting the kernel.”信息。如果内核映像是低装载的,那么解压后的内核就被放在物理地址0x00100000处。否则,如果内核映像是高装载的,那么解压后的内核就被放在位于这个压缩映像之后的临时缓冲区中。然后,解压后的影响就被移动到从物理地址0x00100000开始的最终位置。

    5.跳转到物理地址0x00100000处。

    解压的内核映像以包含在arch/i3 86/kernel/head.S中的另一个startup_32()函数开始。这两个函数使用相同的名字不会产生任何问题(除了使读者容易混淆外),因为这两个函数会跳转到自己的起始物理地址去执行。

    第二个startup_32()函数为第一个Linux进程(进程0)建立执行环境。该函数执行以下操作:

    1.把段寄存器初始化为最终值。

    2.把内核的bss段填充为0。

    3.初始化包含在swapper_pg_dir的临时内核页表,并初始化pg0,以使线性地址一致地映射同意物理地址。

    4.把全局目录的地址放在cr3寄存器中,并通过设置cr0寄存器的PG位启用分页。

    5.为进程0建立内核态堆栈。

    6.该函数再一次清零eflags寄存器的所有位、

    7.调用setup_idt()用空的中断处理程序IDT。

    8.把从BIOS中获得的系统参数和传递给操作系统的参数放入第一个页框中。

    9.识别处理器的型号。

    10.用GDT和IDT表的地址来填充gdtr和idtr寄存器。

    11.跳转到start_kernel()函数。

现代:start_kernel()函数

    start_kernel()函数完成Linux内核的初始化工作。几乎每天内核部件都是由这个函数进行初始化的,我们只提及其中的少部分。

    调用sched_init()函数来初始化调度程序。

    调用build_all_zonelists()函数来初始化内存管理区。

    调用page_alloc_init()函数来初始化伙伴系统分配程序。

    调用trap_init()函数和init_IRQ()函数以完成IDT初始化。

    调用softirq_init()函数初始化TASKLET_SOFTIRQ和HI_SOFTIRQ。

    调用time_init()函数来初始化系统日期和时间。

    调用kmem_cache_init()函数来初始化slab分配器。

    调用calibrate_delay()函数以确定CPU时钟的速度。

    调用kernel_thread()函数为进程1创建内核线程。而这个内核线程又会创建其他的内核线程并执行/abin/init程序。

    在start_kernel()开始执行之后会显示“LInux version 2.6.11 ...”信息,除此之外,在init程序和内核线程执行的最后阶段还会显示很多其他信息。最后,就会在控制台上出现熟悉的登录提示符,通知用户Linux内核已经启动,现在正在运行。

    到此Linux的启动就算完成了。