基于C的Linux驱动开发:字符设备注册与用户空间数据交互实操
Linux驱动开发中,字符设备是最基础也最常用的类型——它像文件一样通过open/read/write/close与用户空间交互。很多嵌入式工程师在写第一个驱动时,往往卡在设备号的申请、cdev的注册以及数据拷贝的细节上。本文以一个虚拟字符设备"mydev"为例,从头到尾走通注册流程和数据交互。
一、设备号与cdev注册
字符设备通过主设备号和次设备号标识。现代内核推荐动态分配主设备号,避免冲突。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mydev"
#define CLASS_NAME "myclass"
static int major;
static struct class *my_class;
static struct cdev my_cdev;
// 设备结构体(存放私有数据)
struct mydev_data {
char buffer[256];
size_t len;
};
static struct mydev_data dev_data;
static int __init my_init(void) {
dev_t dev_num;
// 1. 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
major = MAJOR(dev_num);
// 2. 创建设备类(用于udev自动创建设备节点)
my_class = class_create(THIS_MODULE, CLASS_NAME);
device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
// 3. 初始化cdev并添加到内核
cdev_init(&my_cdev, &my_fops); // my_fops在后面定义
my_cdev.owner = THIS_MODULE;
cdev_add(&my_cdev, dev_num, 1);
// 初始化设备数据
memset(dev_data.buffer, 0, sizeof(dev_data.buffer));
dev_data.len = 0;
printk(KERN_INFO "mydev: registered with major %d\n", major);
return 0;
}
class_create和device_create让用户空间的udev在/dev/下自动生成设备节点,省去手动mknod的麻烦。
二、file_operations实现
字符驱动的核心是填充struct file_operations,告诉内核当用户调用open/read/write/release时该做什么。
static int my_open(struct inode *inode, struct file *filp) {
filp->private_data = &dev_data; // 将设备数据关联到文件指针
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
struct mydev_data *data = filp->private_data;
size_t available = data->len - *f_pos;
size_t to_copy = min(count, available);
if (to_copy == 0) return 0; // EOF
// 从内核空间拷贝到用户空间
if (copy_to_user(buf, data->buffer + *f_pos, to_copy)) {
return -EFAULT;
}
*f_pos += to_copy;
return to_copy;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
struct mydev_data *data = filp->private_data;
size_t to_copy = min(count, sizeof(data->buffer));
// 从用户空间拷贝到内核空间
if (copy_from_user(data->buffer, buf, to_copy)) {
return -EFAULT;
}
data->len = to_copy;
*f_pos = 0; // 简单实现,每次写重置位置
return to_copy;
}
static int my_release(struct inode *inode, struct file *filp) {
return 0;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
关键点:
copy_to_user和copy_from_user必须使用,绝不能直接用memcpy——内核空间不能直接访问用户指针,否则会引发缺页异常或安全问题。
filp->private_data是连接文件实例和设备数据的桥梁,每个open都可以有自己的上下文。
返回值表示实际传输的字节数,错误返回负值(如-EFAULT)。
三、模块卸载
卸载时需要逆序清理:
static void __exit my_exit(void) {
dev_t dev_num = MKDEV(major, 0);
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "mydev: unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
四、用户空间测试
编译驱动后insmod mydev.ko,在/dev/mydev出现。可以用简单的C程序测试:
int fd = open("/dev/mydev", O_RDWR);
write(fd, "Hello driver!", 13);
char buf[256];
read(fd, buf, sizeof(buf));
printf("Read: %s\n", buf);
close(fd);
五、三个翻车高发点
忘记copy_to_user返回值的检查:返回非零表示拷贝失败,必须返回-EFAULT,否则用户空间读到垃圾数据。
设备节点未自动创建:如果class_create/device_create没写,需要手动mknod /dev/mydev c major 0。推荐用udev自动管理。
open时不设置private_data:后续read/write中filp->private_data为NULL,访问即崩溃。务必在open中赋值。
字符设备驱动是Linux驱动的入门必修课。掌握设备号分配、cdev注册、file_operations实现和用户空间数据拷贝,就能应对大多数简单的硬件交互需求。在此基础上,可以进一步学习阻塞IO、poll机制、ioctl等高级特性。





