Linux 系统启动过程详解

时间:2022-01-19 16:24:31

以RedHat9.0i386平台为例----

BIOS

第一步:PC在上电以后,CPU从地址FFFF:0000开始执行(这个地址在ROM BIOS中,ROM BIOS一般是在FEOOOhFFFFFh),无论是Award BIOS还是AMI BIOS,这里只是一条跳转指令,跳到系统BIOS中真正的启动代码处。

第二步: BIOS的首先进行POSTPowerOn Self Test,加电后自检),检测系统中一些关键设备是否存在和能否正常工作,例如内存和显卡等设备。此时显卡还没有初始化,如果发现了一些致命错误,例如没有找到内存或者内存有问题(此时只会检查640K常规内存),BIOS会直接控制喇叭发声来报告错误,声音的长短和次数代表了错误的类型。

第三步:查找显卡BIOS。存放显卡BIOSROM芯片的起始地址通常设在C0000H处,找到后就调用它的初始化代码初始化显卡,此时多数显卡都会在屏幕上显示出一些初始化信息,介绍生产厂商、图形芯片类型等内容。接着系统BIOS会查找其它设备的BIOS,初始化其他相关的设备。

第四步:查找完所有其它设备的BIOS之后,系统BIOS将显示自己的启动画面,其中包括有系统BIOS的类型、序列号和版本号等内容。

第五步:接着系统BIOS将检测和显示CPU的类型和工作频率,测试RAM,并显示内存测试进度。

第六步:内存测试通过后, BIOS开始检测一些标准硬件设备,包括硬盘、CDROM、串口、并口、软驱等设备。

第七步:标准设备检测完毕后,检测和配置系统中安装的即插即用设备,每次找到设备后, BIOS都会在屏幕上显示出设备的名称和型号等,同时为设备分配中断、DMAI/O端口等资源。

第八步:到这一步硬件检测配置完毕,多数BIOS会列出系统中安装的硬件,以及它们使用的资源和一些相关参数。

第九步:接下来更新ESCDExtended System Configuration Data,扩展系统配置数据,ESCD是系统BIOS用来与操作系统交换硬件配置信息的一种手段,这些数据被存放在CMOS之中)。

第十步: ESCD更新完毕后,系统BIOS的启动代码将进行它的最后一项工作,即根据用户指定的启动顺序把第0道第0扇区读入内存中07C0:0000(07C00h),这是IBM系列PC的特性。

GRUB启动

主引导扇区(MBRMaster Boot Record,大小为512字节)是启动设备(磁盘或者其他可引导设备,一般是第一个硬盘)的第一个扇区,并不一定只有硬盘才有。计算器上电启动时BIOS会把MBR加载到0x7C00处,然后把控制权交给它。

 

MBR组成(来自IBM官方网站)

GRUB引导的系统,MBR的前446字节为stage1.s。它的任务是将start.s拷贝到内存中,并跳转执行。start.s位于第二个扇区(固定位置,此时还不能识别文件系统。在MBR和第一个分区之间有62个扇区--折合32k的保留空间,足够GRUB—GRUB内核压缩后24k左右),任务是加载stage1.5,只有加载了stage1.5才能识别文件系统。stage1.5位于第三个扇区开始到约10k的位置上,此时可以识别boot目录所在的文件系统(如果boot位于/的话需识别根文件系统),找到stage2stage2和其他的stage1.5位于/boot分区)并加载。stage2实现了一个mini OS,它采用了类似SHELL的方式来解释并运行用户设计好的脚本(/boot/grub.conf)或者接受用户输入的指令,可以灵活地引导不同的操作系统。

 

Grub中预先设置的指令是在builtins.c文件中实现的。典型的GRUB如下所示:

title Red Hat Enterprise Linux AS-up (2.6.9-11.EL)

   root (hd0,2)

   kernel /boot/vmlinuz-2.6.9-11.EL ro root=LABEL=/ rhgb quiet

   initrd /boot/initrd-2.6.9-11.EL.img

title Other

   rootnoverify (hd0,1)

   chainloader +1

如果boot是单独分区的,则如下所示:

title Fedora (2.6.24.3-50.fc8)

         root (hd0,8)

         kernel /vmlinuz-2.6.24.3-50.fc8 ro root=LABEL=/ rhgb quiet

         initrd /initrd-2.6.24.3-50.fc8.img

# title Fedora (2.6.24.3-34.fc8)

 

Root指令

指定grubroot(不是linuxOS),调用root指令的函数是builtins.c中的root_func (char *arg, int flags)。第一个参数指定了磁盘驱动器,如hd0是指第一块硬盘,第二个参数是分区号。root_func()中调用real_root_func (char *arg, int attempt_mount),并把参数arg传给real_root_func。如果传入的arg为空,则直接使用默认驱动器。然后调用set_device(),从字符串中提取出驱动器和分区号。

 

kernel指令

调用kernel指令的函数是builtins.c文件中kernel_func (char *arg, int flags)。首先解析传进来的参数:如果有“--type=TYPE”,根据传入的参数设置suggested_type变量,然后把内核的文件路径赋值给mb_cmdline变量,然后通过load_image()函数载入核心,并且返回核心的类型。如果返回的核心类型是grub不支持得类型,即kernel_type == KERNEL_TYPE_NONE返回1,成功则返回0root=LABEL=/的意思是使用标签为/的分区作为root分区。

 

Initrd指令

调用kernel指令的函数是builtins.c文件中initrd_func (char *arg, int flags),用于指定initrdinitial RAM disk)文件。initrd是在系统引导过程中挂载的一个临时根文件系统,与内核绑定在一起,并作为内核引导过程的一部分进行加载。initrd文件中包含了各种可执行程序和驱动程序,它可以用来挂载实际的根文件系统,然后再将这个 initrd RAM卸载,并释放内存。在很多嵌入式Linux 系统中,initrd就是最终的根文件系统。

 

Rootnoverify指令

设置GRUB的主设备指向一个扇区。

 

chainloader

'+1'表明GRUB需要从起始分区读一个扇区(即引导记录)。

 

boot指令

调用boot指令的函数是在builtins.c文件中的boot_func (char *arg, int flags)函数。如果核心类型已知,则调用unset_int15_handler()函数,接着根据grub操作系统调用相应的启动程序。如果启动的内核为BSD则调用bsd_boot ()函数;如果启动的内核为LINUX则调用linux_boot()函数,如果链式启动,则调用chain_stage1()函数;如果多重启动则调用multi_boot()函数。

 

注:有的MBR(非GRUB,如BSD)先把自己复制到0x0600处(为什么0x0600呢?按照dos的传统:0x0 - 0x3ff 用于中断向量表;0x400 - 0x4ff 用于 bios数据区;0x500用于BIOS Print Screen  0x501-0x5ff可能用于不同版本的MS-DOS),然后在磁盘中找到活动分区。如果活动分区存在则将这个分区的第一个扇区(bootsect)加载到0x7C00处,并将控制权交给它(bootsect一般是在某一个分区里,而MBR是磁盘或者其他可引导设备的第一个扇区,这个就是所谓的区别)。

Linux操作系统引导

接上文,linux_boot最终将跳转到linux的入口代码bootsect.S(位于“arch/i386/boot”),开始初始化过程。bootsect.S主要做以下工作:

1.      把自己从0x7c00拷贝到0x90000,共512Bytes。(为什么拷贝呢?后面setup.S会把内核拷贝到0x10000-64k,加上最初假设内核小于512k,所以最小的可用地址为0x90000。这里的0x90000为地址0x90000,以后雷同)

2.      RAM中创建新的磁盘参数表(最大扇区数36ED2.88驱动器支持的最大值),重新启动磁盘控制器,使磁盘参数设置生效

3.      setup从第2扇区开始的4个扇区拷贝到0x90200bootsect.Ssetup.S在磁盘上连续存放)

4.      BIOS获得磁盘扇区数(如果没有则3618159依次测试),接下来读取光标的位置,并打印"Loading"信息

5.      把真正的内核映像(system)从磁盘读到内存0x10000处(后面setup.S会把内核搬到0x0。为什么不直接读呢?因为物理地址0开始存放着BIOS中断向量表,Setup.S还需要使用BIOS中断来获取PC硬件系统的信息。此处调用的函数为:read_it,实际读取的大小为0x7F00*16=508k<512k。如果是bzImage大内核,则直接拷贝到0x100000处)。

6.      最后,跳到0x90200setup.S)处执行

 

setup.S利用BIOS中断读取机器配置(同时调用video.S检测显示器和显示模式),并保存到0x90000(覆盖掉bootsect.S),供内核程序使用,参数和内存位置如下表:

内存地址  长度(字节)        名称          描述

0x90000    2                 光标位置       列号,行号

0x90002    2                 扩展内存数     系统从1M开始的扩展内存数量

0x90004    2                 显示页面       当前显示页面

0x90006    1                 显示模式

0x90007    1                 字符列数

0x90008    2                 

0x9000A     1                  显示内存       0x0-64K,0x1-128K,0x02-192K,0x03=256K

0x9000B    1                 显示状态       彩色还是单色

0x9000C    2                  特性参数       显卡特性参数

……

0x90080    16                硬盘参数表   第一个硬盘参数表

0x90090    16                硬盘参数表   第二个硬盘参数表(如果没有,清零)

 

然后内核禁止中断,将自己(小于512k)从0x1000移至0x0处,设置gdtidt,开启A20地址线(使能1M以上内存)、重新设置两个中断控制芯片8259A,最后设置CPU的控制寄存器CR0,进入32位保护模式,并跳转到0x1000arch/i386/boot/compressed/head.S中的startup_32()(对bzImage 0x100000)。

 

head.S

然后调用decompress_kernel()解压内核映像,首先显示"Uncompressing Linux...",解压完后显示 "OK, booting the kernel."。内核解压后的映像被放置到0x100000arch/i386/kernel/head.S文件中的startup_32(),因为通过物理地址跳转,相同的函数名没有问题)。

Setup.S结束后内存布局

 

函数startup_32()linux的入口,它为Linux第一个进程设定环境,IDTGDTLDT被装入,处理器初始化完毕,初始化内存页表,进入分页方式,最终调用start_kernel()

 

start_kernel()

"init/main.c"中定义,是系统第一个C语言函数。它调用一系列初始化函数,以完成kernel环境的设置。在函数最后,调用 init()函数,创建第一个进程。

    1) 调度器初始化,调用sched_init()

  2) 调用build_all_zonelists函数初始化内存区;

  3) 调用page_alloc_init()mem_init()初始化伙伴系统分配器;

  4) 调用trap_init()init_IRQ()对中断控制表IDT进行最后的初始化;

  5) 调用softirq_init()初始化TASKLET_SOFTIRQHI_SOFTIRQ

  6) 调用Time_init()对系统日期和时间进行初始化;

  7) 调用kmem_cache_init()初始化slab分配器;

  8) 调用calibrate_delay()计算CPU时钟频率;

  9) 调用rest_init() -> kernel_thread(kernel_init…)启动 init 内核线程(以前的版本为init)Kernel_thread设置入口地址和段寄存器后,调用do_fork获取task_structpid,创建内核线程(没有用户栈空间,其pid1)。(内核从2.6.22开始,rest_init()除了创建线程kernel_init以外,还创建kthreadd线程-pid2。根据以前的说法 init 进程1是除进程0以外的所有进程的父进程,因为除了进程0外,其他进程都是init进程或其子进程不断fork出来的。当一个进程的父进程结束时,这个进程变成孤儿进程,孤儿进程的父进程也是init进程。2.6.22 及以后,很多内核线程由kthreadd线程create_kthread出来,所以’ps –ef’显示的父进程PID2)

 

kernel_init() 主要做以下三部分工作:

do_pre_smp_initcalls(); //调用__initcall_start__early_initcall_end函数

do_basic_setup();

driver_init();   //device, bus, class, firmware, hypervisor, platform_bus,

//system_bus, cpu/memory

init_irq_proc();//

do_initcalls()            //调用从__early_initcall_end__initcall_end的函数

init_post() -> run_init_process("/sbin/init"); //调用kernel_execve启动系统调用,从而切换成init用户进程,因为直接切换所以不会回来继续执行。

 

内核中与initcall有关的代码如下(include/linux/init.h):

#define __define_initcall(level,fn,id) \

         static initcall_t __initcall_##fn##id __used \

         __attribute__((__section__(".initcall" level ".init"))) = fn

 

/* Early initcalls run before initializing SMP. Only for built-in code, not modules. */

#define early_initcall(fn)                 __define_initcall("early",fn,early)

 

/* A "pure" initcall has no dependencies on anything else, and purely initializes variables that couldn't be statically initialized. This only exists for built-in code, not for modules. */

#define pure_initcall(fn)                  __define_initcall("0",fn,0)

 

#define core_initcall(fn)                  __define_initcall("1",fn,1)

#define core_initcall_sync(fn)                __define_initcall("1s",fn,1s)

#define postcore_initcall(fn)          __define_initcall("2",fn,2)

#define postcore_initcall_sync(fn)        __define_initcall("2s",fn,2s)

#define arch_initcall(fn)         __define_initcall("3",fn,3)

#define arch_initcall_sync(fn)                 __define_initcall("3s",fn,3s)

#define subsys_initcall(fn)              __define_initcall("4",fn,4)

#define subsys_initcall_sync(fn)    __define_initcall("4s",fn,4s)

#define fs_initcall(fn)                       __define_initcall("5",fn,5)

#define fs_initcall_sync(fn)             __define_initcall("5s",fn,5s)

#define rootfs_initcall(fn)               __define_initcall("rootfs",fn,rootfs)

#define device_initcall(fn)              __define_initcall("6",fn,6)

#define device_initcall_sync(fn)    __define_initcall("6s",fn,6s)

#define late_initcall(fn)          __define_initcall("7",fn,7)

#define late_initcall_sync(fn)                  __define_initcall("7s",fn,7s)

 

#define __initcall(fn) device_initcall(fn)

 

注:普通驱动程序若编入内核(不以模块方式插入),则:

include/linux/init.h:

#define module_init(x) __initcall(x);

#define __initcall(fn) device_initcall(fn)

因此driver的入口函数都放在.initcall6.init段中。

 

Initcall中有众多的函数,有不少与驱动程序关系非常密切的,这些以后用到的时候再说。这里说一下rootfs_initcall。先看源码start_kernel() -> mnt_init():

void __init mnt_init(void)

{

……

init_rootfs();                     //注册rootfs文件系统

init_mount_tree();  // 挂载rootfs文件系统,挂载点默认为“/”。

……

}

到此rootft文件系统已经注册,后面的rootfs_initcall会填充其内容。搜索内核(2.6.31)有以下两行:

Initramfs.c (init):rootfs_initcall(populate_rootfs);

Noinitramfs.c (init):rootfs_initcall(default_rootfs);

根据内核是否支持 initrdinit/MakefileCONFIG_BLK_DEV_RAM决定了是否支持initrd),如果不支持,则default_rootfs;如果支持则populate_rootfs

default_rootfs:在prepare_namespace()中调用mount_root挂载真实的文件系统,最后在init_post()中通过kernel_execve执行根文件系统中的 /sbin/init

populate_rootfs:如果是cpio格式的initrd(压缩过的),则调用unpack_to_rootfs直接解压到之前mount的根目录上(这里两种情况:根内核编译在一起的initrdbootloader加载的initrd。前者位于.init.ramfs段中,全局变量__initramfs_start__initramfs_end分别指向这个数据段的起始地址和结束地址;后者由bootloader把起始地址和结束地址传递给内核,全局变量initrd_startinitrd_end分别指向其起始地址和结束地址)。如果是bootloader加载的老式块设备格式,则将initrd_startinitrd_end之间的数据拷贝到/initrd.image。后面的prepare_namespace()将会创建设备/dev/ram并加载image,然后把/dev/ram挂载到/root。若是cpio格式的,在init_post()中调用/init,启动udevd加载必要的设备驱动程序,挂载真正的根文件系统,最后执行真正的根文件系统上的initrd,启动过程就顺利的交接;若是老式块设备格式,执行里面的linuxrc挂载真实的文件系统,然后切换到新的根文件系统。

 

Initrd里面的/init脚本实际执行了以下过程:

1)加载proc, sysfs等文件系统

2)创建设备节点

3)加载必需的设备驱动

4)以只读方式挂载实际根文件系统(这是根文件系统的第一次挂载)

5)切换根目录并执行的/sbin/init (switchroot

 

注:RAM disk是将内存中的一块区域作为物理磁盘来使用的一种技术,对于用户来说, RAM disk等同于通常的硬盘分区(如/dev/hda1)。initrdinit RAM Disk)则是一个包含了被压缩过的小型根目录的RAM disk,它含有Linux启动所必须的目录、驱动程序、可执行文件和启动脚本(如initbininsmod等)。initrd在实际根文件系统可用之前被挂载到系统中,initrd可使用loop设备来创建。

Sys V init初始化阶段

/sbin/init进程是所有进程的父进程,它主要做以下工作:

1)确定启动后的运行级别(id:5:initdefault:)

2)执行系统初始化脚本(si::sysinit:/etc/rc.d/rc.sysinit),以下为其主要工作:

脚本

作用

/etc/sysconfig/network

设置主机名称及网络

Mount /proc, /proc/bus/usb, /sys

Fsck works

/etc/init.d/functions

设置环境变量如umaskPATH等,其间会调用/etc/sysconfig/init

selinux

有关SELinux的设置

/sbin/setsysfont

设置字体

显示欢迎画面(Welcome to $/etc/redhat-release

/bin/dmesg

这里不是要显示系统信息,而是在设置系统纪录的等级高低,在此以刚刚提到的functions文件设置环境变量时的LOGLEVEL变量为基准

/sbin/start_udev

加载的相关模块

/proc/sys/kernel/modprobe

设置内核参数modprobe, hotplug

echo -n $"Initializing hardware... "

kmodule | while read devtype mod ; do modprobe $mod

加载设备驱动,IDESCSINETWORKAUDIO

load_module floppy

软盘

/etc/sysconfig/network-scripts

load_module $DEVICE, network

Load_module audio

音频

sysctl -e -p /etc/sysctl.conf

配置内核参数,

/etc/sysconfig/clock

设置系统时间种类,如UTC

/sbin/hwclock

设置系统时间

/etc/sysconfig/keyboard

键盘

Insmod kernel/drivers/acpi/*

激活ACPI功能

cat /fsckoptions

检查文件是否存在,决定后面是否要调用fsck检查硬盘扇区

awk '/ \/ / && ($3 !~ /rootfs/) { print $3 }' /proc/mounts

查找FOOTFSTYPErootdevmount文件系统(出于安全的原因,使用ro参数),并unmount。文件系统在initcall的时候已经通过register_filesystem注册了。

/sbin/quotacheck

disk quota的检查

mount -n -o remount,rw /

重新挂载根文件系统,读写模式

/sbin/vgchange

LVM initialization

mount -f /

mount -f /proc

mount -f /sys

mount -f /dev/pts

mount -f -t usbfs usbfs /proc/bus/usb

mount -f -t devfs devfs /dev

Clear mtab后, 根据fstab重新挂载文件系统(已经挂载的文件系统写入mtab

/sbin/quotaon

local filesystem quotas

/usr/bin/rhgb

通过这个程序将图形开机画面的背景加载,这时才会有图形化的系统开机画面及开机程序的计时bar呈现在用户面前

swapon -a -e

Start up swapping

/etc/sysconfig/harddisk

Turn on harddisk optimization

/usr/bin/rhgb-client

告知系统要离开rc.sysinit,转回inittab

 

3) 以运行级别X为参数执行/etc/rc.d/rc脚本,该文件停止所有/etc/rc.d/rcX.dK开头的服务(守护进程),并启动S开头的服务:

  amd:自动安装NFS守护进程

  apmd:高级电源管理守护进程

  arpwatch:记录日志并构建一个在LAN接口上看到的以太网地址和IP地址对数据库

  autofs:自动安装管理进程automount,与NFS相关,依赖于NIS

  crondLinux下的计划任务的守护进程

  namedDNS服务器

  netfs:安装NFSSambaNetWare网络文件系统

  network:激活已配置网络接口的脚本程序

  nfs:打开NFS服务

  portmapRPC portmap管理器,它管理基于RPC服务的连接

  sendmail:邮件服务器sendmail

  smbSamba文件共享/打印服务

  syslog:一个让系统引导时起动syslogklogd系统日志守候进程的脚本

  xfsX Window字型服务器,为本地和远程X服务器提供字型集

  Xinetd:支持多种网络服务的核心守护进程,可以管理wuftpsshdtelnet等服务

4) 启动虚拟终端/sbin/mingetty

5) 如果运行级别为5,启动X11

这时呈现给用户的就是最终的登录界面。