Chinaunix首页 | 论坛 | 博客
  • 博客访问: 354073
  • 博文数量: 83
  • 博客积分: 5322
  • 博客等级: 中校
  • 技术积分: 1057
  • 用 户 组: 普通用户
  • 注册时间: 2010-04-11 11:27
个人简介

爱生活,爱阅读

文章分类

全部博文(83)

文章存档

2015年(1)

2013年(1)

2012年(80)

2011年(1)

分类: LINUX

2012-10-18 08:43:51

The kernel boot process

内核启动过程

前面的博文解释了计算机是如何引导启动的,我们正讲到:在将内核映像(kernel image)加载到内存之后,bootloader即将跳入到内核入口点(kernel entry point)。关于“引导”(boot)的最后一篇博文则从内核内部来研究操作系统是如何启动的。由于我是一个“实用主义者(empirical bent )”,我将主要引用Linux 2.6.25.6内核源代码。如果你对C语言语法熟悉的话,那么源代码非常易读;尽管你可能错过一些细节,但仍然能够知道内核启动的要点。现在,最主要的困难在于缺少对一些代码上下文的理解。例如,什么时候运行以及为什么运行,或者一些机器底层特征(underlying features of the machine)。我希望能够帮助你理解这些上下文。由于本文较为简洁(brevity),一些有趣的东西---例如中断与内存现在仅仅获得一个大概的了解。本文的末尾给出了Windows引导的要点(highlight)。

此时,Intel x86运行在实模式(real-mode)下,可以寻址1兆内存,对于现代Linux系统而言,RAM看起来如下图所示:

 
RAM contents after boot loader is done

内核映像已经被bootloader通过使用BIOS磁盘I/O服务加载到了内存。该镜像是硬盘驱动器上内核文件的精确拷贝,例如/boot/vmlinuz-2.6.22-14-server。内核被分成两部分:一个较小的、保存着实模式的内核代码被加载到640K之下的区域;剩余的运行在保护模式下、较大的内核块则被加载到到内存的1兆内存之后。

由内核实模式头(real-mode kernel header)开始执行的动作在上图中已经列出。该内存区域用于实现bootloader与内核之间的Linux引导协议(boot protocol)。bootloader工作时将从该区域读取一些值。这包括可读(human-readable)的内核版本字符串。也包括关键的信息,例如实模式内核的大小。Bootloader也会向该区域写入一些值,例如,用户在引导菜单中设定的命令行参数(command-line parameters)的内存地址。一旦bootloader结束,那么内核头(the kernel header)所需要的所有参数均被填充完成。接下来就跳转到内核入口点。下图列出了内核初始化的代码顺序,包括源目录,文件以及行号:

 

Architecture-specific Linux Kernel Initialization

早期的Intel架构Linux内核启动代码位于文件中。它是用汇编语言编写的,尽管这在内核代码中很少见,但在启动阶段却很普遍。该文件的开始通常包含有引导扇区代码(boot sector code),它是早期Linux代码的遗留产物,那时候Linux可以不依赖于bootloader启动。现在,该扇区仅仅向用户打印“bugger_off_msg”信息并重新启动。现代操作系统通常会忽略该遗留代码(ignore the legacy code)。在引导扇区代码之后,是15字节的实模式内核头(real-mode kernel header);这两部分组成了512字节,也是Intel架构下传统的磁盘扇区大小。

在这512字节之后的0x200偏移处,我们找到了Linux内核运行的第一条指令:实模式的入口点(the real-mode entry point)。它位于header.S110处,且机器码为0x3aeb的二字节跳转指令。你可以通过使用hexdump执行内核映像的方法来确认这一点。Bootloader在完成后将跳转到这个位置,然后接着跳转到header.S:229,该位置有一个称作的汇编语言程序。该小程序为实模式内核建立栈,并为bss段填充零(该区域包含静态变量,所以它们应当被赋予零值)。然后跳转到 C代码。

 

Main()函数主要是检查内存布局,设置视频模式等。然后调用函数。在CPU切换至保护模式之前,必须完成一些任务。这主要包括两方面:中断与内存。在实模式下,处理器的中断向量表(interrupt vector table)总是位于内存地址0x00处,而保护模式下,中断向量表存储在一个称作IDTRCPU寄存器中。另外,在保护模式与实模式中,逻辑地址(logical memory addresses)(程序管理的内存)向线性地址(linear memory addresses)(地址从0到内存最高值)的转换方式也不相同。保护模式下,为了转换内存,需要从将(全局描述表:Global Descriptor TableGDT地址加载到GDTR寄存器。所以,go_to_protected_mode()调用 来建立临时中断描述表(interrupt descriptor table)和全局表述表(global descriptor table)。

现在已经准备好进入保护模式了,进入该模式是通过实现的,这是一个汇编程序。该函数通过置位CPUCR0寄存器的PE位来使能保护模式。此时系统运行在禁用分页机制下。分页机制是处理器的一个可选特性。尽管在保护模式下,但仍然可能不使用它。最重要的是,现在我们不再受限于640K的内存,而是可以访问4G RAM。该函数之后调用32位内核入口点(kernel entry point),即压缩内核的函数。该函数做一些基本的寄存器初始化工作,然后调用C函数来解压内核。

打印出熟悉的消息“Decompressing Linux…”。解压缩是在原有的内存空间进行的(in-place),一旦完成后,解压缩后的内核镜像将会取代第一个图表中显示的压缩内核镜像。因而解压缩后的内容也是从1兆的内存地址开始的。decompress_kernel() 然后打印出“done.”,以及令人舒服的“Booting the kernel.”。这里的“Booting”的意思是跳入整个启动过程的最终入口点(the final entry point),即保护模式内核的入口点在RAM的第二兆字节0x100000。这个神圣的位置包含一个叫做的函数,但这个函数在不同的目录下,你会看到它。

第二种形式的startup_32仍然是一个汇编程序,但它包含了32-位模式的初始化。它为保护模式内核(这是真正的内核,它将一直运行知道机器重启或者关闭)清除bss段,为内存建立最终的全局描述表(global descriptor table),构建分页表(page table)以使能分页,初始化堆栈,创建最终的中断描述表,并最终调用架构无关的(architecture-independent)的内核启动函数。下图说明最后启动阶段的代码流:


Architecture-independent Linux Kernel Initialization

Start_kernel()看起来更像传统的内核代码,几乎所有代码均为C代码,且是独立于结构的。该函数进行一些列的调用来初始化不同的内核自己系统和数据结构。包括调度器(scheduler)、内存区域(memory zone)、定时器(time keeping)等等。Start_kernel()然后调用,此时大部分子系统已经开始工作。创建一个内核线程,它将kernel_init()函数作为参数,该函数是内核初始化的入口点。Rest_init()然后调用schedule()进行进程调度,然后通过调用进入睡眠,该内核线程为linux内核的空闲线程。Cpu_idle()作为进程0一直处于运行当中。当有任何工作要做的时候一个可以运行的进程---进程零将让出(get booted out ofCPU,直到没有可运行的进程才返回。

空闲循环(idle loop)是自从系统引导开始的“很长的线程的终点”(This idle loop is the end of the long thread),是处理器上电之后执行多次jump的最终结果。所有这些都很杂乱,从重启向量(reset vector)到BIOSMBRbootloader到实模式内核到保护模式内核,经过所有这些,最终我们来到这里。一步一步直到CPU进入空闲循环,cpu_idle真的很酷。然而,这不是整个内核启动的终点,否则,计算机无法正常工作。

此时,之前启动的内核线程已经准备好取代进程零与空闲线程。在kernel_init()作为线程入口点传入init_rest时,其已经开始运行。Kernel_init()负责初始化系统中在引导时被终止的、剩余的CPU。现在我们所看到的所有代码均是在单CPU上执行的,该CPU称作引导处理器(boot processor)。其它的应用处理器(application processor)也会从实模式开始并运行一些初始化。很多代码路径公共的,正如你所看到的函数startup_32代码,它们均由后来的应用CPU负责执行的。

最后,kernel_init 调用init_post(),该函数试图按下面的路径执行用户模式的进程:/sbin/init,/etc/init,/bin/init,以及/bin/sh。如果所有路径均失败告终,那么内核即将完蛋(panic)。幸好,init通常就在那里且进程ID1。它检查配置文件来确定加载那些进程,可能包括X11,登录串口程序,网络守护程序等等。此时,引导过程结束了,而另一个Linux box开始运行了。希望你没有遇到太多麻烦。

……



阅读(4857) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~