前言嵌入式Linux开发中字符设备驱动是最基础、最常见的驱动类型。韦东山老师的课程从这一框架切入让我们快速建立对Linux驱动模型的认知。本文基于课程思路整理出一个完整、可编译运行的字符设备驱动框架并带你一步步理解cdev、file_operations、设备号自动分配、自动创建设备节点等核心知识点。读完这篇文章你将收获一个真正能insmod后生成/dev节点的驱动模板对字符设备驱动注册/注销流程的透彻理解可直接复用的代码和 Makefile一、字符设备驱动核心概念在写代码之前先梳理三个关键的数据结构和概念。1.1 设备号dev_tLinux 内核通过主设备号major和次设备号minor来唯一标识一个设备。typedefu32dev_t;// 高12位主设备号低20位次设备号静态分配register_chrdev_region()需手动指定主设备号可能冲突动态分配alloc_chrdev_region()由内核分配一个未使用的主设备号推荐本驱动采用动态分配避免手选设备号带来的冲突问题。1.2 cdev 结构体structcdev{structkobjectkobj;structmodule*owner;conststructfile_operations*ops;structlist_headlist;dev_tdev;unsignedintcount;};cdev是字符设备的抽象内核用它将设备号和文件操作file_operations绑定在一起。1.3 file_operations 结构体这是驱动与用户程序的接口。每一个系统调用open、read、write等都会对应到这个结构体中的某个函数指针。structfile_operations{structmodule*owner;ssize_t(*read)(structfile*,char__user*,size_t,loff_t*);ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);int(*open)(structinode*,structfile*);int(*release)(structinode*,structfile*);// ...还有很多其他成员};我们首先实现open、release、read、write四个最基本的接口。二、驱动设计思路整个驱动的生命周期可以概括为模块加载动态申请一个可用的主设备号初始化cdev绑定file_operations向内核注册cdev创建/sys/class/下的类为了自动创建/dev/节点根据类创建设备节点/dev/chrdev_test用户访问用户程序调用open(/dev/chrdev_test, ...)→ 触发chrdev_openread/write触发对应函数本示例仅打印内核日志模块卸载严格按照“先创建的后销毁”顺序销毁/dev节点销毁 class删除cdev释放设备号三、代码实现与详细解析3.1 头文件和全局变量#includelinux/module.h#includelinux/fs.h// file_operations, alloc_chrdev_region...#includelinux/cdev.h// cdev_init, cdev_add...#includelinux/device.h// class_create, device_create#includelinux/uaccess.h// copy_to_user, copy_from_user (备用)#includelinux/slab.h// kmalloc, kfree (备用)#defineDEVICE_NAMEchrdev_test// 设备名称最终出现在 /dev/chrdev_test#defineCLASS_NAMEchrdev_class// 在 /sys/class/ 下创建的同名类staticdev_tdev_num;// 设备号staticstructcdevmy_cdev;// 字符设备staticstructclass*my_class;// 设备类指针staticstructdevice*my_device;// 设备指针dev_num保存动态分配的设备号MAJOR(dev_num)获取主设备号。my_class和my_device用于自动创建设备节点这样就不用自己mknod。3.2 文件操作函数实现这四个函数目前只是打印日志不操作实际硬件或数据目的是验证框架能跑通。staticintchrdev_open(structinode*inode,structfile*file){pr_info(chrdev: device opened\n);return0;}staticintchrdev_release(structinode*inode,structfile*file){pr_info(chrdev: device closed\n);return0;}staticssize_tchrdev_read(structfile*file,char__user*buf,size_tcount,loff_t*f_pos){pr_info(chrdev: read called, count %zu\n,count);return0;// 返回0表示文件末尾EOF}staticssize_tchrdev_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){pr_info(chrdev: write called, count %zu\n,count);returncount;// 假装全部写入}返回值含义chrdev_read返回 0 会让cat认为文件已结束chrdev_write返回count告诉上层所有数据都已“写入”。3.3 绑定 file_operations 结构体staticstructfile_operationschrdev_fops{.ownerTHIS_MODULE,.openchrdev_open,.releasechrdev_release,.readchrdev_read,.writechrdev_write,};.owner THIS_MODULE用于防止模块在使用中被卸载是必须的。3.4 模块初始化函数重点staticint__initchrdev_init(void){intret;// 1. 动态申请设备号主设备号可能为0由内核分配retalloc_chrdev_region(dev_num,0,1,DEVICE_NAME);if(ret0){pr_err(chrdev: failed to allocate device number\n);returnret;}pr_info(chrdev: allocated major%d, minor%d\n,MAJOR(dev_num),MINOR(dev_num));// 2. 初始化 cdev 并添加到内核cdev_init(my_cdev,chrdev_fops);my_cdev.ownerTHIS_MODULE;retcdev_add(my_cdev,dev_num,1);if(ret){pr_err(chrdev: cdev_add failed\n);gotoerr_cdev_add;}// 3. 创建 class自动设备节点需要my_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err(chrdev: class_create failed\n);retPTR_ERR(my_class);gotoerr_class_create;}// 4. 在 /dev/ 下创建设备节点my_devicedevice_create(my_class,NULL,dev_num,NULL,DEVICE_NAME);if(IS_ERR(my_device)){pr_err(chrdev: device_create failed\n);retPTR_ERR(my_device);gotoerr_device_create;}pr_info(chrdev: module loaded successfully, /dev/%s created\n,DEVICE_NAME);return0;// 错误回滚路径goto链式处理err_device_create:class_destroy(my_class);err_class_create:cdev_del(my_cdev);err_cdev_add:unregister_chrdev_region(dev_num,1);returnret;}关键解释alloc_chrdev_region次设备号从 0 开始连续申请 1 个。成功后dev_num会被填上分配的主次设备号。cdev_init将file_operations与cdev绑定。cdev_add告诉内核该cdev代表的设备号范围只有调用后设备才真正可用。创建 class 和 device 是借助udev机制加载驱动后会自动在/dev下生成设备文件无需手动mknod。错误处理使用了经典的goto 链式回滚确保中间任何一步出错都会正确清理已申请的资源。3.5 模块卸载函数staticvoid__exitchrdev_exit(void){// 严格遵守“先创建的后销毁”device_destroy(my_class,dev_num);class_destroy(my_class);cdev_del(my_cdev);unregister_chrdev_region(dev_num,1);pr_info(chrdev: module unloaded\n);}3.6 完整的驱动代码可直接复制将上面的片段合并到chrdev_base.c中再加上模块入口/出口宏/* * chrdev_base.c * 一个完整的字符设备驱动框架加载后自动生成 /dev/chrdev_test * 可直接编译运行适用于 Linux 5.x4.x 也可 * 作者[你的ID] */#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h#includelinux/slab.h#defineDEVICE_NAMEchrdev_test#defineCLASS_NAMEchrdev_classstaticdev_tdev_num;staticstructcdevmy_cdev;staticstructclass*my_class;staticstructdevice*my_device;staticintchrdev_open(structinode*inode,structfile*file){pr_info(chrdev: device opened\n);return0;}staticintchrdev_release(structinode*inode,structfile*file){pr_info(chrdev: device closed\n);return0;}staticssize_tchrdev_read(structfile*file,char__user*buf,size_tcount,loff_t*f_pos){pr_info(chrdev: read called, count %zu\n,count);return0;}staticssize_tchrdev_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){pr_info(chrdev: write called, count %zu\n,count);returncount;}staticstructfile_operationschrdev_fops{.ownerTHIS_MODULE,.openchrdev_open,.releasechrdev_release,.readchrdev_read,.writechrdev_write,};staticint__initchrdev_init(void){intret;retalloc_chrdev_region(dev_num,0,1,DEVICE_NAME);if(ret0){pr_err(chrdev: failed to allocate device number\n);returnret;}pr_info(chrdev: allocated major%d, minor%d\n,MAJOR(dev_num),MINOR(dev_num));cdev_init(my_cdev,chrdev_fops);my_cdev.ownerTHIS_MODULE;retcdev_add(my_cdev,dev_num,1);if(ret){pr_err(chrdev: cdev_add failed\n);gotoerr_cdev_add;}my_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err(chrdev: class_create failed\n);retPTR_ERR(my_class);gotoerr_class_create;}my_devicedevice_create(my_class,NULL,dev_num,NULL,DEVICE_NAME);if(IS_ERR(my_device)){pr_err(chrdev: device_create failed\n);retPTR_ERR(my_device);gotoerr_device_create;}pr_info(chrdev: module loaded successfully, /dev/%s created\n,DEVICE_NAME);return0;err_device_create:class_destroy(my_class);err_class_create:cdev_del(my_cdev);err_cdev_add:unregister_chrdev_region(dev_num,1);returnret;}staticvoid__exitchrdev_exit(void){device_destroy(my_class,dev_num);class_destroy(my_class);cdev_del(my_cdev);unregister_chrdev_region(dev_num,1);pr_info(chrdev: module unloaded\n);}module_init(chrdev_init);module_exit(chrdev_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(Your Name);MODULE_DESCRIPTION(A complete character device driver template);MODULE_VERSION(1.0);四、编写 Makefile在内核模块开发中Makefile 的写法非常固定只需指定内核源码路径即可。这里提供一份可直接在x86 Ubuntu或树莓派上使用的 Makefile。# Makefile for chrdev_base # 内核源码目录使用当前运行内核的构建目录 KERNEL_DIR : /lib/modules/$(shell uname -r)/build # 当前目录 PWD : $(shell pwd) # 编译目标 obj-m : chrdev_base.o all: make -C $(KERNEL_DIR) M$(PWD) modules clean: make -C $(KERNEL_DIR) M$(PWD) clean交叉编译说明编译给 ARM 开发板用将KERNEL_DIR修改为开发板内核源码的路径已配置好的并设置环境变量例如exportARCHarmexportCROSS_COMPILEarm-linux-gnueabihf-make五、编译与测试5.1 编译将chrdev_base.c和Makefile放在同一目录下执行make成功后目录下会生成chrdev_base.ko。5.2 加载驱动sudoinsmod chrdev_base.ko查看内核输出dmesg|tail-n5预期输出类似chrdev: allocated major239, minor0 chrdev: module loaded successfully, /dev/chrdev_test created检查设备节点是否生成ls-l/dev/chrdev_test预期看到crw------- 1 root root 239, 0 May 27 10:00 /dev/chrdev_test其中239是你这次得到的主设备号每次可能不同。5.3 测试设备用cat读取设备会触发open→read→releasesudocat/dev/chrdev_test再用dmesg查看chrdev: device opened chrdev: read called, count 131072 chrdev: device closed用echo写入sudosh-cecho hello /dev/chrdev_test日志中会增加chrdev: device opened chrdev: write called, count 6 chrdev: device closed如果普通用户无法访问可用sudo chmod 666 /dev/chrdev_test临时赋权。5.4 卸载驱动sudormmod chrdev_basedmesg会显示chrdev: module unloaded此时/dev/chrdev_test会自动消失。六、常见问题与解决方法insmod 报错Required key not available原因当前系统开启了 Secure Boot且模块未签名。解决暂时关闭 Secure Boot或用mokutil --disable-validation禁用验证后重启。insmod 报错Unknown symbol in module原因可能是内核版本不匹配或依赖的其他模块未加载。解决确认编译时的内核头版本与运行内核一致uname -r。加载成功但没有/dev/chrdev_test检查dmesg是否有device_create失败的 log。确认系统有 udev 且运行正常ps aux | grep udev。设备节点权限不足解决方法编写 udev 规则或在测试时直接chmod。七、总结与下篇预告本文从零搭建了一个标准的字符设备驱动框架涵盖了设备号申请、cdev 注册、自动节点创建等全部流程加载后即可在/dev下看到设备并能通过最简的系统调用触发内核打印。这个框架是后续一切字符驱动开发的基石。下一篇文章我们将在read/write中实现内核空间与用户空间的数据拷贝copy_to_user/copy_from_user演示一个简单的内存缓冲区设备真正做到“打开、读写、关闭”一个字符设备。参考与推荐韦东山《嵌入式Linux应用开发完全手册》Linux 内核文档Documentation/driver-model/如果本文对你有帮助欢迎点赞、收藏、关注你的支持是我持续输出的动力有任何疑问也欢迎在评论区留言交流