Linux字符设备驱动开发(二):实现数据交互——内核与用户空间的内存拷贝
前言在上一篇文章中我们搭建了一个可以动态创建设备节点的字符设备驱动框架但read和write仅仅是打印了内核日志并没有进行实际的数据传输。本文直接在这个框架上扩展实现一个真正的内存缓冲区设备用户程序可以向设备中write数据之后再通过read读出就像操作一个文件一样。背后的核心技术就是copy_to_user和copy_from_user它们是内核空间与用户空间安全通信的桥梁。读完本文你将掌握如何使用copy_from_user从用户空间获取数据如何使用copy_to_user将数据返回给用户空间构建一个简单但完整的、可读写的内存字符设备一、设计思路在驱动内部开辟一个内核缓冲区作为虚拟的“存储空间”写入write用户程序调用write时通过copy_from_user把数据从用户缓冲区复制到内核缓冲区并记录有效数据长度。如果写入超过缓冲区容量1024字节多余部分会被截断。读取read用户程序调用read时通过copy_to_user把内核缓冲区的数据拷贝到用户空间并清空整个内核缓冲区消费模型。下次读取将直接返回 EOF。错误处理所有地址拷贝操作均严格检查返回值若发生错误则返回-EFAULT。这样我们就得到了一个“一次性”内存管道设备写入一次读出一次读完即空。注意本实现未加并发锁仅适用于单线程测试。实际产品中需要引入互斥锁将在后续文章讲解。二、关键内核 API 解析2.1 copy_from_userunsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn);功能将n字节数据从用户空间地址from拷贝到内核空间地址to。返回值成功返回 0失败返回未能拷贝的字节数。该函数内部已做权限检查不需要手动判断地址合法性。2.2 copy_to_userunsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongn);功能将n字节数据从内核空间地址from拷贝到用户空间地址to。返回值同上成功为 0失败为未拷贝成功的字节数。这两对函数是内核与用户空间交换数据的唯一安全方式绝对不能直接使用memcpy去操作用户空间指针。三、完整驱动代码新建文件chrdev_buffer.c将以下代码直接复制即可编译运行。/* * chrdev_buffer.c * 一个具备实际数据读写功能的字符设备驱动。 * 通过 copy_from_user / copy_to_user 实现内核与用户空间的数据交互。 * 加载后生成 /dev/chrdev_buf可像普通文件一样使用。 * 作者[你的ID] * 适配内核Linux 5.x (4.x 亦可) */#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h/* copy_to_user, copy_from_user */#defineDEVICE_NAMEchrdev_buf// 设备节点名#defineCLASS_NAMEchrdev_buf_class#defineBUF_SIZE1024// 内核缓冲区大小字节staticdev_tdev_num;staticstructcdevmy_cdev;staticstructclass*my_class;staticstructdevice*my_device;/* 内核缓冲区及相关变量 */staticcharkernel_buf[BUF_SIZE];// 数据缓冲区staticsize_tdata_size;// 当前有效数据长度字节/* 打开设备 */staticintchrdev_open(structinode*inode,structfile*file){pr_info(chrdev_buf: device opened\n);return0;}/* 关闭设备 */staticintchrdev_release(structinode*inode,structfile*file){pr_info(chrdev_buf: device closed\n);return0;}/* 读取设备将内核缓冲区中的数据拷贝到用户空间之后清空缓冲区 */staticssize_tchrdev_read(structfile*file,char__user*buf,size_tcount,loff_t*f_pos){ssize_tret_bytes;// 实际拷贝的字节数unsignedlongnot_copied;/* 如果缓冲区为空返回 0 表示文件结束EOF */if(data_size0){pr_info(chrdev_buf: buffer empty, read returns EOF\n);return0;}/* 计算本次可读字节数不能超过缓冲区现存数据量 */ret_bytes(countdata_size)?count:data_size;/* 将数据从内核空间拷贝到用户空间 */not_copiedcopy_to_user(buf,kernel_buf,ret_bytes);if(not_copied!0){pr_err(chrdev_buf: copy_to_user failed, %lu bytes not copied\n,not_copied);return-EFAULT;}pr_info(chrdev_buf: read %zd bytes from buffer\n,ret_bytes);/* 消费模式读取后清空缓冲区下次读将返回 EOF */memset(kernel_buf,0,BUF_SIZE);data_size0;returnret_bytes;}/* 写入设备将用户空间提供的数据拷贝到内核缓冲区覆盖之前的内容 */staticssize_tchrdev_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){unsignedlongnot_copied;size_twrite_bytes;/* 限制写入字节数不能超过缓冲区容量 */write_bytes(countBUF_SIZE)?count:BUF_SIZE;/* 从用户空间拷贝数据到内核缓冲区 */not_copiedcopy_from_user(kernel_buf,buf,write_bytes);if(not_copied!0){pr_err(chrdev_buf: copy_from_user failed, %lu bytes not copied\n,not_copied);return-EFAULT;}/* 更新缓冲区有效数据长度 */data_sizewrite_bytes;pr_info(chrdev_buf: written %zu bytes to buffer\n,data_size);/* 返回实际写入的字节数超出 BUF_SIZE 的部分已被截断 */returnwrite_bytes;}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_buf: failed to allocate device number\n);returnret;}pr_info(chrdev_buf: 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_buf: cdev_add failed\n);gotoerr_cdev_add;}my_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err(chrdev_buf: 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_buf: device_create failed\n);retPTR_ERR(my_device);gotoerr_device_create;}/* 初始化内核缓冲区 */memset(kernel_buf,0,BUF_SIZE);data_size0;pr_info(chrdev_buf: module loaded, /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_buf: module unloaded\n);}module_init(chrdev_init);module_exit(chrdev_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(Your Name);MODULE_DESCRIPTION(A simple buffer char device driver);MODULE_VERSION(1.0);四、Makefile# Makefile for chrdev_buffer KERNEL_DIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) obj-m : chrdev_buffer.o all: make -C $(KERNEL_DIR) M$(PWD) modules clean: make -C $(KERNEL_DIR) M$(PWD) clean将chrdev_buffer.c和 Makefile 放在同一目录执行make即可生成chrdev_buffer.ko。五、测试与验证5.1 加载驱动sudoinsmod chrdev_buffer.ko检查日志dmesg|tail# chrdev_buf: allocated major239, minor0# chrdev_buf: module loaded, /dev/chrdev_buf created确认设备节点ls-l/dev/chrdev_buf# crw------- 1 root root 239, 0 May 27 10:00 /dev/chrdev_buf若需普通用户访问临时赋权sudochmod666/dev/chrdev_buf5.2 写入测试echoHello Linux Driver!/dev/chrdev_buf查看dmesg输出chrdev_buf: device opened chrdev_buf: written 19 bytes to buffer chrdev_buf: device closed5.3 读取测试cat/dev/chrdev_buf# 终端会打印Hello Linux Driver!对应内核日志chrdev_buf: device opened chrdev_buf: read 19 bytes from buffer chrdev_buf: device closed5.4 验证“消费”特性再次执行cat /dev/chrdev_buf终端无任何输出。查看日志chrdev_buf: device opened chrdev_buf: buffer empty, read returns EOF chrdev_buf: device closed说明数据已被清空符合预期。5.5 卸载驱动sudormmod chrdev_buffer此时/dev/chrdev_buf会自动消失。六、注意事项与常见错误禁止直接操作用户空间指针绝对不要尝试memcpy(kernel_buf, buf, count)必须使用copy_from_user。检查拷贝函数的返回值若copy_to/from_user返回非零说明部分数据未拷贝成功应返回-EFAULT告知调用者。缓冲区溢出保护本示例用write_bytes min(count, BUF_SIZE)限制写入长度有效防止内核缓冲区溢出。并发风险本驱动没有加锁多进程同时读写会引发数据混乱。在生产环境中必须通过互斥锁mutex保护临界区。“消费”与“非消费”模式本驱动采用“读取后清空”适合管道类设备。若需要模拟可反复读取的存储设备则应保留数据并正确维护文件偏移*f_pos。七、总结与下篇预告本文在上一篇文章的驱动框架基础上实现了真正的数据交互。通过copy_from_user和copy_to_user我们构建了一个可读可写的内存缓冲区设备让字符设备驱动有了“血肉”。这个骨架可以支撑更复杂的设备驱动无论是传感器、串口、还是自定义协议设备核心逻辑无非是在read/write中接入硬件操作。下篇预告我们将引入并发控制使用内核互斥锁mutex保护共享数据让驱动在多进程环境下安全稳定地运行。如果本文对你有帮助欢迎点赞、收藏、关注你的支持是我持续输出的动力。任何问题欢迎在评论区留言交流