ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux设备模型详解:从kobject到probe匹配的完整链路

Linux设备模型详解:从kobject到probe匹配的完整链路 干嵌入式这些年如果让我挑一篇真正“相见恨晚”的文章来写我会毫不犹豫选Linux设备模型。回想我第一次调I2C触摸屏驱动时的狼狈设备树配好了驱动也写了可Probe死活不进来dmesg里一会儿报Failed to create sysfs entry一会儿driver_register返回负数我对着源码看半天也不知道问题出在哪个环节。后来啃完drivers/base目录下那几百KB代码才恍然大悟——原来所有总线、设备、驱动之间真正的“隐藏主线”就是设备模型。这篇文章我准备把Linux设备模型一次讲透从核心数据结构到注册匹配的完整链路再到sysfs实操和嵌入式内核源码阅读路径全部整理出来。适合正在做内核驱动开发、嵌入式系统移植或者面试前想系统梳理linux内核知识的朋友。1. linux内核里的隐藏主线设备模型到底解决了什么1.1 一台设备进入内核后的“物业管理”逻辑先举个生活中大家都能瞬间理解的例子。一座小区要正常运转必须有清楚的“物业登记系统”哪一栋楼有哪些房间设备对象每个房间用来做什么类Class谁负责管理这栋楼总线Bus进来提供服务的人是谁驱动Driver。如果没有这套统一的管理体系小区就是一堆无规则的房间别说收水电费连消防检查都无从下手。内核面对的局面比小区复杂得多PCI设备、USB设备、I2C设备、SPI设备、平台设备保守估计型号超过几千种。如果没有设备模型这个统一抽象每个驱动都按自己的方式乱注册系统根本没法管理电源、热插拔和用户态交互。设备模型干的事情就是给所有设备对象提供一套统一的注册、枚举、匹配、生命周期管理机制并最终通过sysfs和uevent实现与用户空间的对接。这套机制解决了三个核心问题一是设备和驱动的自动配对不用人类去手动指定谁对应谁二是设备资源可以随时创建和销毁尤其热插拔场景下设备来了就注册、走了就注销三是把电源管理统一到设备生命周期里系统休眠时知道按什么顺序关设备、唤醒时按什么顺序开设备。理解了这些你就知道设备模型不是一个“可学可不学”的东西而是linux内核驱动开发的公共地基。1.2 设备模型藏在内核源码的哪个角落设备模型的主体代码在drivers/base/目录下核心文件包括core.c、bus.c、dd.c、driver.c、class.c和platform.c。很多人第一次翻开这个目录会觉得代码量太大但其实主线非常清晰core.c负责设备的注册和删除bus.c负责总线对象的建立和匹配入口dd.cdd是driver device的缩写负责设备与驱动的最终绑定platform.c则是嵌入式linux开发最常见的“平台总线”实现。更重要的是PCI、USB、I2C、SPI、GPIO这些子系统本质上是建立在设备模型之上的“上层建筑”。它们各自的总线类型最终都注册进同一个bus机制里设备注册时照样走device_add这条主线。哪怕你搞的是linux内核虚拟化方向VirtIO驱动的热插拔逻辑和设备绑定链路同样没有绕开这层底层抽象。所以学会设备模型等于拿到了一把打开大半内核驱动代码的钥匙。2. 从最小细胞kobject到ksetsysfs里的一切都靠引用计数2.1 kobject是对象能在/sys出现的前提设备模型最小的单元不是device而是kobject。你可以把kobject理解为设备模型里的“细胞膜”任何结构体想被设备模型管理、想出现在sysfs的目录树里就必须内嵌一个struct kobject。打开include/linux/kobject.h能看到它的核心成员struct kobject { const char *name; struct list_head entry; struct kobject *parent; struct kset *kset; struct kobj_type *ktype; struct sysfs_dirent *sd; struct kref kref; #ifdef CONFIG_DEBUG_KOBJECT_RELEASE struct delayed_work release; #endif };需要注意的字段就三个name是对象在sysfs里的名字parent决定它挂在哪个父目录下kref是引用计数。Linux设备模型里所有对象都通过引用计数管理生命周期这不只是理论概念。比如device_register()成功后内核持有对设备的一个引用而driver绑定后又会增加引用。每次kobject_get()增加计数每次kobject_put()减少计数当计数降到0时就会调用ktype里定义的release()函数释放内存。这里有一个新手最容易踩的坑kobject_init()和kobject_add()必须成对调用而且release()回调绝对不能为空否则内核报“kobject: no release function”警告甚至直接内存泄漏。我见过不少同事在自定义结构体里随手内嵌一个kobject结果忘了写release函数模块卸载时系统提示kobject还在使用只能重启解决。2.2 kset与uevent用户空间怎么知道设备来了单个kobject是孤独的需要被分组管理这个分组容器就是kset。kset本质上是一个kobject的集合同时承担发送uevent的任务。一个经典的例子是/sys/bus/下每个总线都有对应的kset所有挂在同一总线上的设备对象都会加入这个集合。uevent是设备模型向用户空间广播消息的机制。当设备注册时kset会调用uevent_ops拼出一组环境变量比如ACTIONadd、DEVPATH/devices/platform/xxx、SUBSYSTEMplatform通过kobject_uevent()发送出去。用户空间的udev或者嵌入式环境里的mdev收到这些消息后就会在/dev下创建设备节点、加载固件或者触发用户态脚本。你平时插U盘能立刻看到/dev/sda出现背后就是这一整套链路在工作。这也就是为什么很多调驱动的人喜欢用udevadm monitor看设备事件。设备模型在用户态的表现不只是一个静态目录树而是一条实时事件流。理解kset与uevent的关系你就明白了“插上设备内核如何感知”这个问题的答案。3. device、driver、bus三巨头谁找谁怎么对上眼3.1 三个结构体里必须关注的字段设备模型里真正的主角是三巨头三个结构体分别在include/linux/device.h里定义。内核里叫device、device_driver和bus_type我来把关键的字段挑出来讲。先看struct device注册设备时你需要初始化这几项init_name或dev_set_name()设置设备在sysfs下的名字parent指定父设备控制sysfs目录层级bus声明它挂在哪个总线上release回调设备释放时必定会被调用没实现它注册就会失败。再看struct device_driver驱动侧的字段要简单不少name是驱动的名字在很多总线匹配中会被拿来和设备的name作比较bus表示这个驱动属于哪条总线probe和remove是两个最重要的回调分别为设备成功匹配后初始化硬件、以及设备移除时清理资源。最后是struct bus_type它是连接设备和驱动的桥梁name是总线名称比如platform、i2c、spimatch回调负责判断驱动和设备是否匹配uevent回调在发送用户态事件时补充环境变量。这三兄弟的关系用一句话概括总线是媒婆设备是待嫁的姑娘驱动是相亲对象match是相亲条件probe是确定关系。总线把大家聚在一起设备把自己“挂牌”驱动按条件“应征”。3.2 match匹配机制和probe流程以嵌入式开发最常见的platform总线为例。总线在匹配设备时依次尝试三条路首先是设备树节点的compatible字段与驱动of_match_table中的compatible字符串是否完全一致其次是设备名字与驱动名字是否一致最后是驱动id_table中声明的ID是否匹配。platform_match()的源码就在drivers/base/platform.c里你可以打开看看逻辑非常直白。匹配成功的下一步就是probe。整个调用链大致是总线触发bus_probe_device()进入device_attach()最终调到drivers/base/dd.c中的really_probe()。在really_probe里内核先确认设备没有被占用然后调用driver-probe()也就是你写的那个probe函数。如果probe返回0设备和驱动就正式绑定如果probe返回负数内核会判定匹配失败。这里要解释一个很多初学linux内核的人都会困惑的点probe失败不等于匹配失败。match只负责判断“这个驱动有没有资格管这个设备”probe才是真正去操作硬件并返回是否成功。一上来先把match做得太严格驱动就永远碰不到probe反过来match太宽松probe会被反复调用硬件没准备好就报错这也是很常见的问题。3.3 EPROBE_DEFER内核的“延迟再试”所有probe流程里最值得单独拎出来讲的是-EPROBE_DEFER。这个返回值的意思很简单这个设备依赖的某个资源还没准备好请内核过一会儿再试一次。举个例子一个触摸屏驱动通过regulator框架依赖某个电源控制芯片而电源芯片驱动还没注册这时触摸屏probe如果直接失败就会永久失败。正确的做法是返回-EPROBE_DEFER内核会把设备放入延迟队列等电源芯片驱动注册成功后再触发一轮重新探测。实际调试中很多人看到设备反复尝试probe以为是内核卡死了其实这是设备模型正常的工作方式。只要在dmesg里能看到类似probe of ... returned -517的日志就说明设备被延迟探测了。你还可以通过/sys/kernel/debug/devices_deferred文件查看哪些设备在等待延迟探测。开发排错时这条信息比瞎猜要可靠得多。4. 实操侧写用常用命令“摸”设备模型再写一个平台驱动4.1 sysfs实操几个linux常用命令就够设备模型看得见摸得着的部分就是sysfs挂载在/sys目录。先花十分钟在任意一台嵌入式板子或者虚拟机里敲几个linux常用命令比你读十篇文档更有用。# 查看系统里已注册的总线 ls /sys/bus/ # 查看platform总线上的设备 ls /sys/bus/platform/devices/ # 查看按功能分类的设备 ls /sys/class/ # 查看一个设备对象的uevent内容 cat /sys/bus/platform/devices/xxx/uevent # 实时监听设备事件热插拔实验必备 udevadm monitor我最推荐的做法是先打开udevadm monitor然后手动绑定或解绑一个设备echo demo-device /sys/bus/platform/drivers/demo-driver/bind。这时屏幕上会立刻出现uevent消息你能真实感受到设备模型在用户态呈现的样子。用cat查看uevent时里面会打印出设备路径和子系统类型这就是kset在注册时生成并发送的环境变量集合。这种“用系统反推机制”的方法我一直很推荐。你不必一开始就沉浸在源码里先从sysfs这种冰山一角开始摸摸熟了你自然想知道这些东西是哪段代码制造的带着问题去读源码效率比漫无目的翻代码高很多。4.2 最小平台驱动示例从insmod到probe动手写一个最小的platform驱动代码量并不大。下面这个demo没有任何实际硬件操作却能让你完整观察设备模型里注册、匹配到probe的过程。#include linux/module.h #include linux/platform_device.h #include linux/of.h static int demo_probe(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, demo probe, name %s\n, pdev-name); return 0; } static int demo_remove(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, demo remove\n); return 0; } static const struct of_device_id demo_of_match[] { { .compatible vendor,demo-device }, { } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct platform_driver demo_driver { .probe demo_probe, .remove demo_remove, .driver { .name demo-driver, .of_match_table demo_of_match, }, }; module_platform_driver(demo_driver); MODULE_LICENSE(GPL);编译用最基础的Makefile就行注意KDIR指向你当前内核版本的构建目录obj-m : demo.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean但光有驱动还触发不了probe它得先匹配到一个设备。如果板子上用设备树就加一个节点demo-device { compatible vendor,demo-device; reg 0x1f000000 0x1000; };如果用老的板级文件方式或者想快速在pc上验证可以直接用platform_device_register()动态注册一个同名的平台设备。两种方式最终都会走到同一个匹配流程。加载后看dmesg你会看到probe里打印的那句话。再去/sys/bus/platform/drivers/看驱动目录下已经出现了设备名称的链接。4.3 设备树、resource与probe里的“标配操作”在实际产品里probe不是只打印一行日志就完事的。probe中需要拿到的信息主要有三类设备树里的属性配置、寄存器物理地址范围、中断号。这些资源的获取方式设备模型都已经帮你封装好了。static int demo_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); irq platform_get_irq(pdev, 0); if (irq 0) return irq; dev_info(pdev-dev, base %px, irq %d\n, base, irq); return 0; }这里用devm_开头的接口是设备模型里很贴心的设计。你可以把它理解为“随设备生命周期自动释放的资源管理”probe里申请的资源即使probe中途失败也不用你一个个手动释放remove时内核会自动清理。在嵌入式内核源码里现在新代码几乎统一使用devm系列接口手写手动释放的老接口式代码反而不常见了。这也提醒我们写驱动时先看看有没有现成的devm版本可用。5. 嵌入式内核源码阅读哪些文件、哪些函数值得啃5.1 核心文件地图与阅读顺序建议很多初学者面对嵌入式内核源码无从下手以为要把整个drivers目录都读完。其实设备模型部分主线的源码量并不算大关键是顺序和方法。我建议按下面的路径走一遍drivers/base/dd.c - really_probe, device_attach drivers/base/bus.c - bus_probe_device, bus_add_device drivers/base/core.c - device_add, device_initialize drivers/base/platform.c - platform_match, platform_drv_probe第一站应该是dd.c。理由很简单设备模型的最终目标就是让设备和驱动碰头probe就是这个“碰头瞬间”的动作。从really_probe()开始读你会有目的地去问“谁调用了它”自然会追溯到bus层再到core层。这种从目标反推调用源的方式比从module_init开始顺着读要高效得多。第二站读bus.c看总线怎么把设备组织起来怎么发起探测。第三站读core.c里device_add()这个函数它是所有设备注册的统一入口前一脚物理总线驱动把设备信息填好后一脚设备模型接管生命周期。最后一站回到platform.c看嵌入式linux最常用的平台总线在match阶段的优先级逻辑。5.2 版本变化和我的踩坑记录linux内核版本迭代很快设备模型的总体框架非常稳定几乎十年没有动过核心设计但细节一直在变。比如5.x以后platform_get_resource()在许多场景下开始被platform_get_mem_or_io()这类新接口替代设备树API也越来越多地推荐使用device_property_read_*()取代of_property_read_*()以便统一兼容ACPI和设备树。我自己的两次惨痛教训值得说一下。第一次是写驱动时只实现了driver侧的of_match_table没有核对设备树里的compatible大小写结果probe始终不进入最后用dtc反编译设备树才发现字符串大小写不一致。第二次是probe里用request_irq却忘了在remove里free_irq导致模块卸载后中断还注册着一有中断就崩溃。后来用devm_request_irq替换彻底省心。调设备模型有个不为人注意但极其有用的开关。内核配置里打开CONFIG_DEBUG_DRIVER后整个设备模型的注册、匹配、绑定过程会打印出非常详细的日志基本等于给你开了透视眼。生产环境当然不会开但开发和调试阶段这个选项比一堆printk有用得多。6. 设备模型常见问题与排查技巧6.1 问题速查表我把实际调试中常见的问题整理成一个速查表方便你直接对照。现象可能原因常用排查手段驱动probe始终不进入compatible不匹配、设备树解析失败、设备未被枚举dmesg、dtc反编译设备树、查看/sys/bus/platform/devicesprobe返回EPROBE_DEFER但在循环依赖的驱动未加载、资源等待超时cat /sys/kernel/debug/devices_deferred、查看依赖模块是否加载注册设备时报sysfs目录冲突name重复、parent设置错误dmesg里的kobject错误信息、检查是否同一parent下重名模块卸载时崩溃或内存泄漏release回调未实现、引用计数不平衡CONFIG_DEBUG_KOBJECT_RELEASE、kobject_get/put调用检查/sys下能看到设备但/dev下没节点uevent没正确发出、udev/mdev规则缺失udevadm monitor、cat设备uevent、检查用户态服务状态6.2 通用排查链路和我的习惯排查设备模型问题时我有一套固定的排查链路分享出来供你参考。第一步永远先看dmesg无论多自信先确认内核打印出的真实错误。第二步看sysfs实际状态重点检查设备有没有被枚举到总线上。第三步如果probe没执行就去/sys/kernel/debug/devices_deferred看有没有延迟探测。第四步才是打开源码逐行分析。这套顺序看起来简单但它能避免99%的瞎猜。因为我见过太多人一上来就怀疑内核源码有bug结果排查半天最后发现是设备树少写了一个属性或者模块根本没加载成功。调试设备模型时我还养成了一个习惯每次写驱动前先在sysfs里找到对应子系统目录结构搞清楚这个设备应该挂在哪条总线、哪个class下然后才写代码。设备模型的目录树和内核的代码逻辑是一一对应的你在用户态看到的目录结构就是内核对象注册情况的镜像。多利用这个镜像调试速度能快不少。最后聊一点实在的。设备模型这套东西真正掌握的标准不是能背出多少结构体成员而是你看到dmesg里一行消息就能判断它正处在匹配、绑定、probe还是延迟重试的哪个环节。那种“通透了”的感觉许多linux驱动工程师工作三五年才体会得到。但如果你从这篇文章开始先理解三巨头关系再去sysfs里实操验证最后顺着源码地图把主线读一遍这个框架可能几个星期就能建立起来。无论你是准备linux面试题还是要做嵌入式内核源码移植设备模型都是绕不开的核心模块。就我个人而言这是我在linux内核里第一个真正感到“相见恨晚”的知识点——它不是一堆孤立的数据结构而是整个驱动世界的运行逻辑。希望这篇文章也能成为你打开这扇门的起点。
返回列表