字符设备驱动框架深入解析

张开发
2026/4/6 7:44:02 15 分钟阅读

分享文章

字符设备驱动框架深入解析
昨天调一个串口驱动发现设备节点权限总是不对。ls -l /dev/ttyS0一看权限变成了600普通用户根本打不开。查了半天最后发现是device_create()时没设置正确的dev_t导致系统分配了默认的设备号。这种问题在字符设备驱动开发中太常见了今天咱们就彻底拆解一下字符设备驱动的框架。从那个经典的open()说起用户空间调用open(/dev/mydev, O_RDWR)时内核到底做了什么很多人只知道会调用驱动里的my_open()函数但中间的转换过程才是关键。VFS根据设备节点找到对应的inodeinode-i_cdev指向我们在驱动里注册的cdev结构然后通过cdev-ops找到我们实现的那套file_operations函数集。这个过程要是没理清楚调试的时候根本不知道断点该下在哪。老派做法register_chrdev()先看这个传统接口现在还有不少老驱动在用staticintmajor0;// 写0让内核动态分配写死的话容易冲突module_init(drv_init);staticint__initdrv_init(void){majorregister_chrdev(0,mydev,fops);// 这里踩过坑返回值是主设备号不是错误码if(major0){printk(register failed\n);returnmajor;// 负数是错误码}// 老代码这里经常缺device_create导致/dev下没节点return0;}这种写法简单粗暴一个函数把主设备号、设备名、操作函数全注册了。但问题很明显它自动注册了256个次设备号如果你只需要一个设备那剩下的255个全浪费了。更麻烦的是/dev下不会自动创建设备节点得靠mdev或udev或者手动mknod。新派做法cdev系列现在推荐的是这套组合拳staticstructcdevmy_cdev;staticdev_tdevno;// 这个dev_t包含了主次设备号staticint__initdrv_init(void){// 先申请设备号if(alloc_chrdev_region(devno,0,1,mydev)0){return-EBUSY;// 设备号被占用了}// 初始化cdev结构体cdev_init(my_cdev,fops);my_cdev.ownerTHIS_MODULE;// 这个别漏模块卸载时有用// 添加到系统if(cdev_add(my_cdev,devno,1)0){unregister_chrdev_region(devno,1);return-EFAULT;}// 创建设备节点device_create(cls,NULL,devno,NULL,mydev);// cls需要先创建类return0;}这套流程看起来步骤多但每个环节都可控。特别是alloc_chrdev_region()你可以精确控制申请多少个次设备号。我一般喜欢动态分配主设备号避免和系统已有驱动冲突。关于file_operations的那些坑实现操作函数集时有些细节容易忽略staticstructfile_operationsfops{.ownerTHIS_MODULE,// 这个必须有防止模块在用的时候被卸载.openmy_open,.releasemy_close,// 注意是release不是close.readmy_read,.writemy_write,.unlocked_ioctlmy_ioctl,// 新内核用这个不是ioctl// .compat_ioctl my_compat_ioctl, // 64位系统要兼容32位应用的话需要这个};.release不是.close这个命名历史原因写错了编译不报错但永远不会被调用。.unlocked_ioctl在新内核里替代了老的.ioctl区别在于前者不带大内核锁。如果驱动要支持32位应用调用ioctl还得实现.compat_ioctl。读写函数的经典模式read()和write()的实现有个固定套路staticssize_tmy_read(structfile*filp,char__user*buf,size_tsize,loff_t*offset){charkernel_buf[128];intlen;// 1. 准备数据从硬件或内部bufferlenprepare_data(kernel_buf,sizeof(kernel_buf));// 2. 计算能拷贝多少if(lensize)lensize;if(len0)return0;// 3. 拷贝到用户空间if(copy_to_user(buf,kernel_buf,len)){return-EFAULT;// 用户空间地址有问题}// 4. 更新偏移量*offsetlen;returnlen;// 返回实际拷贝的字节数}copy_to_user()和copy_from_user()这两个函数看似简单但返回0表示成功非0表示失败很多人容易写反判断条件。还有驱动里不要直接操作用户空间指针必须通过这两个函数。ioctl的现代写法以前用_IO()、_IOR()那些宏定义命令号现在更推荐用ioctl命令编码器#defineMY_MAGICx// 随便选个字符别和别的驱动冲突#defineMY_CMD1_IOR(MY_MAGIC,1,int)#defineMY_CMD2_IOW(MY_MAGIC,2,structmy_data)staticlongmy_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){void__user*uarg(void__user*)arg;switch(cmd){caseMY_CMD1:{intvalue;// 从驱动读数据到用户空间if(copy_to_user(uarg,value,sizeof(value)))return-EFAULT;break;}caseMY_CMD2:{structmy_datadata;// 从用户空间写数据到驱动if(copy_from_user(data,uarg,sizeof(data)))return-EFAULT;// 处理databreak;}default:return-ENOTTY;// 不支持的命令}return0;}命令号的定义要保证全局唯一通常用设备名的首字母作为魔数。_IOR是读驱动到用户_IOW是写用户到驱动_IOWR是双向。自动创建设备节点的正确姿势手动mknod太不专业现在都自动创建staticstructclass*cls;staticint__initdrv_init(void){// 先创建类会在/sys/class下出现clsclass_create(THIS_MODULE,myclass);if(IS_ERR(cls)){returnPTR_ERR(cls);}// 创建设备这个会在/dev下创建节点device_create(cls,NULL,devno,NULL,mydev);return0;}staticvoid__exitdrv_exit(void){device_destroy(cls,devno);class_destroy(cls);// 清理顺序不能乱}注意销毁顺序要和创建顺序相反。class_create()现在有些内核版本改名叫class_create()编译时注意看错误提示。调试经验谈字符设备驱动出问题按这个顺序查dmesg看内核打印注册失败这里会有提示cat /proc/devices看设备号是否分配成功ls -l /sys/class/看类是否创建ls -l /dev/看设备节点权限和主次设备号strace跟踪用户空间调用看open()返回什么错误码权限问题最常见检查device_create()的参数或者用udev规则。还有一个坑cdev_add()之后驱动就生效了但device_create()可能失败这时候用户能open但实际没硬件操作这种问题最难查。个人建议别再用register_chrdev()了哪怕你只写一个简单的测试驱动。用cdev那套流程虽然多几行代码但结构清晰调试方便。file_operations里的函数指针没实现的别赋值NULL直接不写就行内核会处理。模块卸载函数一定要写完整cdev_del()、unregister_chrdev_region()、device_destroy()、class_destroy()一个都不能少顺序还得对。我见过太多驱动卸载后设备号没释放重新加载就失败的案例。最后驱动代码里多加点printk用pr_debug或dev_dbg调试完可以关掉。真实硬件调试时你不可能总是用kgdb打印信息是最实在的。

更多文章