3. 字符设备驱动框架:file_operations结构体、cdev注册流程、设备节点与驱动的绑定
字符设备是Linux驱动开发中最基础的设备类型,其核心框架围绕file_operations结构体、cdev注册流程以及设备节点与驱动的绑定三个关键环节展开。理解这一框架是掌握HAL层设备节点访问控制的前提。
3.1 file_operations结构体:驱动与用户空间的接口契约
file_operations结构体是字符设备驱动的核心数据结构,它定义了驱动向用户空间暴露的操作函数指针集合。用户空间通过系统调用(如open、read、write、ioctl)访问设备节点时,VFS(虚拟文件系统)会查找该设备节点对应的file_operations实例,并调用相应的驱动函数。
关键理解: file_operations是驱动与用户空间之间的“函数跳转表”,每个设备节点在打开后都会关联一个file结构,该结构中的f_op指针指向驱动提供的file_operations实例。
3.1.1 核心成员与功能
| 成员函数 | 系统调用对应 | 功能描述 | 典型返回值 |
|---|---|---|---|
open |
open() |
打开设备,初始化硬件或分配资源 | 0(成功)或负错误码 |
release |
close() |
关闭设备,释放资源 | 0 |
read |
read() |
从设备读取数据到用户缓冲区 | 实际读取字节数 |
write |
write() |
从用户缓冲区写入数据到设备 | 实际写入字节数 |
unlocked_ioctl |
ioctl() |
设备特定控制命令(无大内核锁) | 0或命令特定值 |
mmap |
mmap() |
将设备内存映射到用户空间 | 0或负错误码 |
poll |
poll()/select()/epoll() |
查询设备是否可读写 | POLLIN/POLLOUT掩码 |
开发提示: 在HAL层驱动开发中,
unlocked_ioctl是最常用的接口,用于实现设备特定的控制逻辑(如设置采样率、启动/停止采集)。read和write则用于批量数据传输。
3.1.2 结构体定义示例
// 定义驱动实现的函数
static int my_dev_open(struct inode *inode, struct file *filp);
static int my_dev_release(struct inode *inode, struct file *filp);
static ssize_t my_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
static ssize_t my_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
static long my_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
// 填充 file_operations 结构体
static struct file_operations my_dev_fops = {
.owner = THIS_MODULE,
.open = my_dev_open,
.release = my_dev_release,
.read = my_dev_read,
.write = my_dev_write,
.unlocked_ioctl = my_dev_ioctl,
};
常见错误: 忘记设置
.owner = THIS_MODULE会导致模块引用计数失效,在模块卸载时可能引发内核崩溃。另外,如果驱动不需要某个操作,应将其设置为NULL,而不是实现一个空函数。
3.2 cdev注册流程:将驱动注册到内核
cdev(字符设备)结构体是内核中表示字符设备的抽象。注册流程分为三个核心步骤:分配cdev、初始化cdev、添加cdev到内核。只有完成注册后,内核才能将设备节点与驱动关联起来。
3.2.1 注册流程步骤
- 分配设备号:使用
alloc_chrdev_region()动态分配主设备号和次设备号范围,或使用register_chrdev_region()注册静态设备号。 - 初始化cdev:调用
cdev_init(),将cdev结构与file_operations绑定。 - 添加cdev到内核:调用
cdev_add(),将cdev注册到内核的设备管理链表中,并关联设备号范围。 - 创建设备节点(可选但推荐):使用
device_create()在/dev目录下自动创建设备节点,或手动使用mknod命令。
核心原则: cdev_add()必须在设备号分配之后调用,且必须在模块初始化函数中完成。设备节点可以在cdev_add之后随时创建,但只有cdev_add成功后,设备节点才能正确绑定到驱动。
3.2.2 完整注册代码示例
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t my_dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static int __init my_driver_init(void)
{
int ret;
// 步骤1:动态分配设备号(主设备号由内核分配)
ret = alloc_chrdev_region(&my_dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
// 步骤2:初始化cdev,绑定file_operations
cdev_init(&my_cdev, &my_dev_fops);
my_cdev.owner = THIS_MODULE;
// 步骤3:添加cdev到内核
ret = cdev_add(&my_cdev, my_dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(my_dev_num, 1);
return ret;
}
// 步骤4:创建device class和设备节点(自动在/dev下生成)
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
pr_err("Failed to create class\n");
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev_num, 1);
return PTR_ERR(my_class);
}
device_create(my_class, NULL, my_dev_num, NULL, "my_device0");
pr_info("My driver initialized, major=%d, minor=%d\n",
MAJOR(my_dev_num), MINOR(my_dev_num));
return 0;
}
static void __exit my_driver_exit(void)
{
device_destroy(my_class, my_dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev_num, 1);
pr_info("My driver exited\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
3.2.3 注册流程SVG图
3.3 设备节点与驱动的绑定机制
设备节点(/dev/my_device0)与驱动的绑定是通过设备号作为桥梁实现的。当用户空间打开设备节点时,内核VFS层根据设备节点中的主设备号和次设备号,在内核的cdev_map散列表中查找对应的cdev实例,进而获取file_operations指针。
3.3.1 绑定流程详解
- 设备节点创建:
device_create()在/dev下创建节点,节点中存储了dev_t(主设备号+次设备号)。 - 打开操作:用户调用
open("/dev/my_device0", O_RDWR),VFS解析路径找到inode,从中提取dev_t。 - 查找cdev:内核通过
dev_t在cdev_map中查找已注册的cdev对象。 - 绑定fops:找到
cdev后,将file结构的f_op指针设置为cdev->ops(即file_operations)。 - 调用open函数:最后调用
file_operations->open(),驱动获得控制权。
3.3.2 绑定关系SVG架构图
3.4 多设备支持与次设备号管理
在实际HAL层驱动中,一个驱动通常需要管理多个同类型设备(如多个传感器、多个GPIO控制器)。通过次设备号可以区分不同设备实例,驱动在open函数中根据次设备号初始化不同的硬件上下文。
3.4.1 多设备注册示例
// 注册多个次设备号
#define NUM_DEVICES 4
ret = alloc_chrdev_region(&my_dev_num, 0, NUM_DEVICES, "my_device");
// 为每个设备注册独立的cdev
for (int i = 0; i < NUM_DEVICES; i++) {
cdev_init(&my_cdevs[i], &my_dev_fops);
my_cdevs[i].owner = THIS_MODULE;
ret = cdev_add(&my_cdevs[i], MKDEV(MAJOR(my_dev_num), i), 1);
if (ret < 0) {
// 错误处理:回滚已注册的cdev
while (--i >= 0) cdev_del(&my_cdevs[i]);
unregister_chrdev_region(my_dev_num, NUM_DEVICES);
return ret;
}
// 创建设备节点
device_create(my_class, NULL, MKDEV(MAJOR(my_dev_num), i), NULL, "my_device%d", i);
}
3.4.2 在open函数中区分设备
static int my_dev_open(struct inode *inode, struct file *filp)
{
unsigned int minor = iminor(inode); // 获取次设备号
struct my_device_data *dev_data;
// 根据次设备号选择对应的设备数据结构
if (minor >= MAX_DEVICES) {
return -ENODEV;
}
dev_data = &devices[minor];
filp->private_data = dev_data; // 保存到file的私有数据
// 初始化硬件(如使能时钟、配置GPIO)
dev_data->ref_count++;
pr_info("Device %d opened, ref_count=%d\n", minor, dev_data->ref_count);
return 0;
}
最佳实践: 使用
filp->private_data保存设备私有数据结构,避免在后续read/write/ioctl中重复查找设备。这是驱动开发中广泛使用的模式。
3.5 本章小结
| 核心概念 | 关键函数/结构体 | 作用 |
|---|---|---|
| file_operations | struct file_operations |
定义驱动与用户空间的接口函数集 |
| cdev注册 | cdev_init() + cdev_add() |
将字符设备注册到内核,关联设备号与fops |
| 设备节点绑定 | device_create() + cdev_map |
通过设备号将/dev节点与驱动绑定 |
| 多设备管理 | 次设备号 + iminor() |
通过次设备号区分不同设备实例 |