2026/10/5 4:39:56

【linux内核专栏 10】设备模型

【linux内核专栏 10】设备模型 本篇定位:Linux 设备模型是内核的面向对象框架——用 C 实现了类/继承/引用计数/事件通知。你裸机一个全局设备结构体数组,Linux 用 kobject/kset/bus/driver/device 把所有设备组织成树,自动管引用计数、自动生成 sysfs、自动 uevent 通知 udev。本篇讲清这套框架,为 11 篇驱动框架打地基。设备模型是 Linux 驱动的OO 基类,你裸机没有这层抽象你裸机:设备就是一结构体,直接操作。Linux:所有设备继承自struct device,所有驱动继承自struct device_driver,通过bus匹配;kobject是万物基类(管引用计数/sysfs);kref引用计数防悬挂;uevent 通知用户态 udev 自动建设备节点。这套框架是 Linux 驱动跨平台/自动管理的根基。理解它,11 篇驱动框架才看得懂。一、为什么需要设备模型1.1 裸机怎么管设备// 你裸机structuart_devuart0{.base0x10000000,.irq10};structspi_devspi0{.base0x10020000,.irq11};// 全局数组,直接操作,手动管理问题:设备关系无组织(谁挂谁下面?电源依赖?)引用计数手动(谁在用?能关吗?)设备节点手动建(mknod)无统一接口(每个驱动各搞各的)1.2 Linux 设备模型解决什么问题设备模型方案设备关系device 树(parent/child)引用计数kobject/kref 自动统一接口bus/driver/device 抽象设备节点udev 自动建(响应 uevent)配置导出sysfs 文件系统热插拔uevent 通知用户态电源管理沿设备树级联 suspend/resume1.3 设备模型的核心对象kobject ← 万物基类(引用计数 sysfs 节点) ├─ kset ← kobject 容器(集合) ├─ device ← 设备(继承 kobject) ├─ device_driver ← 驱动(管理一类设备) ├─ bus_type ← 总线(匹配 device 和 driver) └─ class ← 类(按功能分组,如 net/block/input)kobject 是 C 实现的基类C 没有面向对象,Linux 用结构体嵌 kobject模拟继承:任何结构体含struct kobject(或含含 kobject 的 device)就自动有引用计数/sysfs/父子关系。这是 C 写 OO 的经典手法(把基类嵌在派生类首成员)。你读内核代码,处处见 kobject。二、kobject:万物基类2.1 struct kobjectstructkobject{constchar*name;// 名字(sysfs 节点名)structlist_headentry;// 挂到 kset 的链表structkobject*parent;// 父对象structkset*kset;// 所属 ksetstructkrefkref;// 引用计数unsignedintstate_initialized:1;unsignedintstate_in_sysfs:1;unsignedintstate_add_uevent_sent:1;unsignedintstate_remove_uevent_sent:1;structkernfs_node*sd;// sysfs 节点...};2.2 kobject 的职责引用计数:kref 管理,谁用谁 get,用完 put,归 0 释放sysfs 节点:每个 kobject 在 sysfs 对应一个目录父子关系:parent 形成树uevent:add/remove 时发事件给用户态2.3 kobject APIstructkobject*kkobject_create_and_add(myobj,parent_kobj);// 创建并加到 sysfs(parent 下建 myobj 目录)kobject_get(k);// 引用 1kobject_put(k);// 引用 -1,归 0 释放kobject_del(k);// 从 sysfs 移除kobject_put(k);// 释放2.4 kref:引用计数原语structkref{atomic_trefcount;};voidkref_init(structkref*kref);// 初始化为 1voidkref_get(structkref*kref);// 1intkref_get_unless_zero(structkref*kref);// 若非 0 才 1intkref_put(structkref*kref,void(*release)(...));// -1,归 0 调 release原子引用计数(靠 atomic_t,07 篇)归 0 调 release 回调释放kobject 的 kref 就是这个2.5 引用计数的意义设备 A 在用: open() → kobject_get(dev-kobj) refcount2 close() → kobject_put(dev-kobj) refcount1 拔出/卸载 → kobject_put refcount0 → release → 真释放设备被用时引用 0,卸载/释放会等引用归 0防悬挂指针:有人用就不会被释放引用计数是设备模型的核心价值你裸机设备生命周期手动管(常出悬挂指针)。Linux kobject/kref自动管:谁用谁 get,用完 put,归 0 才释放。你写驱动用get_device/put_device(封装 kobject_get/put),不用手动算几个人在用。这是 Linux 比 FreeRTOS 安全的关键机制。三、kset:kobject 容器3.1 kset 是什么kset 是一组相关 kobject 的集合本身也是 kobject(继承)管理子 kobject 链表 uevent 操作structkset{structlist_headlist;// 子 kobject 链表spinlock_tlist_lock;structkobjectkobj;// 自身是 kobjectconststructkset_uevent_ops*uevent_ops;// uevent 操作};3.2 kset vs kobjectkobjectkset角色单个对象对象集合自身—也是 kobject用途表示一个实体组织同类对象例子某设备/sys/devices(所有设备集合)3.3 内核主要 kset/sys/ ├── devices/ ← devices_kset(所有设备根) ├── bus/ ← bus_kset(所有总线) ├── class/ ← class_kset(所有类) ├── module/ ← module_kset(所有模块) └── kernel/ ← kernel_kset四、device:设备4.1 struct devicestructdevice{structdevice*parent;// 父设备(设备树)structdevice_private*p;constchar*init_name;// 名字structbus_type*bus;// 挂哪条总线structdevice_driver*driver;// 绑定的驱动void*driver_data;// 驱动私有数据void*platform_data;// 平台数据structdev_pm_infopower;// 电源管理structkobjectkobj;// 基类(继承)conststructdevice_type*type;structclass*class;// 所属类dev_tdevt;// 设备号(字符/块)structdevice_node*of_node;// 设备树节点...};struct device是所有设备的基类具体设备结构体内嵌device(如struct platform_device含 device)parent 形成设备树(USB 控制器 → USB 设备)4.2 设备的注册structdevice*dev...;dev-parentparent_dev;dev-busplatform_bus_type;dev-init_namemydev;device_register(dev);// 加到设备模型,生成 sysfs,发 uevent// 或分解:device_initialize(dev);device_add(dev);4.3 device_register 做了什么初始化 kobject(引用计数)挂到 parent 的子链表(设备树)在 sysfs 建目录(/sys/devices/.../mydev)匹配总线上的 driver(bus-match)匹配上 → 调 driver-probe发 KOBJ_ADD uevent(通知 udev 建设备节点)4.4 设备树(parent/child)平台总线 ├── platform device: uart0 │ ├── sysfs 位置:/sys/devices/platform/uart0 │ └── 子设备:uart0 的子设备(如 GPIO 子设备) ├── platform device: i2c0 │ └── 子设备:i2c 总线上的 client(温度传感器等) └── ...设备树反映硬件拓扑(USB 控制器 → hub → USB 设备)电源管理沿树级联(suspend 子设备前先 suspend 父)4.5 get_device / put_devicestructdevice*get_device(structdevice*dev);// ref 1voidput_device(structdevice*dev);// ref -1封装 kobject_get/put持有 device 引用前必须 get,用完 put,否则可能被释放导致悬挂五、device_driver:驱动5.1 struct device_driverstructdevice_driver{constchar*name;// 驱动名(匹配用)structbus_type*bus;// 挂哪条总线structmodule*owner;// 所属模块(引用计数)constchar*mod_name;bool suppress_bind_attrs;conststructof_device_id*of_match_table;// 设备树匹配表conststructacpi_device_id*acpi_match_table;int(*probe)(structdevice*dev);// 绑定设备时调int(*remove)(structdevice*dev);// 解绑时调void(*shutdown)(structdevice*dev);// 关机int(*suspend)(structdevice*dev,pm_message_tstate);int(*resume)(structdevice*dev);structdriver_private*p;};5.2 驱动注册structdevice_driver*drv...;drv-namemydriver;drv-busplatform_bus_type;drv-probemy_probe;drv-removemy_remove;driver_register(drv);5.3 driver_register 做了什么加到总线的驱动链表遍历总线上的所有设备,调bus-match(dev, drv)匹配匹配上 → 调drv-probe(dev)probe 成功 → 设备和驱动绑定(dev-driver drv)5.4 probe 的意义probe 是驱动认领设备并初始化的回调匹配只判断是不是我的(名字/compatible)probe 才真初始化(映射寄存器/注册中断/建设备节点)probe 是驱动核心入口你写驱动的 80% 工作在 probe:① 映射寄存器;② 申请中断;③ 初始化硬件;④ 注册到子系统(cdev/input/net)。probe 成功 驱动接管设备。remove 反向清理。这是 11 篇驱动框架的核心。六、bus_type:总线6.1 struct bus_typestructbus_type{constchar*name;// 总线名(platform/usb/pci/i2c)constchar*dev_name;structdevice*dev_root;int(*match)(structdevice*dev,structdevice_driver*drv);// 匹配int(*uevent)(structdevice*dev,structkobj_uevent_env*env);int(*probe)(structdevice*dev);// 总线级 probe(转发给 driver)int(*remove)(structdevice*dev);int(*suspend)(...);int(*resume)(...);structsubsys_private*p;// 子系统私有(kset 等)};6.2 match:匹配 device 和 driverintplatform_match(structdevice*dev,structdevice_driver*drv){// 1. 设备树 compatible 匹配(最常用)if(of_driver_match_device(dev,drv))return1;// 2. ACPI 匹配if(acpi_driver_match_device(dev,drv))return1;// 3. id_table 匹配if(platform_drv-id_table){if(platform_match_id(platform_drv-id_table,pdev))return1;}// 4. 名字匹配if(strcmp(pdev-name,drv-name)0)return1;return0;}总线负责判断 device 和 driver 配不配platform 总线:of_match_table(compatible)/id_table/name6.3 常见总线总线作用platform虚拟总线(SoC 内设,无总线协议)——你 BSP 最常用usbUSB 总线pciPCI 总线i2cI2C 总线spiSPI 总线sdioSDIO 总线6.4 platform 总线(重点)platform 总线是虚拟总线,给 SoC 内设用(UART/SPI/I2C 控制器/GPIO 控制器)这些设备不在真实总线(USB/PCI)上,但用设备模型组织你 BSP 写的 SoC 驱动 90% 是 platform 驱动(11 篇详讲)6.5 总线的 probe 转发// bus_type.probe 实际转发给 device_driver.probeintreally_probe(structdevice*dev,structdevice_driver*drv){...if(dev-bus-probe)retdev-bus-probe(dev);// 总线级elseif(drv-probe)retdrv-probe(dev);// 驱动级...}总线可有自己的 probe(转发),或直接调驱动 probeplatform 总线:platform_probe → 调你的 .probe七、class:类7.1 class 是什么按功能分组设备(不是按总线)同 class 的设备有共同接口例:net 类(所有网卡)/block 类(所有块设备)/input 类(所有输入设备)/tty 类7.2 为什么需要 class设备树按硬件拓扑组织(USB → hub → mouse),但用户关心功能(“所有输入设备在哪”)。class 按功能组织,一个设备可既在设备树又在某 class:/sys/devices/pci/.../mouse ← 设备树位置(硬件拓扑) /sys/class/input/mouse0 ← class 位置(功能)—— 符号链接到上面7.3 class APIstructclass*my_classclass_create(THIS_MODULE,myclass);// 设备注册时关联 class,自动在 /sys/class/myclass/ 建链接device_create(my_class,dev,devt,NULL,mydev%d,0);// 同时在 /dev/ 建设备节点(配合 udev)7.4 class 的用途udev 按 class 建设备节点(/dev/input/eventX)用户态按 class 找设备(扫 /sys/class/net/)子系统(input/net/block)用 class 组织八、sysfs:设备模型的用户态视图8.1 sysfs 是什么内存文件系统,挂 /sys把内核数据结构(设备模型)导出成文件每个 kobject 一个目录,属性(attribute)是文件/sys/ ├── devices/ ← 设备树(硬件拓扑) ├── bus/ ← 总线(及总线上的 device/driver) ├── class/ ← 类(按功能) ├── module/ ← 模块 ├── kernel/ ← 内核信息 ├── firmware/ ← 固件 └── fs/ ← 文件系统8.2 sysfs 文件 attribute// 定义属性staticDEVICE_ATTR(my_val,0644,my_val_show,my_val_store);// 注册device_create_file(dev,dev_attr_my_val);// show/store 回调ssize_tmy_val_show(structdevice*dev,structdevice_attribute*attr,char*buf){returnsprintf(buf,%d\n,my_value);}ssize_tmy_val_store(structdevice*dev,structdevice_attribute*attr,constchar*buf,size_tcount){sscanf(buf,%d,my_value);returncount;}读 sysfs 文件 → 调 show写 sysfs 文件 → 调 store用户态通过读写 sysfs 和内核交互8.3 sysfs 的意义cat /sys/class/net/eth0/mtu → 调 show,读 mtu echo 1500 /sys/class/net/eth0/mtu → 调 store,改 mtu配置设备不用专门工具,echo/cat 即可脚本友好一切皆文件的体现sysfs 是你 BSP 调试利器你裸机调试靠 printk/串口。Linux 加 sysfs——设备状态/配置都在 /sys,cat/echo 即可读写。调驱动时,cat /sys/…/xxx 看状态,echo 改配置,不用写测试程序。这是 Linux 驱动开发的便利。九、uevent 与 udev:自动设备节点9.1 uevent 是什么设备 add/remove 时,内核发 uevent(KOBJ_ADD/KOBJ_REMOVE)uevent 经 netlink 发给用户态 udev 守护进程udev 据 uevent 建设备节点(/dev/xxx)9.2 udev 做什么内核:device_add → 发 KOBJ_ADD uevent ↓ netlink udev 收到: → 据 uevent 的 SUBSYSTEM/DEVTYPE/属性 → 查规则(/etc/udev/rules.d/) → mknod /dev/xxx(建设备节点) → 可执行额外动作(modprobe/改名/权限)9.3 设备节点的自动建立老方式:驱动手动device_create(class, dev, devt, ...) udev 自动建节点现代驱动基本不用手动 mknod,udev 全自动9.4 udev 规则# /etc/udev/rules.d/99-my.rules SUBSYSTEMinput, ATTRS{name}mykbd, SYMLINKmykeyboard # 匹配 input 子系统且 namemykbd 的设备,建符号链接 /dev/mykeyboardudev 规则灵活:按属性匹配、改名、建链接、设权限、运行程序9.5 对照裸机 mknod裸机/老 Linux现代 Linux(udev)设备节点手动 mknodudev 自动热插拔手动uevent → udev 自动命名固定规则可定嵌入式视角:嵌入式用 mdev/udev嵌入式常 BusyBox mdev(udev 精简版)。原理同:监听 uevent,按规则建节点。BSP 要配 mdev 规则,让设备节点正确生成。十、设备模型全景:一次 device_register 的连锁反应驱动加载: driver_register(drv) → 加到 bus 的驱动链表 → 遍历 bus 上的 device,match(drv, dev) → 匹配上 → probe(dev) → probe 成功 → dev-driver drv 设备注册(设备树/platform 设备): device_register(dev) → 初始化 kobject(refcount1) → 挂到 parent 子链表(设备树) → sysfs 建目录(/sys/devices/.../dev) → match:找 bus 上匹配的 driver → 匹配上 → driver-probe(dev) → 发 KOBJ_ADD uevent → udev 收到 → 建 /dev/xxx 卸载/拔出: device_unregister(dev) → driver-remove(dev) → sysfs 移除目录 → 发 KOBJ_REMOVE uevent → udev 删 /dev/xxx → put_device → refcount 归 0 → release引用计数贯穿device_register → refcount1get_device(持有引用)→ 1put_device(用完)→ -1device_unregister → -1,但若有人 get 着,不归 0,不释放最后 put_device 归 0 → release 回调真释放这套机制让你不用手动管生命周期裸机设备生命周期全手动,易错。Linux 设备模型 kref:① 谁用谁 get;② 用完 put;③ 归 0 自动释放。你写驱动只要在持有 device 引用处 get_device,用完put_device,框架管其余。十一、platform 设备/驱动(预览,11 篇详讲)11.1 platform_devicestructplatform_device{constchar*name;// 名字(匹配用)intid;// 实例号(-1 表示唯一)structdevicedev;// 基类(继承 device)structresource*resource;// 资源(寄存器/中断/DMA)intnum_resources;...};11.2 platform_driverstructplatform_driver{int(*probe)(structplatform_device*pdev);int(*remove)(structplatform_device*pdev);void(*shutdown)(structplatform_device*pdev);int(*suspend)(...);int(*resume)(...);structdevice_driverdriver;// 基类conststructplatform_device_id*id_table;};11.3 匹配流程(platform)设备树 compatible vendor,mydev 驱动 of_match_table { .compatible vendor,mydev, } device_register(platform_dev) → bus platform_bus_type → match: of_driver_match_device → 比较 compatible → 匹配 → platform_driver.probe(pdev)platform 是你 BSP 主战场SoC 内设(UART/SPI/I2C 控制器/GPIO/定时器)都是 platform 设备。你写 SoC 驱动 写 platform_driver。11 篇详讲 probe/remove/file_operations,本篇先懂device/driver/bus 匹配地基。十二、对照总表概念裸机/FreeRTOSLinux 设备模型设备组织全局数组device 树(parent/child)设备基类无struct device(嵌 kobject)驱动基类无struct device_driver匹配手动bus-match引用计数手动kobject/kref 自动设备节点手动 mknodudev 自动(uevent)配置接口无sysfs 文件热插拔无uevent udev电源管理手动沿设备树级联类分组无class十三、本篇小结设备模型是 Linux 用 C 实现的OO 框架:kobject 基类 device/driver/bus/class 派生kobject:万物基类,管引用计数(kref) sysfs 节点 父子关系 ueventkref:原子引用计数,归 0 自动释放,防悬挂device:设备基类,内嵌 kobject;parent 形成设备树;get_device/put_device 管引用device_driver:驱动,有 probe/remove;注册时遍历设备匹配bus_type:总线,match 判断 device/driver 配不配;platform/usb/pci/i2c/spiclass:按功能分组(net/block/input/tty),/sys/class/ 符号链接sysfs:设备模型的用户态视图,kobject→目录,attribute→文件,cat/echo 交互uevent udev:设备 add/remove 发 uevent,udev 自动建 /dev 节点device_register 连锁:初始化 kobject → 挂设备树 → sysfs 建目录 → match → probe → uevent → udev 建节点引用计数贯穿:get 用前,put 用完,归 0 释放platform 是 SoC 内设的虚拟总线,你 BSP 主战场(11 篇详讲)速查表想干啥用什么创建 kobjectkobject_create_and_add(name, parent)增引用kobject_get / get_device减引用kobject_put / put_device注册设备device_register(dev)注册驱动driver_register(drv)注册总线bus_register(bus)创建 classclass_create(THIS_MODULE, “name”)建设备节点device_create(class, dev, devt, …)定义 sysfs 属性DEVICE_ATTR(name, perm, show, store)注册 sysfs 文件device_create_file(dev, dev_attr_xxx)看 sysfscat /sys/…/xxx改 sysfsecho val /sys/…/xxx看设备树ls /sys/devices/看总线ls /sys/bus/看类ls /sys/class/看已加载模块ls /sys/module/看 ueventudevadm monitor看 udev 规则/etc/udev/rules.d/技术之路漫漫分享是为了更好地交流。如果本文的内容对你有启发希望能得到你的点赞 和收藏 ⭐。如果你在调试过程中遇到了其他问题欢迎在评论区 留言我们一起探讨。也欢迎关注 我一起交流底层开发的那些事儿。