news 2026/5/27 18:36:19

Linux字符设备驱动开发(二):实现数据交互——内核与用户空间的内存拷贝

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux字符设备驱动开发(二):实现数据交互——内核与用户空间的内存拷贝

前言

在上一篇文章中,我们搭建了一个可以动态创建设备节点的字符设备驱动框架,但readwrite仅仅是打印了内核日志,并没有进行实际的数据传输。

本文直接在这个框架上扩展,实现一个真正的内存缓冲区设备:用户程序可以向设备中write数据,之后再通过read读出,就像操作一个文件一样。背后的核心技术就是copy_to_usercopy_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_user

unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn);
  • 功能:将n字节数据从用户空间地址from拷贝到内核空间地址to
  • 返回值:成功返回 0;失败返回未能拷贝的字节数。
  • 该函数内部已做权限检查,不需要手动判断地址合法性。

2.2 copy_to_user

unsignedlongcopy_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 亦可) */#include<linux/module.h>#include<linux/fs.h>#include<linux/cdev.h>#include<linux/device.h>#include<linux/uaccess.h>/* copy_to_user, copy_from_user */#defineDEVICE_NAME"chrdev_buf"// 设备节点名#defineCLASS_NAME"chrdev_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_size==0){pr_info("chrdev_buf: buffer empty, read returns EOF\n");return0;}/* 计算本次可读字节数,不能超过缓冲区现存数据量 */ret_bytes=(count<data_size)?count:data_size;/* 将数据从内核空间拷贝到用户空间 */not_copied=copy_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_size=0;returnret_bytes;}/* 写入设备:将用户空间提供的数据拷贝到内核缓冲区,覆盖之前的内容 */staticssize_tchrdev_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){unsignedlongnot_copied;size_twrite_bytes;/* 限制写入字节数不能超过缓冲区容量 */write_bytes=(count<BUF_SIZE)?count:BUF_SIZE;/* 从用户空间拷贝数据到内核缓冲区 */not_copied=copy_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_size=write_bytes;pr_info("chrdev_buf: written %zu bytes to buffer\n",data_size);/* 返回实际写入的字节数(超出 BUF_SIZE 的部分已被截断) */returnwrite_bytes;}staticstructfile_operationschrdev_fops={.owner=THIS_MODULE,.open=chrdev_open,.release=chrdev_release,.read=chrdev_read,.write=chrdev_write,};/* 模块初始化:与第一篇框架完全一致,仅设备名不同 */staticint__initchrdev_init(void){intret;ret=alloc_chrdev_region(&dev_num,0,1,DEVICE_NAME);if(ret<0){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.owner=THIS_MODULE;ret=cdev_add(&my_cdev,dev_num,1);if(ret){pr_err("chrdev_buf: cdev_add failed\n");gotoerr_cdev_add;}my_class=class_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err("chrdev_buf: class_create failed\n");ret=PTR_ERR(my_class);gotoerr_class_create;}my_device=device_create(my_class,NULL,dev_num,NULL,DEVICE_NAME);if(IS_ERR(my_device)){pr_err("chrdev_buf: device_create failed\n");ret=PTR_ERR(my_device);gotoerr_device_create;}/* 初始化内核缓冲区 */memset(kernel_buf,0,BUF_SIZE);data_size=0;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 major=239, minor=0# 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_buf

5.2 写入测试

echo"Hello Linux Driver!">/dev/chrdev_buf

查看dmesg输出:

chrdev_buf: device opened chrdev_buf: written 19 bytes to buffer chrdev_buf: device closed

5.3 读取测试

cat/dev/chrdev_buf# 终端会打印:Hello Linux Driver!

对应内核日志:

chrdev_buf: device opened chrdev_buf: read 19 bytes from buffer chrdev_buf: device closed

5.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会自动消失。


六、注意事项与常见错误

  1. 禁止直接操作用户空间指针
    绝对不要尝试memcpy(kernel_buf, buf, count),必须使用copy_from_user

  2. 检查拷贝函数的返回值
    copy_to/from_user返回非零,说明部分数据未拷贝成功,应返回-EFAULT告知调用者。

  3. 缓冲区溢出保护
    本示例用write_bytes = min(count, BUF_SIZE)限制写入长度,有效防止内核缓冲区溢出。

  4. 并发风险
    本驱动没有加锁,多进程同时读写会引发数据混乱。在生产环境中,必须通过互斥锁(mutex)保护临界区。

  5. “消费”与“非消费”模式
    本驱动采用“读取后清空”,适合管道类设备。若需要模拟可反复读取的存储设备,则应保留数据并正确维护文件偏移*f_pos


七、总结与下篇预告

本文在上一篇文章的驱动框架基础上,实现了真正的数据交互。通过copy_from_usercopy_to_user,我们构建了一个可读可写的内存缓冲区设备,让字符设备驱动有了“血肉”。

这个骨架可以支撑更复杂的设备驱动:无论是传感器、串口、还是自定义协议设备,核心逻辑无非是在read/write中接入硬件操作。

下篇预告:我们将引入并发控制,使用内核互斥锁(mutex)保护共享数据,让驱动在多进程环境下安全稳定地运行。


如果本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力。任何问题欢迎在评论区留言交流!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/27 18:35:33

基于Rust与AI的命令行纠错工具:从原理到工程实践

1. 项目概述&#xff1a;一个Rust驱动的AI命令行纠错工具作为一个常年与终端打交道的开发者&#xff0c;我太熟悉那种感觉了&#xff1a;手指在键盘上飞舞&#xff0c;敲下一长串复杂的命令&#xff0c;满怀期待地按下回车&#xff0c;结果终端无情地回敬你一个command not fou…

作者头像 李华
网站建设 2026/5/27 18:34:24

8GB内存本地部署语音AI助手:Whisper.cpp与轻量LLM实战指南

1. 项目概述&#xff1a;当你的电脑能听懂你说话“嘿&#xff0c;电脑&#xff0c;帮我查一下明天的天气&#xff0c;然后写封邮件提醒我出门带伞。”几年前&#xff0c;这听起来像是科幻电影里的场景。但现在&#xff0c;借助本地运行的AI智能体&#xff0c;这已经可以成为你桌…

作者头像 李华
网站建设 2026/5/27 18:33:24

Hermes Agent用户通过Taotoken扩展模型选择并管理调用成本

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 Hermes Agent用户通过Taotoken扩展模型选择并管理调用成本 基础教程类&#xff0c;指导Hermes Agent用户根据平台文档&#xff0c;…

作者头像 李华
网站建设 2026/5/27 18:32:22

OpenAI Privacy Filter核心功能揭秘:8大隐私数据类型精准识别

OpenAI Privacy Filter核心功能揭秘&#xff1a;8大隐私数据类型精准识别 【免费下载链接】privacy-filter 项目地址: https://ai.gitcode.com/hf_mirrors/Open-OSS/privacy-filter 在当今数据驱动的时代&#xff0c;隐私保护已成为企业和开发者的首要任务。OpenAI Pri…

作者头像 李华
网站建设 2026/5/27 18:32:16

3步极速优化:Winhance中文版让你的Windows系统焕然新生

3步极速优化&#xff1a;Winhance中文版让你的Windows系统焕然新生 【免费下载链接】Winhance-zh_CN A Chinese version of Winhance. C# application designed to optimize and customize your Windows experience. 项目地址: https://gitcode.com/gh_mirrors/wi/Winhance-z…

作者头像 李华
网站建设 2026/5/27 18:32:12

使用taotoken cli工具在ubuntu上一键配置开发环境

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 使用taotoken cli工具在ubuntu上一键配置开发环境 在团队协作或需要快速搭建多个开发环境的场景下&#xff0c;手动为每个项目或每…

作者头像 李华