在上一篇
計算機啟動過程
文章中介紹了計算機啟動的基本流程,本篇文章主要介紹Linux核心Kernel的啟動過程。
一、核心啟動的基本流程
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)非常有利。
- 便於管理和分發: 壓縮核心映像更便於在各種介質上分發,比如光碟、隨身碟等。一個較小的檔案更容易管理、備份和分發。
- 標準化處理: 使用壓縮核心映像是一種標準做法,啟動載入程式(如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.核心載入過程
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)。
- 顯示管理器執行:顯示管理器提供圖形登入介面,使用者登入後啟動使用者會話。
- 啟動桌面環境:使用者會話啟動後,顯示管理器啟動桌面環境,使用者進入圖形使用者介面。
五、流程圖總結
前面使用了大量文字來說明,這裡使用一張流程圖來做概要總結