4. 设备树初始化流程:unflatten_device_tree()、of_scan_flat_dt()、early_init_dt_scan_nodes()

设备树从扁平二进制数据(DTB)转换为内核可用的运行时结构,是驱动模型初始化的基石。本章聚焦三个核心函数:unflatten_device_tree()、of_scan_flat_dt() 和 early_init_dt_scan_nodes(),解析它们如何协同完成从原始数据到 of_node 树,再到早期设备扫描的完整链路。

核心要点: 扁平DTB → 展开为树形结构 → 扫描关键节点 → 填充内核早期参数。这三个函数按严格顺序执行,任何一步失败都将导致系统无法正确识别内存、中断控制器或控制台。

4.1 整体调用流程与阶段划分

设备树初始化在 start_kernel() 早期完成,分为三个阶段:

阶段 函数 主要职责 执行时机
1. 扁平扫描 of_scan_flat_dt() 遍历DTB,提取chosen、memory、cpu等关键节点信息 极早期,内存管理初始化前
2. 树展开 unflatten_device_tree() 将扁平DTB递归展开为struct device_node树 内存分配器就绪后
3. 节点扫描 early_init_dt_scan_nodes() 扫描已展开的树,填充内核启动参数 树展开完成后立即执行
性能视角: 扁平扫描阶段无法动态分配内存,所有操作基于偏移量和指针算术;树展开阶段才首次调用内存分配,因此其效率直接影响系统启动时间。
设备树初始化三阶段流程 阶段1: 扁平扫描 of_scan_flat_dt() 阶段2: 树展开 unflatten_device_tree() 阶段3: 节点扫描 early_init_dt_scan_nodes() 关键依赖关系 • 阶段1必须在阶段2之前:因为树展开需要内存分配,而内存节点信息由阶段1提取 • 阶段2必须在阶段3之前:early_init_dt_scan_nodes() 操作已展开的 device_node 树 • 三个阶段均在 setup_arch() 内完成,早于任何驱动 probe 调用

4.2 of_scan_flat_dt():极早期扁平扫描

of_scan_flat_dt() 是设备树初始化的入口点,在内存分配器尚未初始化时执行。它直接操作 DTB 的二进制数据,通过回调机制提取关键信息。

4.2.1 函数原型与调用方式

int __init of_scan_flat_dt(int (*it)(unsigned long node,
                                     const char *uname,
                                     int depth,
                                     void *data),
                           void *data);
  • it:回调函数,对每个节点调用一次
  • data:传递给回调的私有数据
  • 返回值:回调返回非0值时停止扫描,返回该值

4.2.2 内部实现机制

该函数的核心逻辑是遍历 DTB 的节点结构体(FDT_BEGIN_NODE / FDT_END_NODE),通过偏移量跳转实现零拷贝访问:

int __init of_scan_flat_dt(int (*it)(unsigned long node,
                                     const char *uname,
                                     int depth,
                                     void *data),
                           void *data)
{
    const void *blob = initial_boot_params;
    const char *pathp;
    int offset, rc = 0, depth = -1;

    for (offset = fdt_off_dt_struct(blob);
         offset < fdt_totalsize(blob);
         offset += fdt_next_tag(blob, offset, &offset)) {
        // 解析节点标签,提取路径和深度
        // 调用回调函数 it()
        rc = it(offset, pathp, depth, data);
        if (rc)
            break;
    }
    return rc;
}
性能陷阱: 扁平扫描阶段不能使用 kmalloc 或 __va,所有操作必须基于物理地址偏移。回调函数内应避免复杂计算,否则会拖慢整个启动流程。

4.2.3 典型回调:提取内存与chosen节点

内核通过注册多个回调完成不同任务:

回调函数 扫描目标 提取内容
early_init_dt_scan_chosen() /chosen 节点 bootargs、stdout-path、initrd-start/end
early_init_dt_scan_root() 根节点 #address-cells、#size-cells
early_init_dt_scan_memory() /memory 节点 reg 属性中的物理内存范围
of_scan_flat_dt() 回调分发机制 DTB 扁平数据 FDT_BEGIN_NODE FDT_PROP FDT_BEGIN_NODE FDT_PROP FDT_END_NODE FDT_END_NODE 扫描引擎 偏移量遍历 + 标签解析 early_init_dt_ scan_chosen() early_init_dt_ scan_root() early_init_dt_ scan_memory() 输出 • bootargs • initrd 地址 • address-cells • size-cells • 内存范围

4.3 unflatten_device_tree():树展开核心

当内存分配器(memblock)初始化完成后,unflatten_device_tree() 将扁平 DTB 展开为 struct device_node 树。这是设备树初始化中计算量最大的阶段。

4.3.1 两遍扫描策略

该函数采用“先计算大小,再分配内存”的两遍扫描策略,避免内存浪费:

  1. 第一遍扫描:遍历 DTB,计算需要分配的节点数和属性数,统计总内存需求
  2. 一次性分配:调用 memblock_alloc() 分配连续内存块
  3. 第二遍扫描:再次遍历 DTB,填充 device_node 和 property 结构体
void __init unflatten_device_tree(void)
{
    __unflatten_device_tree(initial_boot_params, NULL, &of_root,
                            early_init_dt_alloc_memory_arch, false);

    // 建立别名和索引
    of_alias_scan(early_init_dt_alloc_memory_arch);
}

4.3.2 数据结构转换

DTB 扁平表示 展开后内核结构 说明
FDT_BEGIN_NODE + 名称 struct device_node 每个节点对应一个 device_node,包含 full_name、parent、children、properties 链表
FDT_PROP + 数据 struct property 每个属性对应一个 property,包含 name、value、length、next 指针
FDT_END_NODE 子节点链表结束 通过 child 指针链接到父节点
性能优化点: 两遍扫描策略将内存分配次数从 O(n) 降低到 O(1),但代价是两次遍历 DTB。对于大型设备树(数千节点),DTB 遍历本身可能成为瓶颈。ARM64 平台实测显示,1000+ 节点的设备树,unflatten 耗时约 3-8ms。
unflatten_device_tree() 两遍扫描展开 扁平 DTB / { chosen { ... } memory { ... } cpus { cpu@0 { ... } cpu@1 { ... } } } 第一遍扫描 统计节点/属性数量 计算总内存大小 第二遍扫描 填充 device_node 填充 property 展开的 device_node 树 of_root (/) chosen memory cpus cpu@0 cpu@1

4.4 early_init_dt_scan_nodes():节点扫描与参数填充

树展开完成后,early_init_dt_scan_nodes() 对已展开的 device_node 树进行第二次扫描,将关键信息填充到内核全局变量中。

4.4.1 扫描内容

void __init early_init_dt_scan_nodes(void)
{
    /* 扫描 /chosen 节点,提取 bootargs 和 initrd */
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

    /* 扫描根节点,提取 #address-cells 和 #size-cells */
    of_scan_flat_dt(early_init_dt_scan_root, NULL);

    /* 扫描 /memory 节点,注册物理内存 */
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}
注意: 虽然此时树已展开,但 early_init_dt_scan_nodes() 仍然使用 of_scan_flat_dt() 进行扁平扫描,而非遍历 device_node 树。这是因为该函数设计为与架构无关,且扁平扫描更简单可靠。

4.4.2 关键输出变量

全局变量 来源节点 用途
boot_command_line /chosen/bootargs 内核启动参数
initrd_start / initrd_end /chosen/linux,initrd-start / linux,initrd-end 初始 RAM 磁盘地址
dt_root_addr_cells 根节点 #address-cells 地址单元大小(1 或 2)
dt_root_size_cells 根节点 #size-cells 大小单元大小(1 或 2)
memblock 中的内存块 /memory/reg 物理内存区域注册

4.5 性能优化要点总结

  • 减少扁平扫描次数:of_scan_flat_dt() 每次调用都会完整遍历 DTB,应合并回调以减少遍历次数
  • 两遍扫描 vs 动态分配:unflatten_device_tree() 的两遍扫描策略减少了内存碎片,但增加了 DTB 遍历开销
  • 缓存友好性:展开后的 device_node 树在内存中是连续分配的,有利于后续遍历的缓存命中
  • 早期内存分配器选择:early_init_dt_alloc_memory_arch() 通常使用 memblock_alloc(),其分配速度直接影响展开时间
常见问题: 如果 DTB 中 /memory 节点缺失或格式错误,early_init_dt_scan_memory() 将无法注册物理内存,导致系统在后续内存分配时崩溃。调试时应首先检查 DTB 中 memory 节点的 reg 属性是否合法。