3. Linux内核启动流程:从Bootloader到内核初始化,设备树的作用位置

理解设备树在Linux内核启动流程中的精确作用位置,是进行DTS优化的前提。设备树并非在启动一开始就被使用,而是在特定阶段由Bootloader传递给内核,并在内核初始化过程中被解析和消费。本章将拆解从Bootloader到内核完成初始化的完整链路,明确设备树在每个环节的角色与数据流向。

3.1 启动流程总览:三个阶段与设备树的交接

典型的嵌入式Linux启动分为三个阶段:Bootloader阶段、内核解压与早期初始化阶段、内核核心初始化阶段。设备树在这三个阶段中的存在形式和作用各不相同。

关键认知: 设备树是Bootloader与内核之间的硬件描述契约。Bootloader负责加载并传递DTB(Device Tree Blob)物理地址,内核负责解析DTB并据此创建设备模型。任何一方的失误都会导致硬件无法被正确驱动。
Linux内核启动三阶段与设备树交接流程 阶段1: Bootloader 加载DTB到内存 填充/chosen节点 传递DTB物理地址 阶段2: 早期初始化 解压内核 + 固定映射 early_init_dt_scan() 解析内存/cpus/chosen 阶段3: 核心初始化 unflatten_device_tree() 创建device_node树 驱动匹配与probe() 各阶段设备树数据形态 • Bootloader阶段:DTB以二进制blob形式存储在Flash或由Bootloader从存储介质加载。 • 早期初始化阶段:内核通过固定映射访问DTB,仅扫描少量关键节点(内存、CPU、chosen)。 • 核心初始化阶段:DTB被展开为struct device_node树,驱动模型开始全面消费设备节点。

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 获取设备别名映射 方便通过别名引用设备节点
早期初始化阶段设备树扫描流程 setup_machine_fdt() scan_chosen() bootargs / stdout-path scan_root() #address-cells / #size-cells scan_memory() reg 属性解析内存范围 输出: meminfo / boot_command_line / 全局DTB指针

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():设备树如何驱动硬件

设备树展开后,内核的驱动模型开始工作。每个设备节点都会触发驱动匹配流程:

  1. 设备注册:of_platform_populate()遍历设备树,为每个节点创建struct platform_device。
  2. 驱动匹配:总线(platform bus)遍历已注册的驱动列表,通过of_match_table中的compatible字符串与设备节点的compatible属性进行匹配。
  3. 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属性的简洁和准确有助于加速匹配。