3. 字符设备驱动框架:file_operations、设备号与cdev
字符设备驱动,说白了就是Linux世界里最基础的一类驱动。你写的第一行驱动代码,十有八九就是它。我刚开始学驱动时,总觉得这东西玄乎——不就是操作几个结构体吗?后来踩了坑才明白,这几个结构体背后藏着整个VFS(虚拟文件系统)的设计哲学。
今天咱们就把字符设备驱动的骨架拆开来看。你掌握了这个框架,后面写任何字符设备驱动,都只是往这个骨架里填肉而已。
3.1 file_operations:驱动与用户态的桥梁
用户程序调用open()、read()、write()时,内核怎么知道该执行哪段驱动代码?答案就在file_operations结构体里。它就是个函数指针表,把系统调用和驱动函数一一对应起来。
核心结构体定义(Linux 5.x内核):
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
/* ... 还有几十个回调,但常用的就这几个 */
};
我个人习惯,写驱动时至少实现open、release、read、write这四个。其他的按需添加。你想想看,用户程序打不开设备,或者读不到数据,这驱动还有什么意义?
避坑指南:owner字段一定要初始化为THIS_MODULE。我曾经漏掉这个,结果模块卸载时内核直接Oops。这个字段防止模块在被使用时被卸载,相当于一个引用计数锁。
3.2 主次设备号:设备的身份证
Linux用设备号来标识一个设备。主设备号(major)表示设备类型,次设备号(minor)表示同类设备中的第几个。就像小区楼号和门牌号的关系。
| 概念 | 含义 | 示例 |
|---|---|---|
| 主设备号 | 标识设备驱动类型 | 89对应I2C,254对应GPIO |
| 次设备号 | 标识同一驱动下的不同设备 | /dev/tty0的minor=0,/dev/tty1的minor=1 |
| 设备编号 | 主次合并后的32位值 | MKDEV(major, minor)宏生成 |
设备号分配有两种方式:
- 静态分配:调用
register_chrdev_region(),需要你知道一个没被占用的主设备号。我建议新手别用这个,万一冲突了,驱动加载会失败。 - 动态分配:调用
alloc_chrdev_region(),内核自动给你分配一个空闲的主设备号。我个人强烈推荐这种方式,省心。
dev_t dev_num;
int major, minor;
// 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_driver");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("Allocated major=%d, minor=%d\n", major, minor);
注意:动态分配的主设备号每次可能不同。你需要在模块加载后通过cat /proc/devices查看,然后手动创建节点。或者用后面讲的自动创建方式。
3.3 cdev注册与注销:驱动核心的注册
cdev是内核里代表字符设备的核心结构体。你得先初始化它,然后注册到内核的设备表中。流程其实就三步:
- 分配cdev:
cdev_alloc()或静态定义 - 初始化:
cdev_init()绑定file_operations - 注册:
cdev_add()告诉内核这个设备存在了
struct cdev my_cdev;
dev_t dev_num;
// 1. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 2. 注册到内核
cdev_add(&my_cdev, dev_num, 1); // 第三个参数是设备数量
// 注销时
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
嗯,这里要注意顺序。注册时先分配设备号,再添加cdev。注销时反过来,先删除cdev,再释放设备号。我在项目中遇到过有人搞反了顺序,结果设备号已经被别人占用了,cdev却还没删干净,内核报了一堆警告。
3.4 设备节点的创建:让用户态看得见
设备节点就是/dev/目录下的那个文件。没有它,用户程序没法通过open()访问你的设备。创建节点有两种方式:
- 手动创建:
mknod /dev/mydev c 240 0(主设备号240,次设备号0)。调试时可以用,产品里别这么干。 - 自动创建:使用udev或mdev,配合内核的
device_create()接口。这是生产环境的标准做法。
自动创建的代码也很简单:
static struct class *my_class;
// 在模块初始化函数中
my_class = class_create(THIS_MODULE, "my_class");
device_create(my_class, NULL, dev_num, NULL, "my_device");
// 在模块退出函数中
device_destroy(my_class, dev_num);
class_destroy(my_class);
这样,模块加载后/dev/my_device就自动出现了。用户程序直接open("/dev/my_device", O_RDWR)就能用。
小技巧:用device_create()时,最后一个参数是设备节点名。你可以用格式化字符串,比如"mydev%d", minor,这样每个次设备号对应一个独立节点。
3.5 完整框架流程图
下面这张图把整个流程串起来了。从模块加载到设备节点出现,每一步都离不开。
3.6 完整示例代码
把上面所有知识点揉在一起,就是一个能用的字符设备驱动骨架。你拿去改改就能用:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static int my_open(struct inode *inode, struct file *filp) {
printk("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
printk("Device closed\n");
return 0;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
};
static int __init my_init(void) {
// 1. 分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_driver");
// 2. 初始化并注册cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
cdev_add(&my_cdev, dev_num, 1);
// 3. 自动创建设备节点
my_class = class_create(THIS_MODULE, "my_class");
device_create(my_class, NULL, dev_num, NULL, "my_device");
printk("Module loaded, major=%d\n", MAJOR(dev_num));
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk("Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
重要提醒:上面的代码只实现了open和release,没有read/write。如果你真的去cat /dev/my_device,会返回什么?嗯,什么都不会返回,因为read函数没实现。实际项目中,至少要把read和write补上,不然用户程序没法交换数据。
字符设备驱动框架,说白了就是这三板斧:设备号、cdev、设备节点。你把这个流程刻在脑子里,后面学什么LED驱动、按键驱动、I2C驱动,都只是在这个框架上做加法。我当年第一次写完这个骨架,insmod之后看到/dev/my_device出现,那种成就感——嗯,你试了就知道了。