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

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

2.2 copy_to_user

c 复制代码
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
  • 功能:将 n 字节数据从内核空间地址 from 拷贝到用户空间地址 to
  • 返回值:同上,成功为 0,失败为未拷贝成功的字节数。

这两对函数是内核与用户空间交换数据的唯一安全方式绝对不能 直接使用 memcpy 去操作用户空间指针。


三、完整驱动代码

新建文件 chrdev_buffer.c,将以下代码直接复制即可编译运行。

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 */

#define DEVICE_NAME "chrdev_buf"     // 设备节点名
#define CLASS_NAME  "chrdev_buf_class"
#define BUF_SIZE    1024             // 内核缓冲区大小(字节)

static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;

/* 内核缓冲区及相关变量 */
static char kernel_buf[BUF_SIZE];    // 数据缓冲区
static size_t data_size;             // 当前有效数据长度(字节)

/* 打开设备 */
static int chrdev_open(struct inode *inode, struct file *file)
{
    pr_info("chrdev_buf: device opened\n");
    return 0;
}

/* 关闭设备 */
static int chrdev_release(struct inode *inode, struct file *file)
{
    pr_info("chrdev_buf: device closed\n");
    return 0;
}

/* 读取设备:将内核缓冲区中的数据拷贝到用户空间,之后清空缓冲区 */
static ssize_t chrdev_read(struct file *file, char __user *buf,
                           size_t count, loff_t *f_pos)
{
    ssize_t ret_bytes;                // 实际拷贝的字节数
    unsigned long not_copied;

    /* 如果缓冲区为空,返回 0 表示文件结束(EOF) */
    if (data_size == 0) {
        pr_info("chrdev_buf: buffer empty, read returns EOF\n");
        return 0;
    }

    /* 计算本次可读字节数,不能超过缓冲区现存数据量 */
    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;

    return ret_bytes;
}

/* 写入设备:将用户空间提供的数据拷贝到内核缓冲区,覆盖之前的内容 */
static ssize_t chrdev_write(struct file *file, const char __user *buf,
                            size_t count, loff_t *f_pos)
{
    unsigned long not_copied;
    size_t write_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 的部分已被截断) */
    return write_bytes;
}

static struct file_operations chrdev_fops = {
    .owner   = THIS_MODULE,
    .open    = chrdev_open,
    .release = chrdev_release,
    .read    = chrdev_read,
    .write   = chrdev_write,
};

/* 模块初始化:与第一篇框架完全一致,仅设备名不同 */
static int __init chrdev_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("chrdev_buf: failed to allocate device number\n");
        return ret;
    }
    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");
        goto err_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);
        goto err_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);
        goto err_device_create;
    }

    /* 初始化内核缓冲区 */
    memset(kernel_buf, 0, BUF_SIZE);
    data_size = 0;

    pr_info("chrdev_buf: module loaded, /dev/%s created\n", DEVICE_NAME);
    return 0;

err_device_create:
    class_destroy(my_class);
err_class_create:
    cdev_del(&my_cdev);
err_cdev_add:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

static void __exit chrdev_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 复制代码
# 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 加载驱动

bash 复制代码
sudo insmod chrdev_buffer.ko

检查日志:

bash 复制代码
dmesg | tail
# chrdev_buf: allocated major=239, minor=0
# chrdev_buf: module loaded, /dev/chrdev_buf created

确认设备节点:

bash 复制代码
ls -l /dev/chrdev_buf
# crw------- 1 root root 239, 0 May 27 10:00 /dev/chrdev_buf

若需普通用户访问,临时赋权:

bash 复制代码
sudo chmod 666 /dev/chrdev_buf

5.2 写入测试

bash 复制代码
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 读取测试

bash 复制代码
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 卸载驱动

bash 复制代码
sudo rmmod 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)保护共享数据,让驱动在多进程环境下安全稳定地运行。


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

相关推荐
思麟呀1 小时前
C++工业级日志项目(四)日志落地
linux·开发语言·c++·windows
Dymc1 小时前
【Ubuntu系统指令启动】一招解决:Ubuntu 20.04 桌面双击 .desktop 文件不再“用文本编辑器打开”
linux·运维·ubuntu·一键运行
sailing-data1 小时前
【OS zephyr】make与cmake
linux·运维·服务器
❀搜不到2 小时前
ubuntu 更新cmake
linux·运维·ubuntu
Mr_pyx2 小时前
TypeScript 完全入门指南:从基础到项目配置
linux·运维·ubuntu
LinuxRos2 小时前
从 MCU 到 Linux:机器人嵌入式OTA升级原理解密
linux·单片机·嵌入式硬件·物联网·iot
Frank_refuel2 小时前
Linux网络之网络编程套接字
linux·运维·网络
UXbot2 小时前
初创公司如何选择合适的UI工具支撑快速迭代产品?
人工智能·低代码·ios·交互·原型模式
招风的黑耳2 小时前
Axure动态柱状图设计指南:从静态到交互的完整实现
交互·axure·柱状图