当前位置: 首页 > news >正文

Linux platform驱动匹配表与设备树解析流程

Linux platform驱动匹配表与设备树解析流程

平台总线的核心匹配入口在 `drivers/base/platform.c` 的 `platform_match` 函数。该函数是 `struct bus_type platform_bus_type` 的 `.match` 回调,每次新设备注册或新驱动注册时由设备核心层调用。其返回值为非零表示匹配成功。

```c
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);

/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;

/* Then try ACPI */
if (acpi_driver_match_device(dev, drv))
return 1;

/* Then try the ID table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;

/* fall-back to driver name match */
return strcmp(pdev->name, drv->name) == 0;
}
```

代码中匹配顺序是固定的:OF 匹配优先、ACPI 其次、ID table 第三、最后是 name 回退。这个顺序有明确的设计意图:设备树的 compatible 字符串是最准确的设备标识,而 name 回退是为了兼容旧版驱动和没有设备树的平台。实际系统中,一旦 `CONFIG_OF` 被启用但 `drv->of_match_table` 为 NULL,`of_driver_match_device` 会快速返回 false,不会造成额外的查找开销。这里存在一个容易被忽略的边界点:当 `dev->of_node` 非空但驱动没有提供 `of_match_table`,`of_driver_match_device` 返回 0,但 `acpi_driver_match_device` 也返回 0(非 ACPI 平台),此时如果 `pdrv->id_table` 也为 NULL,则降级到 name 匹配。这意味着一个带设备节点的 platform_device 可能因为驱动的 name 字符串和 platform_device.name 一致而绑定,即便 devicetree 中完全没有该驱动的 compatible 条目——这是合法的,但容易导致调试困惑。

`of_driver_match_device` 在 `drivers/of/device.c` 中实现:

```c
int of_driver_match_device(struct device *dev, const struct device_driver *drv)
{
if (!drv->of_match_table)
return 0;
return of_match_device(drv->of_match_table, dev) != NULL;
}
```

其核心是遍历 `of_match_table`,对每个条目调用 `of_match_node`。`of_match_table` 是一张以 `{}}` 结尾的 `struct of_device_id` 数组:

```c
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
```

name、type、compatible 三个字段中,实际匹配中最关键的正是 compatible 字符串。在编写驱动时常见的问题是:`struct of_device_id` 的 `compatible` 数组长度被定义为 128 字节,若设备树中 compatible 字符串长度超过这个限制,匹配会静默失败,且内核不会给出警告——`__of_device_is_compatible` 中直接调用 `strncmp`,截断的部分不会被比较。

`of_match_node` 遍历整个 `matches` 数组,对每个条目调用 `__of_device_is_compatible`:

```c
struct of_device_id *of_match_node(struct of_device_id *matches,
const struct device_node *node)
{
if (!matches)
return NULL;
while (matches->name[0] || matches->type[0] || matches->compatible[0]) {
int match = 1;
if (matches->name[0])
match &= node->name && !strcmp(matches->name, node->name);
if (matches->type[0])
match &= node->type && !strcmp(matches->type, node->type);
if (matches->compatible[0])
match &= __of_device_is_compatible(node, matches->compatible);
if (match)
return matches;
matches++;
}
return NULL;
}
```

这里匹配逻辑是 AND 语义:只有 name、type、compatible 中所有非空的字段都匹配时才算匹配成功。但实践中几乎所有的设备树匹配场景只使用 compatible 字段,name 和 type 基本留空。`__of_device_is_compatible` 的关键实现在 `drivers/of/base.c`:

```c
int __of_device_is_compatible(const struct device_node *device,
const char *compat)
{
const char *cp;
int cplen, l;

cp = __of_get_property(device, "compatible", &cplen);
if (cp == NULL)
return 0;
while (cplen > 0) {
if (of_compat_cmp(cp, compat, strlen(compat)) == 0)
return 1;
l = strlen(cp) + 1;
cp += l;
cplen -= l;
}
return 0;
}
```

compatible 属性是多个以 `\0` 分隔的字符串的串联,匹配时逐个比较。匹配算法是前缀匹配还是精确匹配?关键在 `of_compat_cmp` 的定义。当开启了 `CONFIG_OF_DYNAMIC` 时,它被定义为 `strncmp`;未开启时是 `strcmp`。这一点极易出错:在开启了 `CONFIG_OF_DYNAMIC` 的内核中,`strncmp(cp, compat, strlen(compat))` 意味着如果驱动的 compatible 是 `"vendor,device-a"`,而 DT 中的 compatible 字符串是 `"vendor,device-awesome"`,则 `strncmp` 比较前 `strlen("vendor,device-a")` 个字节后返回 0,误判为匹配成功。这是一个经典的缺陷,`commit b6488f8d5e57 ("of: fix size when dts override phandle")` 修复了该问题,将 `CONFIG_OF_DYNAMIC` 分支改为也使用 `strcmp`。

从性能角度看,`__of_device_is_compatible` 每次匹配时都需要从根节点开始重新遍历 `compatible` 属性中的所有字符串。对于设备树深层嵌套节点配合大量 compatible 条目的场景,每次 probe 触发的匹配都需要 O(m * n) 的比较复杂度(m 是驱动表中 of_device_id 条目数,n 是该节点 compatible 属性的字符串数)。驱动核心层没有对匹配结果做缓存——每次注册设备或驱动时,都会对所有已注册的驱动或设备重新遍历匹配。在系统启动的热路径上,如果存在大量 platform 设备和驱动,`platform_match` 会成为线性扫描的瓶颈。

关于竞态条件:`platform_match` 本身是纯读操作,不涉及加锁。然而,`dev->of_node` 在设备生命周期中可能出现变化。在某些使用 `OF_DYNAMIC` 和 DT overlay 的系统中,Overlay 被卸载后会修改 `dev->of_node` 指针或释放 `device_node` 的内存,而同一时刻仍有驱动正在匹配或 probe。内核通过 `of_node_get`/`of_node_put` 引用计数来保护 `device_node` 的生命周期,但 `platform_match` 在被调用时并不持有该引用计数。调用路径是 `driver_attach` / `bus_probe_device` -> `device_initial_probe` -> `really_probe`,在 `really_probe` 中会在 probe 前后对 `dev->of_node` 调用 `of_node_get`/`of_node_put`,但匹配阶段的引用保护是通过调用者处的 `get_device` 隐式保证的——只要 device 本身未被释放,其 `of_node` 引用的设备节点就不能被释放(因为设备核心在初始化时会对 `dev->of_node` 调用 `of_node_get`)。但如果 DT overlay 在被卸载时更改了某现有设备的 `of_node` 指向(如 `of_detach_node` 后再 `of_attach_node`),匹配时的读取可能不一致。

接下来看 ID table 匹配。`platform_match_id` 遍历 `struct platform_device_id` 数组:

```c
static const struct platform_device_id *platform_match_id(
const struct platform_device_id *id,
struct platform_device *pdev)
{
while (id->name[0]) {
if (strcmp(pdev->name, id->name) == 0) {
pdev->id_entry = id;
return id;
}
id++;
}
return NULL;
}
```

注意 `pdev->id_entry` 在此处被设置,这一设置没有使用任何内存屏障。在 SMP 系统中,如果另一个 CPU 在匹配完成后通过 `platform_get_device_id` 读取 `id_entry`,可能存在读取到陈旧值的问题。不过实践中 `pdev->id_entry` 只会在 probe 路径上被读取,且 probe 和匹配发生在同一线程上下文,因此不会存在数据竞争。

关于 ID table 匹配的一个隐含行为是:`id->name` 与 `pdev->name` 做严格字符串比较。而 `pdev->name` 来源于 `platform_device.name`,该值在设备注册时由 `platform_device_register_full` 设置,可能来源于设备树的 `"of_modalias_node"` 生成的别名。`of_device_get_modalias`(位于 `drivers/of/device.c`)截取 compatible 字符串的第一个条目的 vendor 之后的部分作为 modalias,然后 `platform_device` 的内部 name 会根据这个值来设定。这意味着 ID table 匹配实际上间接依赖了设备树中的 compatible 字符串内容,只是经过了 modalias 的转换。

对于 ACPI 匹配分支,`acpi_driver_match_device` 在非 ACPI 的 DT-only 系统上有 `#define acpi_driver_match_device(...) (0)` 的空操作优化,编译器会将其优化掉,内联后为零开销。

最后,name 回退匹配是直接的字符串比较:

```c
return strcmp(pdev->name, drv->name) == 0;
```

这是最原始的匹配方式,它基于 `platform_driver.driver.name` 与 `platform_device.name` 做匹配。但这里有一个鲜为人知的细节:当 `platform_device.name` 在设备注册时通过 `platform_device_alloc` 分配时,name 是被 `kstrdup` 拷贝的。而 `platform_driver.name` 通常指向一个静态字符串。如果驱动被卸载后重新加载(模块卸载再加载),`pdev->name` 仍然指向之前注册时拷贝的字符串,匹配依然正常。但如果设备本身也经历了重注册(例如热插拔或 DT overlay 操作),name 字符串的生命周期就需要额外关注——`kfree` 后的 name 指针访问是 use-after-free。不过标准的热移除流程 `platform_device_unregister` 会释放 name,而重注册时会重新分配,不会出现悬空指针,除非有人绕过 API 直接修改 name 字段,而这在内核审查中是被禁止的。

关于 `of_match_table` 的存放位置,它位于 `struct device_driver` 中:

```c
struct device_driver {
const char *name;
struct bus_type *bus;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
...
};
```

所有匹配表都是 `const` 修饰的只读数据,共同驻留在 `.init.rodata` 或 `.rodata` 段中。在 `CONFIG_OF` 被禁用时,`of_match_table` 字段仍然存在,但 `of_driver_match_device` 被编译为 `(0)` 的内联空函数,不会访问该字段。这里有个 ABI 兼容性微妙的点:`of_match_table` 在结构体布局中与 `acpi_match_table` 顺序固定,且处于 `struct device_driver` 的末尾附近,任何在中间插入字段的改动都会影响所有驱动——这也是为什么 driver core 的结构体布局非常稳定,极少增加或重排字段。

值得注意的是 `module_platform_driver` 宏展开后的匹配表注册时机。驱动注册通过 `platform_driver_register` -> `driver_register` -> `bus_add_driver` 完成。`bus_add_driver` 在驱动加入总线链表后会立即对该总线上的所有设备执行 `driver_attach`,这意味着在 `module_init` 函数的 `platform_driver_register` 被调用时,可能已经存在成百上千个已注册的 platform_device,每个都需要遍历匹配一遍。如果系统中有大量设备和驱动,这会导致启动阶段的 O(n*m) 匹配风暴。`deferred_probe` 机制可以缓解此问题:当匹配成功但 probe 需要的资源不可用时,设备被加入 `deferred_probe_list`,后续在资源可用时触发重试,但匹配过程在每次重试时都会被重新执行。

设备树覆盖(overlay)场景下,`of_overlay_apply` 触发的 `of_platform_populate` 会导致新的平台设备注册,进而触发新一轮的匹配。如果 overlay 从属的设备树节点 compatible 条目与多个驱动匹配,先注册的驱动会获得绑定机会。这里存在竞态:两个内核模块同时加载,各自的驱动注册在不同 CPU 上,而 overlay 设备注册也在进行中,最终绑定结果取决于总线锁 `bus->p->klist_devices.k_lock` 的内部自旋锁顺序,该顺序由链表遍历先后决定而不可预测。

对于 `platform_match` 的调用栈跟踪,可以通过:

```bash
echo 'p:platform_match platform_match dev=+0($arg1):u32 drv=+0($arg2):u32' > /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/platform_match/enable
cat /sys/kernel/debug/tracing/trace_pipe
```

在启动阶段观察哪些设备与哪些驱动在尝试匹配,对调试 probe 顺序问题非常有效。

最后,PCI/USB 等其他总线有枚举机制,而 platform 总线的匹配完全依赖匹配表的线性扫描。因此在拥有大量平台设备的系统中(如 SoC 全功能板级支持),匹配过程可能占用启动时间的显著比例。`initcall_debug` 参数可以量化每个驱动注册耗时,但没有内置机制统计 `platform_match` 本身的 CPU 开销。通过 ftrace 设置 `function_graph` tracer 并过滤 `platform_match` 可以精确测量延迟。一个典型的优化是确保 `of_match_table` 中条目按匹配频度排序——最可能匹配的放在最前面,因为 `of_match_node` 的第一命中即返回,不会继续遍历后续条目。

http://www.jsqmd.com/news/1010553/

相关文章:

  • 乳腺癌生存预测的多模态机器学习框架解析与应用
  • DownKyi:5步掌握B站视频下载的终极免费方案
  • 碧蓝航线Alas自动化脚本:终极7x24小时全自动游戏管理解决方案
  • 从MC1496到三极管:手把手教你用频谱分析仪对比两种混频器的真实性能
  • 2026年茂名市黄金回收白银回收铂金回收彩金回收测评+本地人气靠前五家靠谱门店介绍推荐及联系方式 - 前途无量YY
  • 从命令行到桌面应用:SillyTavern AI聊天界面桌面化终极指南
  • 从nnU-Net到nnDetection:医学影像AI自动化框架的‘双子星’该怎么选?
  • CefFlashBrowser:如何让Flash经典内容在现代系统中重获新生
  • 2026年5月查重急救|论文AIGC率高别慌,学姐亲测6款降AI工具(附免费名单) - 降AI实验室
  • ClickHouse系统日志TTL配置全攻略:从config.xml修改到表结构变更,守护你的磁盘空间
  • 极端样本不均衡的系统性解决方案:TensorFlow/LightGBM/CatBoost实战
  • 数据清洗不是预处理,而是决定模型成败的核心工程
  • Pandas合并三函数:merge、join、concat场景化选型指南
  • 终极iOS激活锁绕过指南:applera1n工具完整使用教程
  • 时序数据库底层实战:手写极简TSDB,时间分区压缩、降采样查询,适配监控指标_IoT海量打点
  • 从SLC到QLC:一文看懂NAND Flash类型如何‘偷走’你的SSD寿命和钱包
  • 别再踩坑了!Windows10下用VS2019配置EDKII开发环境的完整避坑指南
  • 手把手教你用U盘给创维E900V20C刷当贝桌面(Hi3798MV200芯片保姆级教程)
  • OpenCode:面向VS Code的本地化代码补全引擎
  • 如何快速上手SillyTavern:打造专属AI角色的终极完整指南
  • 密钥派生选HMAC、CMAC还是KMAC?从NIST SP800-108更新看企业安全选型避坑指南
  • 从智能家居到工业物联网:深入聊聊802.11ah(Wi-Fi HaLow)到底能做什么
  • 嵌入式图像处理实战:为ARM开发板(如树莓派)交叉编译libjpeg库并集成到你的C项目
  • 什么品牌学习机好?2026业内公认好用款一文看懂
  • 梯度下降实战指南:从原理到调参排障的工程化落地
  • DeepSeek安全合规应用指南:微调、提示工程与RAG实践
  • 地表温度数据怎么选?一篇讲清MODIS、GLASS、Landsat三大LST产品区别与实战场景
  • 终极指南:使用Legacy iOS Kit让旧iPhone/iPad重获新生
  • 别再只盯着VN1640了!从VN1610到VN1670,手把手教你选对Vector CANoe硬件(附接线图)
  • 别再纠结VMware还是WSL了!根据你的真实开发场景,我帮你选好了(附WSL2内存优化配置)