3. 字符设备驱动框架:file_operations结构体、主次设备号、cdev注册与注销、设备节点的创建(手动与自动)
字符设备是Linux驱动中最基础、最常用的设备类型,其访问模式类似于普通文件——通过open、read、write、close等系统调用进行操作。理解字符设备驱动框架,是掌握Linux驱动开发的基石。本节将深入剖析字符设备驱动的核心组件:file_operations结构体、设备号管理、cdev的注册与注销,以及设备节点的创建方法。
核心概念: 在Linux内核中,一切皆文件。字符设备驱动通过注册一组操作函数(file_operations)到内核,使得用户空间的应用程序可以像操作普通文件一样操作硬件设备。
3.1 file_operations结构体:驱动与用户空间的桥梁
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 *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*flush) (struct file *, fl_owner_t id);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
/* ... 其他成员 */
};
关键成员说明:
- owner:通常设置为
THIS_MODULE,防止模块正在使用时被卸载。 - open:设备打开时调用,用于初始化设备或分配资源。
- release:设备关闭时调用,用于释放资源。
- read:从设备读取数据,数据通过
copy_to_user传递给用户空间。 - write:向设备写入数据,数据通过
copy_from_user从用户空间获取。 - unlocked_ioctl:设备控制命令处理函数,用于执行非标准操作。
注意事项: 在
read 和 write 函数中,用户空间缓冲区指针 __user 不能在内核空间直接解引用,必须使用 copy_to_user 和 copy_from_user 函数进行安全的数据拷贝。
3.2 主次设备号:设备的唯一标识
每个字符设备在内核中都有一个唯一的设备号,由主设备号和次设备号两部分组成。主设备号标识设备对应的驱动程序,次设备号标识由该驱动管理的具体设备实例。
| 概念 | 类型 | 位数 | 作用 | 示例 |
|---|---|---|---|---|
| 主设备号 | major | 12 位 (0-4095) | 标识设备驱动程序 | 1 (内存设备), 4 (tty设备) |
| 次设备号 | minor | 20 位 (0-1048575) | 标识具体设备实例 | 0, 1, 2 ... |
设备号在内核中使用 dev_t 类型表示,这是一个32位的无符号整数。内核提供了宏来操作设备号:
#include <linux/types.h> // dev_t
#include <linux/kdev_t.h> // MAJOR(), MINOR(), MKDEV()
dev_t dev_num = MKDEV(250, 0); // 创建设备号:主=250,次=0
int major = MAJOR(dev_num); // 提取主设备号:250
int minor = MINOR(dev_num); // 提取次设备号:0
设备号分配策略:
- 静态分配:使用
register_chrdev_region(),需要提前知道可用的主设备号。适用于设备号已在内核中预留或已知的情况。 - 动态分配:使用
alloc_chrdev_region(),由内核自动分配未使用的主设备号。推荐用于大多数驱动开发场景,避免设备号冲突。
3.3 cdev结构体:内核中的字符设备对象
cdev 是内核用于管理字符设备的核心数据结构。每个注册到内核的字符设备都对应一个 cdev 实例,它包含了设备号、file_operations 指针以及设备列表等信息。
#include <linux/cdev.h>
struct cdev {
struct kobject kobj; // 内嵌的内核对象
struct module *owner; // 所属模块
const struct file_operations *ops; // 设备操作函数集
struct list_head list; // 设备链表
dev_t dev; // 设备号
unsigned int count; // 设备数量
};
cdev 的初始化与注册流程:
- 分配 cdev 实例:使用
cdev_alloc()动态分配,或静态定义后使用cdev_init()初始化。 - 设置 file_operations:将驱动实现的
file_operations结构体赋值给cdev->ops。 - 设置 owner:将
cdev->owner设置为THIS_MODULE。 - 注册 cdev:调用
cdev_add()将设备添加到内核。 - 注销 cdev:在模块卸载时调用
cdev_del()从内核移除设备。
// 典型 cdev 注册代码示例
static struct cdev my_cdev;
static int __init my_driver_init(void)
{
dev_t dev_num;
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 注册 cdev 到内核
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
printk(KERN_INFO "My driver loaded, major=%d\n", MAJOR(dev_num));
return 0;
}
static void __exit my_driver_exit(void)
{
dev_t dev_num = my_cdev.dev;
// 4. 注销 cdev
cdev_del(&my_cdev);
// 5. 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "My driver unloaded\n");
}
关键顺序:
cdev_add() 必须在设备号分配成功之后调用,且必须在驱动准备好处理设备操作之前完成。一旦 cdev_add() 成功,内核就可以立即调用 file_operations 中的函数,因此所有必要的初始化必须在 cdev_add() 之前完成。
3.4 设备节点的创建:手动与自动
设备节点是用户空间访问设备驱动的入口点,位于 /dev 目录下。创建设备节点有两种方式:手动创建和自动创建。
3.4.1 手动创建设备节点
使用 mknod 命令手动创建设备节点,需要指定设备类型、主设备号和次设备号。
# 语法:mknod /dev/设备名 类型 主设备号 次设备号
# 示例:创建字符设备 /dev/mydevice,主=250,次=0
sudo mknod /dev/mydevice c 250 0
# 设置权限
sudo chmod 666 /dev/mydevice
| 参数 | 说明 | 示例值 |
|---|---|---|
c |
字符设备类型 | c |
b |
块设备类型 | b |
主设备号 |
驱动注册的主设备号 | 250 |
次设备号 |
设备实例编号 | 0 |
手动创建的缺点:
- 需要用户手动执行命令,不便于自动化部署。
- 如果使用动态设备号,每次加载驱动后主设备号可能变化,需要先查看
/proc/devices获取设备号。 - 设备节点在系统重启后消失,需要重新创建。
3.4.2 自动创建设备节点
现代Linux驱动通常使用 udev 或 devtmpfs 机制自动创建设备节点。驱动只需在初始化时创建 device 对象并注册到内核,用户空间的 udev 守护进程会监听内核事件并自动在 /dev 下创建对应的设备节点。
自动创建设备节点需要以下步骤:
- 创建 class:使用
class_create()创建一个设备类。 - 创建设备:使用
device_create()在类下创建具体设备实例。 - 销毁设备:在模块卸载时使用
device_destroy()和class_destroy()清理。
#include <linux/device.h>
static struct class *my_class;
static struct device *my_device;
static int __init my_driver_init(void)
{
dev_t dev_num;
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) return ret;
// 2. 注册 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 3. 创建 class
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
// 4. 创建设备节点(udev 会自动在 /dev 下创建)
my_device = device_create(my_class, NULL, dev_num, NULL, "mydevice");
if (IS_ERR(my_device)) {
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_device);
}
return 0;
}
static void __exit my_driver_exit(void)
{
dev_t dev_num = my_cdev.dev;
// 5. 销毁设备节点
device_destroy(my_class, dev_num);
// 6. 销毁 class
class_destroy(my_class);
// 7. 注销 cdev
cdev_del(&my_cdev);
// 8. 释放设备号
unregister_chrdev_region(dev_num, 1);
}
自动创建的优势:
- 设备节点在驱动加载时自动创建,卸载时自动删除,无需人工干预。
- 支持动态设备号,无需关心具体主设备号数值。
- 设备节点名称可由驱动开发者自定义,用户空间通过固定名称访问设备。
3.5 完整驱动框架总结
字符设备驱动的完整生命周期包括以下步骤,这些步骤在模块加载和卸载函数中成对出现:
| 阶段 | 操作 | 函数 | 说明 |
|---|---|---|---|
| 模块加载 | 1. 分配设备号 | alloc_chrdev_region() |
动态获取主次设备号 |
| 2. 初始化 cdev | cdev_init() |
关联 file_operations 结构体 | |
| 3. 注册 cdev | cdev_add() |
将设备添加到内核 | |
| 4. 创建设备节点 | class_create() + device_create() |
udev 自动在 /dev 下创建设备节点 | |
| 模块卸载 | 5. 销毁设备节点 | device_destroy() + class_destroy() |
移除 /dev 下的设备节点 |
| 6. 注销 cdev | cdev_del() |
从内核移除设备 | |
| 7. 释放设备号 | unregister_chrdev_region() |
归还设备号资源 | ← 上一章 📖 返回目录 下一章 →