字符设备驱动基础:file_operations、设备号与cdev
字符设备驱动,说白了就是 Linux 驱动里最基础、最常用的一类。你写个 LED 灯、按键、串口、I2C 传感器,本质上都是字符设备。我刚开始学驱动时,觉得这玩意儿挺玄乎,后来发现核心就三样东西:file_operations 结构体、设备号、cdev 对象。搞懂这三样,字符驱动你就入门了。
1. file_operations:驱动与应用的“接口契约”
应用层调用 open、read、write、ioctl,内核怎么知道该执行哪段代码?答案就在 file_operations 结构体里。它是一张函数指针表,每个成员对应一个系统调用。
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 *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
// ... 还有不少,但常用的就这几个
};
你想想看,应用层调用 read(fd, buf, len),内核 VFS 层会找到这个设备对应的 file_operations,然后调用你注册的 .read 函数。这就是驱动的入口。
这里有个坑——.owner 一定要赋值为 THIS_MODULE。我曾经漏掉这一行,结果模块卸载时系统直接崩溃。为什么?因为内核需要知道这个结构体属于哪个模块,防止你在模块卸载后还有人调用你的函数。
2. 主次设备号:设备的“身份证”
设备号分两部分:主设备号(major)和次设备号(minor)。主设备号标识驱动类型,次设备号标识同一驱动下的不同设备。
| 概念 | 作用 | 示例 |
|---|---|---|
| 主设备号 | 告诉内核用哪个驱动 | 89 对应 I2C 总线 |
| 次设备号 | 区分同驱动下的不同设备 | 0 号 I2C 控制器、1 号 I2C 控制器 |
分配设备号有两种方式:
- 静态分配:自己指定主设备号,调用
register_chrdev_region()。适合你知道这个号没被占用的场景。 - 动态分配:调用
alloc_chrdev_region(),让内核给你分配一个。我强烈推荐这种方式,省心。
dev_t dev_num;
int major, minor;
// 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("Major: %d, Minor: %d\n", major, minor);
unregister_chrdev_region(),结果第二次加载模块时设备号冲突,驱动死活注册不上。
3. cdev 注册与注销:驱动的“户口登记”
cdev 是内核里代表字符设备的核心结构体。你得先初始化它,然后注册到内核,内核才知道“哦,这个设备号对应这个驱动”。
标准流程分三步:
- 初始化 cdev:
cdev_init(&cdev, &fops),把 file_operations 绑定上去。 - 添加 cdev:
cdev_add(&cdev, dev_num, count),告诉内核这个设备号归你管。 - 注销 cdev:
cdev_del(&cdev),模块卸载时调用。
struct cdev my_cdev;
dev_t dev_num;
// 1. 分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 注册到内核
cdev_add(&my_cdev, dev_num, 1);
// 模块卸载时
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
4. 设备节点创建:让应用层“看见”你的设备
设备节点就是 /dev/my_device 这样的文件。应用层通过它来操作设备。创建节点有两种方式:
- 手动创建:
mknod /dev/my_device c 240 0(c 表示字符设备,240 是主设备号,0 是次设备号)。调试时用用还行,产品里别这么干。 - 自动创建:使用 udev 或 mdev。驱动里注册设备类,内核会自动通知用户空间创建设备节点。
我推荐用自动创建的方式。在驱动里加上这几行:
static struct class *my_class;
// 在模块初始化时
my_class = class_create(THIS_MODULE, "my_device_class");
device_create(my_class, NULL, dev_num, NULL, "my_device");
// 在模块卸载时
device_destroy(my_class, dev_num);
class_destroy(my_class);
这样,驱动加载后 /dev/my_device 就自动出现了。省心又规范。
5. 完整流程与知识结构
把上面几块串起来,就是字符设备驱动的完整生命周期。我画了张图,帮你理清脉络:
这张图把注册流程(左侧绿色)和注销流程(右侧红色)对照着画出来了。你写驱动时,就照着这个顺序来,基本不会出错。
6. 常见问题与排查思路
我整理了几个新手最容易踩的坑,你写代码时多留个心眼:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| open 返回 -ENODEV | cdev_add 没成功,或设备号不匹配 | 检查 dmesg,确认设备号是否正确注册 |
| write 没反应 | file_operations 里 .write 没实现或返回错误 | 加 printk 调试,确认函数是否被调用 |
| 模块卸载时死机 | cdev_del 顺序不对,或 .owner 没设 | 检查卸载函数中的资源释放顺序 |
| /dev 下没有节点 | device_create 没调用,或 udev 规则不对 | 手动 mknod 测试,确认驱动本身没问题 |
嗯,字符设备驱动的基础就这些。说白了就是:分配设备号 → 初始化 cdev → 注册到内核 → 创建节点。这四个步骤串起来,就是一个完整的字符设备驱动骨架。后面的章节我们会在这个骨架上填肉,加上硬件操作、中断处理、锁机制等等。