字符设备驱动file_operations 与读写实现字符设备是 Linux 驱动最基础、最常用的一类。这篇讲字符设备驱动的核心框架设备号、cdev 结构体、file_operations 操作集、open/read/write 实现以及用户空间和内核空间的数据拷贝。大家好我是黒漂技术佬。内核模块搞明白了接下来正式写驱动。字符设备驱动是最基础的一类也是学习其他驱动的基础。串口、GPIO、LED、按键、I2C 设备……底层都是字符设备。这篇讲字符设备的完整框架设备号、cdev 注册、file_operations、读写实现、用户态和内核态的数据交互。一、字符设备是什么特点按字节流顺序访问的设备应用层通过/dev/xxx设备文件操作用 open / read / write / close / ioctl 等标准接口大多数简单外设都是字符设备设备号每个字符设备有一个唯一的设备号由两部分组成主设备号标识是哪一类驱动比如都是串口驱动 次设备号标识同类中的第几个设备设备号 主设备号 20 | 次设备号用dev_t类型存。操作宏#includelinux/kdev_t.hMAJOR(dev)// 提取主设备号MINOR(dev)// 提取次设备号MKDEV(ma,mi)// 合成设备号查看设备号cat/proc/devices# Character devices:# 1 mem# 4 tty# ...ls-l/dev/# crw-rw---- 1 root dialout 4, 64 ... ttyS0# ↑ ↑# 主设备号 次设备号c 开头表示字符设备character。二、设备号的申请和释放静态申请指定主设备号#includelinux/fs.hdev_tdevidMKDEV(200,0);// 主设备号200次设备号0intregister_chrdev_region(devid,count,mydev);优点固定方便缺点可能跟已有设备冲突动态分配推荐dev_tdevid;intalloc_chrdev_region(devid,0,1,mydev);// 第一个参数返回的设备号// 第二个参数次设备号起始// 第三个参数设备数量// 第四个参数设备名/proc/devices里显示的优点自动分配空闲的主设备号不会冲突缺点每次加载主设备号可能不一样释放unregister_chrdev_region(devid,count);模块卸载时一定要释放。三、cdev 结构体是什么内核里用struct cdev表示一个字符设备。关键成员structcdev{structkobjectkobj;structmodule*owner;// 所属模块一般填 THIS_MODULEconststructfile_operations*ops;// 操作函数集合核心structlist_headlist;dev_tdev;// 设备号unsignedintcount;// 设备数量};初始化和注册两步// 1. 初始化 cdev绑定 file_operationsvoidcdev_init(structcdev*cdev,conststructfile_operations*fops);// 2. 加入内核让系统知道这个设备intcdev_add(structcdev*p,dev_tdev,unsignedcount);注销voidcdev_del(structcdev*p);完整流程申请设备号 → cdev_init 绑定操作集 → cdev_add 注册 → 设备可用 ↓ 卸载时cdev_del 注销 → 释放设备号四、file_operations操作函数集合是什么一个函数指针结构体把应用层的系统调用映射到驱动里的具体函数。常用成员structfile_operations{structmodule*owner;int(*open)(structinode*,structfile*);int(*release)(structinode*,structfile*);ssize_t(*read)(structfile*,char__user*,size_t,loff_t*);ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);long(*unlocked_ioctl)(structfile*,unsignedint,unsignedlong);loff_t(*llseek)(structfile*,loff_t,int);unsignedint(*poll)(structfile*,structpoll_table_struct*);int(*mmap)(structfile*,structvm_area_struct*);// ...};对应关系应用层驱动层open().openclose().releaseread().readwrite().writeioctl().unlocked_ioctllseek().llseekselect/poll.pollmmap().mmap定义示例staticintmy_open(structinode*inode,structfile*filp){printk(设备打开了\n);return0;}staticintmy_release(structinode*inode,structfile*filp){printk(设备关闭了\n);return0;}staticconststructfile_operationsmy_fops{.ownerTHIS_MODULE,.openmy_open,.releasemy_release,.readmy_read,.writemy_write,.unlocked_ioctlmy_ioctl,};.owner 必须填 THIS_MODULE防止驱动被使用时卸载。五、用户空间和内核空间的数据拷贝为什么不能直接访问用户空间的指针不能直接在内核里读写地址空间不一样而且要做合法性检查直接访问可能会导致内核崩溃拷贝函数#includelinux/uaccess.h// 从用户空间拷贝到内核空间read 的反方向write 用这个unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn);// 从内核空间拷贝到用户空间read 用这个unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongn);返回值成功返回 0失败返回没拷贝成功的字节数。简单数据的存取单个变量可以用更简单的get_user(x,ptr)// 从用户空间读一个变量到 xput_user(x,ptr)// 把 x 写到用户空间比 copy_*_user 快适合 char/int/long 这种小数据。六、read 和 write 的实现read 函数staticcharbuf[1024];// 内核缓冲区staticssize_tmy_read(structfile*filp,char__user*buf_user,size_tcount,loff_t*f_pos){intlenstrlen(buf);// 实际有多少数据if(countlen)countlen;if(copy_to_user(buf_user,buf,count)){return-EFAULT;}*f_poscount;returncount;// 返回实际读到的字节数}参数说明filp文件结构体buf_user用户空间缓冲区指针__user 标记count用户想读多少字节f_pos文件读写位置返回值实际读到的字节数负数表示错误write 函数staticssize_tmy_write(structfile*filp,constchar__user*buf_user,size_tcount,loff_t*f_pos){if(countsizeof(buf))countsizeof(buf);if(copy_from_user(buf,buf_user,count)){return-EFAULT;}buf[count]\0;// 字符串结尾*f_poscount;printk(收到数据: %s\n,buf);returncount;}错误码返回负数表示错误用标准错误码-EINVAL参数无效-EFAULT地址错误copy 失败-ENOMEM内存不足-EBUSY设备忙-ENODEV设备不存在七、open 和 releaseopen打开设备时调用可以做初始化硬件分配缓冲区检查设备状态私有数据初始化staticintmy_open(structinode*inode,structfile*filp){// 从 inode 里拿到 cdev再拿到我们的私有数据// 后面讲容器_of的时候细说printk(设备打开\n);return0;}release关闭设备时调用做清理释放 open 里分配的资源复位硬件staticintmy_release(structinode*inode,structfile*filp){printk(设备关闭\n);return0;}八、ioctl控制命令作用read/write 是数据流ioctl 是「控制命令」设置参数读取状态执行特定操作复位、校准……命令码构造#includelinux/ioctl.h// 方向_IO无数据 _IOW写 _IOR读 _IOWR读写// type8位魔数每个驱动一个// nr8位命令序号// size数据大小#defineMY_IOC_MAGICx#defineMY_IOC_RESET_IO(MY_IOC_MAGIC,0)#defineMY_IOC_SET_BAUD_IOW(MY_IOC_MAGIC,1,int)#defineMY_IOC_GET_STATUS_IOR(MY_IOC_MAGIC,2,int)实现staticlongmy_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){intval;switch(cmd){caseMY_IOC_RESET:printk(复位设备\n);break;caseMY_IOC_SET_BAUD:if(copy_from_user(val,(int__user*)arg,sizeof(int)))return-EFAULT;printk(设置波特率: %d\n,val);break;caseMY_IOC_GET_STATUS:val0x1234;// 假设的状态值if(copy_to_user((int__user*)arg,val,sizeof(int)))return-EFAULT;break;default:return-EINVAL;}return0;}应用层调用intbaud115200;ioctl(fd,MY_IOC_SET_BAUD,baud);intstatus;ioctl(fd,MY_IOC_GET_STATUS,status);九、完整的字符设备驱动示例#includelinux/init.h#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/uaccess.h#defineBUF_SIZE1024#defineDEV_NAMEmycharstaticdev_tdevid;staticstructcdevmy_cdev;staticcharkbuf[BUF_SIZE];staticintmy_open(structinode*inode,structfile*filp){pr_info(mychar: open\n);return0;}staticintmy_release(structinode*inode,structfile*filp){pr_info(mychar: release\n);return0;}staticssize_tmy_read(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos){intlenstrlen(kbuf);if(countlen)countlen;if(copy_to_user(buf,kbuf,count))return-EFAULT;*f_poscount;returncount;}staticssize_tmy_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_pos){if(countBUF_SIZE)countBUF_SIZE-1;if(copy_from_user(kbuf,buf,count))return-EFAULT;kbuf[count]\0;*f_poscount;pr_info(mychar: write %s\n,kbuf);returncount;}staticconststructfile_operationsmy_fops{.ownerTHIS_MODULE,.openmy_open,.releasemy_release,.readmy_read,.writemy_write,};staticint__initmychar_init(void){intret;// 1. 动态分配设备号retalloc_chrdev_region(devid,0,1,DEV_NAME);if(ret0){pr_err(alloc_chrdev_region failed\n);returnret;}// 2. 初始化 cdevcdev_init(my_cdev,my_fops);my_cdev.ownerTHIS_MODULE;// 3. 注册 cdevretcdev_add(my_cdev,devid,1);if(ret0){pr_err(cdev_add failed\n);gotoerr_cdev;}strcpy(kbuf,hello from kernel\n);pr_info(mychar: loaded, major%d\n,MAJOR(devid));return0;err_cdev:unregister_chrdev_region(devid,1);returnret;}staticvoid__exitmychar_exit(void){cdev_del(my_cdev);unregister_chrdev_region(devid,1);pr_info(mychar: unloaded\n);}module_init(mychar_init);module_exit(mychar_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(Heipiao);MODULE_DESCRIPTION(简单字符设备驱动);应用层测试程序#includestdio.h#includefcntl.h#includeunistd.h#includestring.hintmain(){intfdopen(/dev/mychar,O_RDWR);if(fd0){perror(open);return1;}// 写write(fd,hello driver,12);// 读charbuf[64];lseek(fd,0,SEEK_SET);// 回到开头intnread(fd,buf,sizeof(buf));buf[n]\0;printf(读到: %s\n,buf);close(fd);return0;}十、手动创建设备节点加载驱动后要手动创建设备文件才能用# 查看主设备号cat/proc/devices|grepmychar# 创建设备节点mknod/dev/mychar c 主设备号0每次手动 mknod 很麻烦下一篇讲怎么自动创建udev class/device_create。十一、常见坑坑 1直接访问用户空间指针不用 copy_from_user / copy_to_user直接解引用 __user 指针轻则数据错乱重则内核 panic。坑 2read/write 返回值不对成功返回实际读写的字节数0失败返回负的错误码不要返回 0 表示错误应用层会以为读到文件末尾了坑 3忘了检查 copy 的返回值copy_to_user / copy_from_user 可能失败用户地址非法一定要检查返回值。坑 4cdev_add 失败没回滚初始化过程中某一步失败前面申请的资源要释放。用 goto 做错误处理是内核的标准写法。坑 5file_operations 里的函数没加 static不加 static 会污染内核命名空间可能跟别的驱动重名。坑 6owner 没设 THIS_MODULE.owner 不设的话模块使用中也能被卸载卸载了再调用驱动函数直接 panic。十二、本篇小结字符设备用设备号标识主设备号驱动类别 次设备号具体设备设备号申请alloc_chrdev_region动态推荐/ register_chrdev_region静态struct cdev 表示字符设备cdev_init 绑定操作集cdev_add 注册到内核file_operations 是核心把系统调用映射到驱动函数open / release打开关闭时的初始化和清理read / write数据读写必须用 copy_to_user / copy_from_user 拷贝数据ioctl控制命令用来设置参数、读状态、执行特殊操作用户空间指针不能直接访问必须用 copy_*_user 或 get_user / put_user加载驱动后用 mknod 创建设备节点下一篇讲自动创建下一篇讲设备节点自动创建class、device_create、udev 机制以及私有数据的 container_of 技巧。我是黒漂技术佬。
字符设备驱动:file_operations 与读写实现
字符设备驱动file_operations 与读写实现字符设备是 Linux 驱动最基础、最常用的一类。这篇讲字符设备驱动的核心框架设备号、cdev 结构体、file_operations 操作集、open/read/write 实现以及用户空间和内核空间的数据拷贝。大家好我是黒漂技术佬。内核模块搞明白了接下来正式写驱动。字符设备驱动是最基础的一类也是学习其他驱动的基础。串口、GPIO、LED、按键、I2C 设备……底层都是字符设备。这篇讲字符设备的完整框架设备号、cdev 注册、file_operations、读写实现、用户态和内核态的数据交互。一、字符设备是什么特点按字节流顺序访问的设备应用层通过/dev/xxx设备文件操作用 open / read / write / close / ioctl 等标准接口大多数简单外设都是字符设备设备号每个字符设备有一个唯一的设备号由两部分组成主设备号标识是哪一类驱动比如都是串口驱动 次设备号标识同类中的第几个设备设备号 主设备号 20 | 次设备号用dev_t类型存。操作宏#includelinux/kdev_t.hMAJOR(dev)// 提取主设备号MINOR(dev)// 提取次设备号MKDEV(ma,mi)// 合成设备号查看设备号cat/proc/devices# Character devices:# 1 mem# 4 tty# ...ls-l/dev/# crw-rw---- 1 root dialout 4, 64 ... ttyS0# ↑ ↑# 主设备号 次设备号c 开头表示字符设备character。二、设备号的申请和释放静态申请指定主设备号#includelinux/fs.hdev_tdevidMKDEV(200,0);// 主设备号200次设备号0intregister_chrdev_region(devid,count,mydev);优点固定方便缺点可能跟已有设备冲突动态分配推荐dev_tdevid;intalloc_chrdev_region(devid,0,1,mydev);// 第一个参数返回的设备号// 第二个参数次设备号起始// 第三个参数设备数量// 第四个参数设备名/proc/devices里显示的优点自动分配空闲的主设备号不会冲突缺点每次加载主设备号可能不一样释放unregister_chrdev_region(devid,count);模块卸载时一定要释放。三、cdev 结构体是什么内核里用struct cdev表示一个字符设备。关键成员structcdev{structkobjectkobj;structmodule*owner;// 所属模块一般填 THIS_MODULEconststructfile_operations*ops;// 操作函数集合核心structlist_headlist;dev_tdev;// 设备号unsignedintcount;// 设备数量};初始化和注册两步// 1. 初始化 cdev绑定 file_operationsvoidcdev_init(structcdev*cdev,conststructfile_operations*fops);// 2. 加入内核让系统知道这个设备intcdev_add(structcdev*p,dev_tdev,unsignedcount);注销voidcdev_del(structcdev*p);完整流程申请设备号 → cdev_init 绑定操作集 → cdev_add 注册 → 设备可用 ↓ 卸载时cdev_del 注销 → 释放设备号四、file_operations操作函数集合是什么一个函数指针结构体把应用层的系统调用映射到驱动里的具体函数。常用成员structfile_operations{structmodule*owner;int(*open)(structinode*,structfile*);int(*release)(structinode*,structfile*);ssize_t(*read)(structfile*,char__user*,size_t,loff_t*);ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);long(*unlocked_ioctl)(structfile*,unsignedint,unsignedlong);loff_t(*llseek)(structfile*,loff_t,int);unsignedint(*poll)(structfile*,structpoll_table_struct*);int(*mmap)(structfile*,structvm_area_struct*);// ...};对应关系应用层驱动层open().openclose().releaseread().readwrite().writeioctl().unlocked_ioctllseek().llseekselect/poll.pollmmap().mmap定义示例staticintmy_open(structinode*inode,structfile*filp){printk(设备打开了\n);return0;}staticintmy_release(structinode*inode,structfile*filp){printk(设备关闭了\n);return0;}staticconststructfile_operationsmy_fops{.ownerTHIS_MODULE,.openmy_open,.releasemy_release,.readmy_read,.writemy_write,.unlocked_ioctlmy_ioctl,};.owner 必须填 THIS_MODULE防止驱动被使用时卸载。五、用户空间和内核空间的数据拷贝为什么不能直接访问用户空间的指针不能直接在内核里读写地址空间不一样而且要做合法性检查直接访问可能会导致内核崩溃拷贝函数#includelinux/uaccess.h// 从用户空间拷贝到内核空间read 的反方向write 用这个unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn);// 从内核空间拷贝到用户空间read 用这个unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongn);返回值成功返回 0失败返回没拷贝成功的字节数。简单数据的存取单个变量可以用更简单的get_user(x,ptr)// 从用户空间读一个变量到 xput_user(x,ptr)// 把 x 写到用户空间比 copy_*_user 快适合 char/int/long 这种小数据。六、read 和 write 的实现read 函数staticcharbuf[1024];// 内核缓冲区staticssize_tmy_read(structfile*filp,char__user*buf_user,size_tcount,loff_t*f_pos){intlenstrlen(buf);// 实际有多少数据if(countlen)countlen;if(copy_to_user(buf_user,buf,count)){return-EFAULT;}*f_poscount;returncount;// 返回实际读到的字节数}参数说明filp文件结构体buf_user用户空间缓冲区指针__user 标记count用户想读多少字节f_pos文件读写位置返回值实际读到的字节数负数表示错误write 函数staticssize_tmy_write(structfile*filp,constchar__user*buf_user,size_tcount,loff_t*f_pos){if(countsizeof(buf))countsizeof(buf);if(copy_from_user(buf,buf_user,count)){return-EFAULT;}buf[count]\0;// 字符串结尾*f_poscount;printk(收到数据: %s\n,buf);returncount;}错误码返回负数表示错误用标准错误码-EINVAL参数无效-EFAULT地址错误copy 失败-ENOMEM内存不足-EBUSY设备忙-ENODEV设备不存在七、open 和 releaseopen打开设备时调用可以做初始化硬件分配缓冲区检查设备状态私有数据初始化staticintmy_open(structinode*inode,structfile*filp){// 从 inode 里拿到 cdev再拿到我们的私有数据// 后面讲容器_of的时候细说printk(设备打开\n);return0;}release关闭设备时调用做清理释放 open 里分配的资源复位硬件staticintmy_release(structinode*inode,structfile*filp){printk(设备关闭\n);return0;}八、ioctl控制命令作用read/write 是数据流ioctl 是「控制命令」设置参数读取状态执行特定操作复位、校准……命令码构造#includelinux/ioctl.h// 方向_IO无数据 _IOW写 _IOR读 _IOWR读写// type8位魔数每个驱动一个// nr8位命令序号// size数据大小#defineMY_IOC_MAGICx#defineMY_IOC_RESET_IO(MY_IOC_MAGIC,0)#defineMY_IOC_SET_BAUD_IOW(MY_IOC_MAGIC,1,int)#defineMY_IOC_GET_STATUS_IOR(MY_IOC_MAGIC,2,int)实现staticlongmy_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){intval;switch(cmd){caseMY_IOC_RESET:printk(复位设备\n);break;caseMY_IOC_SET_BAUD:if(copy_from_user(val,(int__user*)arg,sizeof(int)))return-EFAULT;printk(设置波特率: %d\n,val);break;caseMY_IOC_GET_STATUS:val0x1234;// 假设的状态值if(copy_to_user((int__user*)arg,val,sizeof(int)))return-EFAULT;break;default:return-EINVAL;}return0;}应用层调用intbaud115200;ioctl(fd,MY_IOC_SET_BAUD,baud);intstatus;ioctl(fd,MY_IOC_GET_STATUS,status);九、完整的字符设备驱动示例#includelinux/init.h#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/uaccess.h#defineBUF_SIZE1024#defineDEV_NAMEmycharstaticdev_tdevid;staticstructcdevmy_cdev;staticcharkbuf[BUF_SIZE];staticintmy_open(structinode*inode,structfile*filp){pr_info(mychar: open\n);return0;}staticintmy_release(structinode*inode,structfile*filp){pr_info(mychar: release\n);return0;}staticssize_tmy_read(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos){intlenstrlen(kbuf);if(countlen)countlen;if(copy_to_user(buf,kbuf,count))return-EFAULT;*f_poscount;returncount;}staticssize_tmy_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_pos){if(countBUF_SIZE)countBUF_SIZE-1;if(copy_from_user(kbuf,buf,count))return-EFAULT;kbuf[count]\0;*f_poscount;pr_info(mychar: write %s\n,kbuf);returncount;}staticconststructfile_operationsmy_fops{.ownerTHIS_MODULE,.openmy_open,.releasemy_release,.readmy_read,.writemy_write,};staticint__initmychar_init(void){intret;// 1. 动态分配设备号retalloc_chrdev_region(devid,0,1,DEV_NAME);if(ret0){pr_err(alloc_chrdev_region failed\n);returnret;}// 2. 初始化 cdevcdev_init(my_cdev,my_fops);my_cdev.ownerTHIS_MODULE;// 3. 注册 cdevretcdev_add(my_cdev,devid,1);if(ret0){pr_err(cdev_add failed\n);gotoerr_cdev;}strcpy(kbuf,hello from kernel\n);pr_info(mychar: loaded, major%d\n,MAJOR(devid));return0;err_cdev:unregister_chrdev_region(devid,1);returnret;}staticvoid__exitmychar_exit(void){cdev_del(my_cdev);unregister_chrdev_region(devid,1);pr_info(mychar: unloaded\n);}module_init(mychar_init);module_exit(mychar_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(Heipiao);MODULE_DESCRIPTION(简单字符设备驱动);应用层测试程序#includestdio.h#includefcntl.h#includeunistd.h#includestring.hintmain(){intfdopen(/dev/mychar,O_RDWR);if(fd0){perror(open);return1;}// 写write(fd,hello driver,12);// 读charbuf[64];lseek(fd,0,SEEK_SET);// 回到开头intnread(fd,buf,sizeof(buf));buf[n]\0;printf(读到: %s\n,buf);close(fd);return0;}十、手动创建设备节点加载驱动后要手动创建设备文件才能用# 查看主设备号cat/proc/devices|grepmychar# 创建设备节点mknod/dev/mychar c 主设备号0每次手动 mknod 很麻烦下一篇讲怎么自动创建udev class/device_create。十一、常见坑坑 1直接访问用户空间指针不用 copy_from_user / copy_to_user直接解引用 __user 指针轻则数据错乱重则内核 panic。坑 2read/write 返回值不对成功返回实际读写的字节数0失败返回负的错误码不要返回 0 表示错误应用层会以为读到文件末尾了坑 3忘了检查 copy 的返回值copy_to_user / copy_from_user 可能失败用户地址非法一定要检查返回值。坑 4cdev_add 失败没回滚初始化过程中某一步失败前面申请的资源要释放。用 goto 做错误处理是内核的标准写法。坑 5file_operations 里的函数没加 static不加 static 会污染内核命名空间可能跟别的驱动重名。坑 6owner 没设 THIS_MODULE.owner 不设的话模块使用中也能被卸载卸载了再调用驱动函数直接 panic。十二、本篇小结字符设备用设备号标识主设备号驱动类别 次设备号具体设备设备号申请alloc_chrdev_region动态推荐/ register_chrdev_region静态struct cdev 表示字符设备cdev_init 绑定操作集cdev_add 注册到内核file_operations 是核心把系统调用映射到驱动函数open / release打开关闭时的初始化和清理read / write数据读写必须用 copy_to_user / copy_from_user 拷贝数据ioctl控制命令用来设置参数、读状态、执行特殊操作用户空间指针不能直接访问必须用 copy_*_user 或 get_user / put_user加载驱动后用 mknod 创建设备节点下一篇讲自动创建下一篇讲设备节点自动创建class、device_create、udev 机制以及私有数据的 container_of 技巧。我是黒漂技术佬。