Linux內(nèi)核啟動流程詳解(基于 5.15.119 源碼)
本文基于 Linux 5.15.119 實際源碼,從 GRUB 加載到第一個用戶進程 /sbin/init 啟動,完整梳理 x86_64 架構(gòu)的內(nèi)核啟動流程。
0. bzImage 的物理結(jié)構(gòu)
你執(zhí)行 make bzImage 編譯出來的文件,其實不是單一文件,而是兩部分拼接而成:
┌─────────────────┬─────────────────────────────┐
│ setup.bin │ compressed/vmlinux │
│ (實模式代碼) │ (壓縮的真正的內(nèi)核) │
│ ~30KB │ 幾MB ~ 幾十MB │
└─────────────────┴─────────────────────────────┘
↑ ↑
從 bootsect 開始 從 startup_32 開始
Bootloader(GRUB)把它加載到內(nèi)存后,CPU 先執(zhí)行左邊的 setup.bin(還是古老的 16 位實模式),然后才進入右邊的壓縮內(nèi)核。
拼接工作在 arch/x86/boot/tools/build.c 中完成。
1. GRUB 加載 bzImage
GRUB 讀取內(nèi)核文件開頭的 內(nèi)核頭部(Kernel Header),獲取啟動協(xié)議信息。
源碼位置:arch/x86/boot/header.S
這個文件開頭定義了一個兼容 MS-DOS 的頭部(MZ/PE 頭),讓 GRUB 能識別它:
#ifdef CONFIG_EFI_STUB
.word MZ_MAGIC # "MZ" 魔數(shù)
#endif
頭部中包含了 GRUB 需要的關(guān)鍵字段:
| 字段 | 含義 |
|---|---|
setup_sects | setup 部分占多少扇區(qū) |
syssize | 內(nèi)核大小 |
version | 啟動協(xié)議版本 |
cmd_line_ptr | 命令行參數(shù)地址 |
code32_start | 32 位代碼入口點 |
GRUB 根據(jù)這些信息把 bzImage 加載到內(nèi)存,然后把控制權(quán)交給 header.S 中的入口。
2. 實模式(16 位)—— 收集硬件信息
CPU 當前狀態(tài):16 位實模式,只能直接訪問 1MB 內(nèi)存,沒有內(nèi)存保護。
入口函數(shù):main()
源碼位置:arch/x86/boot/main.c
這是整個內(nèi)核啟動的第一個 C 函數(shù):
void main(void)
{
/* 把 header 里的信息復(fù)制到 boot_params */
copy_boot_params();
/* 初始化早期控制臺 */
console_init();
/* 初始化堆內(nèi)存 */
init_heap();
/* 檢查 CPU 是否支持運行這個內(nèi)核 */
if (validate_cpu()) {
puts("Unable to boot...");
die();
}
/* 告訴 BIOS 我們要進 64 位模式 */
set_bios_mode();
/* 用 BIOS 中斷探測內(nèi)存布局 */
detect_memory();
/* 設(shè)置鍵盤 */
keyboard_init();
/* 查詢各種 BIOS 信息 */
query_ist();
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
query_edd();
#endif
/* 設(shè)置顯示模式 */
set_video();
/* ? 進入保護模式 */
go_to_protected_mode();
}
這些代碼看起來古老,但非常關(guān)鍵——它用 BIOS 中斷(int 0x10、int 0x15 等)收集硬件信息,全部存進全局變量 boot_params,后面內(nèi)核會一直用它。
3. 切換到保護模式(32 位)
go_to_protected_mode() 定義在 arch/x86/boot/pm.c:
void go_to_protected_mode(void)
{
/* 關(guān)閉中斷,執(zhí)行實模式最后的鉤子 */
realmode_switch_hook();
/* 開啟 A20 地址線(歷史遺留,為了訪問 1MB 以上內(nèi)存) */
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}
/* 重置協(xié)處理器 */
reset_coprocessor();
/* 屏蔽所有中斷 */
mask_all_interrupts();
/* 設(shè)置 IDT 和 GDT */
setup_idt();
setup_gdt();
/* ? 真正跳轉(zhuǎn)到保護模式 */
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));
}
真正的切換發(fā)生在匯編文件 pmjump.S 中:
SYM_FUNC_START(protected_mode_jump)
movl %edx, %esi # %edx = boot_params 指針
movl %cr0, %edx
orb $X86_CR0_PE, %dl # 設(shè)置 CR0 的 PE (Protection Enable) 位
movl %edx, %cr0 # 打開保護模式!
# 長跳轉(zhuǎn)到 32 位代碼段
.byte 0x66, 0xea # ljmpl 指令
.long .Lin_pm32 # 目標偏移
.word __BOOT_CS # 代碼段選擇子
.Lin_pm32:
# 設(shè)置 32 位平坦模式的數(shù)據(jù)段
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
jmpl *%eax # 跳轉(zhuǎn)到 32 位入口點
發(fā)生了什么:CPU 從 16 位實模式 → 32 位保護模式?,F(xiàn)在能訪問 4GB 內(nèi)存了,但還不是 64 位。
4. 32 位解壓入口
現(xiàn)在 CPU 開始執(zhí)行 arch/x86/boot/compressed/head_64.S 中的 startup_32。
注意:文件名雖然是 head_64.S,但前面一大段是 .code32(32 位代碼)。
.code32
SYM_FUNC_START(startup_32)
cld
cli # 關(guān)中斷
# 計算"實際加載地址"與"編譯時地址"的偏差
# 因為 bootloader 可能把內(nèi)核加載到了意料之外的位置
leal (BP_scratch+4)(%esi), %esp
call 1f
1: popl %ebp
subl $ rva(1b), %ebp
# 加載新的 GDT
leal rva(gdt)(%ebp), %eax
movl %eax, 2(%eax)
lgdt (%eax)
# 設(shè)置數(shù)據(jù)段寄存器
movl $__BOOT_DS, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %fs
movl %eax, %gs
movl %eax, %ss
然后它繼續(xù):
- 建立臨時的 4GB 頁表(為進入長模式做準備)
- 打開 PAE(Physical Address Extension,
cr4 |= X86_CR4_PAE) - 設(shè)置 EFER.LME(Long Mode Enable)
- 啟用分頁(設(shè)置
cr0.PG = 1),CPU 自動進入兼容模式 - 長跳轉(zhuǎn)到 64 位代碼
5. 64 位模式下的解壓
繼續(xù)在同一份 head_64.S 中,現(xiàn)在 CPU 運行在 64 位模式了。
這段代碼的任務(wù)是:
- 找到壓縮的內(nèi)核數(shù)據(jù)(
piggy.o里包著vmlinux.bin.gz或.xz等) - 把它解壓到最終運行地址
- 跳轉(zhuǎn)到解壓后的內(nèi)核入口
解壓完成后,它跳轉(zhuǎn)到真正的內(nèi)核入口——startup_64(注意:這是另一個文件里的同名符號?。?。
相關(guān)文件:
arch/x86/boot/compressed/head_64.Sarch/x86/boot/compressed/misc.c(解壓算法的 C 實現(xiàn))arch/x86/boot/compressed/piggy.S(把壓縮后的內(nèi)核二進制打包成匯編對象)
6. 真正的內(nèi)核 64 位入口
現(xiàn)在進入 arch/x86/kernel/head_64.S 中的 startup_64。
這是編譯后的 vmlinux 的第一條指令,也是內(nèi)核真正開始建立自己世界的地方。
.code64
SYM_CODE_START_NOALIGN(startup_64)
/* 設(shè)置棧(用 init_task 的棧) */
leaq (__end_init_task - FRAME_SIZE)(%rip), %rsp
/* 調(diào)用 startup_64_setup_env 和 verify_cpu */
leaq _text(%rip), %rdi
pushq %rsi
call startup_64_setup_env
popq %rsi
call verify_cpu
/* 修正頁表(內(nèi)核可能被加載到了與編譯地址不同的位置) */
leaq _text(%rip), %rdi
pushq %rsi
call __startup_64
popq %rsi
/* 形成 CR3 值(頁表基址) */
addq $(early_top_pgt - __START_KERNEL_map), %rax
/* 設(shè)置 CR4(PAE + PGE) */
movl $(X86_CR4_PAE | X86_CR4_PGE), %ecx
movq %rcx, %cr4
/* 切換到新的頁表 */
movq %rax, %cr3
/* 加載內(nèi)核的 GDT */
lgdt early_gdt_descr(%rip)
/* 清空數(shù)據(jù)段寄存器 */
xorl %eax, %eax
movl %eax, %ds
movl %eax, %ss
movl %eax, %es
movl %eax, %fs
movl %eax, %gs
/* 設(shè)置 IDT(早期中斷描述符表) */
pushq %rsi
call early_setup_idt
popq %rsi
/* 設(shè)置 EFER(使能 SYSCALL 等) */
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_SCE, %eax /* Enable System Call */
...
wrmsr
/* 設(shè)置 CR0 */
movl $CR0_STATE, %eax
movq %rax, %cr0
/* 清空中斷標志 */
pushq $0
popfq
/* 準備跳轉(zhuǎn)到 C 代碼 */
movq %rsi, %rdi # real_mode_data 作為參數(shù)
.Ljump_to_C_code:
xorl %ebp, %ebp # 清空幀指針
movq initial_code(%rip), %rax # rax = x86_64_start_kernel
pushq $__KERNEL_CS
pushq %rax
lretq # 遠返回,進入 x86_64_start_kernel
關(guān)鍵點:
initial_code是一個全局變量,值為x86_64_start_kernelinitial_stack指向init_thread_union(這是 PID 0,也就是 swapper/idle 進程的棧)lretq完成一次遠返回,同時切換代碼段和跳轉(zhuǎn)到目標地址
7. x86 架構(gòu)的 C 語言入口
lretq 把你帶到了 arch/x86/kernel/head64.c:
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
/* 編譯期檢查 */
BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
...
/* 初始化 CR4 影子寄存器 */
cr4_init_shadow();
/* 清除臨時頁表(去掉 identity map) */
reset_early_page_tables();
/* 清零 BSS 段 */
clear_bss();
/* 復(fù)制 boot_params 到內(nèi)核空間 */
copy_bootdata(__va(real_mode_data));
/* 加載 CPU 微碼 */
load_ucode_bsp();
/* 設(shè)置正式頁表的高地址映射 */
init_top_pgt[511] = early_top_pgt[511];
x86_64_start_reservations(real_mode_data);
}
void __init x86_64_start_reservations(char *real_mode_data)
{
if (!boot_params.hdr.version)
copy_bootdata(__va(real_mode_data));
x86_early_init_platform_quirks();
/* 特殊平臺處理 */
switch (boot_params.hdr.hardware_subarch) {
case X86_SUBARCH_INTEL_MID:
x86_intel_mid_early_setup();
break;
default:
break;
}
start_kernel(); /* ? 進入體系無關(guān)的通用初始化! */
}
從這里開始,代碼變得體系無關(guān)了——不管是 x86、ARM 還是 RISC-V,最終都會調(diào)用 start_kernel()。
8. 通用內(nèi)核初始化 —— start_kernel()
這是整個內(nèi)核最著名的函數(shù),位于 init/main.c:
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
/* 設(shè)置 init_task(PID 0)的棧結(jié)束魔數(shù) */
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
cgroup_init_early();
/* 關(guān)中斷,初始化期間不允許打擾 */
local_irq_disable();
early_boot_irqs_disabled = true;
boot_cpu_init();
page_address_init();
/* 打印 "Linux version 5.15.119 ..." */
pr_notice("%s", linux_banner);
early_security_init();
/* ? 架構(gòu)相關(guān)初始化(內(nèi)存、CPU、ACPI 等) */
setup_arch(&command_line);
setup_boot_config();
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu();
boot_cpu_hotplug_init();
/* 建立內(nèi)存管理區(qū) */
build_all_zonelists(NULL);
page_alloc_init();
/* 打印命令行 */
pr_notice("Kernel command line: %s\n", saved_command_line);
/* 解析早期參數(shù)(如 mem=, console=) */
parse_early_param();
parse_args("Booting kernel", static_command_line, ...);
/* ? 初始化內(nèi)存管理 */
mm_init();
ftrace_init();
sched_init(); /* ? 初始化調(diào)度器 */
radix_tree_init();
workqueue_init_early();
rcu_init();
trace_init();
context_tracking_init();
early_irq_init();
init_IRQ(); /* 初始化中斷 */
tick_init();
init_timers();
timekeeping_init();
time_init(); /* 初始化時間子系統(tǒng) */
random_init(command_line);
boot_init_stack_canary();
/* 開中斷 */
local_irq_enable();
early_boot_irqs_disabled = false;
console_init(); /* 正式初始化控制臺 */
/* ? 初始化 VFS */
vfs_caches_init();
signals_init();
pagecache_init();
proc_root_init(); /* 創(chuàng)建 /proc */
nsfs_init();
cgroup_init();
...
check_bugs(); /* 檢查 CPU bug */
/* ? 調(diào)用 rest_init() */
arch_call_rest_init();
}
start_kernel() 初始化了內(nèi)核的幾乎所有核心子系統(tǒng):
- 內(nèi)存管理(MM)
- 進程調(diào)度
- 中斷、定時器
- 文件系統(tǒng)緩存
- procfs、cgroups
- …
但最后它自己不變成用戶進程,而是調(diào)用 rest_init()。
9. rest_init() —— 創(chuàng)建兩個"祖先進程"
源碼位置:init/main.c
noinline void __ref rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
/*
* 創(chuàng)建 PID 1:init 進程(用戶態(tài)的祖先)
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
/* 把 init 固定在 boot CPU 上 */
rcu_read_lock();
tsk = find_task_by_pid_ns(pid, &init_pid_ns);
tsk->flags |= PF_NO_SETAFFINITY;
set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
rcu_read_unlock();
/*
* 創(chuàng)建 PID 2:kthreadd(內(nèi)核線程的祖先)
*/
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
/*
* 啟用調(diào)度器檢查
*/
system_state = SYSTEM_SCHEDULING;
complete(&kthreadd_done);
/*
* 當前線程(PID 0)變成 idle 進程,永遠執(zhí)行 schedule()
*/
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE); /* 進入 idle 循環(huán) */
}
這是 Linux 進程樹的起點:
| PID | 進程 | 作用 |
|---|---|---|
| 0 | swapper/idle | 當前線程變身而來,沒事干時執(zhí)行它 |
| 1 | kernel_init | 接下來啟動第一個用戶程序(/sbin/init) |
| 2 | kthreadd | 專門"收養(yǎng)"內(nèi)核線程,所有內(nèi)核線程的父進程 |
10. kernel_init() —— 啟動第一個用戶程序
源碼位置:init/main.c
static int __ref kernel_init(void *unused)
{
/* 等 kthreadd 準備好 */
wait_for_completion(&kthreadd_done);
/* 加載真正的根文件系統(tǒng) */
kernel_init_freeable();
/* 等所有異步 __init 代碼完成 */
async_synchronize_full();
/* 釋放 __init 段內(nèi)存(啟動完成后這些代碼不再需要) */
free_initmem();
mark_readonly();
pti_finalize();
/* 系統(tǒng)正式運行 */
system_state = SYSTEM_RUNNING;
numa_default_policy();
rcu_end_inkernel_boot();
/* 按優(yōu)先級嘗試啟動第一個用戶進程 */
/* 1. initrd 中的 /init */
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
}
/* 2. 命令行 init=xxx 指定的程序 */
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
}
/* 3. 編譯時默認的 init */
if (CONFIG_DEFAULT_INIT[0] != '\0') {
ret = run_init_process(CONFIG_DEFAULT_INIT);
if (!ret)
return 0;
}
/* 4. 默認路徑 */
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
/* 全都失敗 */
panic("No working init found. Try passing init= option to kernel.");
}
一旦 run_init_process("/sbin/init") 成功,內(nèi)核就完成了它的啟動使命。
從這一刻起,系統(tǒng)控制權(quán)交給了用戶空間。
你看到的 systemd、sysvinit、或 bash,就是從這里開始的。
總結(jié):完整啟動路線圖
Mermaid 流程圖

ASCII 路線圖(備用)
GRUB 加載 bzImage
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/header.S │ 內(nèi)核頭部,存放啟動協(xié)議信息
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/main.c main() │ ?── 16位實模式
│ └── go_to_protected_mode() │ 收集硬件信息到 boot_params
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/pmjump.S │ ?── 打開 CR0.PE
│ protected_mode_jump() │ 進入 32 位保護模式
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/compressed/head_64.S │ ?── 32→64位切換
│ startup_32 → startup_64 │ 建立頁表,解壓內(nèi)核
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/kernel/head_64.S │ ?── 真正的內(nèi)核 64 位入口
│ startup_64 → lretq │ 設(shè)置頁表/GDT/IDT
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/kernel/head64.c │ ?── x86 架構(gòu) C 入口
│ x86_64_start_kernel() │ 清零 BSS,加載微碼
│ └── start_kernel() │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ init/main.c start_kernel() │ ?── 初始化所有子系統(tǒng)
│ └── arch_call_rest_init() │ MM、調(diào)度、IRQ、VFS...
│ └── rest_init() │
│ ├── kernel_thread(kernel_init) │ PID 1
│ ├── kernel_thread(kthreadd) │ PID 2
│ └── cpu_startup_entry() │ PID 0 (idle)
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ init/main.c kernel_init() │ ?── 啟動 /sbin/init
│ └── run_init_process() │
└─────────────────────────────────────────────┘
│
▼
用戶空間(systemd / init / bash)
以上就是Linux內(nèi)核啟動流程詳解(基于 5.15.119 源碼)的詳細內(nèi)容,更多關(guān)于Linux內(nèi)核啟動流程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
linux命令實現(xiàn)音頻格式轉(zhuǎn)換和拼接的方法
今天小編就為大家分享一篇linux命令實現(xiàn)音頻格式轉(zhuǎn)換和拼接的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08
centos 7 安裝卸載apache(httpd)服務(wù)的詳細步驟
前面我們已經(jīng)安裝好了mysql,今天安裝httpd,然后試著訪問以下,由于博主已經(jīng)安裝過一次,所以先說卸載,再說安裝,需要的朋友可以參考下2020-07-07
Xshell5連接虛擬機中的Linux的方法以及失敗原因解決
這篇文章主要介紹了Xshell5連接虛擬機中的Linux的方法以及失敗原因解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07

