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