字符设备驱动核心:设备号、file_operations、cdev 与设备节点
字符设备驱动,说白了就是 Linux 驱动里最基础、最常用的一类。你写个 LED 灯、按键、传感器,基本都落在这个范畴里。今天咱们就把它的四个核心要素掰开揉碎讲清楚:设备号、file_operations 结构体、cdev 结构体,还有设备节点的创建。
我个人习惯把这四个东西看作一个「驱动四件套」——少了哪个,驱动都跑不起来。咱们一个一个过。
设备号:驱动的身份证
设备号分两部分:主设备号和次设备号。主设备号标识驱动类型,次设备号标识具体设备实例。内核里用 dev_t 类型来存,32 位,高 12 位是主设备号,低 20 位是次设备号。
关键点:主设备号是驱动和内核的「握手信号」。用户空间 open 设备文件时,内核根据主设备号找到对应的驱动。
设备号注册与注销
注册设备号有两种方式:
- 静态申请:用
register_chrdev_region(),适合你知道设备号的情况。我早期做项目时喜欢这么干,因为调试方便,设备号固定。 - 动态分配:用
alloc_chrdev_region(),让内核给你分配。产品级代码我建议用这个,避免设备号冲突。
注销就简单了,一个函数搞定:
// 动态分配设备号
dev_t dev_num;
alloc_chrdev_region(&dev_num, 0, 1, "my_driver");
printk("主设备号: %d\n", MAJOR(dev_num));
// 注销设备号
unregister_chrdev_region(dev_num, 1);
我曾经踩过的坑:有一次忘了注销设备号,驱动卸载后重新 insmod,结果设备号被占用,注册失败。后来我养成了习惯——在 exit 函数里第一行就写 unregister_chrdev_region。
file_operations:驱动与用户空间的桥梁
这个结构体定义了驱动能提供的操作。用户空间调用 open、read、write、ioctl 时,内核会通过这个结构体找到你写的函数。
常用的成员有这些:
| 成员 | 说明 | 我常用的场景 |
|---|---|---|
.open |
打开设备时调用 | 初始化硬件、递增引用计数 |
.release |
关闭设备时调用 | 释放资源、递减引用计数 |
.read |
从设备读取数据 | 读取传感器值、寄存器状态 |
.write |
向设备写入数据 | 设置参数、控制输出 |
.unlocked_ioctl |
设备控制命令 | 配置模式、执行特殊操作 |
定义起来很简单:
static int my_open(struct inode *inode, struct file *filp) {
printk("设备已打开\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t len, loff_t *off) {
// 从内核空间拷贝数据到用户空间
if (copy_to_user(buf, kernel_buf, len))
return -EFAULT;
return len;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
小技巧:.owner 一定要赋值为 THIS_MODULE,否则模块卸载时可能出问题。我见过有人漏掉这个,结果 rmmod 时系统直接 panic。
cdev 结构体:内核中的设备对象
cdev 是内核用来管理字符设备的核心结构体。你把 file_operations 和设备号绑在一起,注册到内核里,驱动才算真正「活」了。
操作步骤就三步:
- 分配 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. 注册设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
// 3. 添加 cdev 到内核
cdev_add(&my_cdev, dev_num, 1);
卸载时反过来:先 cdev_del(),再 unregister_chrdev_region()。顺序不能乱,你想想看——如果先注销设备号,cdev 还在内核里挂着,系统就找不到对应的设备了。
设备节点:用户空间看到的「文件」
设备节点就是 /dev/ 目录下的那个文件。用户程序通过它来操作硬件。创建方式有两种:
手动创建
用 mknod 命令:
mknod /dev/my_device c 240 0
c 表示字符设备,240 是主设备号,0 是次设备号。这种方式适合开发调试阶段,产品上就别用了——用户还得自己查设备号,体验太差。
自动创建(推荐)
用 udev 或 mdev 自动管理。驱动里需要做两件事:
- 创建
class和device - 内核会通过 uevent 通知用户空间的 udev 自动创建设备节点
static struct class *my_class;
static int __init my_init(void) {
// ... 注册设备号、添加 cdev ...
// 创建类
my_class = class_create(THIS_MODULE, "my_class");
// 创建设备节点
device_create(my_class, NULL, dev_num, NULL, "my_device");
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);
}
核心逻辑:驱动只负责向内核注册设备信息,设备节点的创建和删除由 udev 自动完成。你只需要提供 class 和 device 信息,剩下的交给系统。
知识结构总览
下面这张图把整个流程串起来了,我建议你多看几遍:
完整驱动框架示例
把上面所有东西串起来,就是一个完整的字符设备驱动骨架:
#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("my_device: open\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
printk("my_device: release\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_device");
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加 cdev
cdev_add(&my_cdev, dev_num, 1);
// 4. 自动创建设备节点
my_class = class_create(THIS_MODULE, "my_class");
device_create(my_class, NULL, dev_num, NULL, "my_device");
printk("my_device: 加载成功,主设备号 %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("my_device: 卸载成功\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
我的建议:把这个框架存成模板,以后写字符设备驱动直接往上加功能。我自己的代码库里就留着这个 skeleton,每次新建驱动都从它开始改。
嗯,到这里字符设备驱动的四个核心要素就讲完了。设备号是身份证,file_operations 是操作手册,cdev 是内核里的登记表,设备节点是用户看到的入口。四者缺一不可,顺序也不能乱。你写驱动的时候,按这个流程走,基本不会出大问题。