ARTICLE DETAIL

资讯详情

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

Linux SPI设备驱动:class_register与设备节点创建机制解析

Linux SPI设备驱动:class_register与设备节点创建机制解析 这么说吧做Linux SPI设备驱动的时候似乎绕不开“设备节点”这三个字。以前调一块SPI温度传感器驱动起来之后想让应用程序通过read/write直接访问寄存器第一反应就是去创建一个/dev下的节点。搜出来的答案常常是class_create加device_create但很多人把class_create换成class_register就懵了或者明明注册了class/dev下就是没有设备然后一头雾水。这篇系列文章正好讲到SPI驱动部分的第七篇我就单独把class_register这层窗户纸捅破连带讲清楚class、device、devtmpfs之间到底是怎么协作的以及在SPI驱动里应该在哪个环节注册class、怎么避免卸载时宕机。内容安排上适合刚把spi_driver跑通、打算给驱动加用户空间接口的人也适合那些看过内核spi.c和spidev源码但还没搞明白class体系的朋友。我不会把设备模型全部展开只挑和SPI驱动最相关的那几条线尽量用实际的代码和踩坑记录说话。1. 从SPI驱动框架说起为什么需要class_register1.1 Linux设备模型那张网Linux内核里的设备模型经常被形容成“一堆结构体互相指来指去”这话糙理不糙。bus_type、device、device_driver、class这四类对象是最核心的骨架。SPI子系统本身就注册了一个bus_type那就是spi_bus_typeSPI控制器是deviceSPI客户端驱动是device_driverspi_register_driver就是把这个driver挂到spi_bus_type上。这组关系解决的是“哪个驱动可以支持哪个设备”的匹配问题和用户空间怎么访问设备没有直接关系。class则是另一条线。class解决的是“设备属于什么类别”“用户空间去哪里找设备节点”它在设备模型里不参与匹配不影响probe但它直接决定了/sys/class下面出现什么目录也决定了/dev下自动创建设备节点时的名字和权限。很多SPI驱动都涉及data、flash、触摸屏等设备用户空间需要统一入口这时候class就派上用场了。class_register正是这个class对象进入内核的入口。这里要区分清楚spi_register_driver和class_register是两条毫不冲突的路径。前者是总线侧后者是设备模型侧。一个SPI驱动完全可以只调用spi_register_driver不注册任何class如果你只是做一个内核内部使用的驱动或者通过netlink、miscdevice等其他方式暴露接口class不是必需品。但绝大多数SPI驱动最终要给上层应用用比如spidev、spi-nor的mtd设备用户空间访问都需要一个属于class的设备节点于是class_register/class_create就成了绕不开的函数。1.2 SPI子系统中class的典型位置先看现实中的例子。spidev驱动是SPI通用设备驱动它注册了一个名为spidev的class然后为每一个打开的SPI设备创建设备节点spidevB.C。节点的生成路径是driver在probe里调用device_create或者device_create_with_groups把device挂到class下devtmpfs监听到uevent后自动在/dev下生成对应设备文件。SPI控制器自身也有一个class。在内核的drivers/spi/spi.c里spi_master设备注册时会把它挂到一个spi_master的class下面这样你在/sys/class/spi_master/下能看到spi0、spi1这样的控制器目录。这两个class都涉及相同的底层机制class_register。所以你会发现只要是走Linux设备模型的正规军几乎处处都有人注册class。把class_register搞懂不仅SPI驱动能用任何用device_create创建设备节点的驱动都能用。这也是我建议SPI驱动开发者在学完spi_transfer之后一定要把class机制补上的原因。1.3 class_register和misc_register的适用边界有些朋友会问SPI驱动想暴露用户空间接口直接用misc_register注册一个miscdevice不也能在/dev下生成节点吗为什么还要专门搞class这块确实值得说道说道。miscdevice内部也是基于class的misc子系统维护了一个misc class自动创建设备。它的优势是代码简单、不需要手动管理主设备号适合那些不占满一整个主设备号、设备数量很少的驱动。但miscdevice有几个局限它不灵活设备节点名字由miscdevice的name决定属性文件、设备与类的关系都由misc子系统包办类别永远是misc想按业务分类做/sys/class/xxx路径就比较别扭一个模块如果需要创建设备数量很大、或需要多个不同名字的子设备用miscdevice也没法精细控制。SPI驱动面向的往往是一整组设备比如同一块SPI总线上挂多个flash、多个传感器主设备号共享但每个设备节点有自己的名字和独立属性用class加device_create的方式最顺手。misc_register内部虽省事但少了学习价值。理解class_register之后再看miscdevice其实就是“class的一个固定用法”。所以我的建议是如果只是临时demomisc_register能用如果要做正式驱动尤其要加sysfs属性、按类管理设备老老实实走class_register。2. class_register到底注册了什么2.1 class结构体关键字段struct class在内核头文件include/linux/device.h里定义长得挺长但真正需要关心的字段不多。name就是/sys/class/目录下显示的名字owner通常是THIS_MODULEclass_attrs和dev_groups用于生成sysfs属性文件dev_uevent是写热插拔事件时用的回调class_release和dev_release则是释放时的回调。这里特别要强调release回调。内核里经常出现“没有release函数就不能卸载”的问题这在class对象的注册上同样存在。class结构体里class_release会在class_unregister后被调用如果你用class_create分配的class对象class_destroy内部会处理free但如果你自己静态定义一个struct class变量然后直接调用class_register就需要在class_release里做一些清理并且注销时只能用class_unregister而不是class_destroy否则内核会尝试去kfree一块静态内存直接报错。还要理解dev_groups和class_attrs的区别。class_attrs是挂在class目录下的属性文件比如/sys/class/spi_master/下可能有global属性dev_groups则是以设备名称为目录、挂在具体设备属性目录下的属性组。SPI驱动自定义传感器参数、校准值、控制模式时通常用dev_groups比较合理因为它和具体设备绑定不会多个设备互相乱串。2.2 class_register与class_create的取舍class_register是底层函数原型为int class_register(struct class *cls);它要求你事先准备好一个struct class实例包括所有回调。class_create则是一个便捷封装在include/linux/device.h中被定义成宏或内联函数具体实现是分配一个class对象填充owner和name然后调用class_register。它的好处就是简单class_create(THIS_MODULE, my_spi_class)一行完事。对应销毁时用class_destroy它会调用class_unregister再释放内存。什么时候直接用class_register当你想注册一个带更多自定义字段、带class_release、需要在注册时绑各种属性的class时用class_create很难把initial代码传进去你就自己填充struct class再调用class_register。我在某些SPI flash驱动里看到过自定义class_release的用法目的就是为了能在类销毁时统一释放一块分配给类级别属性的缓存。这种场景直接用class_create就不方便因为那内核分配class对象的时机你控制不了。比较稳妥的原则九成以上的SPI驱动用class_create就够它内部已经做了正确分配和注册但你必须知道背后是class_register否则看到网上代码里一会儿class_register一会儿class_create就会晕。知道class_create等价于kzallocclass_register很多问题就能想通。2.3 class注册之后的内核行为class_register成功之后内核会在sysfs中创建/sys/class/name目录并且会在设备模型里维护一个class链表。所有后续device_create操作都会在这个class目录下生成一个符号链接或者子目录指向真实设备在/devices下的路径。同时当设备被创建时devtmpfs会读取class下的uevent信息在/dev下生成设备节点。这背后是设备模型自身的事件机制。设备在device_add时会调用devtmpfs_create_node并且发送uevent到用户空间udev/systemd-udevd会读取/sys/class/xxx下的设备信息应用自定义规则设置节点权限和名字。很多时候你发现设备节点权限不对其实不是驱动问题而是udev规则没写。驱动这边要做的就是正确地把class注册好把device挂在class下。class_register注册成功只是第一步后面device_create才是节点能否生成的关键但class_register如果name和已有class重名会返回错误码device_create自然也就无从谈起。3. 在SPI设备驱动里实操class_register3.1 注册流程与编码参考写一个标准的SPI客户端驱动目标是通过class注册设备节点让用户空间可以用open/read/write/ioctl访问一块SPI从设备。主设备号不必固定可以用alloc_chrdev_region动态分配。整体流程分四步分配主设备号、注册class、注册cdev、在SPI probe里创建具体的device节点。先看初始化侧代码#include linux/device.h #include linux/fs.h #include linux/cdev.h #include linux/spi/spi.h static dev_t spi7_devno; static struct class *spi7_class; static struct cdev spi7_cdev; static int spi7_major; static int __init spi7_init(void) { int ret; ret alloc_chrdev_region(spi7_devno, 0, 1, spi7); if (ret 0) return ret; spi7_class class_create(THIS_MODULE, spi7); if (IS_ERR(spi7_class)) { ret PTR_ERR(spi7_class); goto err_region; } cdev_init(spi7_cdev, spi7_fops); spi7_cdev.owner THIS_MODULE; ret cdev_add(spi7_cdev, spi7_devno, 1); if (ret) goto err_class; spi7_major MAJOR(spi7_devno); pr_info(spi7: registered class and chrdev, major %d\n, spi7_major); return 0; err_class: class_destroy(spi7_class); err_region: unregister_chrdev_region(spi7_devno, 1); return ret; }这段代码里用的是class_create而不是class_register。如果非要用class_register你就得自己定义一个struct class变量填充name和owner然后调用class_register。大多数新手喜欢用class_create没毛病但你要清楚class_destroy的对称关系。注意class_create返回的不是错误码而是错误指针必须用IS_ERR和PTR_ERR不能直接if (!spi7_class)判空这块我见过太多人写错了。3.2 与device_create配合暴露用户空间接口class注册好了cdev也添加上去了但/dev下还没有节点必须在设备真正存在的时点创建设备节点。SPI驱动里合适的时机就是probe回调因为probe代表设备已经匹配成功、可以被服务了。在probe里调用device_createstatic int spi7_probe(struct spi_device *spi) { struct device *dev; dev device_create(spi7_class, spi-dev, spi7_devno, NULL, spi7.%d.%d, spi-master-bus_num, spi-chip_select); if (IS_ERR(dev)) return PTR_ERR(dev); dev_set_drvdata(spi-dev, dev); /* 后续还可以创建设备属性文件 */ return 0; }device_create的第二个参数是parent device这里填spi-dev这样在sysfs里这个节点会挂到SPI设备下父子关系清楚。第三个参数是设备号也就是我们要给这个设备分配的dev_t。最后一个可变参数是设备节点的名字格式。SPI设备习惯用spiX.Y的格式X是controller numberY是片选号比如spi7.0、spi7.1这样用户空间看到名字就知道挂在哪个总线。这里很多人的疑问是dev_t已经被cdev_add过了device_create又用同一个dev_t会不会冲突不会。cdev_add负责字符设备层的设备号分配device_create负责在设备模型里生成设备节点并绑定到class。二者通过dev_t关联open打开设备时内核通过dev_t找到cdev。class只是负责生成设备和提供sysfs信息它的存在不上cdev。3.3 卸载侧的对称操作与坑有注册就有卸载对称性必须严格保证。卸载时同样有顺序问题很多人是这样写的static void __exit spi7_exit(void) { device_destroy(spi7_class, spi7_devno); cdev_del(spi7_cdev); class_destroy(spi7_class); unregister_chrdev_region(spi7_devno, 1); }看起来挺对但SPI驱动的device是在probe里创建的所以卸载应该在remove里先销毁节点然后在exit里再销毁class。如果你的驱动只支持一个设备顺序无所谓如果有多个SPI从设备probe被调用多次你的device_destroy就不能只调一次。每个probe里创建的device都要在对应的remove里destroy然后再在module_exit里统一销毁class和cdev。另一个经典坑是卸载时忘记销毁设备节点。模块卸载了但/sys/class/spi7下还残留着目录设备节点也还在/dev下你可以open但会挂。原因就是device_destroy没有配对。解决思路是在spi_driver结构体里的.remove回调中用dev_get_drvdata拿到之前保存的struct device指针然后device_destroy(spi7_class, dev-devt)再释放私有数据。正确的remove大概这样static void spi7_remove(struct spi_device *spi) { struct device *dev dev_get_drvdata(spi-dev); if (dev) device_destroy(spi7_class, dev-devt); dev_set_drvdata(spi-dev, NULL); }到这里整个注册、创建设备节点、卸载、销毁节点的闭环就完整了。class_register在这个闭环里看起来就是个开头但它决定了后来所有设备的归属。一旦class名字重复、属性初始化带错、或者class_release回调缺失后面设备创建和卸载都会出连锁反应。4. 常见问题与排查实录4.1 设备节点没生成这是SPI驱动配合class机制时遇到最多的现象insmod之后/sys/class/spi7目录存在/sys/class/spi7/下面也能看到设备子目录但/dev/spi7.0就是不存在。先别怀疑class_register大概率问题出在devtmpfs或udev。检查内核配置CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT是否开启。很多嵌入式系统为了省内存默认没开devtmpfs设备节点必须靠busybox的mdev脚本从/sys/class里扫描生成但脚本里没有对应规则设备节点自然出不来。这种情况下手动mknod /dev/spi7.0 c 240 0如果节点能工作那就说明驱动和class都没问题是用户空间的设备管理策略缺失。还可以用uevent手动触发一次echo /sys/class/spi7/spi7.0 /sys/class/spi7/spi7.0/uevent或者对设备目录下执行echo 1 /sys/class/spi7/spi7.0/uevent这会重新发一次uevent。如果是udev的规则问题触发后系统日志里能看到相关事件。4.2 class属性文件读写报错我在某些SPI驱动里给设备的class目录加了属性怎么读都返回Permission denied仔细一看是权限文件设置不对。class属性文件用的是struct class_attribute里面show/store函数的参数比较特殊show函数签名为ssize_t (*show)(struct class *class, struct class_attribute *attr, char *buf)store函数签名为ssize_t (*store)(struct class *class, struct class_attribute *attr, const char *buf, size_t count)。它不像普通device_attribute那样第一个参数是device。如果你把device属性文件写法套到class属性上编译能过但运行必崩因为参数类型完全不一样内核通过container_of取出来的地址是错的。另外在使用class_create创建的class上挂class_attribute需要在class_create返回的class结构体里直接赋值class_attrs数组或者用sysfs_create_group在class目录下建属性组。如果只是给每个设备建独立属性用dev_groups字段更合适传给device_create即可。我自己的经验是SPI驱动的用户空间接口属性尽量绑定到具体设备不要放到class层面否则多个SPI设备实例会互相干扰很难排查。4.3 sysfs目录冲突和模块加载失败第三种典型情况是class_register返回-EEXISTinsmod直接失败。原因往往是已经有同名class存在。比如你给自己的驱动起了一个class名字叫spi_master内核本身的spi_master_class早就注册了这就撞车了。遇到的时候先查一下ls /sys/class/看一下是不是已经有同名目录。注意名字冲突不仅在/sys/class下能看出来在内核里也是全系统唯一。class_register处理不了重名这不是动态覆盖只会直接报错。还有一点容易被忽视同一个模块在多次insmod/rmmod过程中如果上一次没有完全清理class_destroy调用失败或device_destroy没执行第二次insmod也可能遇到类目录残留在内存里。所以rmmod后最好立即ls /sys/class/spi7确认目录消失。我之前遇到过rmmod报Device or resource busy一查是有个进程一直占着/dev节点没close导致device_destroy全部失败class_destroy也连带失败。解决方法是先kill掉打开该设备的进程再rmmod。对class_register的理解我个人最大的收获是它看起来只是设备模型里的一个小函数但从它身上能把bus、class、devtmpfs、udev这一整套链路串起来。做SPI驱动时不要只盯着spi_transfer和spi_async把class这套用户空间接口机制搞顺才能让驱动不只是“能跑”而是真正好用。后面如果继续写SPI系列我会接着讲device_create里的groups属性组怎么做、SPI DMA路径在注册设备节点时有什么额外注意事项这回先到这里。
返回列表