一个典型的 Linux 开发者可能熟悉以下内容:知道 这些问题的答案全部藏在内核启动的全链路中。掌握它不仅能帮助你在启动阶段排错(比如 现代 x86 服务器统一采用 UEFI 固件,其启动分为若干阶段: SEC 和 PEI 阶段对软件开发者基本不可见,它们把一个"什么都没有"的平台变成一个有完整内存映射的系统调用环境。DXE 阶段则加载 UEFI 规范要求固件在 ESP 分区上查找 UEFI 会把内核映像和可选的 早期内核启动代码( GRUB2 是一个 mini-OS,它有自己的文件系统驱动、LBA 磁盘访问层和命令行分析器。其启动分三步: 对于 UEFI 系统,GRUB 会编译为 当用户选择 GRUB 菜单中的 Linux 条目时,GRUB 执行: GRUB 会完成以下关键操作: - 命令行参数( - initrd 地址与大小( - 视频模式参数 - EFI 运行时服务地址(对于 UEFI 启动) 最后跳转到入口地址(对于 bzImage 是 这是内核启动最复杂的部分,也是 bzImage "混搭"风格代码的集中体现。 关键过渡:CPU 从 16-bit 实模式跳转到 32-bit 保护模式时,GDT 已临时设置好平坦模型(flat model),以便直接访问 4GB 物理空间。 进入 现在我们在 long mode 下运行,使用的是恒等映射的 1GB 大页表( 解压完成后通过 在真正挂载根文件系统之前, microcode 早期加载就是放在 一旦 systemd 接管后执行的任务包括: 至此,用户已经可以看到登录提示符,整个启动流程完成。 通过 Linux 内核启动是一个严密的序列化过程,每一步都建立在前一步的基础上: 关键认知:内核启动中最晚被初始化的东西往往就是程序员最早调用的东西。这就是为什么内核代码设计对顺序如此敏感——一次错误的 initcall 顺序,可能导致整个系统静默崩溃数小时才被发现。Linux 内核启动全链路深度实战:从 UEFI 到第一个用户进程
不了解内核启动过程,就无法真正理解设备模型、内存子系统、中断系统和驱动程序之间的依赖关系。本文将沿着 CPU 加电后一条完整的执行链路,深入剖析 Linux 内核从固件接手控制权到用户态 init 进程创建的全过程。我们将结合 QEMU 源码调试、固件日志和内核源码追踪,构建一条清晰的启动路径。
一、为什么要研究生启动全过程?
sys_write 如何进入 VFS 层,理解 schedule() 如何选择下一个进程,甚至能分析缺页异常的完整路径。但如果追问"这些子系统在被调用之前是如何初始化的?"很多问题就会暴露出来:
kmalloc 最早什么时候可用?答案是 mm_init() 之后,但 slab 分配器的初始化本身依赖调度器和中断。prepare_namespace() 中完成。init 迟迟无法启动,或 early params 解析失败),更能让你理解各个子系统之间的冷热依赖关系。
二、阶段一:固件层(UEFI)— 平台初始化
2.1 UEFI 启动流程概述
阶段
名称
运行方式
关键任务
SEC
Security Phase
CPU 缓存即内存(CAR)
验证固件完整性,建立临时内存
PEI
Pre-EFI Initialization
无 DRAM,用缓存模拟内存
初始化内存控制器、training DDR
DXE
Driver eXecution Environment
已识别完整 DRAM
加载 UEFI 驱动、枚举 PCIe 设备
BDS
Boot Device Selection
运行时
解析 BootOrder 项,加载 OS Loader
.efi 驱动,其中就包含了我们要用到的 EFI System Partition (ESP) 文件系统驱动和 NVRAM 变量读写驱动。2.2 UEFI 如何找到 Bootloader
\EFI\BOOT\BOOTX64.EFI(x86-64 架构)作为默认启动项。实际生产中,这个路径往往是一个指向具体 OS loader(如 GRUB)的 symlink 或链式加载器。initrd 映像读取进来,然后调用 ExitBootServices() 放弃对硬件的控制。在这之后所有 Boot Services 函数都不能再使用,内核获得对硬件的完全控制权。// UEFI 加载完成后,系统进入 "运行时" 状态。
// 关键数据结构:EFI_SYSTEM_TABLE 和 EFI_BOOT_SERVICES
struct boot_params {
// ...
u64 efi_systab; // EFI 系统表物理地址
u64 efi_memmap; // EFI 内存映射表
u64 efi_memmap_size;
u64 efi_memdesc_size;
u32 efi_memdesc_version;
// ...
};arch/x86/boot/compressed/eboot.c)会通过 efi 子结构体获取这些信息,并将其转换为内核自己的 e820 内存映射表,这是理解后续内存管理的关键基础。
三、阶段二:Bootloader(GRUB2)— 内核加载与参数传递
3.1 GRUB2 的执行模型
0x7C00,负责跳转到 diskboot.img\EFI\BOOT\BOOTX64.EFI,由 UEFI 固件直接执行,因此 LBA 模式的前两步可以跳过。3.2 GRUB2 加载 Linux 内核的关键步骤
# 伪代码:GRUB 加载 vmlinuz 和 initrd
linux /vmlinuz-6.6.8 root=/dev/nvme0n1p2 console=ttyS0,115200
initrd /initramfs-6.6.8.img
boot
struct boot_params(即所谓的 "zero page"),包括:struct boot_params + 0x1E4 偏移处的 cmd_line_ptr)ramdisk_image / ramdisk_size)0x200 处的 start_of_setup),开始执行内核 setup 代码。3.3 bzImage 的内存布局
+---------------------------+ 0x000000
| 实模式中断向量表 IVT | 1 KB
+---------------------------+ 0x000400
| BIOS数据区 BDA | 256 B
+---------------------------+ 0x010000
| boot_params (zero page) | 4 KB <-- GRUB 填充
+---------------------------+ 0x011000
| command_line | 512 B
+---------------------------+ 0x011800
| real-mode kernel code | <-- 16-bit setup_sects
+---------------------------+ 0x100000 (1 MB)
| 32-bit protected mode |
| startup_32 |
+---------------------------+
| compressed kernel |
| (vmlinux.bin.bz2) |
+---------------------------+ (最终由 32-bit 解压到 0x1000000 或高端地址)
四、阶段三:内核的早期启动:从实模式到 64 位
4.1 16-bit 实模式入口:
start_of_setup; arch/x86/boot/header.S
.globl start_of_setup
start_of_setup:
;此时 CS:IP 指向这里,仍在 16-bit 实模式
; GRUB 已把 boot_params 填好,bootsect_helper 地址也在
; 第一步:设置 DS=SS 指向 boot_params
movw %cs, %ax
movw %ax, %ds
; GDT 已经由 header.S 中固定的 boot_gdt 提供
; 但实模式下用不到,只为了之后的 far jump
; 第二步:检查加载位置和 loader 类型
movb $0x1, %cs:loader_type ; 非 legacy BIOS 则为 0x3
; 第三步:启用 A20(现代系统由 GRUB 已处理)
call enable_a20
; 第四步:探测内存大小
; E820 调用:int 15h, ax=E820
; 结果存入 e820_entry[] 表
call detect_memory
; 第五步:设置初始视频模式
call set_video
; 第六步:关中断,切换到保护模式
cli
movl %cr0, %eax
orl $0x1, %eax
movl %eax, %cr0
ljmp $CODE32_SEG, $boot_offset ; 跳到 32-bit 入口4.2 32-bit 保护模式:
startup_32arch/x86/boot/compressed/head_32.S 的 startup_32 后:
EFER.LME 使能extract_kernel(); PAE + long mode 切换
movl %cr4, %eax
orl $(X86_CR4_PAE), %eax
movl %eax, %cr4
; 加载 PML4 物理地址到 CR3
movl $early_top_pgt, %eax
movl %eax, %cr3
; 使能 long mode (EFER.LME)
movl $MSR_EFER, %ecx
rdmsr
orl $EFER_LME, %eax
wrmsr
; 使能分页
movl %cr0, %eax
orl $(X86_CR0_PG|X86_CR0_PE), %eax
movl %eax, %cr0
; 通过 far jump 进入 64-bit 代码段
ljmp $CS_L, $startup_644.3 64-bit 模式:
startup_64 与解压early_top_pgt),把 [0, MAXMEM) 一一映射到物理地址。; arch/x86/boot/compressed/head_64.S
.globl startup_64
startup_64:
; 重定位 GDT
; 初始化 SSE/AVX
; 验证解压器
; 调用 extract_kernel() C 函数extract_kernel() 是 arch/x86/boot/compressed/misc.c 中的函数,它做三件事:
0x1000000)jmp *%rax 跳转到 x86_64_start_kernel()(即 64-bit 内核的入口)。
五、阶段四:内核
start_kernel() — 子系统级联初始化start_kernel() 位于 init/main.c,它是整个启动流程中最重要的函数,也是子系统初始化的统一入口。asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_off();
/*
* 注意顺序:在调度器可用之前,大部分初始化代码不能阻塞。
* 以下调用顺序是经过数十年演进形成的,几乎不可更改。
*/
boot_cpu_init();
setup_arch(&command_line); // 架构相关:内存布局、fixmap、IOMMU 早期初始化
mm_init_cpumask(&init_mm);
setup_per_cpu_areas(); // per-cpu 数据区设置
smp_prepare_boot_cpu(); // APIC 早期初始化
build_all_zonelists(NULL); // 构建内存区域(DMA/normal/highmem)
page_alloc_init(); // 伙伴系统(buddy system)就绪
parse_early_param();
after_dashes = parse_args(...);
jump_label_init();
setup_log_buf(0);
pidhash_init();
vfs_caches_init_early();
sort_main_extable();
trap_init(); // 中断描述符表(IDT)安装,缺页/GPF handler
mm_init(); // slab 分配器初始化 -> kmalloc 可用!
sched_init(); // 调度器初始化,idle 进程创建
rcu_init(); // RCU 子系统
init_IRQ(); // 中断控制器(IO-APIC/Local APIC/iommu-irq)
init_timers(); // 时间子系统(jiffies, wall time)
hrtimers_init();
softirq_init();
timekeeping_init();
time_init(); // clocksource, clockevent 注册
perf_event_init();
profile_init();
call_function_init();
WARN(!irqs_disabled(), "IRQs not disabled");
kmem_cache_init_late();
console_init();
lockdep_init();
lockdepoff();
locking_selftest();
numa_policy_init();
calib_delay();
pidmap_init();
anon_vma_init();
acpi_early_init();
#ifdef CONFIG_EFI
efi_enter_virtual_mode(); // EFI 进入虚拟寻址模式
#endif
#ifdef CONFIG_THREAD_INFO_IN_TASK
thread_info_cache_init();
#endif
cred_init();
fork_init(); // task_struct 相关
proc_caches_init();
uts_ns_init();
key_init();
security_init();
dbg_late_init();
vfs_caches_init(); // dentry cache, inode cache
signals_init();
page_writeback_init();
proc_root_init();
nsfs_init();
cpuset_init();
cgroup_init();
taskstats_init_early();
delayacct_init();
check_bugs();
acpi_subsystem_init();
arch_post_acpi_subsys_init();
sfi_init_late();
efi_enabled(EFI_LOADER) -> efi_vmalloc_setup();
ftrace_init();
rest_init(); // 创建 kernel_init 和 kthreadd
}关键依赖关系可视化
start_kernel()
├─ setup_arch()
│ ├─ x86_init.resources.reserve_resources()
│ ├─ init_mem_mapping() # 建立 full direct mapping
│ ├─ init_mem_mapping_ext()
│ └─ early_ioremap_setup()
│
├─ page_alloc_init() # kmem_cache_init 前不可用
│ └─ set_pageblock_order()
│
├─ mm_init() # kmalloc 才真正可用
│ ├─ mem_init() # 统计可用内存,释放 bootmem
│ ├─ kmem_cache_init() # slab/slub
│ ├─ pgtable_cache_init()
│ ├─ vmalloc_init()
│ └─ kmap_init()
│
├─ sched_init()
│ ├─ init_idle() # 创建 idle 进程 (PID 0)
│ └─ 初始化 root 负载均衡
│
├─ init_IRQ() # 中断子系统里程碑
│ ├─ apic_intr_init()
│ ├─ x86_init.irqs.intr_init() # APIC 模式选择
│ └─ 最终覆盖 static_branch_enable(irqs_disabled)
│
├─ rest_init()
│ ├─ kernel_thread(kthreadd, ...) # PID 2: kthreadd
│ └─ kernel_thread(kernel_init, ...) # PID 1 前身: kernel_init (最终执行 /init)
│
└─ *idle* # start_kernel() 自身进入 idle 循环 (PID 0)
六、阶段五:
kernel_init — 从内核线程到用户态 PID 16.1 挂载根文件系统
start_kernel() 末尾的 rest_init() 创建 kernel_init 内核线程(PID 1 的前身),它的主要责任是:
bootmem / 释放 init 段代码所在的内存区域/init 程序init// init/main.c: kernel_init()
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
/* 此时 init 段代码已被释放,不能再调用 __init 函数 */
async_synchronize_full();
ftrace_free_init_mem();
jump_label_invalidate_init();
mark_readonly();
system_state = SYSTEM_RUNNING;
num_default_serial_console();
rcu_end_inkernel_boot();
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
// 依次尝试这些路径
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
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. Use=bin/init, "
"linuxrc\". See Linux Documentation/admin-guide/init.rst.");
}6.2
prepare_namespace() — 解析 root= 参数kernel_init_freeable() 中会调用 prepare_namespace(),它负责:
root= 参数(可以是 /dev/xxx, PARTUUID=xxx, LABEL=xxx, major:minor 等)name_to_dev_t() 将名称转换为主/次设备号mount_root() 通过指定的文件系统类型挂载6.3 initramfs 的生存之道
initramfs 的关键优势在于:它不是一个模块,而是一个嵌入内核的 cpio 文件。解压过程和根文件系统切换由内核直接完成,无需额外文件系统驱动。# 查看 initramfs 中的内容(以 CentOS/Fedora 为例)
lsinitrd /boot/initramfs-$(uname -r).img | head -30
# 输出示例:
# ========================================
# early_cpio
# kernel/
# kernel/x86/
# kernel/x86/microcode/
# kernel/x86/microcode/AuthenticAMD.bin
# kernel/x86/microcode/GenuineIntel.bin
# usr/
# usr/lib/
# usr/lib/modules/
# usr/lib/modules/6.6.8/
# usr/lib/modules/6.6.8/modules.dep
# usr/lib/firmware/
# ...
sbin
bin -> usr/bin
lib -> usr/lib
usr/
init <-- 这是第一个执行的脚本early_cpio 段中,由最早的解压代码处理,远在文件系统驱动加载之前完成。
七、阶段六:用户态 init — PID 1 的世界
run_init_process() 成功返回(实际上是永不返回的),CPU 就从内核态切换到用户态,执行 init 程序。现代发行版大多使用 systemd。$ cat /proc/1/comm
systemd
$ ls -la /proc/1/exe
lrwxrwxrwx 1 root root 0 ... /proc/1/exe -> /usr/lib/systemd/systemd
/etc/inittab(兼容 SysV init)default.target(通常是 multi-user.target 或 graphical.target)[email protected],显示登录提示
八、实战中的调试技巧
8.1 通过
dmesg 查看启动时间戳$ dmesg -H --color=always | less -R
[ 0.000000] Linux version 6.6.8 (root@build) ...
[ 0.000000] Command line: BOOT_IMAGE=/vmlinuz-6.6.8 root=UUID=xxx ...
[ 0.000000] BIOS-provided physical RAM map:
[ 0.004000] tsc: Detected 3000.000 MHz processor
[ 0.004169] Calibrating delay loop (skipped)...
[ 0.452328] smp: Bringing up secondary CPUs ...
[ 4.182966] VFS: Mounted root (ext4 filesystem) readonly on device 8:2.
[ 4.199011] devtmpfs: mounted
[ 4.215734] Freeing unused kernel memory: 2048K
[ 4.249586] Run /sbin/init as init process
[ 5.873417] systemd[1]: Detected architecture x86-64.systemd-analyze 可以获取更详细的分析:$ systemd-analyze
Startup finished in 3.456s (firmware) + 1.234s (loader) +
2.567s (kernel) + 5.678s (userspace) = 12.935s
graphical.target reached after 5.623s in userspace8.2 使用 ftrace/perf 追踪启动过程
# 使用 initcall_debug 参数追踪每个 initcall 的执行时间
$ cat /proc/cmdline
... initcall_debug
$ dmesg | grep initcall
[ 0.123456] initcall calibrate_delay_jiffies+0x0/0x20 returned 0 after 34567 usecs
[ 0.156789] initcall usbcore_init+0x0/0x80 returned 0 after 8765 usecs8.3 使用 QEMU 调试内核启动
# 启动 QEMU 并等待 gdb 连接
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0 nokaslr" \
-nographic \
-s -S
# 另一个终端:gdb
(gdb) target remote :1234
(gdb) break *0x100000 # startup_64
(gdb) break start_kernel
(gdb) break x86_64_start_kernel
(gdb) continue8.4 用 ftrace 的早期事件追踪 boot
# 通过 bootconfig 在启动阶段即启用 ftrace
# /etc/bootconfig
ftrace {
tracer=function_graph;
initcall;
}
九、常见启动故障排查路径
故障现象
可能原因
排查命令
Kernel panic: VFS unable to mount root
root= 参数错误 / 缺少文件系统驱动dmesg 搜索 VFS:
Initramfs unpacking failed
initramfs 损坏 / 格式不支持
提取
initrd 对比 size
"No working init found"
initramfs 里没有
/initlsinitrd 检查
Emergency mode
fstab 有错 / fsck 失败
journalctl -xb -1
启动慢于预期
某个 initcall 卡住
initcall_debug
kexec 崩溃
EFI runtime services 未正确重定位
dmesg 搜索 kexec
十、总结
boot_params、跳转到 start_of_setupstart_kernel() 串行初始化所有子系统(mm/sched/irq/timer/vfs/cgroup)kernel_init 准备根文件系统,尝试 initramfs 中的 /init
作者注:本文基于 Linux 6.6 系列内核源码分析。启动代码随版本演进而调整(如 KASLR 引入随机化、EFI stub 替代部分 bootloader 功能),但整体框架自 2.6 以来保持稳定。建议结合
scripts/bootgraph.pl 和 systemd-analyze plot 生成 SVG 可视化启动火焰图,对照本文的时间线理解。

发表评论 取消回复