3. 字符设备驱动框架:file_operations结构体、主次设备号、cdev注册与注销流程
字符设备是Linux驱动开发中最基础、最常用的设备类型,其核心框架围绕设备号管理、cdev对象与file_operations操作集三个要素展开。理解这三者之间的绑定关系与生命周期,是编写稳定字符设备驱动的第一步。
核心要点:字符设备驱动的本质是将内核中的
cdev对象与一组文件操作函数(file_operations)绑定,并通过设备号暴露给用户空间。用户态通过open/read/write/close系统调用触发内核回调,完成硬件或虚拟设备的访问。
3.1 字符设备驱动核心架构图
下图展示了字符设备驱动在Linux内核中的整体框架,包括设备号、cdev、文件操作与VFS(虚拟文件系统)之间的协作关系。
3.2 file_operations结构体:驱动与VFS的契约
struct file_operations是字符设备驱动与虚拟文件系统(VFS)之间的接口契约。驱动开发者通过填充该结构体的函数指针,定义设备在用户态文件操作下的具体行为。
3.2.1 核心成员解析
| 成员 | 函数原型 | 触发时机 | 典型用途 |
|---|---|---|---|
.owner |
struct module *owner |
模块引用计数 | 通常设为 THIS_MODULE |
.open |
int (*open)(struct inode *, struct file *) |
用户调用 open() |
设备初始化、权限检查、递增使用计数 |
.release |
int (*release)(struct inode *, struct file *) |
用户调用 close() |
设备关闭、资源释放、递减使用计数 |
.read |
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) |
用户调用 read() |
从设备读取数据到用户空间 |
.write |
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) |
用户调用 write() |
从用户空间写入数据到设备 |
.llseek |
loff_t (*llseek)(struct file *, loff_t, int) |
用户调用 lseek() |
调整文件读写位置 |
.unlocked_ioctl |
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) |
用户调用 ioctl() |
设备控制命令(无大内核锁) |
.poll |
__poll_t (*poll)(struct file *, struct poll_table_struct *) |
用户调用 select/poll/epoll |
设备可读/可写状态查询 |
最佳实践:并非所有成员都需要实现。对于简单的字符设备,通常只需实现
.open、.release、.read 和 .write。未实现的成员应设为 NULL,内核会返回 -ENOTTY 或 -EINVAL。
3.2.2 典型定义示例
// 定义设备操作函数
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 count, loff_t *f_pos) {
// 从内核缓冲区拷贝数据到用户空间
if (copy_to_user(buf, kernel_buf, count))
return -EFAULT;
return count;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
// 从用户空间拷贝数据到内核缓冲区
if (copy_from_user(kernel_buf, buf, count))
return -EFAULT;
return count;
}
// 填充 file_operations 结构体
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = NULL, // 使用默认行为
};
3.3 主次设备号:设备在内核中的身份证
设备号是内核识别字符设备的唯一标识,由主设备号(major)和次设备号(minor)组成,类型为 dev_t(32位无符号整数)。
3.3.1 设备号结构
3.3.2 设备号分配方式
| 分配方式 | 函数 | 适用场景 | 注意事项 |
|---|---|---|---|
| 静态分配 | register_chrdev_region() |
已知主设备号(如已分配或实验用) | 需确保主设备号未被占用,否则注册失败 |
| 动态分配 | alloc_chrdev_region() |
推荐方式,由内核自动分配主设备号 | 通过 /proc/devices 查看分配结果 |
// 动态分配设备号示例
dev_t dev_num;
int major, minor;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
major = MAJOR(dev_num);
minor = MINOR(dev_num);
pr_info("Allocated device: major=%d, minor=%d\n", major, minor);
// 使用完毕后释放
unregister_chrdev_region(dev_num, 1);
避坑指南:动态分配的主设备号每次加载模块时可能不同。用户空间需要通过
/proc/devices 或 udev 自动创建设备节点,切勿在应用程序中硬编码主设备号。
3.4 cdev注册与注销流程
struct cdev 是内核中表示字符设备的对象,必须经过初始化 → 添加 → 注销的完整生命周期管理。
3.4.1 完整注册流程
3.4.2 完整代码示例
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#define DEVICE_NAME "my_char_dev"
#define MINOR_COUNT 1
static dev_t dev_num;
static struct cdev *my_cdev;
// file_operations 实现(略,同3.2.2节)
static int __init my_init(void)
{
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated major=%d minor=%d\n", MAJOR(dev_num), MINOR(dev_num));
// 2. 分配cdev对象
my_cdev = cdev_alloc();
if (!my_cdev) {
pr_err("Failed to allocate cdev\n");
unregister_chrdev_region(dev_num, MINOR_COUNT);
return -ENOMEM;
}
// 3. 初始化cdev并绑定file_operations
cdev_init(my_cdev, &my_fops);
my_cdev->owner = THIS_MODULE;
// 4. 将cdev添加到内核
ret = cdev_add(my_cdev, dev_num, MINOR_COUNT);
if (ret < 0) {
pr_err("Failed to add cdev\n");
cdev_del(my_cdev);
unregister_chrdev_region(dev_num, MINOR_COUNT);
return ret;
}
pr_info("Character device registered successfully\n");
return 0;
}
static void __exit my_exit(void)
{
// A. 从内核移除cdev
cdev_del(my_cdev);
// B. 释放设备号
unregister_chrdev_region(dev_num, MINOR_COUNT);
// C. 释放cdev内存(cdev_alloc分配的需要手动释放)
kfree(my_cdev);
pr_info("Character device unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Debug Expert");
MODULE_DESCRIPTION("Character device driver example");
3.5 关键函数对比与避坑
| 函数 | 功能 | 注意事项 |
|---|---|---|
cdev_alloc() |
动态分配 struct cdev 内存 |
必须配合 kfree() 释放;若cdev为静态变量则无需调用 |
cdev_init() |
初始化cdev并绑定 file_operations |
会覆盖 cdev->ops,必须在 cdev_add() 之前调用 |
cdev_add() |
将cdev注册到内核设备表 | 调用后设备立即可用;失败需回滚之前的资源分配 |
cdev_del() |
从内核设备表移除cdev | 调用后设备立即不可用;不会释放cdev内存 |
alloc_chrdev_region() |
动态分配设备号 | 主设备号由内核自动分配,避免冲突 |
register_chrdev_region() |
静态分配设备号 | 需手动指定主设备号,可能与其他驱动冲突 |
常见错误:
- 顺序错误:先
cdev_add()再cdev_init(),导致内核访问未初始化的cdev。 - 内存泄漏:使用
cdev_alloc()后忘记kfree(),或使用静态cdev却调用了kfree()。 - 设备号泄漏:在
cdev_add()失败后未调用unregister_chrdev_region()。 - 并发访问:
cdev_add()之后设备立即可用,但此时驱动可能尚未完成初始化,需使用标志位或互斥锁保护。
3.6 本章小结
- file_operations 是驱动与VFS的接口契约,通过函数指针实现
open/read/write/release等操作。 - 设备号 由主设备号(12位)和次设备号(20位)组成,推荐使用
alloc_chrdev_region()动态分配。 - cdev生命周期 遵循
alloc → init → add → del → unregister → free的严格顺序。 - 错误处理 必须完整:每一步失败都要回滚之前已成功分配的所有资源。