Linux 内核底层机制:设备树(Device Tree)的数据结构与内存组织

文章目录

  • [Linux 内核底层机制:设备树(Device Tree)的数据结构与内存组织](#Linux 内核底层机制:设备树(Device Tree)的数据结构与内存组织)
  • 一、核心结论
  • [二、属性层级:单向链表(struct property)](#二、属性层级:单向链表(struct property))
    • [2.1 内核结构体定义](#2.1 内核结构体定义)
    • [2.2 内存组织示例](#2.2 内存组织示例)
    • [2.3 为什么采用单向链表?](#2.3 为什么采用单向链表?)
  • [三、节点层级:First Child / Next Sibling 多叉树(struct device_node)](#三、节点层级:First Child / Next Sibling 多叉树(struct device_node))
    • [3.1 内核结构体定义](#3.1 内核结构体定义)
    • [3.2 为什么不是子节点数组?](#3.2 为什么不是子节点数组?)
    • [3.3 内存组织示例](#3.3 内存组织示例)
    • [3.4 节点遍历方式](#3.4 节点遍历方式)
    • [3.5 Overlay 与 RCU](#3.5 Overlay 与 RCU)
  • 四、核心对比
  • 五、总结
  • [六、设备树的生命周期:从 DTB 到驱动绑定](#六、设备树的生命周期:从 DTB 到驱动绑定)
      • [6.1 阶段一:获取基础启动信息 ------ `early_init_dt_scan()`](#6.1 阶段一:获取基础启动信息 —— early_init_dt_scan())
      • [6.2 阶段二:构建内存设备树 ------ `unflatten_device_tree()`](#6.2 阶段二:构建内存设备树 —— unflatten_device_tree())
      • [6.3 阶段三:生成总线设备触发驱动匹配 ------ `of_platform_populate()`](#6.3 阶段三:生成总线设备触发驱动匹配 —— of_platform_populate())
      • [6.4 核心流程总结图](#6.4 核心流程总结图)

Linux 内核底层机制:设备树(Device Tree)的数据结构与内存组织

Linux 内核中的设备树(Device Tree,简称 DT)用于描述硬件平台信息。内核启动时,会将设备树二进制文件(DTB)解析为一系列 struct device_nodestruct property 对象,并组织成一棵完整的设备树,供驱动程序查询和匹配。

设备树在内存中的组织主要分为两个层级:

  • 节点(Device Node):表示一个硬件设备或总线。
  • 属性(Property) :表示节点内部的配置项,例如 compatibleregstatus 等。

由于两者承担的职责不同,因此 Linux 内核采用了不同的数据结构进行组织。


一、核心结论

Linux 内核针对设备树的两个层级采用了不同的数据结构:

  • 设备树属性(Property) :采用单向链表组织。
  • 设备树节点(Device Node) :采用 First Child / Next Sibling(左孩子-右兄弟)表示法构建一棵多叉树。

对于现代 Linux(5.x/6.x)内核,节点遍历主要依赖树形结构和遍历接口(Iterator);而早期 Linux 内核(4.x 及以前)曾额外维护一条 allnext 全局单向链表用于快速遍历所有节点。


二、属性层级:单向链表(struct property)

设备树中的属性(Property)表示节点的具体配置项,例如:

  • compatible
  • reg
  • status
  • interrupts
  • max-brightness-levels

每个节点都拥有自己的属性链表。


2.1 内核结构体定义

源码位置:

text 复制代码
include/linux/of.h
c 复制代码
struct property {
    char *name;                 /* 属性名称 */
    int length;                 /* 属性值长度(字节) */
    void *value;                /* 属性值 */

    struct property *next;      /* 下一个属性 */

    unsigned long _flags;
    unsigned int unique_id;
};

其中真正用于组织属性链表的是:

c 复制代码
struct property *next;

因此,一个节点内部所有属性都是通过单向链表连接起来的。


2.2 内存组织示例

例如下面的设备树:

dts 复制代码
&i2c1 {
    status = "okay";

    gp7101@58 {
        compatible = "gp7101-backlight";
        reg = <0x58>;
        max-brightness-levels = <255>;
        default-brightness-level = <100>;
    };
};

解析后的属性组织如下:

复制代码
properties
    │
    ▼
+------------------------------+
| compatible                   |
| value = "gp7101-backlight"   |
| next ----------------------┐ |
+----------------------------│-+
                             ▼
+------------------------------+
| reg                          |
| value = <0x58>               |
| next ----------------------┐ |
+----------------------------│-+
                             ▼
+------------------------------+
| max-brightness-levels        |
| value = <255>                |
| next ----------------------┐ |
+----------------------------│-+
                             ▼
+------------------------------+
| default-brightness-level     |
| value = <100>                |
| next = NULL                  |
+------------------------------+

整个节点内部形成一条单向链表。


2.3 为什么采用单向链表?

设备树属性具有以下特点:

  • 每个节点通常只有几个到十几个属性;
  • 驱动程序主要根据属性名顺序查找对应属性;
  • 属性数量较少,不需要复杂的数据结构;
  • 大多数情况下属性在设备树展开后保持稳定,驱动主要以只读方式访问。

因此,Linux 采用最简单的单向链表即可满足需求,实现简单且内存开销较低。

例如:

c 复制代码
of_property_read_u32(np, "reg", &value);

其内部最终就是遍历属性链表,根据 name 找到对应的 Property。

需要说明的是,对于支持 Device Tree Overlay 的系统,属性也可能在运行时动态增加或删除,因此内核保留了 deadprops 等机制管理失效属性,并通过 RCU 保证并发访问的安全性。


三、节点层级:First Child / Next Sibling 多叉树(struct device_node)

设备树中的节点(Device Node)表示一个硬件设备。

例如:

dts 复制代码
i2c@40000000
serial@50000000
gp7101@58
gpio@10000000

每个节点对应一个:

c 复制代码
struct device_node

对象。


3.1 内核结构体定义

源码位置:

text 复制代码
include/linux/of.h
c 复制代码
struct device_node {
    const char *name;
    phandle phandle;
    const char *full_name;

    struct fwnode_handle fwnode;

    struct property *properties;
    struct property *deadprops;

    struct device_node *parent;
    struct device_node *child;
    struct device_node *sibling;

#if defined(CONFIG_OF_KOBJ)
    struct kobject kobj;
#endif

    unsigned long _flags;
    void *data;
};

描述树结构的关键成员只有三个:

c 复制代码
parent
child
sibling

3.2 为什么不是子节点数组?

Linux 并没有为每个节点维护:

c 复制代码
children[100]

这样的子节点数组。

原因是:

不同节点拥有的子设备数量完全不同。

例如:

复制代码
root
 ├── cpu
 ├── memory
 ├── gpio
 ├── uart
 ├── spi
 ├── i2c
 ├── usb
 └── ...

有的节点只有一个子节点,

有的可能几十个。

如果使用数组:

  • 需要预留大量空间;
  • 插入删除效率低;
  • 内存浪费严重。

因此 Linux 采用了经典的数据结构:

First Child / Next Sibling(左孩子-右兄弟)表示法

每个节点只需要三个指针:

复制代码
parent
child
sibling

即可表示任意规模的多叉树。


3.3 内存组织示例

例如:

dts 复制代码
/ {
    i2c@40000000 {

        gp7101@58 {
        };

        sensor@60 {
        };
    };

    uart@50000000 {
    };
};

解析后形成如下关系:

复制代码
[ root ]
    │
    │ child (大儿子)
    ▼
 [ i2c ] ───────── sibling (二弟) ────────▶ [ uart ] ── sibling ──▶ NULL
    │                                          │
    │ child (大儿子)                            │ child (无子节点 无子节点!!!)
    ▼                                          ▼
[ gp7101 ] ─────── sibling (二弟) ────▶ [ sensor ] ── sibling ──▶ NULL
    │                                          │
    │ child                                    │ child
    ▼                                          ▼
   NULL                                       NULL

对应关系:

复制代码
root
 ├── i2c
 │    ├── gp7101
 │    └── sensor
 │
 └── uart

整个树只依赖:

复制代码
parent
child
sibling

三个指针即可表示。


3.4 节点遍历方式

现代 Linux 内核(5.x/6.x)主要基于树形结构进行遍历,例如:

  • 遍历子节点;
  • 查找父节点;
  • 深度优先遍历(DFS);
  • 使用设备树遍历接口(Iterator)。

例如:

c 复制代码
for_each_child_of_node(parent, child)

就是沿着:

复制代码
child → sibling → sibling

依次访问所有子节点。

需要注意的是,**早期 Linux 内核(4.x 及以前)**曾在 struct device_node 中维护一个 allnext 指针,将所有节点串成一条全局单向链表,方便快速遍历整个设备树。

而在现代 Linux 内核中,allnext 成员已经移除,节点遍历主要依赖树形结构和专门的遍历接口,不再维护独立的全局节点链表。


3.5 Overlay 与 RCU

现代 Linux 支持 Device Tree Overlay,可以在系统运行过程中动态增加或删除节点及属性。

为了保证遍历期间的数据一致性,设备树相关数据结构采用 RCU(Read-Copy-Update) 机制进行保护:

  • 读端通常无需加锁即可访问设备树;
  • 写端修改节点或属性时,会在 RCU 机制下更新指针;
  • 待所有读者退出临界区后,再释放旧数据。

因此,无论节点还是属性,在支持 Overlay 的场景下,都能够实现安全的并发访问。


四、核心对比

对比项 Property Device Node
对应结构体 struct property struct device_node
数据结构 单向链表 First Child / Next Sibling(左孩子-右兄弟)表示的多叉树
关键指针 next parentchildsibling
典型访问方式 顺序遍历属性链表 遍历父子关系、兄弟关系或使用 Iterator
生命周期 基本稳定,可被 Overlay 修改 基本稳定,可被 Overlay 动态增删
并发保护 Overlay 修改时由 RCU 保护 RCU
设计目标 保存节点配置项 表达硬件拓扑关系

五、总结

Linux 内核根据设备树不同层级的特点,采用了不同的数据结构:

  • **属性(Property)**采用单向链表组织。由于每个节点的属性数量较少,驱动主要按照属性名顺序查找,因此单向链表实现简单、内存占用低,能够满足绝大多数访问需求。
  • **节点(Device Node)**采用 First Child / Next Sibling(左孩子-右兄弟)表示法 构建多叉树,仅依靠 parentchildsibling 三个指针即可描述任意复杂的硬件拓扑关系。

此外,现代 Linux 内核支持 Device Tree Overlay,允许设备树在运行时动态增删节点和属性。为了保证遍历期间的并发安全,内核使用 RCU(Read-Copy-Update) 对相关数据结构进行保护,使读操作无需加锁,同时保证写操作的安全性。

理解设备树节点与属性在内存中的组织方式,有助于深入理解 Linux 内核中设备树的解析过程、驱动匹配机制以及设备枚举流程,也能够帮助开发者更准确地使用设备树相关 API 进行驱动开发。

六、设备树的生命周期:从 DTB 到驱动绑定

前面我们了解了设备树在内存中的数据结构(单向链表与多叉树),那么内核是如何一步步将编译好的二进制文件(DTB)转化为这些数据结构,并最终和驱动程序绑定起来的呢?

整个过程主要经历三个核心阶段:早期扫描、解构设备树、以及平台设备转换。这三步对应了三个至关重要的内核函数:

6.1 阶段一:获取基础启动信息 ------ early_init_dt_scan()

在内核启动极早期(setup_arch() 阶段),内存管理子系统(如 buddy system)还未建立。此时内核无法大规模分配内存来构建树形结构,只能对 DTB 进行原地扫描

  • 执行动作: 内核直接读取物理内存中扁平的 DTB 数据。
  • 核心任务:
  1. 校验 DTB 的 Magic Number,确认设备树合法。
  2. 扫描 /chosen 节点,获取内核启动参数(bootargs)和 initrd 地址。
  3. 扫描根节点,获取 #address-cells#size-cells
  4. 扫描 /memory 节点,获取系统物理内存的基地址和大小,从而初始化早期内存分配器(memblock)。
  • 状态总结: 此时设备树依然是扁平的二进制数据(FDT),并没有生成 struct device_node

6.2 阶段二:构建内存设备树 ------ unflatten_device_tree()

当 memblock 早期内存分配器就绪后,内核终于有了分配内存的能力。此时,内核会将扁平的 DTB "解压"成我们在第三节提到的 First Child / Next Sibling 多叉树

  • 执行动作: 解析 DTB,动态分配内存,实例化节点与属性。

  • 核心任务:

  • 通常包含两轮遍历(Pass 1 & Pass 2):

  • 第一轮: 快速遍历一遍 DTB,计算出所有的 device_nodeproperty 需要占用多少总内存空间,并一次性分配。

  • 第二轮: 真正开始解析数据,填充各个 struct device_nodestruct property 的指针成员(即前面提到的 parentchildsiblingnext),建立完整的树形和链表关系。

  • 状态总结: 执行完毕后,全局指针 of_root 指向设备树的根节点,此时 逻辑设备树 已经在内存中完全建立。

6.3 阶段三:生成总线设备触发驱动匹配 ------ of_platform_populate()

设备树只是提供硬件信息的"数据库",要让驱动程序跑起来,内核需要将这些信息转化为 Linux 设备模型(Device Model)认识的 struct platform_device 对象。

  • 执行动作: 遍历 of_root 设备树,将符合条件的节点转换为平台设备(Platform Device)并注册到内核的 platform 总线上。

  • 核心任务:

  • 遍历根节点下的子节点,以及带有 simple-bus 等兼容属性的节点。

  • 为这些节点分配并初始化 struct platform_device

  • 将设备树节点指针(np)绑定到 platform_device.dev.of_node 上。

  • 调用 device_register() 将设备挂载到 platform 总线上。

  • 状态总结: 一旦设备挂载到总线上,就会触发总线的 match 机制。总线会对比设备的 compatible 属性与驱动程序的 of_match_table。一旦匹配成功,就会调用驱动程序的 probe() 函数,完成最终的驱动绑定。


6.4 核心流程总结图

为了更直观地理解,你可以用下面这张图来记忆整个生命周期:

text 复制代码
+-------------------+
|     DTB 文件      | (Bootloader 传递到内存)
+--------+----------+
         │
         ▼
[ early_init_dt_scan() ] 
  提取 memory、chosen 参数,初始化早期内存
         │
         ▼
[ unflatten_device_tree() ]
  构建 struct device_node (左孩子-右兄弟) 
  构建 struct property (单向链表)
         │
         ▼
[ of_platform_populate() ]
  遍历树,实例化 struct platform_device,注册到总线
         │
         ▼
+-------------------+
|  Platform Bus匹配 | (根据 compatible 属性)
+--------+----------+
         │
         ▼
[ 驱动 probe() 运行 ] 
  通过 dev->of_node 再次读取特定 Property,初始化硬件

面试连招提醒:

大厂面试官如果在考察这部分时,通常会追问:"是不是所有的设备树节点都会被转换成 platform_device "

你的回答应该是:不是 。比如 I2C 和 SPI 总线下的子设备节点,是由对应的 I2C/SPI 总线控制器驱动在自身的 probe() 函数中解析并注册为 i2c_clientspi_device 的,它们挂在对应的 I2C/SPI 总线上,而不是直接挂在顶层的 Platform 总线上。

相关推荐
其实防守也摸鱼1 小时前
前端应用的离线暂停更新策略:构建稳定可靠的渐进式部署方案
服务器·前端·数据库·学习·ai·命令行·linux系统
Android系统攻城狮1 小时前
Linux PipeWire深度解析之pw_context_add_spa_lib调用流程与实战(二十九)
linux·运维·服务器·音频进阶·pipewire音频进阶
太平洋月光1 小时前
Antv G2中自定义技巧📊
前端·数据可视化
小七在进步1 小时前
数据结构:树的遍历
数据结构
shmily麻瓜小菜鸡1 小时前
前端“伪防盗链”方案
前端·javascript·vue.js·bootstrap·echarts
Dovis(誓平步青云)1 小时前
《如何在CentOS 7中添加Plex官方软件源:解决文件磁盘难管理难题》
linux·运维·服务器·后端·生成对抗网络·centos
mayaairi1 小时前
JS数组完全指南(含十大操作详解)
开发语言·前端·javascript
j7~1 小时前
【数据结构初阶】队列的实现(链式队列 + 循环队列)--详解
c语言·开发语言·数据结构·学习·队列·queue·c\c++
草莓熊Lotso1 小时前
【Linux网络】深入理解Linux IO多路复用:select服务器完善、内核原理与poll实战
linux·运维·服务器·c语言·网络·c++
king_linlin2 小时前
算法基础——算法复杂度
c语言·开发语言·数据结构·算法