1. 嵌入式Linux内核启动流程概述:从Bootloader到内核启动的完整链路
嵌入式Linux系统的启动是一个多阶段、高度可配置的过程,从硬件上电复位到用户空间init进程运行,涉及Bootloader、内核解压、硬件初始化、驱动加载等多个关键环节。理解这一完整链路是进行内核启动日志分析与调试的基础。
1.1 启动流程总览
典型的嵌入式Linux启动分为以下四个主要阶段:
- 第一阶段:Bootloader(引导加载程序) — 初始化硬件、加载内核映像到内存。
- 第二阶段:内核解压与早期初始化 — 解压压缩内核、设置页表、初始化CPU和内存。
- 第三阶段:内核核心初始化 — 执行start_kernel(),完成调度器、内存管理、中断等核心子系统初始化。
- 第四阶段:设备驱动与用户空间启动 — 枚举设备、加载驱动模块、挂载根文件系统、启动init进程。
1.2 Bootloader阶段详解
Bootloader是系统上电后执行的第一个软件,负责从低级硬件初始化过渡到内核执行环境。嵌入式领域最常用的Bootloader是U-Boot(Das U-Boot)。
| 子阶段 | 主要任务 | 关键代码/函数 |
|---|---|---|
| Stage 1 (SPL) | 初始化CPU、时钟、DDR控制器;加载Stage 2到SRAM | start.S、spl_init() |
| Stage 2 (U-Boot proper) | 初始化串口、存储设备、网络;执行bootcmd加载内核 | board_init_f()、board_init_r() |
| 内核加载 | 从Flash/MMC/NFS读取内核映像到DDR指定地址 | bootm、load命令 |
| 传递参数 | 设置ATAGS或设备树(DTB)传递给内核 | setup_start_tag() / fdt_chosen() |
调试提示: 在U-Boot命令行中使用
bdinfo 查看板级信息,使用 printenv 查看启动参数,使用 bootm 的详细输出可以定位内核加载失败的原因。
1.3 内核解压与早期汇编阶段
当Bootloader将控制权交给内核入口点(通常是 stext 或 _start)时,内核开始执行。对于压缩内核(zImage),首先执行解压缩代码。
// arch/arm/kernel/head.S (ARM架构示例)
ENTRY(stext)
@ 设置SVC模式,关闭中断
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE
@ 检查CPU ID
mrc p15, 0, r9, c0, c0 @ 读取MIDR
@ 创建初始页表
bl __create_page_tables
@ 使能MMU
ldr r13, =__mmap_switched
adr lr, BSYM(1f)
mov r8, r4 @ 保存DTB指针
ldr r12, [r10, #PROCINFO_INITFUNC]
add r12, r12, r10
ret r12
1: b start_kernel
早期汇编阶段的关键操作包括:
- 设置异常向量表 — 确保中断和异常能正确路由。
- 创建临时页表 — 使能MMU,从物理地址映射到虚拟地址。
- 保存设备树指针 — 将Bootloader传递的DTB地址保存在寄存器中。
- 跳转到start_kernel() — 进入C语言世界。
1.4 start_kernel() 核心初始化
start_kernel() 是内核初始化C语言的入口点,位于 init/main.c。它按严格顺序调用各个子系统的初始化函数。
| 初始化顺序 | 子系统 | 关键函数 | 启动日志标记 |
|---|---|---|---|
| 1 | 锁与架构 | lockdep_init()、setup_arch() |
Booting Linux on physical CPU 0x0 |
| 2 | 内存管理 | mm_init()、kmem_cache_init() |
Memory: 1024MB available |
| 3 | 调度器 | sched_init() |
sched: registering cpufreq notifier |
| 4 | 中断与定时器 | init_IRQ()、time_init() |
NR_IRQS: 256 |
| 5 | 控制台 | console_init() |
console [ttyS0] enabled |
| 6 | 设备模型 | do_basic_setup() → driver_init() |
Linux version 5.10.0 |
| 7 | 根文件系统 | prepare_namespace() → mount_root() |
VFS: Mounted root (ext4 filesystem) |
| 8 | 用户空间 | run_init_process() |
init started: /sbin/init |
关键观察: 启动日志中
console [ttyS0] enabled 之前的输出通常通过 early_printk 或 DEBUG_LL 实现。如果串口驱动初始化失败,你将看不到任何内核日志,这是调试中最常见的问题之一。
1.5 设备树与驱动加载
现代嵌入式Linux使用设备树(Device Tree, DTB)来描述硬件拓扑。内核在 setup_arch() 中解析DTB,并在 do_basic_setup() 中枚举设备并加载驱动。
驱动加载分为两种方式:
- 内置驱动(built-in) — 编译进内核,在
do_initcalls()中按优先级顺序执行。 - 可加载模块(LKM) — 在用户空间通过
insmod或modprobe动态加载。
// 驱动初始化调用顺序(initcall 优先级)
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define device_initcall(fn) __define_initcall(fn, 6)
#define late_initcall(fn) __define_initcall(fn, 7)
常见陷阱: 如果驱动依赖的资源(如时钟、GPIO)在更晚的initcall级别才初始化,驱动probe会失败并返回
-EPROBE_DEFER。内核会将该驱动加入延迟队列,稍后重试。在启动日志中搜索 probe deferred 可以定位此类问题。
1.6 根文件系统挂载与init启动
内核挂载根文件系统后,启动第一个用户空间进程 /sbin/init(通常是systemd或BusyBox init)。
根文件系统的挂载参数由Bootloader传递的 root= 和 rootfstype= 指定:
# U-Boot bootargs 示例
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rw init=/sbin/init'
内核挂载根文件系统的典型日志输出:
[ 3.456789] VFS: Mounted root (ext4 filesystem) readonly on device 179:2.
[ 3.463210] devtmpfs: mounted
[ 3.466789] Freeing unused kernel memory: 1024K
[ 3.471234] Run /sbin/init as init process
1.7 启动日志关键节点速查表
以下表格列出了启动日志中最重要的标记行及其含义,帮助快速定位启动阶段:
| 日志内容(示例) | 所处阶段 | 调试意义 |
|---|---|---|
Booting Linux on physical CPU 0x0 |
start_kernel 入口 | 确认内核已获得控制权 |
Machine: Freescale i.MX6 Quad |
setup_arch | 确认板级匹配 |
Memory: 1024MB available |
mm_init | 确认内存大小正确 |
console [ttyS0] enabled |
console_init | 串口输出就绪,后续日志可见 |
of: fdt: machine model: MY_BOARD |
DTB解析 | 确认设备树加载成功 |
mmc0: new high speed SDHC card |
驱动probe | 存储设备识别成功 |
VFS: Mounted root (ext4 filesystem) |
根文件系统 | 根文件系统挂载成功 |
init started: /sbin/init |
用户空间 | 系统启动完成 |
调试技巧: 使用
dmesg 命令可以查看内核环形缓冲区中的启动日志。如果系统无法启动,可以在Bootloader中设置 bootargs 添加 earlyprintk 或 debug 参数获取更多早期调试信息。