以RedHat9.0和i386平台为例----
BIOS
第一步:PC在上电以后,CPU从地址FFFF:0000开始执行(这个地址在ROM BIOS中,ROM BIOS一般是在FEOOOh到FFFFFh中),无论是Award BIOS还是AMI BIOS,这里只是一条跳转指令,跳到系统BIOS中真正的启动代码处。
第二步: BIOS的首先进行POST(Power-On Self Test,加电后自检),检测系统中一些关键设备是否存在和能否正常工作,例如内存和显卡等设备。此时显卡还没有初始化,如果发现了一些致命错误,例如没有找到内存或者内存有问题(此时只会检查640K常规内存),BIOS会直接控制喇叭发声来报告错误,声音的长短和次数代表了错误的类型。
第三步:查找显卡BIOS。存放显卡BIOS的ROM芯片的起始地址通常设在C0000H处,找到后就调用它的初始化代码初始化显卡,此时多数显卡都会在屏幕上显示出一些初始化信息,介绍生产厂商、图形芯片类型等内容。接着系统BIOS会查找其它设备的BIOS,初始化其他相关的设备。
第四步:查找完所有其它设备的BIOS之后,系统BIOS将显示自己的启动画面,其中包括有系统BIOS的类型、序列号和版本号等内容。
第五步:接着系统BIOS将检测和显示CPU的类型和工作频率,测试RAM,并显示内存测试进度。
第六步:内存测试通过后, BIOS开始检测一些标准硬件设备,包括硬盘、CD-ROM、串口、并口、软驱等设备。
第七步:标准设备检测完毕后,检测和配置系统中安装的即插即用设备,每次找到设备后, BIOS都会在屏幕上显示出设备的名称和型号等,同时为设备分配中断、DMA和I/O端口等资源。
第八步:到这一步硬件检测配置完毕,多数BIOS会列出系统中安装的硬件,以及它们使用的资源和一些相关参数。
第九步:接下来更新ESCD(Extended System Configuration Data,扩展系统配置数据,ESCD是系统BIOS用来与操作系统交换硬件配置信息的一种手段,这些数据被存放在CMOS之中)。
第十步: ESCD更新完毕后,系统BIOS的启动代码将进行它的最后一项工作,即根据用户指定的启动顺序把第0道第0扇区读入内存中07C0:0000(即07C00h处),这是IBM系列PC的特性。
GRUB启动
主引导扇区(MBR,Master 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位于/的话需识别根文件系统),找到stage2(stage2和其他的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指令
指定grub的root(不是linux的OS),调用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,成功则返回0。root=LABEL=/的意思是使用标签为/的分区作为root分区。
Initrd指令
调用kernel指令的函数是builtins.c文件中initrd_func (char *arg, int flags),用于指定initrd(initial 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拷贝到0x9000:0,共512Bytes。(为什么拷贝呢?后面setup.S会把内核拷贝到0x10000-64k,加上最初假设内核小于512k,所以最小的可用地址为0x90000。这里的0x9000:0为地址0x90000,以后雷同)
2. 在RAM中创建新的磁盘参数表(最大扇区数36,ED2.88驱动器支持的最大值),重新启动磁盘控制器,使磁盘参数设置生效
3. 把setup从第2扇区开始的4个扇区拷贝到0x9020:0(bootsect.S、setup.S在磁盘上连续存放)
4. 从BIOS获得磁盘扇区数(如果没有则36,18,15,9依次测试),接下来读取光标的位置,并打印"Loading"信息
5. 把真正的内核映像(system)从磁盘读到内存0x1000:0处(后面setup.S会把内核搬到0x0。为什么不直接读呢?因为物理地址0开始存放着BIOS中断向量表,Setup.S还需要使用BIOS中断来获取PC硬件系统的信息。此处调用的函数为:read_it,实际读取的大小为0x7F00*16=508k<512k。如果是bzImage大内核,则直接拷贝到0x100000处)。
6. 最后,跳到0x9020:0(setup.S)处执行
setup.S利用BIOS中断读取机器配置(同时调用video.S检测显示器和显示模式),并保存到0x9000:0(覆盖掉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处,设置gdt和idt,开启A20地址线(使能1M以上内存)、重新设置两个中断控制芯片8259A,最后设置CPU的控制寄存器CR0,进入32位保护模式,并跳转到0x1000即arch/i386/boot/compressed/head.S中的startup_32()(对bzImage是 0x100000)。
head.S
然后调用decompress_kernel()解压内核映像,首先显示"Uncompressing Linux...",解压完后显示 "OK, booting the kernel."。内核解压后的映像被放置到0x100000(arch/i386/kernel/head.S文件中的startup_32(),因为通过物理地址跳转,相同的函数名没有问题)。
Setup.S结束后内存布局
函数startup_32()为linux的入口,它为Linux第一个进程设定环境,IDT、GDT和LDT被装入,处理器初始化完毕,初始化内存页表,进入分页方式,最终调用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_SOFTIRQ和HI_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_struct和pid,创建内核线程(没有用户栈空间,其pid为1)。(内核从2.6.22开始,rest_init()除了创建线程kernel_init以外,还创建kthreadd线程-pid为2。根据以前的说法 init 进程1是除进程0以外的所有进程的父进程,因为除了进程0外,其他进程都是init进程或其子进程不断fork出来的。当一个进程的父进程结束时,这个进程变成孤儿进程,孤儿进程的父进程也是init进程。2.6.22 及以后,很多内核线程由kthreadd线程create_kthread出来,所以’ps –ef’显示的父进程PID为2。)
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);
根据内核是否支持 initrd(init/Makefile中CONFIG_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的根目录上(这里两种情况:根内核编译在一起的initrd和bootloader加载的initrd。前者位于.init.ramfs段中,全局变量__initramfs_start和__initramfs_end分别指向这个数据段的起始地址和结束地址;后者由bootloader把起始地址和结束地址传递给内核,全局变量initrd_start和initrd_end分别指向其起始地址和结束地址)。如果是bootloader加载的老式块设备格式,则将initrd_start和initrd_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)。initrd(init RAM Disk)则是一个包含了被压缩过的小型根目录的RAM disk,它含有Linux启动所必须的目录、驱动程序、可执行文件和启动脚本(如init,bin,insmod等)。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 |
设置环境变量如umask、PATH等,其间会调用/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 |
加载设备驱动,IDE,SCSI,NETWORK,AUDIO等 |
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 |
查找FOOTFSTYPE和rootdev,mount文件系统(出于安全的原因,使用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.d中K开头的服务(守护进程),并启动S开头的服务:
amd:自动安装NFS守护进程
apmd:高级电源管理守护进程
arpwatch:记录日志并构建一个在LAN接口上看到的以太网地址和IP地址对数据库
autofs:自动安装管理进程automount,与NFS相关,依赖于NIS
crond:Linux下的计划任务的守护进程
named:DNS服务器
netfs:安装NFS、Samba和NetWare网络文件系统
network:激活已配置网络接口的脚本程序
nfs:打开NFS服务
portmap:RPC portmap管理器,它管理基于RPC服务的连接
sendmail:邮件服务器sendmail
smb:Samba文件共享/打印服务
syslog:一个让系统引导时起动syslog和klogd系统日志守候进程的脚本
xfs:X Window字型服务器,为本地和远程X服务器提供字型集
Xinetd:支持多种网络服务的核心守护进程,可以管理wuftp、sshd、telnet等服务
4) 启动虚拟终端/sbin/mingetty
5) 如果运行级别为5,启动X11
这时呈现给用户的就是最终的登录界面。