在上一篇電腦啟動過程文章中介紹了電腦啟動的基本流程,本篇文章主要介紹Linux內核Kernel的啟動過程。 一、內核啟動的基本流程 sequenceDiagram participant Bootloader participant Kernel participant InitProcess ...
在上一篇
電腦啟動過程
文章中介紹了電腦啟動的基本流程,本篇文章主要介紹Linux內核Kernel的啟動過程。
一、內核啟動的基本流程
sequenceDiagram participant Bootloader participant Kernel participant InitProcess Bootloader->>Kernel: 載入內核映像 Kernel->>Kernel: 內核解壓 Kernel->>Kernel: 內核啟動 Kernel->>Kernel: 調用start_kernel函數 Kernel->>InitProcess: 啟動初始進程1. 啟動載入程式 (Bootloader)
啟動載入程式(如GRUB、LILO、syslinux等)負責將內核映像從存儲設備載入到記憶體中,並準備好內核啟動所需的環境。
- 載入內核映像:啟動載入程式將壓縮的內核映像(如vmlinuz)從硬碟載入到記憶體中。內核映像通常是一個gzip或其他格式壓縮的二進位文件。
- 載入initrd/initramfs:如果使用initrd(初始RAM盤)或initramfs(初始RAM文件系統),啟動載入程式也會將這些文件載入到記憶體中,以便內核在啟動時使用。
2. 內核解壓階段
在內核映像的開頭,有一個小的解壓縮程式,它負責解壓內核的主體部分。
- 解壓內核:內核映像被載入到記憶體後,解壓縮程式會運行並將壓縮的內核映像解壓到適當的記憶體位置。
- 跳轉到解壓後的內核:一旦解壓完成,控制權會被移交給解壓後的內核代碼的入口點。
3. 內核啟動(Kernel Startup)
解壓後的內核代碼會從一個固定的入口點開始執行,這個入口點是平臺和架構相關的。對於x86架構,通常是startup_32或startup_64函數。
- 架構特定的初始化:根據具體的硬體架構,內核會執行一些必要的初始化步驟,比如設置CPU的運行模式,初始化分頁機制,建立基本的記憶體映射等。
- 初始化內核堆棧:內核設置好自己的堆棧,以便後續的函數調用和操作。
- 調用
start_kernel
函數:完成基礎的硬體初始化後,內核會調用start_kernel函數,這是內核初始化的核心部分。
4. start_kernel函數
start_kernel函數位於init/main.c文件中,負責完成大部分內核的初始化工作。
- 初始化控制台:設置內核的印表機制,以便後續的輸出可以顯示出來。
- 初始化記憶體管理子系統:建立初始的記憶體管理結構,準備好記憶體分配機制。
- 檢測和初始化硬體設備:內核會檢測並初始化系統中的各種硬體設備和驅動程式。
- 啟動中斷處理機制:設置和啟動中斷處理機制,使得內核可以響應硬體中斷。
- 初始化內核調度器:初始化內核調度器,以便管理進程調度。
- 載入初始進程:內核創建並啟動第一個用戶空間進程,通常是/sbin/init。
5. 啟動初始進程
init進程是用戶空間的第一個進程,負責進一步的系統初始化工作,包括啟動系統服務和守護進程。
- init進程的初始化:init進程執行系統初始化腳本,設置各種系統參數和啟動服務。
- 啟動用戶空間服務:最終,init進程啟動配置的所有用戶空間服務和守護進程,從而完成系統的啟動過程。
二、內核文件載入及解壓縮
1.為什麼是壓縮文件
Linux內核映像通常是一個壓縮文件,主要有以下原因:
- 減少存儲空間: 壓縮內核映像可以顯著減少其在存儲設備上的占用空間。這對嵌入式系統、存儲資源有限的設備以及需要快速分發和更新內核的環境尤其重要
- 加快載入速度: 壓縮文件占用的空間更小,這意味著啟動載入程式從磁碟讀取文件到記憶體中的時間會更短。雖然解壓縮內核映像需要一些時間,但現代處理器的解壓縮速度非常快,通常解壓縮的時間比從存儲設備讀取更多數據的時間要少。這會整體上加快啟動過程。
- 提高傳輸效率: 在網路上傳輸內核映像時,壓縮文件可以顯著減少帶寬使用量。這對於需要遠程更新內核的系統(OTA)非常有利。
- 便於管理和分發: 壓縮內核映像更便於在各種介質上分發,比如光碟、U盤等。一個較小的文件更容易管理、備份和分發。
- 標準化處理: 使用壓縮內核映像是一種標準做法,啟動載入程式(如GRUB)已經能夠很好地支持這種格式,能自動識別並處理壓縮的內核映像。這使得系統啟動過程更簡單可靠。
2.文件類型vmlinuxz和bzImage
在連接壓縮映像文件之前,我們先來瞭解一下未經壓縮的編譯文件vmlinux
。
2.1 什麼是vmlinux?
vmlinux
是內核編譯過程
中生成的一個包含所有內核代碼和數據的二進位文件
。它是未經壓縮和未經過處理的內核映像,通常位於內核源碼目錄的根目錄下,特性如下:
- 未壓縮:vmlinux 是內核的未壓縮映像。它包含所有內核代碼、內核模塊以及相關的數據結構。
- ELF 格式:vmlinux 通常是一個 ELF(Executable and Linkable Format)文件,這是一個標準的可執行文件格式,用於存儲可執行文件、目標代碼和共用庫等。
- 符號信息:vmlinux 文件中包含調試符號和符號表信息,這些信息對內核調試和分析非常重要。
- 沒有文件尾碼:雖然 vmlinux 通常沒有文件尾碼,但它是一個標準的 ELF 文件,可以通過文件頭信息識別其格式。
2.2 vmlinux的生成過程
編譯Linux內核時,vmlinux是在鏈接階段生成的。以下是一個簡化的生成過程:
- 編譯各個源文件:內核的各個源文件(
.c
和.S
文件)首先被編譯為目標文件(.o 文件)。 - 鏈接目標文件:所有目標文件通過鏈接器(如
ld
)鏈接在一起,生成一個完整的內核映像,這個映像就是 vmlinux。
鏈接命令舉例:
ld -o vmlinux [object files] [linker scripts]
2.3 vmlinuxz和bzImage的生成過程
在獲得編譯文件vmlinux後,通常使用壓縮工具做進一步處理。
- 壓縮內核映像:將 vmlinux 壓縮生成 vmlinuz。通常使用 gzip 或其他壓縮工具。
壓縮命令:
gzip -c vmlinux > vmlinuz
- 生成引導載入程式格式的內核映像:一些系統需要特定格式的內核映像,例如 bzImage(適用於 x86 架構)。
生成命令:
make bzImage
2.4 其他壓縮格式
vmlinuz
、bzImage
、zImage
和 uImage
都是不同的 Linux 內核映像文件格式,它們各自有不同的用途和特性。
- vmlinuz:通用的壓縮內核映像名稱,主要用於各種 Linux 發行版。通常使用 gzip 壓縮。
- bzImage:大內核映像,解決了早期 zImage 的記憶體限制問題。用於 x86 架構,支持較大的內核映像。
- zImage:較老的內核映像格式,適用於小內核映像,受限於低記憶體地址空間。
- uImage:U-Boot 使用的內核映像格式,廣泛用於嵌入式系統。包含 U-Boot 頭部信息,支持多種壓縮演算法。
2.5 Android系統文件
在 Android 系統中,內核的壓縮文件格式通常是zImage
或Image.gz
,具體取決於所使用的啟動載入程式和設備的要求。
- zImage:在一些早期的 Android 設備上,內核映像可能採用 zImage 格式。這種格式的內核映像通常會被啟動載入程式直接載入並解壓,然後啟動內核。
- Image.gz:Image.gz 是指經過 gzip 壓縮的內核映像。這種格式的內核映像通常是 Linux 內核編譯過程中生成的 vmlinuz 文件,只是在 Android 系統中可能被重新命名為 Image.gz。啟動載入程式會載入這個壓縮的內核映像,併在載入到記憶體後解壓縮,然後啟動內核。
3.內核載入過程
sequenceDiagram participant BootLoader participant KernelEntry participant DecompressionCode participant DecompressionFunction participant KernelStartup BootLoader->>KernelEntry: 跳轉到內核入口點 KernelEntry->>DecompressionCode: 跳轉到解壓縮代碼 DecompressionCode->>DecompressionFunction: 調用解壓縮函數 DecompressionFunction->>DecompressionFunction: 解壓內核映像 DecompressionFunction->>DecompressionCode: 返回解壓結果 DecompressionCode->>KernelStartup: 跳轉到內核啟動3.1 內核映像載入到記憶體中
啟動載入程式(Bootloader)負責將壓縮的內核映像載入到記憶體中,並準備好啟動內核的環境。
- 載入內核和initrd:GRUB 會根據配置文件(通常是 grub.cfg)載入壓縮的內核映像和可選的 initrd/initramfs 文件。
- 設置內核參數:GRUB 會設置內核啟動參數,這些參數可以通過命令行傳遞給內核。
- 跳轉到內核入口點:GRUB 將控制權轉移到內核映像的入口點。對於 x86 架構,這個入口點通常在內核映像的開頭。
3.1.1 啟動BootLoader
以BIOS為例
- CPU在重置後執行的第一條指令的記憶體地址
0xfffffff0
,它包含一個 jump 指令,這個指令通常指向BIOS入口點。 - BIOS會進行一系列硬體初始化和自檢,然後根據設置(例如啟動順序)選擇一個啟動設備(如硬碟、光碟、USB 等)
- 將控制權轉移到啟動設備的啟動扇區代碼。
3.1.2 載入內核文件
- 啟動設備的啟動扇區代碼被執行,通常這段代碼非常小,只占用一個扇區(512位元組)。
- 啟動扇區代碼負責完成一些基本的初始化操作,然後跳轉到更複雜的引導載入程式,如 GRUB 的核心映像(
core image
)。 - 核心映像開始執行,它負責進一步的初始化操作,如載入GRUB的模塊和配置文件(grub.cfg)。
- 根據
grub.cfg
文件中的配置,GRUB載入壓縮的內核映像(vmlinuz
)和可選的initrd/initramfs
文件。 - 內核映像載入完成後,GRUB 將控制權轉移給內核的入口點代碼,完成控制權從 BIOS 到內核的轉移。
3.2 內核解壓
以下以x86系統為例
3.2.1 關鍵文件和代碼路徑
- arch/x86/boot/header.S:啟動代碼的彙編部分,定義了內核入口點。
- arch/x86/boot/compressed/head_64.S 和 arch/x86/boot/compressed/misc.c:解壓縮代碼。
- arch/x86/kernel/head_64.S 和 arch/x86/kernel/head.c:解壓後的內核啟動代碼。
3.2.2 主要步驟
3.2.2.1 啟動載入程式跳轉到內核入口點:
- BootLoader根據
grub.cfg
文件中的配置載入內核映像(vmlinuz
)到記憶體,並跳轉到內核映像的入口點,即內核代碼的起始地址。
3.2.2.2 解壓縮程式的初始化:
- 內核入口點代碼(在 header.S 中)會設置初始的 CPU 狀態和記憶體環境,然後跳轉到解壓縮代碼的入口。
- 32位方法
startup_32
,64位方法startup_64
。(長模式的32到64轉換這裡不做討論,有興趣的可以自行查閱資料)
ENTRY(startup_32)
// 設置 CPU 狀態和記憶體環境
jmp decompress_kernel // 跳轉到解壓縮代碼
3.2.2.3 解壓縮代碼執行:
- 解壓縮代碼的入口點在 arch/x86/boot/compressed/head_64.S 中。
- arch/x86/boot/compressed/head_64.S 會設置解壓環境,如設置段寄存器、建立臨時堆棧等。
ENTRY(decompress_kernel)
// 設置硬體環境
// 調用解壓縮函數入口方法
jmp decompress_kernel_method
3.2.2.4 調用解壓縮函數:
- 在arch/x86/boot/compressed/misc.c 中,decompress_kernel 函數負責選擇解壓演算法並解壓內核映像。
void decompress_kernel(...) {
// 選擇解壓演算法
// 調用相應的解壓函數
decompress_method(); // 調用特定的解壓演算法,如 inflate()
}
3.2.2.5 解壓縮完成後跳轉到內核入口:
- 解壓完成後,解壓縮代碼會跳轉到解壓後的內核入口點。
- arch/x86/boot/compressed/head_64.S中定義了一個跳轉指令,內核入口點的地址載入到寄存器中(例如 %eax),通常是內核主函數(
start_kernel
),跳轉後即將控制權轉移到解壓後的內核代碼。
jmp *%eax
3.2.3 名詞解釋
3.2.3.1 跳轉入口點和控制權轉移
- 在技術實現上跳抓入口點和控制權轉移是一致的,都是通過改變程式計數器(Program Counter,PC)或指令指針(Instruction Pointer,IP)的值來實現的。
程式計數器或指令指針是一個特殊的寄存器,用於存儲正在執行的指令的記憶體地址。當處理器執行一條指令時,程式計數器會自動遞增到下一條指令的地址,從而控制執行流程。這樣就實現了執行流程的轉移,從而使得程式執行從一個代碼段轉移到另一個代碼段。
- "跳轉到入口點"強調了執行流程從某個特定的位置(入口點)開始執行,而"控制權轉移"則更加廣泛地描述了執行流程從一個執行上下文到另一個執行上下文的轉移過程。
- 在內核載入和啟動的上下文中,這兩個術語通常可以互換使用,因為在設置了內核入口點後,執行流程的轉移也意味著控制權的轉移。
3.2.3.2 initrd/initramfs文件
載入 vmlinuz(Linux 內核映像)時,通常還會載入 initrd(initial ramdisk)或 initramfs(initial ram filesystem)文件。initrd 和 initramfs 文件的主要作用是在內核啟動的早期階段提供一個臨時的根文件系統,幫助內核完成啟動過程。
特性如下:
- 硬體驅動支持: 在系統啟動時,內核可能需要載入某些硬體驅動程式(如文件系統驅動、磁碟驅動、網路驅動等)來訪問根文件系統。這些驅動程式可能並未內置在內核映像中,而是作為模塊存在。initrd/initramfs 提供了一個早期的文件系統,內核可以從中載入必要的模塊。
- 根文件系統掛載:在一些複雜的存儲配置中,如 LVM(Logical Volume Manager)、RAID、加密文件系統等,內核需要在掛載實際根文件系統之前進行一些初始化操作。這些操作通常通過 initrd/initramfs 中的腳本完成。
- 通用內核:發行版通常提供通用內核以支持多種硬體配置。使用 initrd/initramfs 可以在啟動時動態載入適配不同硬體配置的模塊,而無需為每種硬體配置編譯一個特定的內核。
載入過程:
- 啟動載入程式(BootLoader)將內核映像和initrd/initramfs文件載入到記憶體中,並將控制權交給內核。
- 內核啟動時會識別並載入initrd/initramfs文件,將其作為初始根文件系統掛載。
- 內核從臨時根文件系統中載入必要的模塊並運行初始化腳本。
- 初始化腳本完成必要的硬體初始化和配置後,會掛載實際的根文件系統(如 /dev/sda1)。
- 初始化腳本切換到實際根文件系統,然後移除initrd/initramfs文件。
三、內核啟動(start_kernel)
start_kernel
是Linux內核中非常重要的一個函數,它是整個內核初始化的核心函數,負責初始化內核的各個子系統、驅動程式以及其他關鍵組件,並最終將控制權轉移到用戶空間。
1.start_kernel方法介紹
1.1 第一個C函數的位置
start_kernel
方法的定義通常位於init/main.c
文件中,也是Linux啟動過程中執行的第一個C函數
1.2 主要功能
- 初始化內核的基本設置:如記憶體管理、進程管理等。
- 初始化各個子系統:如文件系統、網路子系統、設備驅動程式等。
- 啟動第一個用戶進程:將控制權從內核轉移到用戶空間。
2.start_kernel源碼解析
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
extern const struct kernel_param __start___param[], __stop___param[];
/* ... 其他初始化代碼 ... */
/* 設置頁表和記憶體管理 */
paging_init();
mem_init();
kmem_cache_init();
/* 設備和驅動程式初始化 */
driver_init();
init_irq_proc();
softirq_init();
time_init();
console_init();
/* 文件系統初始化 */
vfs_caches_init_early();
mnt_init();
init_rootfs();
init_mount_tree();
/* 初始化進程 */
pid_cache_init();
proc_caches_init();
/* 啟動 init 進程 */
rest_init();
/* ... 其他初始化代碼 ... */
/* 調用內核參數解析函數 */
kernel_param_init(karg_strings, num_args);
/* ... 其他初始化代碼 ... */
/* 永遠不會返回 */
cpu_idle();
}
四、啟動初始進程(init process)
1.進程概念介紹
1.1 內核進程(Kernel Thread)和用戶進程(User Process)
1.1.1 內核進程(Kernel Thread)
內核進程是由內核創建和調度的線程,運行在內核態,用於處理內核的各類任務。與用戶進程不同,內核進程不直接與用戶空間交互,主要用於執行內核內部的工作,如處理中斷、管理設備、調度任務等。
- 運行空間:內核進程運行在內核地址空間,而普通用戶進程運行在用戶地址空間。
- 許可權:內核進程可以直接訪問內核數據結構,而用戶進程通過系統調用與內核交互。
- 交互:內核進程通常不與用戶交互,其生命周期完全由內核管理。
- 創建方式:內核進程的創建通常通過kernel_thread函數實現。
註意內核進程是獨立的,與0、1、2號進程無關
1.1.2 用戶進程(User Process)
用戶進程是在用戶空間中執行的進程,用戶通過編寫和執行應用程式來創建用戶進程。用戶進程通過系統調用與內核交互,進行資源分配、文件操作、網路通信等。
- 運行空間:用戶進程運行在用戶態,受限於用戶空間的許可權,不能直接訪問硬體和內核數據結構。
- 許可權:內核線程運行在內核態,具有更高的許可權,能夠直接操作內核資源。
1.2 0號進程、1號進程、2號進程
0號進程
:是內核進程,運行在內核態,負責在系統空閑時執行。1號進程
:是用戶進程,雖然最初由內核創建,但主要運行在用戶態,負責系統初始化和管理用戶空間的其他用戶進程。2號進程
:是內核進程,運行在內核態,負責創建和管理其他內核線程。這些內核線程通常用於執行內核中的非同步任務,如磁碟I/O、網路操作等。
1.2.1 0號進程(swapper/idle/空閑進程)
- 0號進程是Linux啟動的第一個進程,它的task_struct的comm欄位為"swapper",也稱為swapper進程、idel進程、空閑進程。
- 0號進程是在系統引導過程中由內核創建的第一個進程。它的任務是進入空閑迴圈,當系統中沒有其他可運行的進程時,它會被調度執行,以避免CPU閑置。
0號進程(idle進程)是在系統引導過程中,由內核初始化代碼創建的。在x86架構中,這個過程發生在彙編啟動代碼(通常在arch/x86/kernel/head.S中),該代碼會設置基本的CPU和記憶體環境,然後跳轉到C語言的start_kernel函數。
1.2.2 1號進程(init進程)和2號進程(kthreadd進程)
- 1號進程和2號進程都是在
rest_init
函數中創建的 - 1號進程通過kernel_init創建
- 2號進程通過kthreadd創建
2.rest_init
函數-初始化入口
rest_init函數負責創建初始進程併進行一些進一步的初始化工作。其代碼實現如下:
static noinline void __ref rest_init(void)
{
// 通知RCU(Read-Copy Update)子系統,調度器即將開始。這是確保RCU在調度器開始運行前正確初始化的關鍵步驟。
rcu_scheduler_starting();
// 創建pid=1的1號進程
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
/** 處理1號進程相關代碼 **/
// 創建pid=2的2號進程
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
/** 處理2號進程相關代碼 **/
/** 其他初始化代碼 **/
}
2.1 kernel_thread
函數
int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return do_fork(flags | CLONE_VM | CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0);
}
do_fork
:這是內核中實現創建新進程(或線程)的核心函數。通過該函數,內核可以複製當前進程的上下文,生成一個新的進程(或線程)。
2.2 kernel_init
函數 - 初始化1號進程(init)
kernel_init負責啟動初始用戶空間進程(/sbin/init或指定的init進程)。
static int __ref kernel_init(void *unused)
{
/** 其他初始化代碼 **/
// 啟動用戶空間進程
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
} else if (execute_command) {
run_init_process(execute_command);
} else {
run_init_process("/sbin/init");
}
return 0;
}
3. 系統啟動
init進程啟動後,通過後續工作完成了操作系統的載入和啟動
3.1 系統初始化腳本
init進程讀取系統的初始化腳本(如/etc/inittab、/etc/init.d/腳本)或systemd的單元文件(unit files),執行系統初始化任務。這包括設置系統環境、掛載文件系統、啟動網路服務、啟動守護進程等。
3.2 啟動用戶界面
圖形登錄管理器
:如果系統配置為使用圖形界面,init進程會啟動圖形登錄管理器(如GDM、LightDM、SDDM)。這些登錄管理器負責提供圖形化的登錄界面,供用戶輸入用戶名和密碼。啟動桌面環境
:用戶登錄成功後,登錄管理器會啟動用戶的桌面環境(如GNOME、KDE、Xfce)。桌面環境提供完整的圖形用戶界面,允許用戶運行應用程式、管理文件、設置系統等。
3.3 圖形界面啟動流程(systemd示例)
- systemd初始化:systemd作為init進程啟動,讀取其配置文件(通常在/lib/systemd/system/和/etc/systemd/system/)。
- 啟動目標(target):systemd根據配置文件啟動系統目標(如graphical.target)。graphical.target包含了啟動圖形界面所需的所有服務。
- 啟動顯示管理器:systemd啟動圖形顯示管理器服務(如gdm.service、lightdm.service)。
- 顯示管理器運行:顯示管理器提供圖形登錄界面,用戶登錄後啟動用戶會話。
- 啟動桌面環境:用戶會話啟動後,顯示管理器啟動桌面環境,用戶進入圖形用戶界面。
五、流程圖總結
前面使用了大量文字來說明,這裡使用一張流程圖來做概要總結
graph TD A[電腦通電] -->|載入BIOS| C(BIOS) C -->|解壓vmlinuz| D(內核Kernel) D -->|start_kenerl| E(init進程) E -->|載入操作系統| F[登錄界面]