3. Linux内核启动流程:从Bootloader到内核初始化,设备树的作用位置
理解设备树在Linux内核启动流程中的精确作用位置,是进行DTS优化的前提。设备树并非在启动一开始就被使用,而是在特定阶段由Bootloader传递给内核,并在内核初始化过程中被解析和消费。本章将拆解从Bootloader到内核完成初始化的完整链路,明确设备树在每个环节的角色与数据流向。
3.1 启动流程总览:三个阶段与设备树的交接
典型的嵌入式Linux启动分为三个阶段:Bootloader阶段、内核解压与早期初始化阶段、内核核心初始化阶段。设备树在这三个阶段中的存在形式和作用各不相同。
关键认知: 设备树是Bootloader与内核之间的硬件描述契约。Bootloader负责加载并传递DTB(Device Tree Blob)物理地址,内核负责解析DTB并据此创建设备模型。任何一方的失误都会导致硬件无法被正确驱动。
3.2 Bootloader阶段:设备树的加载与传递
Bootloader(如U-Boot、TF-A+UEFI)是设备树的第一个消费者。它负责将DTB从存储介质(Flash、eMMC、网络)加载到内存中,并在跳转到内核之前,将DTB的物理地址通过特定寄存器(如ARM64的x0寄存器)传递给内核。
3.2.1 U-Boot中的设备树处理流程
| 步骤 | U-Boot命令/操作 | 说明 |
|---|---|---|
| 1 | load mmc 0:1 $fdt_addr_r /boot/board.dtb |
从MMC分区加载DTB到内存地址$fdt_addr_r |
| 2 | fdt addr $fdt_addr_r |
设置FDT工作地址,使U-Boot可以解析和修改DTB |
| 3 | fdt set /chosen bootargs "console=ttyS0,115200" |
在chosen节点中设置内核启动参数 |
| 4 | booti $kernel_addr_r - $fdt_addr_r |
启动内核,传递DTB地址(第三个参数) |
优化启示: Bootloader阶段是DTS优化的第一道关口。如果DTB体积过大,加载时间会显著增加。精简DTS中未使用外设的节点,可以直接减小DTB文件大小,缩短Bootloader加载时间。
3.2.2 chosen节点:Bootloader与内核的通信桥梁
chosen节点是设备树中一个特殊的虚拟节点,它不描述任何硬件,而是用于Bootloader向内核传递运行时参数。常见的属性包括:
- bootargs:内核命令行参数,如console、root、init等。
- stdout-path:指定早期控制台输出设备。
- initrd-start / initrd-end:初始RAM磁盘的内存地址范围。
// 典型的chosen节点示例
chosen {
bootargs = "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rw";
stdout-path = "serial0:115200n8";
initrd-start = <0x84000000>;
initrd-end = <0x86000000>;
};
3.3 内核早期初始化阶段:设备树的首次扫描
当内核接收到Bootloader传递的DTB地址后,在解压完成并建立早期页表后,会立即进入设备树的早期扫描流程。这一阶段发生在内存管理、中断管理、定时器初始化之前,因此只能使用固定映射地址访问DTB。
3.3.1 早期扫描的关键函数调用链
start_kernel()
└─ setup_arch()
└─ setup_machine_fdt(__fdt_pointer) // 接收DTB地址
├─ early_init_dt_scan() // 扫描根节点
│ ├─ early_init_dt_scan_nodes()
│ │ ├─ early_init_dt_scan_chosen() // 解析chosen节点
│ │ ├─ early_init_dt_scan_root() // 获取#address-cells/#size-cells
│ │ └─ early_init_dt_scan_memory() // 扫描memory节点
└─ of_scan_flat_dt() // 遍历所有节点,调用回调函数
注意: 早期初始化阶段只解析极少数关键节点。如果DTS中memory节点配置错误(如reg属性地址范围不正确),内核将无法正确识别物理内存,导致系统在早期阶段崩溃,甚至无法打印任何日志。
3.3.2 早期扫描阶段解析的内容
| 节点/属性 | 解析目的 | 对启动的影响 |
|---|---|---|
/chosen |
获取bootargs、stdout-path | 决定内核命令行参数和早期控制台 |
/memory |
获取物理内存基址和大小 | 建立内存管理子系统的基础 |
/cpus |
获取CPU核数、架构、使能状态 | SMP启动、CPU hotplug |
/aliases |
获取设备别名映射 | 方便通过别名引用设备节点 |
3.4 内核核心初始化阶段:设备树的全面展开
当内存管理、中断控制器、定时器等基础子系统初始化完成后,内核进入核心初始化阶段。此时,设备树从扁平的二进制DTB格式被展开为一棵由struct device_node构成的树形结构,驱动模型开始全面运作。
3.4.1 unflatten_device_tree():从DTB到device_node树
这是设备树处理的核心函数。它将DTB中的节点、属性、字符串表等扁平数据解析为内核可以遍历和操作的树形结构。
// 内核源码: drivers/of/fdt.c
void __init unflatten_device_tree(void)
{
// 1. 分配内存用于存储device_node结构
// 2. 遍历DTB,为每个节点创建struct device_node
// 3. 建立父子、兄弟节点链表关系
// 4. 将属性转换为struct property结构
// 5. 设置of_root全局指针指向根节点
of_root = unflatten_dt_nodes(NULL, initial_boot_params, NULL, NULL);
// 6. 更新CPU映射、中断域等依赖设备树的信息
of_alias_scan();
of_platform_device_create_all();
}
3.4.2 驱动匹配与probe():设备树如何驱动硬件
设备树展开后,内核的驱动模型开始工作。每个设备节点都会触发驱动匹配流程:
- 设备注册:
of_platform_populate()遍历设备树,为每个节点创建struct platform_device。 - 驱动匹配:总线(platform bus)遍历已注册的驱动列表,通过
of_match_table中的compatible字符串与设备节点的compatible属性进行匹配。 - probe()执行:匹配成功后,调用驱动的
probe()函数,驱动从设备树节点中读取reg、interrupts、clocks等资源,完成硬件初始化。
优化切入点: 如果DTS中包含大量
status = "disabled"的节点,内核仍然会为这些节点创建struct device_node和struct platform_device,占用内存和遍历时间。在DTS编译阶段直接删除这些节点(而非仅设置disabled),可以节省约10%~30%的设备树解析时间,具体取决于禁用节点的数量。
3.5 设备树作用位置总结:时间线与关键函数
| 启动阶段 | 时间点 | 关键函数/操作 | 设备树作用 |
|---|---|---|---|
| Bootloader | 加载内核前 | fdt set / booti |
加载、修改、传递DTB地址 |
| 早期初始化 | start_kernel → mm_init | early_init_dt_scan() |
扫描memory/chosen/cpus |
| 核心初始化 | mm_init → rest_init | unflatten_device_tree() |
展开为device_node树 |
| 驱动加载 | late_initcall / module_init | of_platform_populate() |
创建设备、匹配驱动、probe() |
3.6 本章小结:设备树位置对DTS优化的指导意义
- Bootloader阶段:DTB文件大小直接影响加载时间。精简DTS可以减小DTB体积,加速启动。
- 早期初始化阶段:memory节点配置错误会导致系统无法启动。优化时应确保memory节点准确无误。
- 核心初始化阶段:未使用外设的节点即使被disabled,也会消耗解析时间和内存。最佳实践是在DTS中直接删除这些节点。
- 驱动加载阶段:compatible字符串的匹配效率影响驱动加载速度。保持compatible属性的简洁和准确有助于加速匹配。