以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
這時呈現給用戶的就是最終的登錄界面。