Linux设备模型深度解析:总线、设备、驱动与sysfs的协同工作

Linux设备模型深度解析:总线、设备、驱动与sysfs的协同工作
1. 项目概述为什么需要理解Linux设备模型如果你写过Linux字符设备驱动大概率是从register_chrdev、file_operations结构体开始的。这很直接一个主设备号一套操作函数设备就能在/dev下出现。但当你开始接触更复杂的硬件比如一个集成了温度传感器、GPIO控制器和PWM输出的多功能芯片或者当你需要管理一个USB集线器下挂载的多个设备时仅仅靠file_operations就显得力不从心了。你会发现/sys目录下那些层层嵌套的目录和文件远比/dev下的节点包含更多信息设备的电源状态、驱动绑定关系、甚至设备之间的父子依赖。这套支撑/sys文件系统并统一管理设备、驱动、总线之间复杂关系的底层框架就是Linux设备模型。简单来说Linux设备模型是内核中一套用于对计算机上所有设备进行抽象、组织和管理的基础设施。它不是为了直接给应用程序提供读写接口那是/dev和file_operations的事而是为了给内核自身以及其他子系统如电源管理、热插拔、设备树提供一个统一的设备视图和操作模型。对于驱动开发者而言深入理解设备模型意味着你能写出更规范、更健壮、更能融入Linux生态的驱动而不是一个孤立的“能跑就行”的模块。当你的驱动开始涉及设备树、平台设备、设备属性导出或电源管理回调时设备模型的知识就从“加分项”变成了“必需品”。2. 设备模型的核心组件总线、设备、驱动与类Linux设备模型可以看作一个“相亲大会”其中有三大核心角色设备、驱动和总线。/sys文件系统则是这个大会的“信息公告栏”。此外类作为一个逻辑分组概念提供了另一种视角。2.1 总线设备与驱动的“红娘”总线是设备模型的基石它定义了一套匹配规则将探测到的设备与合适的驱动配对。内核中常见的总线有platform_bus_type平台总线、pci_bus_type、i2c_bus_type、spi_bus_type等。总线的核心数据结构是struct bus_type。它最关键的两个成员是match和probe函数指针。match函数当一个新设备或新驱动被注册到总线上时总线核心会调用此函数判断给定的设备和驱动是否匹配。匹配的依据可以是设备树兼容字符串、ACPI ID、设备名称、PCI厂商/设备ID等取决于总线类型。例如对于平台设备match函数通常会比较设备名称或设备树节点的compatible属性与驱动中声明的字符串。probe函数当match成功后总线核心会调用驱动的probe函数注意这里是总线类型的probe它通常会去调用设备驱动自己的probe函数来初始化设备。在/sys/bus/目录下每个注册的总线类型都会有一个子目录如/sys/bus/platform/。其下通常有devices和drivers两个子目录分别链接到该总线下所有已发现的设备和已注册的驱动。2.2 设备硬件实体的抽象设备是硬件在内核中的代表。其核心数据结构是struct device。一个设备必须属于某一条总线即使是虚拟的platform总线。设备结构体包含了大量信息父设备指针用于构建设备树状结构。例如一个USB鼠标设备的父设备是它所连接的USB端口USB端口的父设备是USB主机控制器。这种父子关系在/sys中体现为目录嵌套。设备号对于需要用户空间接口的设备会关联dev_t。驱动指针指向当前绑定到该设备的struct device_driver。私有数据一个void *类型的指针驱动可以用它来存储指向自己私有设备结构体的指针。设备树节点如果设备来自设备树这里会指向对应的struct device_node。属性组用于在/sys/class/...或/sys/devices/...下自动创建属性文件。设备注册的典型API是device_register()或platform_device_register()后者是前者的封装用于平台设备。注册后设备会在对应的/sys/bus/xxx/devices/下出现并等待驱动来匹配。2.3 驱动设备的“灵魂”驱动是知道如何控制特定设备的软件模块。其核心数据结构是struct device_driver对于平台设备更常用的是其子结构struct platform_driver。驱动结构体的关键成员包括名称驱动名称用于匹配。总线类型指向该驱动所属的struct bus_type。probe函数当总线核心为它找到一个匹配的设备时会调用此函数。这是驱动初始化的核心在这里分配资源、注册设备节点、初始化硬件等。remove函数当设备被移除或驱动被卸载时调用用于清理资源。设备ID表一个数组列出了该驱动所能支持的所有设备标识符如设备树兼容字符串列表、PCI ID表等。这是match函数的重要依据。驱动注册的API是driver_register()或platform_driver_register()。注册后驱动会出现在/sys/bus/xxx/drivers/目录下。2.4 类功能视角的逻辑分组总线是从物理连接角度分类而类是从设备功能角度进行的逻辑分组。例如所有的网络接口设备无论是以太网卡、Wi-Fi还是USB网卡都属于net类所有的输入设备键盘、鼠标、触摸屏都属于input类所有的TTY终端设备都属于tty类。类的核心数据结构是struct class。它的一个主要作用是当属于该类的设备成功probe后可以在/sys/class/目录下自动创建一个标准的设备节点如/sys/class/net/eth0并且可以通过class_create_device_file()等API自动创建设备的属性文件。这对于需要向用户空间导出统一管理接口的设备类型非常有用。驱动通常通过device_create()函数在设备probe时将自己关联的device注册到某个类下。3. 设备模型的运作流程从注册到绑定理解了静态组件我们来看动态过程。以一个最简单的平台设备驱动为例看看设备模型是如何将二者联系起来的。3.1 设备与驱动的注册顺序在Linux中设备和驱动的注册顺序可以是任意的设备模型的核心机制能保证它们最终正确匹配。先注册设备假设系统启动时通过设备树解析或硬编码先注册了一个平台设备my_device。platform_device_register()会被调用内核会初始化一个struct device并将其挂载到platform_busplatform_bus_type下。在/sys/bus/platform/devices/下创建对应的目录如my_device。由于此时没有匹配的驱动设备状态为“未绑定”它会进入总线的“未匹配设备”列表等待。后注册驱动随后对应的驱动模块my_driver.ko被加载调用platform_driver_register()。内核会初始化一个struct platform_driver。遍历该总线platform_bus下所有“未匹配设备”列表中的每一个设备。对每个设备调用总线类型的match函数对于平台总线即platform_match。该函数会比较设备名称或设备树的compatible属性与驱动id_table中的字符串或驱动的.driver.name。如果找到匹配项总线核心会调用驱动结构体中指定的probe函数即platform_driver.probe。如果顺序反过来先注册驱动过程也类似驱动注册后会去遍历总线上所有已存在的设备进行匹配尝试。3.2 Probe函数的职责与生命周期驱动的probe函数是设备初始化的核心。一个典型的平台设备probe函数需要完成以下工作static int my_driver_probe(struct platform_device *pdev) { struct my_device_data *data; struct device *dev pdev-dev; int ret; // 1. 获取设备资源内存、IO、中断、时钟等 struct resource *mem_res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!mem_res) { dev_err(dev, Failed to get memory resource\n); return -EINVAL; } // 2. 分配驱动私有数据结构 data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; // 3. 映射寄存器地址 >static const struct of_device_id my_driver_of_match[] { { .compatible vendor,my-device-1 }, { .compatible vendor,my-device-2 }, {}, }; MODULE_DEVICE_TABLE(of, my_driver_of_match); static struct platform_driver my_driver { .driver { .name my-driver, .of_match_table my_driver_of_match, }, .probe my_driver_probe, .remove my_driver_remove, };ACPI匹配在x86等支持ACPI的系统中会尝试ACPI ID匹配。ID表匹配检查驱动的id_tableplatform_device_id数组是否与设备名称匹配。名称匹配最后直接比较驱动的.driver.name和设备的名称。因此为设备树设备编写驱动时首要任务就是正确填写of_match_table。5.3 在驱动中访问设备树属性在驱动的probe函数中可以通过platform_device的dev.of_node成员使用OFOpen FirmwareAPI来读取设备树中的属性struct device_node *np pdev-dev.of_node; const char *label; int value; u32 reg_values[2]; // 读取字符串属性 of_property_read_string(np, label, label); // 读取整数属性 of_property_read_u32(np, clock-frequency, value); // 读取数组属性 if (of_property_read_u32_array(np, reg, reg_values, 2) 0) { // reg_values[0] 通常是地址reg_values[1] 是大小 } // 读取GPIO描述符更现代、安全的方式>static struct class *led_class; // 在驱动初始化函数中或模块初始化函数 led_class class_create(THIS_MODULE, leds); if (IS_ERR(led_class)) { return PTR_ERR(led_class); } // 在单个设备的probe函数中 struct led_classdev *led_cdev; led_cdev devm_kzalloc(dev, sizeof(*led_cdev), GFP_KERNEL); led_cdev-name myled; led_cdev-brightness_set myled_brightness_set; ret led_classdev_register(dev, led_cdev); if (ret) { dev_err(dev, Failed to register LED class device\n); return ret; } // 成功后会在/sys/class/leds/myled/下出现brightness等属性文件。这样用户空间就可以通过/sys/class/leds/myled/brightness来统一控制这个LED与内核中其他LED设备的行为保持一致。理解Linux设备模型是从“让驱动跑起来”到“写出一个符合Linux内核规范的好驱动”的关键一步。它初看复杂但一旦理清了总线、设备、驱动这三者的关系和/sys这个信息窗口很多之前模糊的概念就会变得清晰。下次当你再面对一个复杂的硬件驱动时不妨先从/sys目录开始探索顺着设备模型的线索你就能更快地理解整个驱动的骨架和脉络。