前言
在上一篇文章中,我们搭建了一个可以动态创建设备节点的字符设备驱动框架,但 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_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 会自动消失。
六、注意事项与常见错误
-
禁止直接操作用户空间指针
绝对不要尝试
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)保护共享数据,让驱动在多进程环境下安全稳定地运行。
如果本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力。任何问题欢迎在评论区留言交流!