Linux 內核啟動及文件系統加載過程
當u-boot 開始執行 bootcmd 命令,就進入 Linux 內核啟動階段。普通 Linux 內核的啟動過程也可以分為兩個階段。本文以項目中使用的 linux-2.6.37 版源碼為例分三個階段來描述內核啟動全過程。第一階段為內核自解壓過程,第二階段主要工作是設置ARM處理器工作模式、使能 MMU 、設置一級頁表等,而第三階段則主要為C代碼,包括內核初始化的全部工作。
一、 Linux 內核自解壓過程
在 linux 內核啟動過程中一般能看到圖1內核自解壓界面,本小節本文重點討論內核的自解壓過程。
UncompressingLinux...done, booting the kernel.
這也是由 decompress_kernel函數輸出的,執行完解壓過程,再返回到head.S中的583行,啟動內核
call_kernel: bl cache_clean_flush bl cache_off mov r0, #0 @ must be zero mov r1, r7 @ restore architecture number mov r2, r8 @ restore atags pointer mov pc, r4 @ call kernel
其中 r4 中已經在head.S的第180行處預置為內核鏡像的地址,如下代碼:
#ifdef CONFIG_AUTO_ZRELADDR @determine final kernel image address mov r4, pc and r4, r4, #0xf8000000 add r4, r4, #TEXT_OFFSET #else ldr r4, =zreladdr #endif
這樣就進入Linux內核的第一階段,我們也稱之為stage1 。
二、 Linux 內核啟動第一階段 stage1
承接上文,這里所以說的第一階段 stage1 就是內核解壓完成並出現 Uncompressing Linux...done,booting the kernel. 之后的階段。該部分代碼實現在arch/arm/kernel 的 head.S中,該文件中的匯編代碼通過查找處理器內核類型和機器碼類型調用相應的初始化函數,再建 立頁表,最后跳轉到start_kernel() 函數開始內核的初始化工作。檢測處理器類型是在匯編子函數__lookup_processor_type 中完成的,通過以下代碼可實現對它的調用: bl__lookup_processor_type (在文件head-commom.S 實現)。 __lookup_processor_type調用結束返回原程序時,會將返回結果保存到寄存器中。其中r5 寄存器返回一個用來描述處理器的結構體地址,並對r5進行判斷,如果r5的值為0則說明不支持這種處理器,將進入 __error_p 。r8 保存了頁表的標志位, r9 保存了處理器的 ID 號,r10保存了與處理器相關的struct proc_info_list結構地址。Head.S 核心代碼如下:
ENTRY(stext) setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @設置SVC模式關中斷 mrc p15, 0, r9, c0, c0 @ 獲得處理器ID,存入r9寄存器 bl __lookup_processor_type @ 返回值r5=procinfo r9=cpuid movs r10, r5 THUMB( it eq ) @ force fixup-able long branch encoding beq __error_p @如果返回值r5=0,則不支持當前處理器' bl __lookup_machine_type @ 調用函數,返回值r5=machinfo movs r8, r5 @ 如果返回值r5=0,則不支持當前機器(開發板) THUMB( it eq ) @ force fixup-able long branch encoding beq __error_a @ 機器碼不匹配,轉__error_a並打印錯誤信息 bl __vet_atags #ifdef CONFIG_SMP_ON_UP @ 如果是多核處理器進行相應設置 bl __fixup_smp #endif bl __create_page_tables @最后開始創建頁表
檢測機器碼類型是在匯編子函數__lookup_machine_type (同樣在文件head-common.S 實現) 中完成的。與 __lookup_processor_type類似,通過代碼:“ bl __lookup_machine_type”來實現對它的調 用。該函數返回時,會將返回結構保存放在r5、r6 和 r7三個寄存器中。其中r5 寄存器返回一個用來描述機器(也就是開發板)的結構體地址,並對r5進行判斷,如果r5的值為0 ,則說明不支持這種機器(開發板),將進入__error_a, 打印出內核不支持u-boot傳入的機器碼的錯誤如圖2。 r6保存了 I/O基地址, r7保存了 I/O 的頁表偏移地址。
當檢測處理器類型和機器碼類型結束后,將調用 __create_page_tables子函數來建立頁表,它所要做的工作就是將 RAM 基地址開始的1M 空間的物理地址映射到 0xC0000000開始的虛擬地址處。對本項目的開發板DM3730 而言,RAM 掛接到物理地址 0x80000000 處,當調用 __create_page_tables 結束后 0x80000000 ~ 0x80100000 物理地址將映射到 0xC0000000~0xC0100000 虛擬地址處。當所有的初始化結束之后,使用如下代碼來跳到 C 程序的入口函數start_kernel()處,開始之后的內核初始化工作:bSYMBOL_NAME(start_kernel) 。
三、Linux內核啟動第二階段 stage2
從start_kernel函數開始
Linux內核啟動的第二階段從start_kernel函數開始。start_kernel 是所有Linux 平台進入系統內核初始化后的入口函數,它主要完成剩余的與 硬件平台相關的初始化工作,在進行一系列與內核相關的初始化后,調用第一個用戶進程-init進程並等待用戶進程的執行,這樣整個Linux內核便啟動完畢。該函數位於 init/main.c文件中,主要工作流程如圖 所示:
圖3 start_kernel流程圖
該函數所做的具體工作有 :
1) 調用 setup_arch() 函數進行與體系結構相關的第一個初始化工作;對不同的體系結構來說該函數有不同的定義。對於ARM平台而言,該函數定義在arch/arm/kernel/setup.c 。它首先通過檢測出來的處理器類型進行處理器內核的初始化,然后 通過bootmem_init()函數根據系統定義的 meminfo結構進行內存結構的初始化,最后調用paging_init()開啟MMU,創建內核頁表,映射所有的物理內存和 IO空間。
2) 創建異常向量表和初始化中斷處理函數;
3) 初始化系統核心進程調度器和時鍾中斷處理機制;
4) 初始化串口控制台(console_init);
ARM-Linux 在初始化過程中一般都會初始化一個串口做為內核的控制台,而串口Uart驅動卻把串口設備名寫死了,如本例中 linux2.6.37串口設備名為 ttyO0,而不是常用的ttyS0。有了控制台內核在啟動過程中就可以通過串口輸出信息以便開發者或用戶了解系統的啟動進程。
5) 創建和初始化系統 cache,為各種內存調用機制提供緩存,包括;動態內存分配,虛擬文件系統(VirtualFile System )及頁緩存。
6) 初始化內存管理,檢測內存大小及被內核占用的內存情況;
7) 初始化系統的進程間通信機制(IPC); 當以上所有的初始化工作結束后, start_kernel() 函數會調用 rest_init() 函數來進行最后的初始化,包括創建系統的第一個進程-init 進程來結束內核的啟動。
掛載根文件系統並啟動 init
Linux 內核啟動的下一過程是啟動第一個進程 init ,但必須以根文件系統為載體,所以在啟動init 之前,還要掛載根文件系統。
四、掛載根文件系統
根文件系統至少包括以下目錄:
/etc/ :存儲重要的配置文件。
/bin/ :存儲常用且開機時必須用到的執行文件。
/sbin/ :存儲着開機過程中所需的系統執行文件。
/lib/ :存儲/bin/及/sbin/的執行文件所需的鏈接庫,以及Linux的內核模塊。
/dev/ :存儲設備文件。
注:五大目錄必須存儲在根文件系統上,缺一不可。
以只讀的方式掛載根文件系統,之所以采用只讀的方式掛載根文件系統是因為:此時Linux內核仍在啟動階段,還不是很穩定,如果采用可讀可寫的方式掛載根文件系統,萬一Linux不小心宕機了,一來可能破壞根文件系統上的數據,再者Linux下次開機時得花上很長的時間來檢查並修復根文件系統。
掛載根文件系統的而目的有兩個:一是安裝適當的內核模塊,以便驅動某些硬件設備或啟用某些功能;二是啟動存儲於文件系統中的init 服務,以便讓 init服務接手后續的啟動工作。
執行 init 服務
Linux內核啟動后的最后一個動作,就是從根文件系統上找出並執行init服務。 Linux內核會依照下列的順序尋找init服務:
1) /sbin/ 是否有 init 服務
2) /etc/ 是否有init 服務
3) /bin/ 是否有 init 服務
4)如果都找不到最后執行/bin/sh
找到 init服務后, Linux會讓 init 服務負責后續初始化系統使用環境的工作, init啟動后,就代表系統已經順利地啟動了linux內核。
啟動init服務時,init服務會讀取/etc/inittab文件,根據/etc/inittab中的設置數據進行初始化系統環境的工作。 /etc/inittab定義 init 服務在 linux啟動過程中必須依序執行以下幾個Script :
/etc/rc.d/rc.sysinit
/etc/rc.d/rc
/etc/rc.d/rc.local
/etc/rc.d/rc.sysinit主要的功能是設置系統的基本環境,當init服務執行rc.sysinit時 要依次完成下面一系列工作:
(1)啟動udev
(2)設置內核參數
執行sysctl –p ,以便從 /etc/sysctl.conf 設置內核參數
(3)設置系統時間
將硬件時間設置為系統時間
(4)啟用交換內存空間
執行 swpaon –a –e,以便根據/etc/fstab的設置啟用所有的交換內存空間。
(5)檢查並掛載所有文件系統
檢查所有需要掛載的文件系統,以確保這些文件系統的完整性。檢查完畢后以可讀可寫的方式掛載文件系統。
(6)初始化硬件設備
Linux除了在啟動內核時以靜態驅動程序驅動部分的硬件外,在執行rc.sysinit 時,也會試着驅動剩余的硬件設備。 r c.sysinit 驅動的硬件設備包含以下幾項:
a)定義在/etc/modprobe.conf 的模塊
b) ISA PnP的硬件設備
c) USB設備
(7)初始化串行端口設備
Init服務會管理所有的串行端口設備,比如調制解調器、不斷電系統、串行端口控制台等。Init 服務則通過rc.sysinit來初始化linux 的串行端口設備。當rc.sysinit 發現 linux 才能在這 /etc/rc.serial 時,才會執行 /etc/rc.serial ,借以初始化所有的串行端口設備。因此,你可以在 /etc/rc.serial 中定義如何初始化 linux所有的串行端口設備。
(8)清除過期的鎖定文件與IPC文件
(9)建立用戶接口
在執行完3個主要的 RC Script 后, init服務的最后一個工作,就是建立linux的用戶界面,好讓用戶可以使用 linux 。此時init 服務會執行以下兩項工作:
(10)建立虛擬控制台
Init 會在若干個虛擬控制台中執行 /bin/login,以便用戶可以從虛擬控制台登陸 linux 。 linux 默認在前6個虛擬控制台,也就是 tty1~tty6 ,執行 /bin/logi 登陸程序。當所有的初始化工作結束后,cpu_idle()函數會被調用來使系統處於閑置( idle)狀態並等待用戶程序的執行。至此,整個Linux內核啟動完畢。整個過程見圖4。
圖4:linux內核啟動及文件系統加載全過程