3. 字符设备基础:cdev结构体、设备号分配、file_operations接口注册
字符设备是Linux驱动模型中最基础、最常用的设备类型,其核心在于通过cdev结构体、设备号和file_operations接口三要素完成内核与用户态的桥梁搭建。本节将逐一拆解这三者的定义、关系与注册流程。
3.1 设备号:主设备号与次设备号
每个字符设备在内核中由唯一的设备号标识。设备号是一个32位无符号整数(dev_t),高12位为主设备号,低20位为次设备号。
| 概念 | 位数 | 作用 | 示例 |
|---|---|---|---|
| 主设备号 | 12 bit (0~4095) | 标识设备对应的驱动程序 | 1 (内存设备)、8 (SCSI磁盘) |
| 次设备号 | 20 bit (0~1048575) | 区分同一驱动管理的多个设备实例 | 0、1、2… |
提示:使用
MAJOR(dev_t) 和 MINOR(dev_t) 宏可以从设备号中提取主/次编号;使用 MKDEV(major, minor) 宏可以将主/次编号合并为 dev_t 类型。
3.1.1 静态分配 vs 动态分配
- 静态分配:调用
register_chrdev_region(dev_t first, unsigned int count, const char *name),需手动指定起始设备号。适用于已知且固定的主设备号场景。 - 动态分配:调用
alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name),由内核自动分配未使用的主设备号。推荐用于模块化驱动,避免冲突。
注意:动态分配后,需通过
/proc/devices 查看分配的主设备号,或使用 printk 输出。卸载模块时必须调用 unregister_chrdev_region(dev_t dev, unsigned int count) 释放设备号。
3.2 cdev结构体:字符设备的内核抽象
struct cdev 是内核用于管理字符设备的核心数据结构,定义在 <linux/cdev.h> 中。每个字符设备在内核中对应一个 cdev 实例。
struct cdev {
struct kobject kobj; // 内嵌内核对象,用于设备模型
struct module *owner; // 指向所属模块,通常设为 THIS_MODULE
const struct file_operations *ops; // 设备操作函数集
struct list_head list; // 内核链表节点
dev_t dev; // 设备号
unsigned int count; // 次设备号数量
};
3.2.1 cdev 初始化与注册流程
- 分配 cdev 对象:静态定义或使用
cdev_alloc()动态分配。 - 初始化 cdev:调用
cdev_init(struct cdev *cdev, const struct file_operations *fops),绑定 file_operations。 - 设置 owner:手动设置
cdev->owner = THIS_MODULE。 - 注册 cdev:调用
cdev_add(struct cdev *cdev, dev_t dev, unsigned int count),将设备加入内核。
关键点:
cdev_add() 必须在设备号分配成功之后调用,且一旦调用,设备立即对用户态可见。建议在 cdev_add() 之前完成所有初始化工作,避免设备暴露时状态不一致。
3.3 file_operations接口:用户态与内核的桥梁
struct file_operations 定义了用户态程序通过系统调用操作设备时,内核调用的驱动函数指针。每个成员对应一个系统调用。
| 成员 | 系统调用 | 典型用途 |
|---|---|---|
open |
open() | 打开设备,初始化私有数据 |
release |
close() | 关闭设备,释放资源 |
read |
read() | 从设备读取数据到用户空间 |
write |
write() | 从用户空间写入数据到设备 |
unlocked_ioctl |
ioctl() | 设备特定控制命令(无大内核锁) |
mmap |
mmap() | 将设备内存映射到用户空间 |
poll |
poll()/select() | 查询设备是否可读/写 |
3.3.1 典型 file_operations 定义示例
static int my_open(struct inode *inode, struct file *filp) {
// 获取私有数据,初始化硬件
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
// 清理资源
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
// 从内核缓冲区拷贝数据到用户空间
// 使用 copy_to_user()
return count;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
// 从用户空间拷贝数据到内核缓冲区
// 使用 copy_from_user()
return count;
}
static long my_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg) {
// 处理设备控制命令
return 0;
}
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,
};
避坑指南:用户空间指针
__user *buf 不能直接解引用,必须使用 copy_to_user() / copy_from_user() 进行安全拷贝。直接访问会导致内核崩溃或安全漏洞。
3.4 完整注册流程与SVG架构图
将设备号分配、cdev初始化、file_operations注册串联起来,形成字符设备驱动的标准初始化流程。
3.5 完整代码模板
以下是一个最小字符设备驱动的初始化与退出代码框架,整合了上述所有要素。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major;
static struct cdev my_cdev;
static dev_t dev_num;
static int my_open(struct inode *inode, struct file *filp) {
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
pr_info("Device closed\n");
return 0;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
};
static int __init my_init(void) {
// 1. 动态分配设备号
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
pr_err("Failed to allocate device number\n");
return -1;
}
major = MAJOR(dev_num);
pr_info("Allocated major number: %d\n", major);
// 2. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 注册cdev
if (cdev_add(&my_cdev, dev_num, 1) < 0) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
pr_info("Char device registered successfully\n");
return 0;
}
static void __exit my_exit(void) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("Char device unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("Minimal char device driver example");
最佳实践:实际项目中,建议在
cdev_add() 之后使用 class_create() 和 device_create() 自动在 /dev 下创建设备节点,避免手动 mknod。这部分内容将在后续章节详细展开。
3.6 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| insmod 失败,提示“Device or resource busy” | 设备号冲突或 cdev_add 重复 | 使用动态分配,检查 /proc/devices |
| open 设备返回 -ENODEV | cdev 未成功注册或设备节点主次号不匹配 | 检查 cdev_add 返回值,核对 mknod 参数 |
| read/write 返回 -EFAULT | 直接解引用用户空间指针 | 改用 copy_to_user / copy_from_user |
| rmmod 后设备节点仍存在 | 未调用 cdev_del 或 device_destroy | 确保 exit 函数中正确清理 |