尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux设备驱动模型:从kobject到总线驱动的核心机制与实践

Linux设备驱动模型:从kobject到总线驱动的核心机制与实践 1. 从“裸奔”到“有组织”为什么我们需要设备驱动模型干了这么多年Linux驱动开发我见过太多人一上来就对着file_operations结构体猛写open、read、write把驱动当成一个孤立的、直接操作硬件的“黑盒子”来对待。早期的Linux内核2.4及以前也确实是这样驱动直接向内核注册自己管理自己的资源内核就像一个“大集市”谁都能来摆摊但秩序混乱。设备多了驱动复杂了问题就来了电源怎么统一管理设备热插拔了内核怎么知道不同设备之间怎么建立清晰的父子、兄弟关系设备信息怎么让用户空间程序方便地获取这就是Linux设备驱动模型Device Driver Model DDM要解决的核心问题。它不是一个具体的驱动而是一套在内核中构建的、用于管理所有设备、总线、驱动和类的基础设施和框架。你可以把它理解为给内核这个“大集市”建立了一套完善的“工商管理系统”和“市政规划图”。每个设备、驱动都成了这个系统里的“注册商户”它们之间的关系、状态、资源都被这套模型清晰地管理起来。这套模型带来的好处是实实在在的。最直观的就是/sys文件系统的出现。在/sys下你能以树形结构看到整个系统的设备拓扑每个设备的属性、状态一目了然。这背后就是设备驱动模型在支撑。它实现了统一设备模型让千差万别的硬件USB设备、PCI设备、平台设备都能用一套抽象的逻辑来表示。它实现了电源管理系统休眠时模型能按正确顺序通知所有设备进入低功耗状态。它更是热插拔的基础当你在USB口插入一个U盘内核能自动找到匹配的驱动并加载。所以学习设备驱动模型绝不是为了应付面试或者炫技。它是你从“会写驱动”到“写好驱动”、从“实现功能”到“理解系统”的关键一步。它能让你写的驱动更健壮、更标准更能融入Linux内核的生态。接下来我们就抛开那些枯燥的概念从几个核心“零件”开始看看这套“管理系统”到底是怎么运转起来的。2. 模型的核心基石kobject, kset 与 ktype如果把设备驱动模型比作一座大厦那么kobject、kset和ktype就是浇筑这座大厦最基础的钢筋混凝土。很多资料一上来就讲struct device和struct device_driver让人云里雾里。其实理解了这三个底层结构上面的device和driver就非常好理解了。2.1 kobject一切对象的“基类”kobject内核对象是模型中最基础的数据结构。它本身不完成具体功能但提供了所有设备模型对象都需要的基础能力。你可以把它想象成面向对象编程里的“基类”。一个kobject主要干这几件事引用计数内核对象生命周期复杂谁在用、能不能销毁全靠kref这个引用计数器来管理。这是内核内存安全的重要保障。在sysfs中提供目录每个kobject在/sys文件系统中都对应一个目录。这是实现用户空间可视化的基础。对象关联通过parent指针kobject可以组织成层次结构树形这正好对应了设备的物理或逻辑拓扑。属性支持可以在kobject对应的sysfs目录下创建文件属性用于读写设备的状态或配置。在代码里你很少会直接创建一个kobject。它通常是作为更大结构体的一个成员被“嵌入”使用。比如struct device里面就包含了一个struct kobject成员。这种“嵌入”的方式使得device自动拥有了kobject的所有能力引用计数、sysfs接口等这就是面向对象中“继承”思想在内核C语言里的实现。// 概念性示意非完整代码 struct my_device { char name[32]; int id; struct kobject kobj; // 嵌入一个kobject // ... 其他设备特定数据 };2.2 kset对象的“集合”与“容器”kset内核对象集合可以看作是一个kobject的容器或集合。它本身也是一个kobject它内嵌了一个所以它自己也会在sysfs里创建一个目录。但这个目录的特殊之处在于它下面可以“收纳”属于这个集合的所有kobject。kset的主要作用分组管理把具有相同类型或属性的kobject归到一起。比如所有PCI设备可以放在一个kset里所有USB设备放在另一个kset里。热插拔事件传播当kset中的kobject状态发生变化如被添加或移除时kset负责将热插拔事件uevent上报给用户空间。这是实现udev动态管理设备的基础。提供共同的默认属性可以为kset中的所有kobject定义一些共同的默认属性。在/sys文件系统中/sys/devices、/sys/bus、/sys/class这些顶层目录其实都是不同的kset。2.3 ktype对象的“类型”与“行为”ktype内核对象类型描述了特定类型的kobject所具有的“行为”。它主要包含两个重要的函数指针release函数当这个kobject的引用计数降为0时谁负责释放它占用的内存就是这个release函数。它定义了对象的“析构”行为。sysfs_ops当用户通过sysfs读写这个kobject的属性文件时应该调用哪个函数来处理sysfs_ops提供了默认的属性访问方法。同一个kset里的kobject通常共享同一个ktype因为它们属于同一类对象释放方式和默认属性操作应该一致。一个简单的比喻想象一家公司内核。kobject就像每个员工对象他们有工号引用计数在公司的组织架构图sysfs上有一个位置。kset就像部门如研发部、市场部把员工分组管理部门经理kset负责向总部报告本部门的人员变动热插拔事件。ktype就像员工的“职位类型”如工程师、销售定义了这类员工入职时的通用培训默认属性和离职时的交接流程release函数。理解了这三块基石我们再去看device和driver就会发现它们不过是更高级的、功能更具体的“特殊员工”罢了其底层管理逻辑依然离不开kobject、kset和ktype这套机制。3. 模型的骨架总线、设备与驱动基石打好了就要往上盖房子了。设备驱动模型的骨架由三个核心结构体搭成bus_type总线、device设备和device_driver驱动。它们之间的关系是理解整个模型如何工作的关键。3.1 总线设备的“集散中心”与“婚介所”总线bus_type是一个逻辑上的概念它代表了一类具有相同连接方式和通信协议的设备集合。常见的总线有platform平台总线、pci、usb、i2c、spi等。总线的核心职责有两个设备枚举与管理总线知道自己这条“路”上可能连接了哪些设备。对于PCI、USB这类真实总线内核或固件会去扫描、枚举出物理设备。对于platform这种虚拟总线则需要开发者显式地“注册”设备。驱动匹配这是总线最重要的“婚介”功能。总线上注册了设备device和驱动device_driver。总线类型定义了一套规则match函数当有新的设备或驱动注册时总线就负责根据这套规则为它们寻找“另一半”。struct bus_type { const char *name; // 总线名称如platform, pci int (*match)(struct device *dev, struct device_driver *drv); // 匹配函数 int (*uevent)(struct device *dev, struct kobj_uevent_env *env); // 热插拔事件 // ... 其他操作函数如探测、移除等 };match函数是灵魂。它通常比较设备的标识如PCI的厂商/设备IDplatform的设备名i2c的从机地址和名称和驱动所声明的支持设备列表。匹配成功总线就会调用驱动的probe函数来初始化设备匹配失败它们就各自等待。3.2 设备硬件的“内核代言人”device结构体代表一个具体的、物理的或逻辑的设备。它内嵌了一个kobject因此具备生命周期管理和在sysfs中可见的能力。一个device需要提供的关键信息包括归属struct bus_type *bus指向它所属的总线。父设备struct device *parent指向它的父设备这构成了设备树。例如一个USB鼠标的父设备是它所连接的USB集线器。驱动匹配成功后struct device_driver *driver会指向控制它的驱动。平台数据void *platform_data或struct device_node *of_node用于设备树这是驱动识别和配置该设备所需的私有数据。标识符如devt设备号、id等。设备注册device_register后它就会被添加到所属总线的设备列表中并出现在/sys/devices下的相应位置等待它的“有缘驱动”。3.3 驱动设备的“灵魂操控者”device_driver结构体代表一个能控制一类设备的软件模块。同样它也内嵌了kobject。驱动的核心信息包括归属struct bus_type *bus指向它所属的总线。名称const char *name驱动名称。匹配表const struct of_device_id *of_match_table设备树或const struct platform_device_id *id_table平台设备这个表里列出了该驱动支持的所有设备标识。探测与移除int (*probe)(struct device *dev)和int (*remove)(struct device *dev)。这是驱动开发者的主战场。probe函数在总线匹配成功后调用负责初始化设备、申请资源、注册字符设备或网络设备等。remove则在设备断开或驱动卸载时进行清理。驱动注册driver_register后它会被添加到所属总线的驱动列表中。总线会立刻用它去匹配所有已注册的、还未匹配的设备同时之后新注册的设备也会用所有已注册的驱动去尝试匹配。3.4 匹配与绑定一场由总线主导的“联姻”整个过程可以概括为以下步骤注册总线内核初始化或模块加载时各种总线platform、pci等首先向系统注册自己。注册设备系统启动或热插拔时设备被注册到对应的总线。例如对于嵌入式系统我们会在板级初始化代码中注册多个platform_device。注册驱动驱动模块被加载时向对应的总线注册自己。总线匹配每当步骤2或3发生总线核心就会调用该总线的match函数遍历另一方的列表进行匹配。比较的依据就是驱动id_table里的标识与设备提供的标识。执行探测一旦匹配成功总线核心就会调用该驱动的probe函数并将匹配到的device结构体指针传递给它。绑定状态匹配成功后设备的driver指针会指向该驱动驱动的设备列表里也会加入这个设备。在/sys/bus/xxx/devices和/sys/bus/xxx/drivers下可以看到它们的符号链接关系。这个“总线-设备-驱动”模型实现了设备与驱动的解耦。设备只需要描述“我是谁”驱动只需要声明“我能控制谁”具体的配对工作交给总线这个“中介”。这极大地提高了系统的可扩展性和动态性是支持热插拔的架构基础。4. 模型的分类与呈现类与sysfs设备驱动模型不仅管理设备还对设备进行分类并以一种直观的方式呈现给用户空间。这就是class和sysfs的功劳。4.1 类按功能划分的设备“朋友圈”class类是从功能角度对设备进行二次分组的机制。一个设备可以属于多个类但通常只属于一个主类。例如一个/dev/ttyS0串口设备它从连接方式上属于platform或serial总线但从功能上它属于tty类。同样所有的块设备硬盘、U盘都属于block类所有的输入设备键盘、鼠标都属于input类。class也是一个内嵌了kobject和kset的结构体。它在/sys/class目录下创建一个子目录。所有声明属于该类的设备都会在这个类目录下创建一个指向/sys/devices中实际设备目录的符号链接。类的核心价值在于它为用户空间提供了一种稳定、统一的访问接口而不必关心设备的底层总线连接细节。例如无论你的显卡是PCIe接口还是AGP接口只要它提供了DRMDirect Rendering Manager功能它就会出现在/sys/class/drm目录下。桌面环境或管理工具通过/sys/class来发现和管理特定类型的设备比遍历复杂的/sys/devices树要简单可靠得多。在驱动中我们通常使用class_create()和device_create()函数来为我们的设备创建一个类并在类下创建设备节点。这比手动调用mknod创建/dev下的节点更规范也更能融入设备模型。4.2 sysfs内核到用户空间的“橱窗”sysfs是一个存在于内存中的虚拟文件系统通常挂载在/sys。它是设备驱动模型对外的“橱窗”将内核中kobject构成的层次结构以目录和文件的形式暴露给用户空间。sysfs的目录结构清晰地反映了设备模型的层次/sys/devices/这是核心以树形结构展示系统所有的设备体现物理或逻辑连接关系。/sys/bus/按总线类型组织下面有pci、usb、platform等子目录。每个总线目录下通常有devices链接到/sys/devices下的设备和drivers已注册的驱动两个子目录。/sys/class/按设备类组织如net网络接口、block块设备、input输入设备等。/sys/dev/提供按设备号char和block分类的视图方便通过主次设备号查找设备。sysfs里的文件被称为“属性”它们不是普通的文件而是内核中变量或函数的映射。读取这些文件就是调用内核中对应的“show”函数写入则是调用“store”函数。这为用户空间配置内核参数、查询设备状态提供了标准接口。例如你可以通过echo 1 /sys/class/leds/led0/brightness来点亮一个LED通过cat /sys/class/net/eth0/address来查看网卡MAC地址。一个综合视角假设你插入一个USB键盘。内核USB核心会探测到它创建一个usb_device设备注册到usb总线。usbhid驱动驱动与之匹配成功执行probe。在probe中驱动会进一步创建一个input设备并将其注册到input子系统类。于是在/sys下你会看到/sys/devices/.../usb1/1-1/1-1.2/具体的USB设备路径/sys/bus/usb/devices/1-1.2 - ../../devices/.../总线视图下的链接/sys/class/input/input100/功能类视图/sys/class/input/input100/device - ../../../devices/.../类到设备的链接通过sysfs这个设备的完整生命周期和归属关系一目了然。5. 实战编写一个符合设备模型的简单平台驱动理论说得再多不如动手写一遍。我们以一个最简单的虚拟“平台设备”驱动为例看看如何将上述概念落地。这个驱动不控制真实硬件只是在probe和remove时打印信息但完整地走通了设备模型的注册、匹配、绑定流程。5.1 定义平台设备与驱动首先我们需要定义“设备”。在平台总线中设备信息可以通过两种方式提供1) 在板级平台代码中静态定义2) 通过设备树Device Tree动态传递。为了简单我们采用第一种并以内核模块的形式同时提供设备和驱动。设备侧我们需要定义一个platform_device。// my_platform_device.c (设备模块) #include linux/module.h #include linux/platform_device.h #define DEVICE_NAME my_platform_dev #define DEVICE_NUM 1 // 平台设备资源例如内存、中断等这里我们简单起见不定义资源 static struct resource my_dev_resources[] { // 可以在这里定义内存区域、中断号等 }; // 平台设备私有数据可选 static struct my_device_platform_data { int gpio_pin; const char *label; } my_pdata { .gpio_pin 123, .label My Virtual Device, }; // 定义平台设备结构体 static struct platform_device my_platform_device { .name DEVICE_NAME, // 设备名称这是匹配的关键 .id DEVICE_NUM, .num_resources ARRAY_SIZE(my_dev_resources), .resource my_dev_resources, .dev { .platform_data my_pdata, // 将私有数据指针传递给驱动 .release my_device_release, // 设备释放函数必须提供 }, }; static void my_device_release(struct device *dev) { printk(KERN_INFO my_platform_device: device released.\n); // 通常在这里释放设备私有数据占用的内存 } static int __init my_device_init(void) { int ret; printk(KERN_INFO my_platform_device: Initializing...\n); // 向平台总线注册设备 ret platform_device_register(my_platform_device); if (ret) { printk(KERN_ERR my_platform_device: Registration failed: %d\n, ret); return ret; } printk(KERN_INFO my_platform_device: Registered successfully.\n); return 0; } static void __exit my_device_exit(void) { printk(KERN_INFO my_platform_device: Unregistering...\n); platform_device_unregister(my_platform_device); } module_init(my_device_init); module_exit(my_device_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple platform device);驱动侧我们需要定义一个platform_driver。// my_platform_driver.c (驱动模块) #include linux/module.h #include linux/platform_device.h #define DRIVER_NAME my_platform_drv // 驱动的探测函数匹配成功后调用 static int my_driver_probe(struct platform_device *pdev) { struct my_device_platform_data *pdata; printk(KERN_INFO my_platform_driver: Probing device...\n); // 获取从设备侧传递过来的平台私有数据 pdata dev_get_platdata(pdev-dev); if (pdata) { printk(KERN_INFO my_platform_driver: Got platform data: gpio_pin%d, label%s\n, pdata-gpio_pin, pdata-label); } // 这里进行实际的硬件初始化映射IO内存、申请中断、注册字符设备等 // 例如devm_ioremap_resource(pdev-dev, res); // 例如request_irq(irq, handler, flags, name, dev); printk(KERN_INFO my_platform_driver: Device probed successfully.\n); return 0; // 返回0表示成功 } // 驱动的移除函数设备断开或模块卸载时调用 static int my_driver_remove(struct platform_device *pdev) { printk(KERN_INFO my_platform_driver: Removing device...\n); // 这里进行资源释放释放中断、iounmap、注销设备等 // 例如free_irq(irq, dev); printk(KERN_INFO my_platform_driver: Device removed.\n); return 0; } // 定义驱动支持的设备ID表用于匹配 static struct platform_device_id my_driver_id_table[] { { DEVICE_NAME, 0 }, // 名称与 platform_device.name 匹配 { } // 哨兵表示结束 }; MODULE_DEVICE_TABLE(platform, my_driver_id_table); // 定义平台驱动结构体 static struct platform_driver my_platform_driver { .probe my_driver_probe, .remove my_driver_remove, .driver { .name DRIVER_NAME, // 驱动名称通常与设备名一致或用于匹配 .owner THIS_MODULE, }, .id_table my_driver_id_table, // 指向设备ID表 }; static int __init my_driver_init(void) { int ret; printk(KERN_INFO my_platform_driver: Initializing...\n); // 向平台总线注册驱动 ret platform_driver_register(my_platform_driver); if (ret) { printk(KERN_ERR my_platform_driver: Registration failed: %d\n, ret); return ret; } printk(KERN_INFO my_platform_driver: Registered successfully.\n); return 0; } static void __exit my_driver_exit(void) { printk(KERN_INFO my_platform_driver: Unregistering...\n); platform_driver_unregister(my_platform_driver); } module_init(my_driver_init); module_exit(my_driver_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple platform driver for my_platform_dev);5.2 编译、加载与观察编译为这两个模块编写简单的Makefile使用内核构建系统Kbuild进行编译。加载设备模块sudo insmod my_platform_device.ko。此时一个名为my_platform_dev.1的设备被注册到平台总线。你可以查看/sys/bus/platform/devices/应该能看到它。同时因为还没有匹配的驱动它的driver链接是无效的。加载驱动模块sudo insmod my_platform_driver.ko。驱动注册时平台总线的match函数默认比较platform_device.name和platform_driver.id_table中的名称或platform_driver.driver.name会开始工作。由于我们定义了匹配表{ my_platform_dev, 0 }与设备名匹配成功。匹配与探测总线核心调用驱动的my_driver_probe函数并将platform_device结构体指针传给它。在dmesg中你应该能看到驱动打印的“Probing device...”和获取到的平台数据信息。观察sysfsls -l /sys/bus/platform/devices/my_platform_dev.1/driver现在这个链接应该指向/sys/bus/platform/drivers/my_platform_drv表示绑定成功。ls -l /sys/bus/platform/drivers/my_platform_drv/my_platform_dev.1驱动目录下也会出现指向设备的链接。cat /sys/bus/platform/devices/my_platform_dev.1/uevent可以看到设备的环境变量其中DRIVERmy_platform_drv。卸载先卸载驱动模块sudo rmmod my_platform_driverremove函数会被调用。再卸载设备模块sudo rmmod my_platform_device设备的release函数会被调用。顺序很重要因为驱动需要先释放它管理的设备资源。通过这个简单的例子你亲手实现了一个完整的“总线-设备-驱动”匹配流程。虽然它没有操作真实硬件但骨架已经齐全。在实际开发中你只需要在probe和remove函数中填充具体的硬件操作代码如ioremap、request_irq、register_chrdev等即可。6. 深入匹配过程与热插拔事件理解了基本流程我们再来深入两个关键机制匹配的细节和热插拔uevent事件。这是设备驱动模型动态性的核心体现。6.1 匹配函数的多种玩法总线的match函数是连接设备与驱动的桥梁。不同的总线类型其匹配逻辑各不相同平台总线最常用。默认的platform_match函数按以下顺序尝试匹配设备树兼容性如果设备来自设备树dev-of_node不为空则比较驱动的of_match_table中的compatible字符串与设备树节点中的compatible属性。这是现代嵌入式Linux的主流方式实现了驱动与板级信息的解耦。ACPI匹配用于支持ACPI的系统。ID表匹配比较驱动的id_table中的名称与设备的name。名称直接匹配最后直接比较驱动的driver.name与设备的name。 在我们的例子中就是通过第3种方式id_table匹配的。PCI总线pci_match_device函数主要比较厂商IDVendor ID和设备IDDevice ID。每个PCI设备在硬件中都有唯一的VID/PID。驱动在pci_device_id表中声明自己支持的VID/PID列表。当内核扫描PCI总线发现设备时就用它的VID/PID去驱动列表中寻找。USB总线usb_match_device函数同样比较厂商ID、产品ID、设备版本号、设备类/子类/协议等。USB驱动通过usb_device_id表声明支持的范围。I2C/SPI总线匹配通常基于从机地址address和设备名称。对于使用设备树的系统同样依赖compatible字符串。编写驱动时的关键点你必须根据设备所属的总线类型正确填写驱动的匹配表。对于平台设备如果使用设备树重点就是精心设计compatible字符串并在驱动的of_match_table中声明。一个驱动可以支持多个compatible设备实现一个驱动适配多个硬件变种。6.2 热插拔与用户空间通知热插拔是设备驱动模型的“高光特性”。当设备动态地加入或离开系统时如插入U盘、拔出SD卡内核需要通知用户空间以便udev、mdev等工具能自动加载驱动、创建设备节点、设置权限等。这个过程的核心是uevent用户空间事件。当设备状态变化时注册、注销、绑定驱动、解绑驱动等内核会生成一个uevent。这个事件包含了动作类型ACTIONadd、remove、bind、unbind和设备的主要标识如SUBSYSTEM、DEVPATH、DRIVER等。事件传递的路径如下内核产生事件例如device_add()函数在设备添加到内核后会调用kobject_uevent(dev-kobj, KOBJ_ADD)。总线/类过滤与增强事件首先会经过设备所属总线类型bus_type的uevent回调函数如果定义了。这个函数可以添加总线特定的环境变量如PCI总线的PCI_SLOT_NAME。接着如果设备属于某个类class也会经过类的uevent回调。发送至用户空间最终事件通过内核的netlink套接字NETLINK_KOBJECT_UEVENT广播到用户空间。用户空间处理udevd守护进程监听这个netlink套接字。收到事件后它根据一套规则/lib/udev/rules.d/进行匹配并执行相应的动作如modprobe加载驱动、mknod创建设备节点、设置sysfs属性、执行自定义脚本等。一个典型的热插拔序列插入一个USB存储设备USB核心探测到新设备创建usb_device调用device_add()。生成ACTIONaddSUBSYSTEMusb的uevent。udevd收到事件可能根据udev规则暂时不做特殊处理。usb-storage驱动与设备匹配成功绑定调用驱动的probe。在probe中usb-storage驱动会创建对应的scsi设备sd。scsi子系统添加新磁盘生成新的ACTIONaddSUBSYSTEMblockDEVTYPEdisk的uevent。udevd收到这个block事件根据规则如60-persistent-storage.rules为磁盘创建持久的/dev/disk/by-*链接并可能通知桌面环境弹出“发现新硬件”提示。理解uevent机制对于调试设备识别问题、编写自定义的udev规则来实现特定的设备管理策略如自动挂载、权限设置至关重要。你可以通过udevadm monitor --kernel --property --subsystem-matchblock命令实时观察block子系统相关的内核uevent。7. 设备模型在驱动开发中的高级应用与避坑指南掌握了基础我们来看看在实际项目中如何利用设备模型写出更健壮、更专业的驱动以及有哪些常见的“坑”。7.1 资源管理与设备树现代嵌入式Linux驱动开发设备树Device Tree几乎已成为标准。它彻底将硬件描述从内核代码中剥离出来实现了“一个内核多种板卡”。驱动侧不再通过platform_data获取硬件信息而是通过of_系列函数从设备树节点中读取。// 在 probe 函数中 struct device_node *np pdev-dev.of_node; int irq_num; u32 reg_val; if (!np) { dev_err(pdev-dev, No device tree node found.\n); return -EINVAL; } // 获取中断号 irq_num platform_get_irq(pdev, 0); // 推荐使用这个API // 或 irq_num irq_of_parse_and_map(np, 0); // 获取寄存器地址资源 res platform_get_resource(pdev, IORESOURCE_MEM, 0); base_addr devm_ioremap_resource(pdev-dev, res); // 自动管理内存映射 // 读取设备树中的自定义属性 of_property_read_u32(np, my-custom-clock-frequency, reg_val);驱动的匹配表也变为of_device_id。static const struct of_device_id my_driver_of_match[] { { .compatible vendor,my-device-1.0 }, { .compatible vendor,my-device-2.0 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_driver_of_match); static struct platform_driver my_driver { .driver { .name ..., .of_match_table my_driver_of_match, // 关键 }, .probe ..., .remove ..., };设备树侧在板级的.dts或.dtsi文件中描述硬件。soc { // 假设是某个SOC节点 my_device: my-device10000000 { compatible vendor,my-device-1.0; // 必须与驱动中的字符串完全匹配 reg 0x10000000 0x1000; // 寄存器地址和大小 interrupts GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH; // 中断号 my-custom-clock-frequency 50000000; // 自定义属性 status okay; }; };避坑点compatible字符串必须精确匹配包括标点符号。驱动中的of_match_table顺序决定了匹配优先级通常把最具体、最新的兼容性字符串放在前面。7.2 电源管理集成设备驱动模型为电源管理如系统休眠、唤醒提供了完美支持。驱动的struct device_driver或struct class中可以挂载pm_ops电源管理操作集。static const struct dev_pm_ops my_device_pm_ops { .suspend my_device_suspend, // 系统进入休眠时调用 .resume my_device_resume, // 系统从休眠恢复时调用 .freeze my_device_freeze, // 休眠前冻结进程时调用 .thaw my_device_thaw, // 解冻后、恢复前调用 .poweroff my_device_poweroff, // 关机前调用 .restore my_device_restore, // 从休眠镜像恢复后调用 // 还有 runtime PM 相关的 .runtime_suspend/.runtime_resume }; static struct platform_driver my_driver { .driver { .name ..., .pm my_device_pm_ops, // 挂载电源管理操作 }, ... };在suspend函数中你需要保存设备状态可能的话将其置入低功耗模式。在resume函数中恢复设备状态。内核在系统休眠/唤醒流程中会按照设备树的依赖顺序子设备先于父设备suspend晚于父设备resume调用所有设备的电源管理回调。如果你的设备不支持某种状态对应的回调可以留空或返回0。避坑点电源管理回调函数中不能进行可能休眠的操作如mutex_lock、wait_event等因为此时系统进程可能已被冻结。runtime PM运行时电源管理是另一个话题用于设备空闲时自动进入低功耗状态实现更精细的能耗控制。7.3 sysfs属性文件创建除了总线、类自动创建的属性驱动也可以为自己的设备创建自定义的sysfs属性文件用于调试或配置。// 定义属性 static ssize_t my_attr_show(struct device *dev, struct device_attribute *attr, char *buf) { return sprintf(buf, %d\n, some_internal_value); } static ssize_t my_attr_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { int ret; ret kstrtoint(buf, 10, some_internal_value); if (ret) return ret; return count; } static DEVICE_ATTR_RW(my_attr); // 创建可读写的属性 my_attr // 在 probe 函数中创建属性文件 ret device_create_file(pdev-dev, dev_attr_my_attr); if (ret) { dev_err(pdev-dev, Failed to create sysfs attribute.\n); goto err_out; } // 在 remove 函数中移除 device_remove_file(pdev-dev, dev_attr_my_attr);更现代和推荐的方式是使用属性组一次性创建/移除一组属性。static struct attribute *my_dev_attrs[] { dev_attr_my_attr.attr, dev_attr_another_attr.attr, NULL, }; ATTRIBUTE_GROUPS(my_dev); // 定义属性组宏 static struct device_driver my_driver { .driver { .name ..., .groups my_dev_groups, // 驱动级别的属性组 }, }; // 或者在 device 的 .groups 成员中指定避坑点sysfs属性文件的show和store函数会由用户空间的读/写操作直接调用运行在进程上下文。必须做好并发控制如使用mutex保护共享数据并且store函数必须验证用户输入的有效性防止内核崩溃或安全漏洞。属性文件的命名应清晰避免与内核已有的属性冲突。7.4 驱动与设备的多对多关系一个驱动可以匹配多个设备如一个USB摄像头驱动可以驱动多个同型号摄像头这是常见的一对多。但设备驱动模型也支持更复杂的多对多关系主要通过驱动绑定属性实现。在/sys/bus/xxx/devices/xxx/目录下有一个driver_override文件。用户空间可以向这个文件写入一个驱动名称强制该设备绑定到指定的驱动即使它不是最佳匹配。同样在/sys/bus/xxx/drivers/xxx/目录下有bind和unbind文件。向bind文件写入设备名可以强制驱动绑定该设备向unbind写入设备名可以解除绑定。这个机制常用于调试、驱动测试或者在某些特殊场景下覆盖默认的匹配策略。例如一个设备可能被一个通用的驱动匹配但你想测试一个正在开发的新驱动就可以用driver_override来强制绑定。核心经验设备驱动模型是一个强大的框架但“能力越大责任越大”。你必须清晰地管理好设备的生命周期probe/remove、资源使用devm_系列API进行自动管理、并发和电源状态。深入理解kobject、kset、ktype的引用计数机制是避免内存泄漏和use-after-free错误的关键。当你写的驱动能完美地处理热插拔、电源管理并通过sysfs提供丰富的调试接口时你才算真正掌握了Linux设备驱动开发的精髓。
返回列表