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() |
扫描已展开的树,填充内核启动参数 | 树展开完成后立即执行 |
性能视角: 扁平扫描阶段无法动态分配内存,所有操作基于偏移量和指针算术;树展开阶段才首次调用内存分配,因此其效率直接影响系统启动时间。
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 属性中的物理内存范围 |
4.3 unflatten_device_tree():树展开核心
当内存分配器(memblock)初始化完成后,unflatten_device_tree() 将扁平 DTB 展开为 struct device_node 树。这是设备树初始化中计算量最大的阶段。
4.3.1 两遍扫描策略
该函数采用“先计算大小,再分配内存”的两遍扫描策略,避免内存浪费:
- 第一遍扫描:遍历 DTB,计算需要分配的节点数和属性数,统计总内存需求
- 一次性分配:调用
memblock_alloc()分配连续内存块 - 第二遍扫描:再次遍历 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。
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 属性是否合法。