3. 字符设备驱动基础:设备号、file_operations 与 cdev
字符设备是 Linux 驱动开发中最基础、最常用的设备类型,其核心特征是数据按字节流顺序访问,典型的字符设备包括串口、键盘、鼠标、LED 等。本章将深入剖析字符设备驱动的三大基石:设备号(主/次设备号)、file_operations 结构体以及 cdev 的注册与注销。掌握这些概念是编写任何字符设备驱动的第一步。
核心要点:字符设备驱动的本质是“用户空间通过文件接口(open/read/write/close)操作硬件”,内核通过设备号定位驱动,通过 file_operations 结构体将系统调用映射到驱动函数,通过 cdev 将设备号与操作集绑定到内核。
3.1 设备号:主设备号与次设备号
在 Linux 中,每个设备在内核中都有一个唯一的数字标识,称为设备号(dev_t)。设备号是一个 32 位无符号整数,高 12 位表示主设备号(major number),低 20 位表示次设备号(minor number)。
- 主设备号:标识设备对应的驱动程序。内核通过主设备号找到正确的驱动。
- 次设备号:由驱动内部使用,用于区分同一驱动管理的多个物理或逻辑设备。
| 概念 | 位数 | 范围 | 作用 |
|---|---|---|---|
| 主设备号 | 12 位 | 0 ~ 4095 | 标识驱动程序 |
| 次设备号 | 20 位 | 0 ~ 1048575 | 标识具体设备实例 |
设备号操作宏:内核提供了一组宏来操作 dev_t 类型,驱动开发者应优先使用这些宏,而非手动位运算。
#include <linux/types.h> // dev_t
#include <linux/kdev_t.h> // 宏定义
dev_t dev = MKDEV(major, minor); // 由主次设备号合成 dev_t
int major = MAJOR(dev); // 从 dev_t 提取主设备号
int minor = MINOR(dev); // 从 dev_t 提取次设备号
3.1.1 静态分配与动态分配
设备号的分配有两种方式:
- 静态分配:驱动开发者手动指定一个主设备号(如 240),使用
register_chrdev_region()注册。适用于已知且未被占用的设备号,但容易与其它驱动冲突。 - 动态分配:调用
alloc_chrdev_region()让内核自动分配一个未使用的主设备号。推荐用于大多数驱动,避免冲突。
避坑指南:静态分配主设备号时,务必先通过
cat /proc/devices 查看当前已占用的设备号列表,避免冲突。动态分配是更安全、更现代的做法。
3.2 file_operations 结构体
file_operations 是字符设备驱动的核心数据结构,它定义了用户空间通过文件系统接口操作设备时,内核应调用的驱动函数指针。该结构体定义在 <linux/fs.h> 中。
| 成员函数 | 系统调用触发 | 典型用途 |
|---|---|---|
open |
open() | 初始化设备、递增使用计数 |
release |
close() | 释放资源、递减使用计数 |
read |
read() | 从设备读取数据到用户空间 |
write |
write() | 从用户空间写入数据到设备 |
unlocked_ioctl |
ioctl() | 设备控制命令(无大内核锁) |
llseek |
lseek() | 修改文件读写位置 |
典型 file_operations 定义示例:
#include <linux/fs.h>
static int my_open(struct inode *inode, struct file *filp) {
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
pr_info("Device closed\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t len, loff_t *off) {
// 实现读取逻辑
return 0;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t len, loff_t *off) {
// 实现写入逻辑
return len;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
关键点:file_operations 结构体中的
.owner 必须设置为 THIS_MODULE,用于防止模块正在使用时被卸载。如果某个操作不需要实现,直接设为 NULL,内核会使用默认行为。
3.3 cdev 结构体:注册与注销
cdev 是内核用于表示字符设备的核心结构体,定义在 <linux/cdev.h> 中。它负责将设备号与 file_operations 绑定在一起,并注册到内核的设备管理子系统中。
3.3.1 cdev 初始化与注册步骤
- 分配设备号:使用
alloc_chrdev_region()或register_chrdev_region()。 - 初始化 cdev:调用
cdev_init(),将 cdev 与 file_operations 关联。 - 添加 cdev 到内核:调用
cdev_add(),将 cdev 与设备号绑定并激活。
完整注册示例:
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t my_dev;
static struct cdev my_cdev;
static int __init my_driver_init(void) {
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&my_dev, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated device number: major=%d, minor=%d\n",
MAJOR(my_dev), MINOR(my_dev));
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加 cdev 到内核
ret = cdev_add(&my_cdev, my_dev, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(my_dev, 1);
return ret;
}
pr_info("Character device registered successfully\n");
return 0;
}
3.3.2 cdev 注销步骤
- 删除 cdev:调用
cdev_del(),将 cdev 从内核中移除。 - 释放设备号:调用
unregister_chrdev_region()。
完整注销示例:
static void __exit my_driver_exit(void) {
// 1. 删除 cdev
cdev_del(&my_cdev);
// 2. 释放设备号
unregister_chrdev_region(my_dev, 1);
pr_info("Character device unregistered\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
重要顺序:注销时必须先调用
cdev_del() 再调用 unregister_chrdev_region()。如果先释放设备号,cdev 仍然指向一个无效的设备号,可能导致内核崩溃。
3.4 完整驱动框架模板
将上述三个核心要素整合在一起,形成一个可编译的字符设备驱动模板:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t my_dev;
static struct cdev my_cdev;
static int my_open(struct inode *inode, struct file *filp) {
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
pr_info("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) {
int ret;
ret = alloc_chrdev_region(&my_dev, 0, 1, "my_device");
if (ret < 0) return ret;
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, my_dev, 1);
if (ret < 0) {
unregister_chrdev_region(my_dev, 1);
return ret;
}
pr_info("Module loaded: major=%d minor=%d\n",
MAJOR(my_dev), MINOR(my_dev));
return 0;
}
static void __exit my_exit(void) {
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev, 1);
pr_info("Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
编译与测试:将上述代码保存为
my_char_dev.c,编写 Makefile 编译为内核模块。加载后通过 cat /proc/devices 查看分配的主设备号,使用 mknod /dev/mydev c major 0 创建设备节点,即可通过 echo 和 cat 测试。
3.5 本章小结
| 组件 | 作用 | 关键函数/宏 |
|---|---|---|
| 设备号 | 标识驱动和设备实例 | MKDEV, MAJOR, MINOR, alloc_chrdev_region, unregister_chrdev_region |
| file_operations | 定义系统调用到驱动函数的映射 | struct file_operations, .owner = THIS_MODULE |
| cdev | 将设备号与操作集绑定并注册到内核 | cdev_init, cdev_add, cdev_del |