☰
Linux设备模型全解析:从kobject到platform总线的驱动匹配机制
2026/9/26 10:56:04 网站建设 项目流程

1. 设备模型要解决的根本问题:从一次驱动加载失败说起

我最早接触Linux设备模型,不是从教科书开始,而是被一次实际的驱动问题逼着去研究的。当时我在一块ARM板子上调试一个外挂的ADC芯片驱动,模块编译好,insmod也执行成功,/dev目录下却看不到任何设备节点。更诡异的是,/sys/bus/i2c/devices/下也找不到对应的设备目录。我反复检查注册函数,确认地址没错,id_table也没填错,可设备就是"不出现"。

这种体验,凡是搞过内核驱动的人应该都不陌生。说白了:你写的驱动能不能和硬件对上,靠的不是你有多熟悉芯片手册,而是你懂不懂内核那套设备模型——它决定了驱动如何被找到、如何被匹配、如何被唤醒,甚至如何被用户空间的udev规则识别。

设备模型并不是一个单独的子系统,而是渗透在整个内核驱动框架里的隐形脉络。它从2.6内核开始被引入,核心代码在drivers/base/目录下,包含kobject、kset、bus、device、driver、class、power管理等一系列机制。它的价值简单概括就是:用一套统一的对象模型,把系统里所有硬件资源、驱动代码、总线关系、电源状态全管理起来。

这个模型对三类人最有用:

  • 做嵌入式驱动开发的人:必须明白platform设备和设备树是怎么通过设备模型绑定的,不然probe函数永远不执行。
  • 做内核学习和面试准备的人:设备模型是内核面试的常客,kobject/kset、bus/driver/device的关系是绕不开的考点。
  • 做系统调试和故障排查的人:看懂/sys目录的目录结构,就是看懂设备模型最好的入口。

这篇内容,我会从最底层的kobject讲起,一路往上走到platform总线,中间穿插我自己遇到的坑和排查链路。不晒源码刷篇幅,重点把数据结构之间的关系和匹配流程讲透,这些才是"相见恨晚"的部分。

2. kobject与kset:设备模型的底层细胞

2.1 kobject:一个"隐形"的公共基类

如果你读过内核源码,一定会发现一个现象:struct device、struct device_driver、struct bus_type这些结构体的第一个成员,全是struct kobject kobj或者struct kobject *kobj。这并不是巧合,而是刻意设计的——kobject是整个设备模型的公共基类。

它的作用可以拆成三块:

  1. 引用计数管理:通过内嵌的kref结构管理对象的生命周期,解决"谁在用这个设备对象、能不能卸载"的问题。
  2. 层级关系表达:每个kobject都可以有parent,形成父子树状结构,对应sysfs里的目录层级。
  3. sysfs接口映射:kobject一旦注册到内核,就会在sysfs里对应一个目录,kobject_create_and_add()执行完,你就能在/sys下看到对应的目录路径。

很多人初次看kobject会被绕晕,是因为它从来不单独使用,而是被嵌入到其他结构体中。比如你定义一个平台设备:

struct my_platform_device { struct platform_device pdev; void __iomem *base; int irq; };

platform_device内部有struct device dev,而dev里又有struct kobject kobj。整个对象体系是这样嵌套出来的。等到驱动框架需要操作你的设备时,它拿到的往往只是struct device *dev,但通过container_of宏就能得到完整的struct my_platform_device *:

struct my_platform_device *mydev = container_of(dev, struct my_platform_device, pdev.dev);

这是内核C语言里非常经典的"利用结构体成员的地址反推结构体首地址"的手法。不理解kobject怎么嵌入,后面看任何驱动框架代码都觉得是魔法。

2.2 kset:把同类对象"圈"起来

kobject解决的是"每个对象怎么表示",kset解决的是"一堆同类的对象应该放在哪个集合里"。

打个比方:kobject是居民,kset是小区。每个居民必须知道自己属于哪个小区,而小区负责管理里面所有居民。在内核代码里,bus_type就有两个kset成员:drivers_kset和devices_kset——分别装着挂在这条总线上的所有驱动和设备。这就是为什么你在/sys/bus/platform下能看到drivers/和devices/两个子目录。

kset还有一个重要的伴生机制:当一个kobject加入kset时,内核会发送uevent,告知用户空间"有新的设备对象出现了"。udev/mdev就是靠这个事件去创建设备节点的。所以,kobject和kset不只是内核内部的数据组织方式,它们直接决定了用户空间的/dev和/sys是怎么变的。

2.3 生命周期与release回调:最容易踩的坑

kobject的引用计数机制看起来简单,用起来容易踩坑。我之前写一个虚拟设备驱动,模块卸载时直接kobject_del()然后释放内存,结果内核立刻报错:

WARNING: CPU: 0 PID: xxx at lib/kobject.c:xxx kobject_release

原因是我没有设置release回调函数。kobject的规则是:引用计数降到0时,会调用kobj->release来完成最后的释放动作。如果你没提供release,内核就没办法安全释放这个对象,只能告警。

正确做法是在kobj_type里指定release函数:

static void my_kobj_release(struct kobject *kobj) { struct my_obj *obj = container_of(kobj, struct my_obj, kobj); kfree(obj); } static struct kobj_type my_kobj_type = { .release = my_kobj_release, .sysfs_ops = &kobj_sysfs_ops, };

经验总结:只要看到代码里创建了kobject,就必须想清楚三件事——谁持有引用、谁释放引用、release函数里做什么。引用计数失衡的典型表现就是模块卸载后系统异常、设备节点残留、或者直接kernel panic。

3. device、driver、bus三角关系:驱动匹配与probe背后的完整链路

3.1 三个结构体各自的"职责边界"

理解了kobject和kset后,设备模型的主干就清晰了。整个模型最核心的三张表就是struct device、struct device_driver、struct bus_type。

用一句话概括三角关系:bus是媒人,device是待嫁的姑娘,driver是来相亲的小伙子。bus维护两个kset(devices集合和drivers集合),然后不停地调用match函数,把条件合适的device和driver拉在一起;配对成功后就触发driver的probe方法,让驱动正式接管设备。

  • struct device:描述一个真实的硬件设备,包含名字、bus指针、电源管理状态、DMA参数、资源信息等。设备由内核设备模型管理,不代表驱动。
  • struct device_driver:描述一段驱动代码,包含name、bus指针、probe/remove/shutdown等回调函数。它不绑定具体硬件,只描述"我能驱动什么设备"。
  • struct bus_type:描述一类总线,最重要的成员是match回调,以及uevent、probe、remove这些辅助逻辑。

这三者一旦结合,对开发者来说最直观的收益就是:当你调用driver_register()时,不用你自己遍历设备列表,内核的bus子系统会自动帮你做匹配,并在合适时机调用probe。

3.2 匹配过程:platform_match的优先级顺序

驱动匹配是所有驱动开发者的必修课。不同总线的match逻辑不同,但最典型的是platform总线的platform_match函数,它的匹配顺序大致是:

  1. 强制兼容匹配:如果driver有id_table,遍历表中的每个platform_device_id,比较name字段是否与设备名一致。
  2. 设备树匹配:如果驱动里定义了of_match_table,内核会用设备树的compatible属性与表中的.compatible字符串比较。这是目前嵌入式Linux最常用的方式。
  3. ACPI匹配:在x86平台上使用的匹配路径,一般嵌入式开发用不到。
  4. 名称匹配:兜底逻辑,直接比较driver的name和设备名。

这个顺序决定了你写驱动时的"套路"。比如你设备树里写的compatible是"mycompany,myadc",驱动的of_match_table里必须严格对应:

static const struct of_device_id myadc_of_match[] = { { .compatible = "mycompany,myadc", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, myadc_of_match);

曾经有朋友问我:为什么设备树明明改了compatible,驱动就是不probe?我让他检查modinfo,发现alias字段不完整。MODULE_DEVICE_TABLE的作用不只是给内核匹配用,它还负责生成模块的别名信息。没有这行,即使设备树匹配逻辑对,模块自动加载也可能失败。

我还遇到过更隐蔽的情况:同一段compatible被两个驱动注册时,先注册的驱动会"抢"到设备。所以不要同时让两个驱动声明支持同一个compatible,否则设备模型只能把设备绑定给先来的那个。

3.3 probe调用时机与"驱动不存在"时的行为

很多人对设备模型有个误解,以为"有设备就一定会有驱动probe"。实际上match过程需要满足两个前提:设备在bus上、驱动在bus上。如果设备树里写了某个节点,但对应的驱动没编译进内核,那么bus上只有device没有driver,probe永远不会被调用,/sys/bus/platform/devices/下只有设备目录,没有driver符号链接。

调试这个问题时,最直接的办法是:

ls -l /sys/bus/platform/devices/ cat /sys/bus/platform/devices/<设备名>/uevent

如果uevent文件里能正常输出设备信息,说明设备已挂到总线。再查看:

ls /sys/bus/platform/drivers/

看对应驱动是否注册成功。两边都有但没绑定,就要回头查match逻辑了。

3.4 嵌套结构体与资源传递

device和driver匹配完成后,probe函数会被调用,参数是struct platform_device *pdev。到这里,设备模型的处理就算完成了,剩下的就是你自己的驱动业务逻辑了。但有几个常用API值得注意:

  • platform_get_resource():从设备资源里拿IO地址、中断号。
  • devm_platform_ioremap_resource():完成IO地址映射,并在驱动卸载时自动释放。
  • device_property_read_u32():读取设备树里的自定义属性。

devm_前缀的函数是设备模型管理内存的精华。它们的资源会在device对象释放时一并清理,这能有效防止驱动卸载时忘记free/ioremap导致的内存泄漏。

4. class、uevent与电源管理:设备模型的三个隐藏支柱

4.1 class机制:/dev节点是怎么"变"出来的

很多驱动教程会让你手动mknod创建设备节点,但在现代内核里,这个操作早就不需要人工介入了。靠的就是设备模型中的**class(类)**机制。

class的作用,是把功能相似的设备归为一类。比如网络设备属于net类,输入设备属于input类,块设备属于block类,字符设备属于tty类。当你调用class_create()创建类之后,每次device_create()注册一个设备,内核就会在/sys/class/<类名>/下建立对应目录,并生成一个dev文件,里面记录设备号。

这一步完成后,内核会发送uevent给用户空间,udev收到后读取dev文件,自动在/dev/下创建节点。所以驱动里必须有类似这样的代码:

static struct class *myadc_class; static struct device *myadc_device; myadc_class = class_create("myadc"); myadc_device = device_create(myadc_class, NULL, devno, NULL, "myadc%d", 0);

如果这一套没搞明白,就会出现我开头说的那种情况:模块加载成功,但/dev下没有节点,应用层完全无法访问设备。注意,device_create最后一个参数是设备节点名字,udev会优先使用这个名称而不是默认的dev内容。这也是很多新手困惑的:明明注册的设备名是myadc,/dev下却出现了myadc0。

4.2 uevent链路:从内核到udev的完整链路

uevent是设备模型与用户空间的"信使"。它的工作方式有两种:一种是通过netlink直接发送给用户空间监听进程;另一种是通过内核提供的uevent_helper调用用户空间的辅助工具(比如mdev)。

在嵌入式最小系统里,没有完整的udev,常见做法是用busybox的mdev。然后在启动脚本里执行:

echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s

mdev -s会扫描sysfs,为每个已注册的设备创建节点;设备热插拔时,内核通过hotplug机制调用mdev。不理解uevent,生产环境遇到"设备插入不产生节点"的情况,你会完全不知道往哪个方向排查。

排查路径一般这样走:

  1. 确认设备是否真的被内核识别:ls /sys/bus/usb/devices/。
  2. 确认设备模型是否生成了class信息:ls -l /sys/class/gpio/。
  3. 确认udev规则是否过滤掉了该设备:udevadm monitor观察内核事件是否到达用户空间。
  4. 如果事件都没到达,检查/proc/sys/kernel/hotplug是否被清空或指向错误。

4.3 电源管理:设备模型不只是"管设备"

设备模型中有一块很容易被忽略,但实际非常重要的分支:电源管理。Linux电源管理框架完全建立在设备模型之上。

每个struct device里都有一个dev_pm_ops相关的指针,驱动通过driver.pm成员注册suspend/resume回调。系统进入睡眠时,电源管理核心会按照设备树的拓扑顺序,依次调用每个已绑定设备的suspend回调;唤醒时反向调用resume。这一整套动作不需要你自己遍历设备列表,设备模型自动完成。

还有一个越来越常用的机制是runtime PM。它允许设备在空闲时自动进入低功耗状态,在需要时自动恢复。驱动中典型调用:

pm_runtime_enable(dev); pm_runtime_get_sync(dev); // 访问硬件 pm_runtime_put(dev);

我最初学设备模型时完全没在意电源管理,直到做量产项目的低功耗调试,才意识到:如果设备模型里的设备没有正确注册到电源域,suspend回调可能执行顺序反了,导致外设电源时序错乱。后来写任何驱动,我都习惯性地把pm信息填完整,哪怕是简单的pm_runtime_enable也加上。

5. platform总线与设备树:嵌入式开发最常打交道的B面

5.1 为什么存在platform总线

内核管理的设备千奇百怪,有些设备挂在USB总线上,有些挂在PCIe总线上,这些总线都有真实的硬件协议。但还有一类设备,它们没有物理总线——SoC内部的UART、GPIO、I2C控制器、DMA控制器,它们直接挂在CPU的内存地址空间上。你不能说它属于哪条"物理总线",但驱动模型需要一个统一的挂载点,于是内核创造了platform总线。

它本质是一条虚拟总线,用来收纳所有"直接在CPU地址空间上工作"的设备和驱动。platform_device就是稍微封装过的struct device,platform_driver就是带probe/remove/shutdown回调的struct device_driver。

5.2 驱动骨架:platform_driver的注册流程

一个标准的platform驱动骨架长这样:

static int myadc_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); // 初始化硬件、注册中断、创建字符设备等 return 0; } static int myadc_remove(struct platform_device *pdev) { // 释放资源 return 0; } static const struct of_device_id myadc_of_match[] = { { .compatible = "mycompany,myadc" }, { } }; MODULE_DEVICE_TABLE(of, myadc_of_match); static struct platform_driver myadc_driver = { .probe = myadc_probe, .remove = myadc_remove, .driver = { .name = "myadc", .of_match_table = myadc_of_match, }, }; module_platform_driver(myadc_driver);

module_platform_driver是个封装宏,展开后就是module_init和module_exit,注册时把platform_driver挂到platform总线上。接下来就是platform总线的match流程接管了。

有人会问:既然已经有设备树了,为什么还需要platform_device手动注册?实际上在现代内核中,设备树节点在启动阶段会被内核解析为platform_device,然后放到platform总线上。所以设备树和platform_device不是二选一,前者是后者的数据来源。

没有设备树的老式系统(比如一些简化的ARM或x86平台),则需要手动注册platform_device:

static struct platform_device myadc_pdev = { .name = "myadc", .id = -1, .dev = { .platform_data = &myadc_platform_data, }, }; platform_device_register(&myadc_pdev);

5.3 嵌入式开发里最容易出的三个问题

platform总线看起来简单,但实际项目中问题频发。我总结三个最常见的:

问题一:probe没有被调用。原因通常是of_match_table没写好,或者compatible拼写不一致。这时候用设备树工具检查:

dtc -I dtb -O dts -o /tmp/device.dts /sys/firmware/fdt

再用grep确认节点是否存在。设备树里必须保证:

myadc@49000000 { compatible = "mycompany,myadc"; reg = <0x49000000 0x1000>; interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>; };

二进制的DTB和驱动里的字符串一字不差,才能匹配得上。endian、大小写、下划线,哪一处错了都不行。

问题二:资源获取失败。用platform_get_resource拿不到值时,要么是reg写错了,要么是struct resource类型不对。比如中断应该用IORESOURCE_IRQ,IO地址用IORESOURCE_MEM。很多人混用导致返回NULL。

问题三:重复注册。如果有人把platform_device的注册放进了init函数,而init又被多次调用(比如某些驱动模块被重复加载),就会出现platform_device_register失败,因为设备已经挂在bus上了。这类问题的典型错误是-EEXIST,排查时检查/sys/bus/platform/devices/目录是否已经存在同名设备。

5.4 skip probe:内核为什么没走probe也能工作

补充一个有些迷惑性的细节:有些子系统设备虽然挂载在platform总线上,但驱动并不走probe流程。典型的比如GPIO控制器、clock控制器,它们继承自struct platform_driver,但对外提供的接口是gpio_chip、clk_hw这些框架结构。它们内部的probe函数主要负责注册底层框架。

所以读驱动代码时,别看到module_platform_driver,就断定设备的逻辑全在probe里。probe可能只是"接线",真正的业务在框架注册函数里。

6. 从sysfs反向阅读设备模型:一条实用的调试路径

6.1 四个关键目录代表了什么

设备模型是抽象的数据结构,但sysfs把它变成了你能用ls看到的目录树。理解了sysfs,等于拿到了设备模型的"可视化工具"。

  • /sys/devices:真实的设备树,按层级结构排列,是所有设备的"家"。
  • /sys/bus:按总线分类的逻辑视角,其下每个总线都有devices和drivers软链接指向/sys/devices里的真实设备。
  • /sys/class:按功能分类的逻辑视角,比如/sys/class/net、/sys/class/gpio。
  • /sys/block:块设备的视图,现代内核里它逻辑上属于class的block类。

这四个目录不是孤立的,它们通过符号链接互相指引。比如你在/sys/bus/platform/devices/看到一个设备目录,用ls -l可以看到它能回溯到/sys/devices/platform/...的真实路径。

6.2 实操示例:从sysfs反推一个设备的匹配过程

假设调试一块mii接口的以太网控制器,先在设备树里搜compatible,然后在驱动里找of_match_table。如果驱动已经probe,sysfs下会同时出现:

/sys/bus/platform/devices/eth_controller -> ../../../devices/platform/eth_controller /sys/bus/platform/drivers/eth_driver -> ../../../devices/platform/eth_controller/driver

driver符号链接存在,说明绑定成功。如果不存在,就看设备目录下的uevent文件输出:

cat /sys/bus/platform/devices/eth_controller/uevent

如果DRIVER=eth_driver没有出现,说明match失败,需要检查of_match_table。如果设备目录里只有一个空的uevent文件和modalias,说明设备模型甚至没给它生成完整的资源信息,重点排查设备树节点的compatible属性。

还有一个非常实用的调试入口:/sys/bus/platform/drivers/eth_driver/目录下有一个bind和unbind文件。你可以手动触发绑定测试驱动是否正常工作:

echo eth_controller > /sys/bus/platform/drivers/eth_driver/bind echo eth_controller > /sys/bus/platform/drivers/eth_driver/unbind

这在设备已经被其他驱动占用、或者想测试驱动在热插拔场景下行为时很管用。比反复insmod/rmmod模块高效得多。

6.3 设备模型相关的内核调试选项

除了sysfs,内核还提供了一些tracepoint和调试选项:

  • CONFIG_DEBUG_DRIVER:打开后,内核会在设备模型注册、匹配、绑定过程中输出大量调试信息。启用后通过dmesg能看到类似platform myadc: driver myadc driver registered之类的日志。
  • CONFIG_DEBUG_DEVRES:打印devres资源调试信息,看驱动有没有泄漏。
  • tracepoint:driver:driver_register、driver:driver_unregister、device:device_add等跟踪点,配合ftrace可以精确分析设备模型时序。

我调试驱动一直起不来的时候,最常用的就是打开CONFIG_DEBUG_DRIVER重新编译内核,然后看dmesg里的绑定日志。它会把match函数的调用结果逐条打出来,比自己去读源码找逻辑快很多。

6.4 设备模型的隐性成本与边界

最后说点设备模型的边界。它的抽象层级很多,带来了便利,也有代价:

  • 内存开销:每一个设备,都会在内存里建立对应的kobject、device结构体,在sysfs里建立目录。设备很多时,启动阶段构建这些目录树是有时间成本的,不算快,但一般可以接受。
  • 匹配开销:每注册一个driver,内核都要把它和该总线上所有设备做一次match;每注册一个设备,也要和该总线上所有driver匹配。设备数量多、match函数写复杂了,会有性能损耗。例如USB核心的match函数就相对复杂,这也是为什么USB设备启动枚举时CPU开销较大。
  • 不适合的用法:如果是纯粹的软件逻辑单元,不需要和硬件设备对应,就不要强行套用设备模型。内核里很多subsystem并不用device结构体承载业务逻辑,而是用kset和kobject做目录组织即可。

我自己写过不少驱动的感想是:设备模型是那种"不知道它的时候觉得神秘,知道它之后觉得本来就是如此"的知识。它不复杂,但知识点密集,并且几乎每一层都和前一层环环相扣。kobject是基础,bus/device/driver是骨架,class和uevent是接口,电源管理是上层保障,platform总线是实际落地的主力场景。把这几块串起来,再去看任何子系统驱动,你都会发现它只是在设备模型的大框架里填充具体内容的某个切片。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询