4. cdev 结构体:初始化、注册与设备操作集
在 Linux 字符设备驱动中,cdev 是内核用于描述字符设备的核心结构体。它连接了设备编号、设备操作集 file_operations 以及设备实例。本章将深入剖析 cdev 的初始化、注册与注销流程,并详细解读 file_operations 中关键接口的实现规范。
4.1 cdev 结构体定义与核心字段
cdev 结构体定义在 <linux/cdev.h> 中,是字符设备在内核中的“身份证”。其核心字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
kobj |
struct kobject |
内核对象基类,用于设备模型、引用计数与 sysfs 管理 |
owner |
struct module * |
指向所属模块的指针,防止模块被意外卸载 |
ops |
const struct file_operations * |
设备操作集指针,驱动核心功能入口 |
list |
struct list_head |
用于将 cdev 链接到系统 cdev 链表 |
dev |
dev_t |
设备号(主设备号 + 次设备号) |
count |
unsigned int |
关联的次设备号数量 |
关键理解:
cdev 是内核 VFS 层与具体硬件驱动之间的桥梁。当用户空间通过 open() 打开设备文件时,VFS 通过 cdev 找到对应的 file_operations,从而调用驱动实现的函数。
4.2 cdev 初始化:静态与动态两种方式
驱动开发者有两种方式初始化 cdev 结构体:
4.2.1 静态初始化(运行时初始化)
使用 cdev_init() 函数,适用于 cdev 已分配内存(全局或动态分配)的场景:
#include <linux/cdev.h>
struct cdev my_cdev;
// 1. 初始化 cdev,绑定 file_operations
cdev_init(&my_cdev, &my_fops);
// 2. 设置 owner 字段,防止模块被卸载
my_cdev.owner = THIS_MODULE;
4.2.2 动态分配与初始化
使用 cdev_alloc() 函数,由内核自动分配 cdev 内存并初始化:
struct cdev *my_cdev;
my_cdev = cdev_alloc();
if (!my_cdev) {
pr_err("Failed to allocate cdev\n");
return -ENOMEM;
}
my_cdev->ops = &my_fops;
my_cdev->owner = THIS_MODULE;
推荐实践: 在大多数现代驱动中,推荐将
cdev 作为自定义设备结构体的成员进行静态初始化,这样可以更好地管理设备私有数据,避免内存泄漏。
4.3 cdev_add 与 cdev_del:注册与注销
初始化完成后,必须将 cdev 注册到内核,设备才能被系统识别。注销时则需从内核中移除。
4.3.1 cdev_add:向系统注册字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
- p:已初始化的
cdev指针 - dev:设备起始编号(已通过
alloc_chrdev_region或register_chrdev_region获取) - count:关联的次设备号数量
- 返回值:0 成功,负值失败
重要时序:
cdev_add() 调用后,设备立即可被用户空间访问。因此,必须在 cdev_add() 之前完成所有硬件初始化、缓冲区分配等准备工作,避免设备打开时驱动尚未就绪。
4.3.2 cdev_del:从系统注销字符设备
void cdev_del(struct cdev *p);
- 该函数将
cdev从内核链表中移除 - 调用后,设备文件不再关联到驱动
- 通常在模块卸载函数中调用
4.3.3 完整注册与注销流程
标准模板: 字符设备驱动的生命周期通常遵循以下顺序:
// 模块加载
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;
// 3. 注册 cdev
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
return 0;
}
// 模块卸载
static void __exit my_driver_exit(void)
{
dev_t dev_num = my_cdev.dev;
// 1. 注销 cdev
cdev_del(&my_cdev);
// 2. 释放设备号
unregister_chrdev_region(dev_num, 1);
}
4.4 设备操作集 file_operations 详解
file_operations 是字符设备驱动的核心接口表,定义了用户空间通过系统调用操作设备文件时,内核应调用的驱动函数。其定义在 <linux/fs.h> 中。
4.4.1 核心成员与函数原型
| 成员 | 函数原型 | 触发系统调用 | 说明 |
|---|---|---|---|
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() |
从用户空间写入数据到设备 |
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() |
查询设备是否可读/可写,支持 I/O 多路复用 |
mmap |
int (*mmap)(struct file *, struct vm_area_struct *) |
mmap() |
将设备内存映射到用户空间 |
4.4.2 典型 file_operations 实例化
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
.poll = my_poll,
.mmap = my_mmap,
};
注意: 对于不需要实现的操作,可以将其设置为
NULL。内核会为 NULL 的操作返回默认行为(如 -EINVAL 或 -ENOTTY)。owner 字段必须设置为 THIS_MODULE,以支持模块引用计数。
4.5 cdev、设备号与 file_operations 的关系
下图展示了 cdev、设备号、file_operations 以及 VFS 层之间的协作关系:
4.6 私有数据与 container_of 宏
在实际驱动中,通常需要将 cdev 嵌入到自定义的设备结构体中,以便在操作函数中访问设备私有数据。这是通过 container_of 宏实现的:
struct my_device {
struct cdev cdev; // 必须作为第一个成员(推荐)
spinlock_t lock;
void __iomem *base_addr;
unsigned int buffer_size;
// ... 其他私有数据
};
static int my_open(struct inode *inode, struct file *filp)
{
// 从 inode->i_cdev 获取 cdev 指针,再通过 container_of 获取 my_device
struct my_device *mydev = container_of(inode->i_cdev, struct my_device, cdev);
// 将私有数据存入 file->private_data,供其他操作使用
filp->private_data = mydev;
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
struct my_device *mydev = filp->private_data;
// ... 使用 mydev 进行读取操作
}
设计模式: 将
cdev 嵌入自定义结构体,并通过 container_of 获取父结构体,是 Linux 字符设备驱动的标准设计模式。这种方式避免了全局变量,支持多实例设备。
4.7 本章小结
- cdev 初始化:使用
cdev_init()或cdev_alloc(),并设置owner和ops字段。 - cdev_add:在设备号分配后调用,将设备注册到内核,调用后设备立即可用。
- cdev_del:在模块卸载时调用,从内核中移除设备。
- file_operations:驱动核心接口表,必须实现
open/release,根据需求实现read/write/ioctl/poll等。 - 私有数据管理:通过
container_of和file->private_data实现设备实例与操作函数的绑定。
常见错误:
- 在
cdev_add之后才进行硬件初始化,导致设备打开时驱动未就绪。 - 忘记设置
owner = THIS_MODULE,导致模块引用计数失效,可能引发内核崩溃。 - 在
file_operations中使用了ioctl而非unlocked_ioctl,导致在 5.x 以上内核中编译警告或功能异常。