ARM Linux啟動過程分析
5) 調用 Linux內核映像
Bootloader完成的最后一項工作便是調用 Linux內核。如果 Linux 內核存放在 Flash 中,并且可直接在上面運行(這里的 Flash 指 Nor Flash),那么可直接跳轉到內核中去執行。但由于在 Flash 中執行代碼會有種種限制,而且速度也遠不及 RAM 快,所以一般的嵌入式系統都是將 Linux內核拷貝到 RAM 中,然后跳轉到 RAM 中去執行。不論哪種情況,在跳到 Linux 內核執行之前 CUP的寄存器必須滿足以下條件:r0=0,r1=處理器類型,r2=標記列表在 RAM中的地址。
3. Linux內核的啟動過程
在 bootloader將 Linux 內核映像拷貝到 RAM 以后,可以通過下例代碼啟動 Linux 內核:call_linux(0, machine_type, kernel_params_base)。
其中,machine_tpye 是 bootloader檢測出來的處理器類型, kernel_params_base 是啟動參數在 RAM 的地址。通過這種方式將 Linux 啟動需要的參數從 bootloader傳遞到內核。Linux 內核有兩種映像:一種是非壓縮內核,叫 Image,另一種是它的壓縮版本,叫zImage。根據內核映像的不同,Linux 內核的啟動在開始階段也有所不同。zImage 是 Image經過壓縮形成的,所以它的大小比 Image 小。但為了能使用 zImage,必須在它的開頭加上解壓縮的代碼,將 zImage 解壓縮之后才能執行,因此它的執行速度比 Image 要慢。但考慮到嵌入式系統的存儲空容量一般比較小,采用 zImage 可以占用較少的存儲空間,因此犧牲一點性能上的代價也是值得的。所以一般的嵌入式系統均采用壓縮內核的方式。
對于 ARM 系列處理器來說,zImage 的入口程序即為 arch/arm/boot/compressed/head.S。它依次完成以下工作:開啟 MMU 和 Cache,調用 decompress_kernel()解壓內核,最后通過調用 call_kernel()進入非壓縮內核 Image 的啟動。下面將具體分析在此之后 Linux 內核的啟動過程。
3.1 Linux內核入口
Linux 非壓縮內核的入口位于文件/arch/arm/kernel/head-armv.S 中的 stext 段。該段的基地址就是壓縮內核解壓后的跳轉地址。如果系統中加載的內核是非壓縮的 Image,那么bootloader將內核從 Flash中拷貝到 RAM 后將直接跳到該地址處,從而啟動 Linux 內核。不同體系結構的 Linux 系統的入口文件是不同的,而且因為該文件與具體體系結構有關,所以一般均用匯編語言編寫[3]。對基于 ARM 處理的 Linux 系統來說,該文件就是head-armv.S。該程序通過查找處理器內核類型和處理器類型調用相應的初始化函數,再建立頁表,最后跳轉到 start_kernel()函數開始內核的初始化工作。
檢測處理器內核類型是在匯編子函數__lookup_processor_type中完成的。通過以下代碼可實現對它的調用:bl __lookup_processor_type。__lookup_processor_type調用結束返回原程序時,會將返回結果保存到寄存器中。其中r8 保存了頁表的標志位,r9 保存了處理器的 ID 號,r10 保存了與處理器相關的 struproc_info_list 結構地址。
檢測處理器類型是在匯編子函數 __lookup_architecture_type 中完成的。與__lookup_processor_type類似,它通過代碼:“bl __lookup_processor_type”來實現對它的調用。該函數返回時,會將返回結構保存在 r5、r6 和 r7 三個寄存器中。其中 r5 保存了 RAM 的起始基地址,r6 保存了 I/O基地址,r7 保存了 I/O的頁表偏移地址。當檢測處理器內核和處理器類型結束后,將調用__create_page_tables 子函數來建立頁表,它所要做的工作就是將 RAM 基地址開始的 4M 空間的物理地址映射到 0xC0000000 開始的虛擬地址處。對筆者的 S3C2410 開發板而言,RAM 連接到物理地址 0x30000000 處,當調用 __create_page_tables 結束后 0x30000000 ~ 0x30400000 物理地址將映射到0xC0000000~0xC0400000 虛擬地址處。
當所有的初始化結束之后,使用如下代碼來跳到 C 程序的入口函數 start_kernel()處,開始之后的內核初始化工作:
b SYMBOL_NAME(start_kernel)
3.2 start_kernel函數
start_kernel是所有 Linux 平臺進入系統內核初始化后的入口函數,它主要完成剩余的與硬件平臺相關的初始化工作,在進行一系列與內核相關的初始化后,調用第一個用戶進程-init 進程并等待用戶進程的執行,這樣整個 Linux 內核便啟動完畢。該函數所做的具體工作有[4][5]
:
1) 調用 setup_arch()函數進行與體系結構相關的第一個初始化工作;
對不同的體系結構來說該函數有不同的定義。對于 ARM 平臺而言,該函數定義在arch/arm/kernel/Setup.c。它首先通過檢測出來的處理器類型進行處理器內核的初始化,然后通過 bootmem_init()函數根據系統定義的 meminfo 結構進行內存結構的初始化,最后調用paging_init()開啟 MMU,創建內核頁表,映射所有的物理內存和 IO空間。
2) 創建異常向量表和初始化中斷處理函數;
3) 初始化系統核心進程調度器和時鐘中斷處理機制;
4) 初始化串口控制臺(serial-console);
ARM-Linux 在初始化過程中一般都會初始化一個串口做為內核的控制臺,這樣內核在啟動過程中就可以通過串口輸出信息以便開發者或用戶了解系統的啟動進程。
5) 創建和初始化系統 cache,為各種內存調用機制提供緩存,包括;動態內存分配,虛擬文件系統(VirtualFile System)及頁緩存。
6) 初始化內存管理,檢測內存大小及被內核占用的內存情況;
7) 初始化系統的進程間通信機制(IPC);
當以上所有的初始化工作結束后,start_kernel()函數會調用 rest_init()函數來進行最后的初始化,包括創建系統的第一個進程-init 進程來結束內核的啟動。Init 進程首先進行一系列的硬件初始化,然后通過命令行傳遞過來的參數掛載根文件系統。最后 init 進程會執行用 戶傳遞過來的“init=”啟動參數執行用戶指定的命令,或者執行以下幾個進程之一:
execve("/sbin/init",argv_init,envp_init);
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
execve("/bin/sh",argv_init,envp_init)。
linux操作系統文章專題:linux操作系統詳解(linux不再難懂)
評論