Linux 启动过程详解

2019-07-13 00:24发布

摘自:《嵌入式Linux应用程序开发详解》  

一、概述

用户开机启动 Linux 过程总体上是这样的: 首先当用户打开 PC 的电源时,CPU 将自动进入实模式,并从地址 0xFFFF0 开始自动执行程序代码,这个地址通常是 ROM-BIOS 中的地址。这时 BIOS 进行开机自检,并按 BIOS 中设置的启动设备(通常是硬盘)进行启动,接着启动设备上安装的引导程序lilo 或 grub 开始引导 Linux(也就是启动设备的第一个扇区),这时,Linux 才获得了启动权。 接下来的第二阶段,Linux 首先进行内核的引导,主要完成磁盘引导、读取机器系统数据、实模式和保护模式的切换、加载数据段寄存器以及重置中断描述符表等。 第三阶段执行 init 程序(也就是系统初始化工作) init 程序调用了 rc.sysinit 和 rc 等程序,而 rc.sysinit 和 rc 在完成系统初始化和运行服务的任务后,返回 init。 之后的第四阶段,init 启动 mingetty,打开终端供用户登录系统,用户登录成功后进入了Shell,这样就完成了从开机到登录的整个启动过程。 Linux 启动总体流程图如图所示, 其中的 4 个阶段分别由同步棒隔开。由于第一阶段不涉及 Linux 自身的启动过程,因此,下面分别对第二和第三阶段进行详细讲解。   启动过程    

二、内核引导阶段

在 grub 或 lilo 等引导程序成功完成引导 Linux 系统的任务后,Linux 就从它们手中接管了 CPU 的控制权。用户可以从 www.kernel.org 上下载最新版本的源码进行阅读,其目录为:linux-2.6.*.*/arch/i386/boot。在这过程中主要用到该目录下的这几个文件:bootsect.S、setup.S以及 compressed 目录下的 head.S 等。 首先要介绍一下,Linux 的内核通常是压缩过后的,包括如上述提到的那几个重要的汇编程序,它们都是在压缩内核 vmlinuz 中的。因为 Linux 中提供的内核包含了众多驱动和功能,因而比较大,所以在采用压缩内核可以节省大量的空间。   (1)bootsect 阶段 当 grub 读入 vmlinuz 后,会根据 bootsect(正好 512bytes)把它自身和 setup 程序段读到不大于 0x90000 开始的的内存里(注意:在以往的引导协议里是放在 0x90000,但现在有所变化) ,然后 grub 会跳过 bootsect 那 512bytes 的程序段,直接运行 setup 里的第一条指令。就是说 bzImage 里 bootsect 的程序没有再被执行了, bootsect.S 在完成了指令搬移以后就退出了。之后执行权就转到了 setup.S 的程序中。   (2)setup 阶段 setup.S 的主要功能就是利用 ROM BIOS 中断读取机器系统数据, 并将系统参数(包括内存、磁盘等)保存到 0x90000~0x901FF 开始的内存中位置。 此外,setup.S 还将 video.S 中的代码包含进来,检测和设置显示器和显示模式。 最后,它还会设置 CPU 的控制寄存器 CR0(也称机器状态字) ,从而进入 32 位保护模式运行,并跳转到绝对地址为 0x100000(虚拟地址 0xC0000000+0x100000)处。当 CPU 跳到 0x100000 时,将执行 "arch/i386/kernel/head.S"中的 startup_32。
  (3)head.S 阶段 当运行到 head.S 时,系统已经运行在保护模式, head.S 完成的一个重要任务就是将内核解压。就如本节前面提到的,内核是通过压缩的方式放在内存中的。head.S 通过调用 misc.c中定义的 decompress_kernel()函数,将内核 vmlinuz 解压到 0x100000 的。 接下来 head.S 程序完成完成寄存器、分页表的初始化工作,但要注意的是,这个 head.S程序与完成解压缩工作的 head.S 程序是不同的,它在源代码中的位置是arch/i386/kernel/head.S。 在完成了初始化之后,head.S 就跳转到 start_kernel()函数中去了。   (4)main.c 阶段 start_kernel()是“init/main.c”中的定义的函数,start kernel()调用了一系列初始化函数,进行内核的初始化工作。要注意的是,在初始化之前系统中断仍然是被屏蔽的,另外内核也处于被锁定状态,以保证只有一个 CPU 用于 Linux 系统的启动。 在 start_kernel()的最后,调用了 init()函数,也就是下面要讲述的 INIT 阶段。  

三、init 阶段

在加载了内核之后,由内核执行引导的第一个进程就是 INIT 进程,该进程号始终是“1” 。INIT 进程根据其配置文件“/etc/inittab”主要完成系统的一系列初始化的任务。由于该配置文件是 INIT 进程执行的惟一依据,因此先对它的格式进行统一讲解。 inittab 文件中除了注释行外,每一行都有如下格式: id:runlevels:action:process (1)id
    id 是配置记录标识符,由 1~4 个字符组成,对于 getty 或 mingetty 等其他 login 程序项,要求 id 与 tty 的编号相同,否则 getty 程序将不能正常工作。
(2)runlevels     runlevels 是运行级别记录符,一般使用 0~6 以及 S 和 s。其中,0、1、6 运行级别为系统保留:0 作为 shutdown 动作,1 作为重启至单用户模式,6 为重启;S 和 s 意义相同,表示单用户模式,且无需 inittab 文件,因此也不在 inittab 中出现。7~9 级别也是可以使用的,传统的 UNIX 系统没有定义这几个级别。 runlevel 可以是并列的多个值,对大多数 action 来说,仅当 runlevel 与当前运行级别匹配成功才会执行。 (3)action     action 字段用于描述系统执行的特定操作,它的常见设置有:initdefault、sysinit、boot、bootwait、respawn 等。     initdefault 用于标识系统缺省的启动级别。当 init 由内核激活以后,它将读取 inittab 中的initdefault 项,取得其中的 runlevel,并作为当前的运行级别。如果没有 inittab 文件,或者其中没有 initdefault 项,init 将在控制台上请求输入 runlevel。     sysinit、boot、bootwait 等 action 将在系统启动时无条件运行,忽略其中的 runlevel。     respawn 字段表示该类进程在结束后会重新启动运行。 (4)process     process 字段设置启动进程所执行的命令。   以下结合笔者系统中的 inittab 配置文件详细讲解该配置文件完成的功能: 1.确定用户登录模式 在“/etc/inittab”中列出了如下所示的登录模式,主要有单人维护模式、多用户无网络模式、文字界面多用户模式、X-Windows 多用户模式等。其中的单人维护模式(run level 为 1)是类似于 Windows 中的“安全模式”,在这种情况下,系统不加载复杂的模式从而使系统能够正常启动。在这些模式中最为常见的是 3 或 5,其中本系统中默认的为 5,也就是 X-Windows多用户模式。
     
# Default runlevel. The runlevels used by RHS are:
     #    0 - halt (Do NOT set initdefault to this)
     #    1 - Single user mode
     #    2 - Multiuser, without NFS (The same as 3, if you do not have networking)
     #    3 - Full multiuser mode
     #    4 - unused
     #    5 - X11
     #    6 - reboot (Do NOT set initdefault to this)
     #
     id:5:initdefault:

2.执行内容/etc/rc.d/rc.sysinit 在确定了登录模式之后,就要开始将 Linux 的主机信息读入 Linux 系统,其内容就是文件“/etc/rc.d/rc.sysinit”中的。查看此文件可以看出,在这里确定了默认路径、主机名称、“/etc/sysconfig/network”中所记录的网络信息等。
     # System initialization.
     si::sysinit:/etc/rc.d/rc.sysinit
  3.启动内核的外挂模块及各运行级的脚本 在此,主要是选择模块的型态以进行驱动程序的加载。接下来会根据不同的运行级(runlevel)加载不同的模块,启动系统服务。      l0:0:wait:/etc/rc.d/rc 0
     l1:1:wait:/etc/rc.d/rc 1
     l2:2:wait:/etc/rc.d/rc 2
     l3:3:wait:/etc/rc.d/rc 3
     l4:4:wait:/etc/rc.d/rc 4
     l5:5:wait:/etc/rc.d/rc 5
     l6:6:wait:/etc/rc.d/rc 6
    
     # Trap CTRL-ALT-DELETE
     ca::ctrlaltdel:/sbin/shutdown -t3 -r now
     # When our UPS tells us power has failed, assume we have a few minutes
     # of power left. Schedule a shutdown for 2 minutes from now.
     # This does, of course, assume you have powerd installed and your
     # UPS connected and working correctly.
     pf::powerfail:/sbin/shutdown -f -h +2 "Power Failure; System Shutting Down"
     # If power was restored before the shutdown kicked in, cancel it.
     pr:12345:powerokwait:/sbin/shutdown -c "Power Restored; Shutdown Cancelled"
     # Run gettys in standard runlevels
     1:2345:respawn:/sbin/mingetty tty1
     2:2345:respawn:/sbin/mingetty tty2
     3:2345:respawn:/sbin/mingetty tty3
     4:2345:respawn:/sbin/mingetty tty4
     5:2345:respawn:/sbin/mingetty tty5
     6:2345:respawn:/sbin/mingetty tty6
     # Run xdm in runlevel 5
     x:5:respawn:/etc/X11/prefdm -nodaemon