4. 字符设备驱动框架:cdev结构体、设备号分配与file_operations接口实现
字符设备是Linux驱动开发中最基础的设备类型,其核心在于通过cdev结构体将设备与file_operations操作集绑定,并分配唯一的设备号供用户空间访问。本章将深入剖析这三要素的实现原理与编码规范。
核心要点:字符设备驱动的注册本质是“设备号 + cdev + file_operations”三元组的建立过程。设备号是用户空间访问的门牌号,cdev是内核中设备对象的载体,file_operations是驱动行为的函数指针表。
4.1 cdev结构体:字符设备的内核抽象
cdev(character device)是内核用于描述字符设备的核心数据结构,定义在<linux/cdev.h>中。每个注册到系统的字符设备都对应一个cdev实例。
struct cdev {
struct kobject kobj; /* 内嵌的内核对象,用于设备模型 */
struct module *owner; /* 所属模块,通常设为 THIS_MODULE */
const struct file_operations *ops; /* 设备操作函数指针表 */
struct list_head list; /* 用于将cdev链接到系统链表 */
dev_t dev; /* 设备号(主设备号 + 次设备号) */
unsigned int count; /* 关联的次设备号数量 */
};
编程提示:cdev结构体通常作为私有数据结构的一部分动态分配,而非直接定义在全局区。推荐使用
cdev_alloc() 分配,或使用 cdev_init() 初始化栈/堆中的cdev。
4.2 设备号分配:静态与动态策略
设备号由主设备号(major)和次设备号(minor)组成,类型为dev_t(32位无符号整数)。主设备号标识设备驱动类型,次设备号标识同一驱动下的不同设备实例。
| 分配方式 | 核心函数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 静态分配 | register_chrdev_region() |
设备号固定,用户空间可预知 | 需手动申请,可能冲突 | 广泛使用的标准设备(如tty、misc) |
| 动态分配 | alloc_chrdev_region() |
自动分配,避免冲突 | 设备号不固定,需通过udev或sysfs获取 | 大多数自定义驱动 |
4.2.1 静态分配示例
#define MY_MAJOR 240
#define MY_MINOR 0
#define DEVICE_COUNT 4
dev_t devno = MKDEV(MY_MAJOR, MY_MINOR);
int ret = register_chrdev_region(devno, DEVICE_COUNT, "my_device");
if (ret < 0) {
pr_err("Failed to register chrdev region\n");
return ret;
}
4.2.2 动态分配示例
dev_t devno;
int ret = alloc_chrdev_region(&devno, 0, 4, "my_device");
if (ret < 0) {
pr_err("Failed to alloc chrdev region\n");
return ret;
}
pr_info("Allocated major=%d minor=%d\n", MAJOR(devno), MINOR(devno));
避坑指南:静态分配的主设备号应避免使用已占用的号码(如1-10、13、14等系统保留号)。动态分配后,务必通过
MAJOR() 和 MINOR() 宏提取设备号,并在模块卸载时调用 unregister_chrdev_region() 释放。
4.3 file_operations接口实现
file_operations 结构体是驱动与VFS(虚拟文件系统)之间的桥梁,定义了用户空间通过系统调用操作设备文件时内核调用的驱动函数。每个成员都是一个函数指针,驱动只需实现需要的操作。
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = my_llseek,
.unlocked_ioctl = my_ioctl,
};
| 成员 | 对应系统调用 | 典型实现职责 | 返回值 |
|---|---|---|---|
open |
open() |
初始化设备、递增使用计数、分配私有数据 | 0成功,负值错误码 |
release |
close() |
清理资源、递减使用计数 | 0成功 |
read |
read() |
从设备拷贝数据到用户空间 | 读取字节数,0表示EOF |
write |
write() |
从用户空间拷贝数据到设备 | 写入字节数 |
llseek |
lseek() |
修改文件读写位置 | 新位置偏移量 |
unlocked_ioctl |
ioctl() |
设备特定控制命令处理 | 0成功,负值错误码 |
4.3.1 典型实现模板
static int my_open(struct inode *inode, struct file *filp)
{
struct my_device *dev;
dev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = dev; /* 将设备结构体存入filp */
pr_info("Device opened\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_device *dev = filp->private_data;
size_t to_read = min(count, dev->data_len - *f_pos);
if (to_read == 0) return 0;
if (copy_to_user(buf, dev->data + *f_pos, to_read)) {
return -EFAULT;
}
*f_pos += to_read;
return to_read;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_device *dev = filp->private_data;
size_t to_write = min(count, MAX_DATA_SIZE - *f_pos);
if (copy_from_user(dev->data + *f_pos, buf, to_write)) {
return -EFAULT;
}
*f_pos += to_write;
dev->data_len = *f_pos;
return to_write;
}
关键注意:用户空间与内核空间的数据拷贝必须使用
copy_to_user() / copy_from_user(),不能直接使用 memcpy()。这两个函数会检查用户空间地址的合法性,并在访问前进行必要的权限校验。
4.4 完整注册流程:cdev_init + cdev_add
将cdev结构体与file_operations绑定并注册到系统的标准流程如下:
- 分配/初始化cdev:使用
cdev_init()或cdev_alloc() - 设置owner和ops:将file_operations赋值给cdev->ops
- 分配设备号:静态或动态方式获取dev_t
- 注册cdev:调用
cdev_add()将cdev加入系统 - 创建设备节点:手动mknod或通过udev自动创建
static int __init my_driver_init(void)
{
dev_t devno;
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&devno, 0, 1, "my_device");
if (ret < 0) return ret;
/* 2. 初始化cdev并绑定ops */
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
/* 3. 注册cdev到系统 */
ret = cdev_add(&my_cdev, devno, 1);
if (ret < 0) {
unregister_chrdev_region(devno, 1);
return ret;
}
pr_info("My device registered: major=%d minor=%d\n",
MAJOR(devno), MINOR(devno));
return 0;
}
static void __exit my_driver_exit(void)
{
dev_t devno = my_cdev.dev;
cdev_del(&my_cdev); /* 从系统移除cdev */
unregister_chrdev_region(devno, 1); /* 释放设备号 */
pr_info("My device unregistered\n");
}
4.5 用户空间验证
驱动加载后,用户空间可通过以下方式验证:
# 查看设备号
cat /proc/devices | grep my_device
# 手动创建设备节点(主设备号假设为240)
sudo mknod /dev/my_device c 240 0
sudo chmod 666 /dev/my_device
# 测试读写
echo "Hello GPIO" > /dev/my_device
cat /dev/my_device
生产环境建议:使用
class_create() 和 device_create() 自动创建设备节点,配合udev在驱动加载时自动生成 /dev/ 下的设备文件,无需手动mknod。这部分内容将在后续章节详细展开。
通过以上内容,你已经掌握了字符设备驱动的核心框架:cdev结构体的初始化、设备号的静态/动态分配策略、file_operations接口的实现方法,以及完整的注册与注销流程。这是编写任何字符设备驱动(包括GPIO驱动)的基础骨架。