3. 字符设备驱动框架
字符设备是Linux驱动中最基础的设备类型,其核心驱动框架围绕三个关键要素构建:cdev结构体(内核中代表字符设备的对象)、设备号(用户空间与内核空间交互的标识)、以及file_operations接口(驱动行为的具体实现)。本节将系统梳理这三者的关系与实现流程。
3.1 字符设备驱动核心架构
字符设备驱动的整体架构可以抽象为“用户空间 → VFS → 驱动层 → 硬件”的调用链。其中,cdev结构体是驱动在内核中的“身份证”,file_operations是驱动对外提供的“服务菜单”,而设备号则是用户空间访问驱动的“门牌号”。
核心概念速览:
- cdev:内核中描述字符设备的数据结构,包含设备号、操作函数集指针等。
- 设备号:由主设备号(major)和次设备号(minor)组成,用于唯一标识设备。
- file_operations:函数指针结构体,定义了open、read、write、release等标准操作。
3.2 cdev结构体详解
struct cdev是内核中字符设备的“本体”,定义在 <linux/cdev.h> 中。驱动开发者需要分配并初始化一个cdev实例,然后将其注册到内核中。
| 成员 | 类型 | 说明 |
|---|---|---|
kobj |
struct kobject |
内核对象基类,用于设备模型管理 |
owner |
struct module * |
指向所属模块,通常设为 THIS_MODULE |
ops |
const struct file_operations * |
指向该设备的操作函数集 |
list |
struct list_head |
用于将cdev链接到系统设备链表 |
dev |
dev_t |
设备号(主设备号 + 次设备号) |
count |
unsigned int |
该设备管理的次设备号数量 |
编程要点:cdev的初始化通常使用
cdev_init(&my_cdev, &my_fops),该函数会将ops指针赋值,并将owner设为 THIS_MODULE。注册使用 cdev_add(&my_cdev, devno, count),其中devno是已分配的设备号,count是次设备号范围。
3.3 设备号:注册与注销
设备号是用户空间访问驱动的“钥匙”。主设备号标识设备类型,次设备号标识同一类型下的不同实例。设备号的分配有两种方式:静态分配(手动指定主设备号)和动态分配(由内核自动分配)。
3.3.1 设备号分配与注册流程
- 定义设备号变量:
dev_t devno; - 分配设备号:
- 动态分配:
alloc_chrdev_region(&devno, minor_base, count, name) - 静态分配:
register_chrdev_region(devno, count, name)
- 动态分配:
- 提取主/次设备号:
MAJOR(devno)和MINOR(devno) - 初始化并添加cdev:
cdev_init()+cdev_add() - 卸载时注销:
cdev_del()+unregister_chrdev_region()
| 函数 | 用途 | 参数说明 |
|---|---|---|
alloc_chrdev_region |
动态分配设备号 | devno(输出), minor_base, count, name |
register_chrdev_region |
静态注册指定设备号 | devno(输入), count, name |
unregister_chrdev_region |
注销设备号 | devno, count |
常见错误:
- 设备号注销顺序错误:必须先
cdev_del()再unregister_chrdev_region(),否则可能导致内核访问已释放的资源。 - 次设备号范围(count)设置过大,导致与其他驱动冲突。
- 静态分配时使用已被占用的主设备号,导致注册失败。
3.4 file_operations接口实现
struct file_operations 是驱动与VFS之间的“协议”。驱动开发者通过实现该结构体中的函数指针,来定义设备的行为。最常用的接口包括:
| 接口 | 函数原型 | 触发时机 |
|---|---|---|
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() 系统调用时 |
3.4.1 典型实现示例
以下是一个简化的字符设备驱动 file_operations 实现框架:
// 设备打开
static int my_open(struct inode *inode, struct file *filp)
{
// 获取设备结构体指针,通常通过 container_of 或 inode->i_cdev
struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = dev; // 保存到文件私有数据
return 0;
}
// 设备读取
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
struct my_device *dev = filp->private_data;
// 从内核缓冲区拷贝数据到用户空间
if (copy_to_user(buf, dev->buffer, len))
return -EFAULT;
return len;
}
// 设备写入
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
struct my_device *dev = filp->private_data;
// 从用户空间拷贝数据到内核缓冲区
if (copy_from_user(dev->buffer, buf, len))
return -EFAULT;
return len;
}
// 设备释放
static int my_release(struct inode *inode, struct file *filp)
{
return 0;
}
// 定义 file_operations 结构体
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
关键注意事项:
- 用户空间与内核空间数据交换:必须使用
copy_to_user()和copy_from_user(),禁止直接解引用用户空间指针。 - 错误处理:所有接口函数应返回负的错误码(如
-ENOMEM、-EFAULT)表示失败,返回非负值表示成功或实际传输的字节数。 - 并发保护:如果设备可能被多个进程同时访问,需要在
read/write实现中加入互斥锁或信号量。
3.5 完整驱动注册与注销流程
将上述三个要素整合起来,字符设备驱动的生命周期如下:
3.6 本章小结
字符设备驱动框架是Linux驱动开发的基石。理解 cdev、设备号和 file_operations 三者的关系,掌握注册与注销的对称流程,是编写稳定可靠驱动的前提。在实际开发中,建议优先使用动态设备号分配(alloc_chrdev_region)以避免主设备号冲突,并在 file_operations 实现中严格遵循内核API规范,特别是用户空间数据拷贝和并发保护。