深入理解Linux啟動過程

 
    本文詳細分析了Linux桌面作業系統的啟動過程,涉及到BIOS系統、LILO GRUB引導裝載程式,以及bootsectsetupvmlinux等映像檔案,並結合引導、啟動原理和具體的程式碼實現機制由淺入深地進行了分析。

    初學者剛接觸Linux桌面系統會感覺系統啟動速度較慢,那麼,為什麼它的啟動速度慢呢?本文就桌面系統的引導和啟動過程展開分析,以期對初學者熟悉Linux有所幫助。

一、Linux系統的引導過程

    簡單地說,系統的引導和啟動過程就是計算機加電以後所要發生的事情, 比如,加電自檢、載入程式的拷貝和執行、核心的拷貝和執行及使用者程式的執行等。這個過程就是常說的bootstrap,我們把這些歸納為5個過程, 下面來逐一分析。

   1BIOS執行階段

    現代計算機系統的儲存機制是“揮發”性的,一旦關機斷電, 儲存在記憶體中的資訊。連同作業系統本身的對映就丟失了。所以,必須把作業系統(核心) 的映像儲存在某些不“揮發” 的介質中,使得開機加電時由一個不“揮發”介質載入作業系統,並轉入執行的過程。這就是引導,也稱自舉。這些不“揮發” 介質通常是指硬碟或軟盤, 也可以是EPROM F1ash儲存器,還可以是網路中別的節點。要想在開機時從不“揮發” 介質裝入作業系統的映像,系統就要CPU在開機時能執行一段程式,這段程式本身必須儲存在作為系統記憶體一部分的EPROM Flash等儲存器中, 而且它們知道怎樣才能從不“揮發” 介質裝入作業系統的映像。事實上,各種CPU 被設計成一個加電後就從某個特殊的地址開始執行指令,所以這些不揮發儲存器就被安置在這個位置上。比如在i386CPU系統中,計算機在加電的那一刻,RAM 晶片中所包含的是隨機資料,還沒有作業系統,在此刻有一個特殊硬體電路在加電時會在C P U 的一個引腳上產生一個RESET邏輯值,硬體電路設定RESET邏輯值以後,程式碼暫存器CS的內容為0xffff,而指令暫存器的內容為0。也就是說,CPU要從線性地址0xffff0開始處取第一條指令。硬體電路再把這個實體地址對映到RAM 晶片中,BIOS就存放在這裡,這時候處理器就開始執行BIOS程式碼了。我們都知道BIOS中包含了幾個中斷驅動的低階程式,可以使用它們來初始化一些硬體裝置,但它們是在真實模式下工作的。其中真實模式地址是由一個seg段和一個off偏移量組成的,相應的實體地址可以使用“seg*16+off” 來計算。

接下來BIOS要做的就是執行一系列的測試,看看到底系統中有什麼裝置,以及這些裝置是否正常工作。在執行這個過程時,會顯示一些如BIOS系統的版本號等資訊。當檢測到可用的裝置後就進行一些初始化工作,比如初始化PCI裝置以避免I RQ線與I/O埠的衝突,最後顯示系統中安裝的所有PCI裝置的一個列表。

在早期的計算機系統中, 類似於BIOS功能的程式非常小,並且不同時期這段程式的設計也不相同。在PC發展早期, 由於當時儲存晶片大小的限制,使得該段程式的目的和功能都很單一 再說,如此小的一段程式很難依靠自身的力量把龐大的作業系統的映像從磁碟裡讀進來。於足,人們又提出了引導扇區的概念,使得儲存在引導扇區中的程式來協助BIOS完成作業系統的引導工作。但是,引導扇區的大小也不過5l2個位元組, 能夠容納的資訊和程式碼也很有限,所以說,作業系統的引導程式碼是一個循序漸進的過程,它分佈在不同的角落。當BlOS根據設定將相應的啟動裝置的第一個扇區的內容拷貝到RAM 中時, 這些內容被放在實體地址0x0O007c00開始的地方。此後,系統就開始跳到這個地址,並開始執行相應的程式碼。

    2Boot Loader階段

如此小的引導記錄要完成這麼大的任務,壓力是不小的,所以引導扇區的程式及輔助程式必須很簡練,它們都採用組合語言編寫,這些原始碼都存放在arch/ 下具體CPU名下的boot目錄中,如bootsect.Ssetup.Svideo.S。其中bootsect.SLinux引導扇區的原始碼。這樣,經過編譯、彙編和連線以後,形成了3個組成部分,即引導扇區的映像bootsect、輔助程式setup及核心映像本身(通常是vmlinux,有時也用uImage)。嚴格地說,bootsectsetup並不是核心的一部分。

引導裝載程式就是由BIOS來把作業系統的核心映像裝入到RAM 中所呼叫的一個程式。這裡我們選擇用硬碟啟動來說明引導裝載程式的執行過程。說起硬碟,大家都知道它是由許許多多的扇區和柱面組成,其中把第一個扇區稱為主開機記錄(Master Boot RecordMBR),在該扇區中包含了分割槽資訊和一個小程式,這個小程式用來裝載被啟動的作業系統所在分割槽的第一個扇區。說到這裡我們就要注意,這一段Windows系統和Linux系統是有區別的:Windows系統使用分割槽表中所包含的一個active標誌來標識這個分割槽,當然這個分割槽也可以使用FDISK之類的程式進行設定,但有一個條件就是隻有那些核心映像存放在活動分割槽的作業系統才可以啟動。Linux系統的處理方法要更靈活些,它使用GRUB或是LILO程式把這個包含在MBR 中的不完善引導裝載程式給替掉。裝入程式在啟動過程中被執行時,使用者可以選擇裝入哪個作業系統。但LILOGRUB的工作原理又不盡相同,關於它們的詳細介紹可以查閱相關資料。LILO 引導裝入程式被分為兩部分,MBR 或分割槽引導扇區包括一個小的引導裝入程式,由BIOS把這個小程式裝入從地址0xO0007c00開始的RAM 中,這個小程式又把自己移到地址0x0009a000, 然後建立真實模式棧。接著把LILO 的第二部分裝入到從地址0x0009b000開始的RAM 中, 第二部分又讀取可用作業系統的對映表,並給使用者一個提示符號。這個時候使用者可以從中選擇一個作業系統進行啟動,引導裝入程式就可以把相應分割槽的引導扇區拷貝到RAM 中並執行,或者是直接把核心映像拷貝到RAM 中。在拷貝核心的過程中,首先是把核心映像所整合的引導裝入程式拷貝到地址0xO009000,然後把setup()程式碼拷貝到地址0x00090200,最後把核心映像的其餘部分拷貝到地址0x000100000x00100000, 最終系統執行跳到setup()程式碼上。

    3Setup函式執行階段

Setup()是組合語言函式程式碼,它在核心的編譯連結過程中被放到核心的引導裝入程式之後,也就是核心映像檔案的偏移量0x200地址處,實際實體地址0x00090200開始的RAM 中。因為核心不依賴於BIOS, 雖然BIOS已經初始化了大部分硬體裝置,但Linux系統還要以自己的方式重新初始化裝置,以增加可移植性和健壯性。還要注意的是,核心是工作在保護模式下的。總的來說,setup()函式的作用就是初始化計算機中的硬體裝置,併為核心程式的執行建立環境。比如,檢查系統中可用的RAM 數量、設定鍵盤重複延時速率、顯示卡等其他裝置的檢查,以及初始化和切換真實模式到保護模式等。最後,系統執行跳到startup_ 32彙編函式上。

二、Linux系統的啟動過程

當核心映像被裝載到RAM晶片後,就開始執行核心的程式碼,這意味著引導完成,開始進入Linux系統的啟動過程。

    1Startup_32函式的執行階段

在系統的啟動過程中有兩個startup_320()函式,即位於arch/i386/boot/compressed/head.S檔案中實現的。就是在setup()函式結束以後,該函式就被移動到實體地址0X001000000x00001000處,這取決於核心映像是被裝到RAM 的高位還是底位。因為核心映像檔案在編譯連線時所產生的大小不同, 如zImagebzImage大小相差很大,在裝載解壓時所使用的緩衝區也不同,所以他們所處的實體地址是不同的。不過解壓後的映像最終都處在實體地址0x00100000開始的位置。然後跳轉到這個地址處執行解壓後的映像中的另一個startup_32()函式,這個函式為第一個Linux程式(程式0)建立執行環境,該函式初始化段暫存器、為程式0建立核心態堆疊等一系列活動。最後識別處理器模式,並跳轉到start_kernel()函式。將Linux核心的映像裝入記憶體,並且setup()函式做了一些必要的準備,就該startup_32函式開始幹活了。CPU通過一條長程轉移指令轉到映像程式碼段開頭的入口startup_32處,對於SMP結構的系統來說,這個時候執行的只是其中的一個處理器,就是所謂的主CPU。其他的次CPU 處於停機狀態, 等待主CPU 的啟動。次CPU在受到啟動進入核心時,同樣也要從startup_32開始執行,所以從startup_32開始的程式碼是公共的。但有些操作僅由主CPU來執行,另一些操作由次CPU執行, 這並不意味著主CPU 和次CPU 併發地執行這段程式。實際上,主CPU 是開路先鋒,首先執行這段程式,完成以後逐個啟動次CPU執行,並且等待其完成。所以,在同一時間系統中最多隻有一個處理器在執行這段程式。不管是主CPU還是次CPU,進入startup_32時都執行在保護模式下的段式定址方式,等到第二個startup_32函式執行到最後時, 就開始執行start_kernel函式。

    2Start_kernel函式執行階段

到了這個階段才是真正的核心初始化階段,幾乎核心每個部分的初始化工作都是由這個函式來完成,如頁表的初始化、系統日期和時間的初始化等。從某種意義上說,函式Start_kernel就好像一般可執行程式中的主函式main(),系統在進入這個函式之前已經進行了一些最底限度的初始化,為這個函式的執行建立起了一個環境,創造了必要的條件。當然,這個函式還要繼續進行核心的初始化,甚至可以說核心的初始化在這裡才真正開始,但它是較高層次的初始化。這個函式的程式碼在init/main.C中,從現在開始初始化流程不與CPU 型別和系統啟動;方式相關了。此時系統執行在CPU的特權級,也就是我們常說的內

核模式下。start_ kernel函式主要完成一些資料結構的初始化,主要包括

如下:

printk(linux_banner) 輸出

Linux版本資訊;

Setup_arch()(arch/i386/kernel/traps.C)執行與體系結構相關的設定,如記憶體分析分配內

核頁表, 處理啟動命令列等;

Trap_init() 設定各種人口地址,如異常事件處理程式入口, 系統呼叫人口,

IniLIRQ() 初始化IRQ 中斷處理機制;

Sched_init() 設定並啟動第一個程式ini_task0 l

Softirq_init() 對軟中斷子系統進行初始化;

Time_initO 讀取實時時間,重新設定時鐘中斷irq0的中斷服務程式入口等;

Console__init() 初始化控制檯和顯示器;

Init_modules() 初始化

kernel__m odule l

Kmem_cache_init0 對記憶體的slab分配機制初始化{

Mem_init() 虛擬記憶體計算以及初始化;

Kmem_cache_size_jnit() 初始化slab分配器中的內部cashe和全域性cashel

Fork_init() 定義了系統的最大程式數目。此外,還有一些對其他支援的初始化。

隨後,進入resetinit0函式呼叫kernel__thread()函式為程式1建立init核心執行緒,這個核心執行緒又會建立其他的核心執行緒程式,並執行/sbin/init程式。此後start_kernel進入一個空閒等待迴圈(cpu_idle()), 使用系統初始化後CPU 的空閒時間片,init核心執行緒首先要鎖定核心,然後呼叫do_basic_setup()來初始化外部裝置及載入驅動程式。在do_basic_setup()函式呼叫完之後,init()函式會釋放初始化函式所用的記憶體,並且開啟/dev/console裝置重新定向控制檯,使用系統呼叫execve來執行使用者態程式/sbin/init

到目前為止,Linux核心的初始化工作完成,此時系統中已經存在5個執行實體:init執行緒、kflushd核心執行緒、kupdate核心執行緒、kswapd核心執行緒和keventd核心執行緒。本身所在的執行體其實就是一個執行緒,不過是由手工建立的。它在建立了init0執行緒以後就進入cpu_idle迴圈, 不會在程式列表中出現。如果使用pstree命令,則不能列出該執行緒。

最後,init程式會根據inittab檔案中的設定資訊啟動相應的使用者程式。當init得到控制並啟動mingetty顯示登入介面及提示後,系統啟動完成。

三、小結

從加電自檢開始, 引導過程要經歷數十個回合來拷貝執行,使用不同的引導裝載程式所使角的流程也不同。當把核心映像拷貝到RAM中展開後, 核心開始掌管主權,開始了自己的 “事業”。核心執行緒init()的任務仍然還是初始化,當然是進一步的、更高層次上的初始化。

事實上,從引導結束、CPU轉入核心映像開始,一共有三個階段的初始化:第一階段是從進入startup_32()開始, 到進入start_kernel()start_ secondary()。這個階段主要是對CPU 自身的初始化,主CPU和次CPU 都要經歷這種初始化,但是主CPU要多一些貢獻。第二階段是從進入start_kernel()開始,到進入cpu_idle()。這個階段主要是對系統的寶貴資源的初始化, 僅由主CPU進行。第三階段是init()的執行,這是對系統接近使用者層的初始化, 這個時候表面上看已經沒:有主CPU和次CPU之分,但誰執行init()取決於競爭排程的結果。事實上,由於主CPU預先留了一手, 實際上還是由它來執行。