Linux 系統啓動過程詳解

以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_struct pid,創建內核線程(沒有用戶棧空間,其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

這時呈現給用戶的就是最終的登錄界面。

發佈了22 篇原創文章 · 獲贊 7 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章