嵌入式Linux内核模块开发入门:从零编写一个简单的字符设备驱动
Linux内核模块是动态加载到内核中的代码片段,常用于驱动硬件、扩展系统调用或实现新的文件系统。字符设备驱动是最基础的驱动类型——它以字节流的方式与用户空间交互,像操作普通文件一样open/read/write/close。本文从零编写一个名为"mydev"的虚拟字符设备驱动,涵盖模块框架、设备号申请、cdev注册和用户空间测试。
一、模块框架:加载与卸载
每个内核模块必须有module_init和module_exit两个入口点,分别指定初始化函数和退出函数。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mydev"
static int __init my_init(void) {
printk(KERN_INFO "mydev: module loaded\n");
return 0;
}
static void __exit my_exit(void) {
printk(KERN_INFO "mydev: module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
编译后insmod mydev.ko加载,rmmod mydev卸载,通过dmesg查看内核日志。
二、设备号与cdev注册
字符设备通过主设备号和次设备号标识。推荐动态分配设备号,避免冲突。
static int major;
static struct cdev my_cdev;
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. 初始化cdev并关联file_operations
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加cdev到内核
cdev_add(&my_cdev, dev_num, 1);
printk(KERN_INFO "mydev: registered with major %d\n", major);
return 0;
}
static void __exit my_exit(void) {
dev_t dev_num = MKDEV(major, 0);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "mydev: unregistered\n");
}
三、file_operations实现
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;
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。
四、用户空间测试
编译驱动后用insmod加载,创建设备节点(如果使用udev则自动创建),然后运行测试程序:
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);
五、三个翻车高发点
忘记创建设备节点:如果没有udev,需要手动mknod /dev/mydev c major 0,否则open返回-1。
copy_to_user返回值未检查:如果拷贝失败返回非零,应返回-EFAULT,否则用户空间读到垃圾数据。
open时未设置private_data:后续read/write中访问野指针导致内核崩溃。
从零编写字符设备驱动是理解Linux内核运作机制的起点。掌握模块框架、设备号、cdev和file_operations,就能为各种硬件编写驱动程序。在此基础上,可以进一步学习阻塞IO、ioctl、设备树等高级主题。





