概述
Linux内核的启动是一个从硬件裸机到完整用户空间的精密过程。理解这个流程对于调试启动问题、移植内核到新平台、以及深入理解内核架构都至关重要。
一、启动流程总览
+------------------+
| BIOS/UEFI | 硬件初始化,加载Bootloader
+------------------+
↓
+------------------+
| GRUB/U-Boot | 加载内核镜像和initrd
+------------------+
↓
+------------------+
| arch/x86/boot/ | 实模式启动代码 (16-bit)
| setup.S |
+------------------+
↓
+------------------+
| arch/x86/boot/ | 保护模式切换 (32-bit)
| compressed/ | 解压内核
+------------------+
↓
+------------------+
| arch/x86/kernel/| 64位长模式初始化
| head_64.S | 建立初始页表
+------------------+
↓
+------------------+
| init/main.c | start_kernel() - C语言入口
| start_kernel() | 各子系统初始化
+------------------+
↓
+------------------+
| kernel_init() | 内核init线程
| PID=1 | 挂载rootfs,执行/sbin/init
+------------------+
↓
+------------------+
| 用户空间 | systemd/SysVinit
+------------------+
二、BIOS/UEFI阶段
2.1 传统BIOS启动
上电 → BIOS POST → 读取MBR(512字节) → 加载Bootloader
MBR结构:
偏移0x000 - 0x1BD: 446字节 Bootloader代码
偏移0x1BE - 0x1FD: 64字节 分区表(4个主分区,每个16字节)
偏移0x1FE - 0x1FF: 2字节 魔数 0x55 0xAA
2.2 UEFI启动
上电 → UEFI固件 → GPT分区表 → EFI系统分区(ESP) → GRUB EFI → 内核
# 查看UEFI启动信息
efibootmgr -v
# 查看EFI系统分区
ls /boot/efi/EFI/
# 查看当前启动模式
[ -d /sys/firmware/efi ] && echo"UEFI" || echo"BIOS"
# 查看UEFI变量
ls /sys/firmware/efi/efivars/ | head -10
三、Bootloader阶段(GRUB2)
3.1 GRUB2工作流程
Stage 1 (MBR/EFI) → Stage 1.5 (core.img) → Stage 2 (grub模块) → 加载内核
# GRUB2配置文件
cat /boot/grub/grub.cfg | head -50
# 手动生成grub配置
sudo grub-mkconfig -o /boot/grub/grub.cfg
# 查看GRUB变量
sudo grub-editenv list
# 内核命令行参数
cat /proc/cmdline
# 示例:BOOT_IMAGE=/vmlinuz-6.6.0 root=/dev/sda1 ro quiet splash
3.2 内核命令行重要参数
# 常用内核启动参数
root=/dev/sda1 # 根文件系统设备
ro # 以只读方式挂载root
quiet # 减少启动输出
splash # 显示启动画面
nokaslr # 禁用内核地址空间布局随机化(调试用)
console=ttyS0,115200 # 串口控制台
init=/bin/bash # 指定init程序(紧急恢复用)
mem=512M # 限制内存使用
maxcpus=1 # 限制CPU数量
noapic # 禁用APIC
acpi=off # 禁用ACPI
debug # 开启内核调试输出
earlyprintk=serial # 早期printk输出到串口
四、内核实模式启动(x86)
4.1 setup.S阶段
源码位置:arch/x86/boot/setup.S 和 arch/x86/boot/main.c
/* arch/x86/boot/main.c 简化版 */
// 实模式C入口
voidmain(void)
{
/* 初始化堆 */
init_default_io_ops();
/* 检测内存 */
detect_memory();
/* 设置键盘 */
keyboard_init();
/* 查询Intel SpeedStep */
query_ist();
/* 查询APM电源管理 */
query_apm_bios();
/* 查询EDD(增强磁盘驱动)*/
query_edd();
/* 设置视频模式 */
set_video();
/* 跳转到保护模式 */
go_to_protected_mode(); // 不会返回
}
4.2 保护模式切换
/* arch/x86/boot/pm.c - 简化版 */
voidgo_to_protected_mode(void)
{
/* 确保中断被禁用 */
realmode_switch_hook();
/* 启用A20地址线(访问1MB以上内存)*/
if (enable_a20()) {
die("A20 gate not responding");
}
/* 重置协处理器 */
reset_coprocessor();
/* 屏蔽所有中断(8259 PIC)*/
mask_all_interrupts();
/* 设置IDT和GDT */
setup_idt();
setup_gdt();
/* 跳转到保护模式 */
protected_mode_jump(boot_params.hdr.code32_start, ...);
}
五、内核解压与64位初始化
5.1 解压阶段
# vmlinuz的结构
# = 实模式启动代码 + 压缩的内核(gzip/lz4/zstd)
# 解压内核(仅供分析)
ddif=/boot/vmlinuz-$(uname -r) bs=1 skip=$(grep -a -b -o $'\x1f\x8b\x08' \
/boot/vmlinuz-$(uname -r) | head -1 | cut -d: -f1) | \
zcat > /tmp/vmlinux_extracted 2>/dev/null || true
file /tmp/vmlinux_extracted
# 直接提取vmlinux(如果内核带调试符号)
ls -la /boot/vmlinux-$(uname -r) 2>/dev/null || \
ls -la /usr/lib/debug/boot/vmlinux-$(uname -r) 2>/dev/null
5.2 head_64.S关键操作
/* arch/x86/kernel/head_64.S 关键步骤(注释版)*/
/*
* 1. 验证CPU支持64位模式
* 2. 建立初始页表(identity mapping)
* 3. 开启PAE(物理地址扩展)
* 4. 加载CR3(页目录基址)
* 5. 设置EFER.LME(启用长模式)
* 6. 开启分页(CR0.PG=1)→ 进入64位长模式
* 7. 跳转到start_kernel()
*/
SYM_CODE_START_NOALIGN(startup_64)
/* 清零段寄存器 */
xorl %eax, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %ss
movl %eax, %fs
movl %eax, %gs
/* 建立页表 */
leaq early_top_pgt(%rip), %rax
movq %rax, %cr3
/* 跳转到C代码 */
pushq $0
popfq
movq initial_code(%rip), %rax
jmp *%rax /* 跳转到 x86_64_start_kernel() */
SYM_CODE_END(startup_64)
六、start_kernel()详解
6.1 初始化序列
源码位置:init/main.c
/* init/main.c - start_kernel() 关键步骤 */
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
char *command_line;
char *after_dashes;
/* 1. 设置初始任务栈 */
set_task_stack_end_magic(&init_task);
/* 2. SMP处理器ID初始化 */
smp_setup_processor_id();
/* 3. 调试对象初始化 */
debug_objects_early_init();
/* 4. cgroup早期初始化 */
cgroup_init_early();
/* 5. 禁用本地中断 */
local_irq_disable();
/* 6. 早期启动信息 */
pr_notice("%s", linux_banner); // 打印内核版本
/* 7. 架构相关初始化 */
setup_arch(&command_line); // 内存检测、ACPI、PCI等
/* 8. 内存分配器早期初始化 */
mm_core_init();
/* 9. sched_clock初始化 */
sched_clock_init();
/* 10. 解析内核命令行参数 */
parse_early_param();
after_dashes = parse_args(...);
/* 11. 跳转表初始化 */
jump_label_init();
/* 12. 随机数种子 */
setup_log_buf(0);
vfs_caches_init_early();
sort_main_extable();
/* 13. 陷阱门初始化(IDT) */
trap_init();
/* 14. 内存管理初始化 */
mm_init(); // 页分配器、kmem_cache
/* 15. ftrace初始化 */
ftrace_init();
/* 16. 调度器初始化 */
sched_init();
/* 17. RCU初始化 */
rcu_init();
/* 18. 中断初始化 */
init_IRQ();
/* 19. 定时器初始化 */
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();
/* 20. 使能中断 */
local_irq_enable();
/* 21. 进程0(idle进程)初始化完成 */
arch_call_rest_init(); // → rest_init()
}
6.2 setup_arch()的工作
/* arch/x86/kernel/setup.c - setup_arch() 关键步骤 */
void __init setup_arch(char **cmdline_p)
{
/* 解析boot_params(由bootloader填充)*/
memblock_reserve(__pa_symbol(_text), ...);
/* 解析e820内存映射 */
e820__memory_setup(); // 获取BIOS内存布局
/* 解析ACPI表 */
acpi_boot_table_init();
/* 初始化NUMA */
initmem_init();
/* 建立最终页表 */
init_mem_mapping();
/* 初始化中断控制器 */
x86_init.irqs.pre_vector_init(); // APIC初始化
/* 探测CPU类型 */
identify_boot_cpu();
}
6.3 观察启动过程的实验
# 方法1:查看完整启动日志
sudo journalctl -b -o short-monotonic | head -200
# 方法2:查看内核初始化时间线
sudo dmesg --notime=false | head -100
# 方法3:在QEMU中观察启动(带详细输出)
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-append "console=ttyS0 earlyprintk=serial,ttyS0,115200 nokaslr" \
-nographic \
-m 512M \
-initrd rootfs.cpio.gz
# 方法4:使用GDB在start_kernel设断点
qemu-system-x86_64 -kernel bzImage -append "nokaslr" -s -S &
gdb vmlinux
(gdb) target remote :1234
(gdb) break start_kernel
(gdb) continue
(gdb) list # 查看当前代码
(gdb) next # 单步执行
七、rest_init()与进程0/1/2
7.1 三个初始进程
/* init/main.c - rest_init() */
noinline void __ref rest_init(void)
{
structtask_struct *tsk;
int pid;
rcu_scheduler_starting();
/*
* 创建PID=1的内核线程(将来execve为/sbin/init)
* 必须先于kthreadd创建,以确保PID=1
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
/*
* 创建PID=2的kthreadd线程
* 负责创建所有其他内核线程
*/
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
/*
* 当前执行流变成idle进程(PID=0)
* CPU空闲时执行 cpu_idle_loop()
*/
cpu_startup_entry(CPUHP_ONLINE); // 不会返回
}
7.2 kernel_init()流程
/* init/main.c - kernel_init() */
staticint __ref kernel_init(void *unused)
{
int ret;
/* 等待所有CPU上线 */
kernel_init_freeable();
/* 释放init段内存 */
free_initmem();
/* 打开控制台 */
console_on_rootfs();
/*
* 尝试执行用户空间init程序
* 按优先级顺序尝试
*/
/* 先尝试命令行指定的init */
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return0;
}
/* 然后按顺序尝试标准路径 */
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"))
return0;
/* 如果都失败了,内核panic */
panic("No working init found. "
"Try passing init= option to kernel.");
}
7.3 kernel_init_freeable()详解
/* init/main.c - kernel_init_freeable() */
static noinline void __init kernel_init_freeable(void)
{
/* 等待kthreadd就绪 */
wait_for_completion(&kthreadd_done);
/* 初始化SMP */
smp_init(); // 启动所有AP(应用处理器)
sched_init_smp(); // SMP调度初始化
/* 驱动初始化 */
do_pre_smp_initcalls();
/* 延迟初始化(initcall机制)*/
do_initcalls(); // 执行所有__initcall函数
/* 挂载根文件系统 */
prepare_namespace(); // 处理initrd/initramfs
}
八、initcall机制
8.1 initcall级别
/*
* initcall执行顺序(从早到晚):
* pure_initcall - 纯初始化,无依赖
* core_initcall - 核心子系统
* postcore_initcall - 核心子系统后
* arch_initcall - 架构相关
* subsys_initcall - 子系统(总线、驱动框架)
* fs_initcall - 文件系统
* rootfs_initcall - rootfs相关
* device_initcall - 设备驱动(最常用)
* late_initcall - 延迟初始化
*/
// 示例:注册一个子系统初始化函数
staticint __init my_subsys_init(void)
{
pr_info("My subsystem initializing\n");
return0;
}
subsys_initcall(my_subsys_init);
// 等价于:
// static initcall_t __initcall_my_subsys_init \
// __used __attribute__((__section__(".initcall4.init"))) = my_subsys_init;
8.2 观察initcall执行
# 启用initcall调试(内核参数)
# 在 /etc/default/grub 中添加:
# GRUB_CMDLINE_LINUX="initcall_debug"
# sudo update-grub && reboot
# 查看initcall日志
dmesg | grep "calling\|initcall"
# 查看initcall耗时(需要initcall_debug)
dmesg | grep "initcall.*returned" | sort -t= -k2 -n | tail -20
// initcall_demo.c - 演示initcall机制的内核模块
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
MODULE_LICENSE("GPL");
/* 演示各种初始化宏 */
/* __init: 函数只在初始化时使用,之后可以释放其内存 */
staticint __init my_init(void)
{
pr_info("Module init: PID=%d, CPU=%d\n",
current->pid, smp_processor_id());
/* 查看当前jiffies(系统滴答数)*/
pr_info("jiffies=%lu, HZ=%d\n", jiffies, HZ);
/* 查看启动时间 */
structtimespec64ts;
ktime_get_boottime_ts64(&ts);
pr_info("Boot time: %lld.%09ld seconds\n",
(longlong)ts.tv_sec, ts.tv_nsec);
return0;
}
/* __exit: 函数只在退出时使用 */
staticvoid __exit my_exit(void)
{
pr_info("Module exit\n");
}
module_init(my_init);
module_exit(my_exit);
九、initramfs/initrd机制
9.1 initramfs工作原理
GRUB加载bzImage + initramfs.img
↓
内核解压,挂载initramfs为tmpfs根文件系统
↓
执行 /init 脚本(initramfs中的)
↓
加载必要驱动(磁盘/文件系统/加密)
↓
挂载真实根文件系统到 /sysroot
↓
切换根文件系统(pivot_root 或 switch_root)
↓
执行真实 /sbin/init
# 查看initramfs内容
mkdir /tmp/initrd_content
cd /tmp/initrd_content
zcat /boot/initrd.img-$(uname -r) | cpio -id 2>/dev/null || \
lz4cat /boot/initrd.img-$(uname -r) | cpio -id 2>/dev/null
ls -la # 查看initramfs根目录
# 查看initramfs中的init脚本
cat init
# 重建initramfs
sudo update-initramfs -u -k $(uname -r)
# 创建最小initramfs(用于内核测试)
create_minimal_initramfs() {
local ROOTFS=$(mktemp -d)
mkdir -p $ROOTFS/{bin,sbin,proc,sys,dev,etc}
# 复制busybox
cp /bin/busybox $ROOTFS/bin/
$ROOTFS/bin/busybox --install -s $ROOTFS/bin/
# 创建init脚本
cat > $ROOTFS/init << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo"=== Minimal initramfs ==="
echo"Kernel: $(uname -r)"
exec /bin/sh
EOF
chmod +x $ROOTFS/init
# 打包
(cd$ROOTFS && find . | cpio -o -H newc | gzip) > /tmp/initramfs.cpio.gz
rm -rf $ROOTFS
echo"Created: /tmp/initramfs.cpio.gz"
}
create_minimal_initramfs
十、SMP初始化(多核启动)
10.1 BSP与AP
BSP(Bootstrap Processor)- 引导处理器,执行所有初始化
AP(Application Processor)- 应用处理器,等待BSP的INIT-SIPI信号
启动流程:
BSP执行start_kernel()
↓
BSP发送INIT-SIPI给所有AP
↓
AP从实模式启动(trampoline代码)
↓
AP进入64位模式,执行start_secondary()
↓
AP进入idle循环,等待调度
# 查看CPU拓扑
cat /proc/cpuinfo | grep "processor\|physical id\|core id" | head -24
# 查看NUMA拓扑
numactl --hardware
# 查看SMP启动日志
dmesg | grep -E "CPU|SMP|APIC" | head -30
# 查看各CPU状态
cat /sys/devices/system/cpu/cpu*/online
# 动态上下线CPU(hot-plug)
echo 0 | sudo tee /sys/devices/system/cpu/cpu3/online # 下线CPU3
echo 1 | sudo tee /sys/devices/system/cpu/cpu3/online # 上线CPU3
// smp_info.c - 查看SMP信息的内核模块
#include<linux/module.h>
#include<linux/smp.h>
#include<linux/cpumask.h>
#include<linux/topology.h>
MODULE_LICENSE("GPL");
staticvoidprint_cpu_info(void *info)
{
int cpu = smp_processor_id();
pr_info("CPU %d: node=%d, core=%d, online=%d\n",
cpu,
cpu_to_node(cpu),
topology_core_id(cpu),
cpu_online(cpu));
}
staticint __init smp_info_init(void)
{
pr_info("Total CPUs: possible=%d, present=%d, online=%d\n",
num_possible_cpus(),
num_present_cpus(),
num_online_cpus());
/* 在每个CPU上执行函数 */
on_each_cpu(print_cpu_info, NULL, 1);
/* 只在CPU0上执行 */
smp_call_function_single(0, print_cpu_info, NULL, 1);
return0;
}
staticvoid __exit smp_info_exit(void) {}
module_init(smp_info_init);
module_exit(smp_info_exit);
十一、内核时钟与jiffies
11.1 时钟体系
/*
* Linux内核时钟体系:
*
* HZ - 每秒时钟中断次数(通常250或1000)
* jiffies - 系统启动以来的时钟中断次数(全局变量)
* ktime_t - 高精度时间(纳秒精度)
*
* 时钟源优先级(从高到低):
* TSC(Time Stamp Counter) - x86 CPU内部计数器,最快最精确
* HPET(High Precision Event Timer)
* ACPI PM Timer
* PIT(Programmable Interrupt Timer)- 最慢
*/
# 查看时钟源
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
# 查看HZ配置
grep "^CONFIG_HZ=" /boot/config-$(uname -r)
# 查看定时器中断统计
cat /proc/interrupts | grep -E "LOC|timer"
# 高精度时间工具
clock_gettime CLOCK_MONOTONIC
// time_demo.c - 内核时间操作示例
#include<linux/module.h>
#include<linux/jiffies.h>
#include<linux/ktime.h>
#include<linux/timekeeping.h>
#include<linux/delay.h>
MODULE_LICENSE("GPL");
staticint __init time_demo_init(void)
{
ktime_t start, end;
u64 delta_ns;
structtimespec64ts;
/* jiffies使用 */
pr_info("jiffies=%lu, HZ=%d\n", jiffies, HZ);
pr_info("Uptime: %lu seconds\n", jiffies / HZ);
/* jiffies比较(处理绕回)*/
unsignedlongfuture = jiffies + msecs_to_jiffies(1000);
pr_info("1 second from now: jiffies=%lu\n", future);
pr_info("time_before: %d\n", time_before(jiffies, future));
/* 高精度时间测量 */
start = ktime_get();
mdelay(10); /* 忙等待10ms */
end = ktime_get();
delta_ns = ktime_to_ns(ktime_sub(end, start));
pr_info("mdelay(10) took %llu ns\n", delta_ns);
/* 获取实时时间 */
ktime_get_real_ts64(&ts);
pr_info("Real time: %lld.%09ld\n",
(longlong)ts.tv_sec, ts.tv_nsec);
/* 获取单调时钟(不受NTP影响)*/
ktime_get_boottime_ts64(&ts);
pr_info("Boot time: %lld.%09ld\n",
(longlong)ts.tv_sec, ts.tv_nsec);
return0;
}
staticvoid __exit time_demo_exit(void) {}
module_init(time_demo_init);
module_exit(time_demo_exit);
十二、实验:追踪启动全流程
12.1 在QEMU中调试启动过程
#!/bin/bash
# debug_boot.sh - 完整启动调试环境搭建
# 1. 编译带调试信息的内核
cd ~/linux-6.6
cat >> .config << 'EOF'
CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y
CONFIG_FRAME_POINTER=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
EOF
make olddefconfig
make -j$(nproc)
# 2. 启动QEMU(等待GDB连接)
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd /tmp/initramfs.cpio.gz \
-append "nokaslr console=ttyS0 earlyprintk=serial" \
-m 512M \
-nographic \
-s -S & # -s: 监听:1234, -S: 启动时暂停
QEMU_PID=$!
# 3. 连接GDB
gdb -batch \
-ex "file vmlinux" \
-ex "target remote :1234" \
-ex "break start_kernel" \
-ex "break kernel_init" \
-ex "break rest_init" \
-ex "commands 1" \
-ex " printf \"=== start_kernel reached ===\\n\"" \
-ex " continue" \
-ex "end" \
-ex "continue" \
-ex "quit"
wait$QEMU_PID
12.2 关键断点清单
# 在GDB中设置以下断点追踪启动流程
# 架构初始化
break x86_64_start_kernel
break start_kernel
break setup_arch
# 内存初始化
break mm_init
break mem_init
break kmem_cache_init
# 调度器初始化
break sched_init
# 中断初始化
break init_IRQ
break trap_init
# 进程创建
break rest_init
break kernel_init
break kernel_thread
# 文件系统挂载
break prepare_namespace
break mount_root
# 用户空间切换
break run_init_process
实践检查清单
- [ ] 理解BIOS/UEFI → Bootloader → 内核的完整启动链
- [ ] 能用
dmesg 分析内核启动日志,找出慢初始化函数 - [ ] 理解
start_kernel() 中各子系统的初始化顺序 - [ ] 理解 initcall 机制,能编写使用
subsys_initcall 的模块 - [ ] 能在 QEMU + GDB 中在
start_kernel 设断点并单步调试 - [ ] 理解 initramfs 的作用,能创建最小 initramfs
- [ ] 理解 SMP 启动流程,BSP 与 AP 的关系
- [ ] 理解 jiffies、HZ、ktime_t 的区别和使用场景