3. 字符设备驱动框架:核心结构与注册机制
字符设备是嵌入式 Linux 驱动中最基础的设备类型,其驱动框架围绕 file_operations 结构体、设备号管理、cdev 对象以及设备节点创建四个核心要素展开。理解这一框架是编写任何字符设备驱动的起点。
3.1 file_operations 结构体:驱动与用户空间的接口
file_operations 是驱动开发者向 VFS(虚拟文件系统)注册的一组回调函数指针。当用户空间程序调用 open()、read()、write() 等系统调用时,VFS 通过该结构体找到对应的驱动函数执行。
关键字段说明: 驱动只需实现必要的回调,未实现的字段应初始化为
NULL,内核会返回 -ENOSYS。
| 字段名 | 函数原型 | 触发时机 | 典型用途 |
|---|---|---|---|
owner |
struct module * |
模块引用计数 | 设为 THIS_MODULE |
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() |
设备控制命令(无大内核锁) |
llseek |
loff_t (*llseek)(struct file *, loff_t, int) |
用户调用 lseek() |
调整文件读写位置 |
避坑提示: 在
read/write 回调中,用户空间指针 __user 不能直接解引用,必须使用 copy_to_user()/copy_from_user() 内核 API 进行安全拷贝。
// 典型的 file_operations 定义示例
static int mydrv_open(struct inode *inode, struct file *filp) {
pr_info("Device opened\n");
return 0;
}
static ssize_t mydrv_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
char kbuf[64] = "Hello from kernel!";
size_t len = strlen(kbuf) + 1;
if (count < len) return -EINVAL;
if (copy_to_user(buf, kbuf, len)) return -EFAULT;
return len;
}
static struct file_operations mydrv_fops = {
.owner = THIS_MODULE,
.open = mydrv_open,
.read = mydrv_read,
.release = mydrv_open, // 简化示例,实际应使用 release 回调
};
3.2 主次设备号:设备标识与分配策略
设备号是内核识别设备的唯一数字标识,由 主设备号(major)和 次设备号(minor)组成,类型为 dev_t(32 位无符号整数,高 12 位为主设备号,低 20 位为次设备号)。
分配方式对比:
| 方式 | API | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 静态分配 | register_chrdev_region() |
已知主设备号(如 I2C 设备) | 设备号固定,便于文档化 | 可能与其他驱动冲突 |
| 动态分配 | alloc_chrdev_region() |
大多数字符设备驱动 | 避免冲突,自动分配 | 设备号不固定,需查看 /proc/devices |
// 动态分配设备号示例
dev_t dev_num;
int major, minor;
// 申请一个主设备号,次设备号从 0 开始,数量为 1
if (alloc_chrdev_region(&dev_num, 0, 1, "mydrv") < 0) {
pr_err("Failed to allocate device number\n");
return -EIO;
}
major = MAJOR(dev_num);
minor = MINOR(dev_num);
pr_info("Allocated major=%d minor=%d\n", major, minor);
// 模块卸载时释放
unregister_chrdev_region(dev_num, 1);
注意: 使用
alloc_chrdev_region() 后,务必在模块退出路径中调用 unregister_chrdev_region(),否则设备号泄漏会导致后续驱动无法加载。
3.3 cdev 注册与注销:内核设备对象管理
struct cdev 是内核中表示字符设备的对象,必须通过 cdev_init() 初始化并与 file_operations 绑定,然后通过 cdev_add() 将其加入内核设备链表,使设备号与操作函数关联生效。
注册流程(顺序不可颠倒):
- 调用
alloc_chrdev_region()或register_chrdev_region()分配设备号。 - 调用
cdev_alloc()或静态定义struct cdev,然后cdev_init()绑定file_operations。 - 调用
cdev_add()将 cdev 注册到内核,传入设备号和次设备数量。 - 在模块卸载时,先
cdev_del()移除 cdev,再unregister_chrdev_region()释放设备号。
// cdev 注册完整示例
static struct cdev mydrv_cdev;
static int __init mydrv_init(void) {
dev_t dev;
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&dev, 0, 1, "mydrv");
if (ret < 0) return ret;
// 2. 初始化 cdev 并绑定 fops
cdev_init(&mydrv_cdev, &mydrv_fops);
mydrv_cdev.owner = THIS_MODULE;
// 3. 添加 cdev 到内核
ret = cdev_add(&mydrv_cdev, dev, 1);
if (ret < 0) {
unregister_chrdev_region(dev, 1);
return ret;
}
pr_info("mydrv loaded: major=%d minor=%d\n", MAJOR(dev), MINOR(dev));
return 0;
}
static void __exit mydrv_exit(void) {
dev_t dev = mydrv_cdev.dev; // 保存设备号
cdev_del(&mydrv_cdev); // 先删除 cdev
unregister_chrdev_region(dev, 1); // 再释放设备号
pr_info("mydrv unloaded\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
3.4 设备节点创建:从内核到用户空间的桥梁
设备节点是用户空间访问驱动的文件接口,通常位于 /dev/ 目录下。创建方式分为手动和自动两种:
两种创建方式对比:
| 方式 | 命令/API | 特点 | 适用场景 |
|---|---|---|---|
| 手动创建 | mknod /dev/mydrv c major 0 |
需 root 权限,设备号固定 | 调试阶段、静态设备号 |
| 自动创建 | class_create() + device_create() |
驱动加载时自动生成节点,卸载时自动删除 | 生产环境、动态设备号 |
自动创建依赖内核的 设备模型(driver model),通过 class_create() 创建一个设备类,然后 device_create() 在该类下创建设备节点。udev(用户空间设备管理器)会监听内核事件,自动在 /dev/ 下生成节点。
// 自动创建设备节点示例
static struct class *mydrv_class;
static struct device *mydrv_device;
static int __init mydrv_init(void) {
dev_t dev;
int ret;
// 分配设备号
ret = alloc_chrdev_region(&dev, 0, 1, "mydrv");
if (ret < 0) return ret;
// 注册 cdev
cdev_init(&mydrv_cdev, &mydrv_fops);
mydrv_cdev.owner = THIS_MODULE;
ret = cdev_add(&mydrv_cdev, dev, 1);
if (ret < 0) {
unregister_chrdev_region(dev, 1);
return ret;
}
// 创建设备类
mydrv_class = class_create(THIS_MODULE, "mydrv_class");
if (IS_ERR(mydrv_class)) {
cdev_del(&mydrv_cdev);
unregister_chrdev_region(dev, 1);
return PTR_ERR(mydrv_class);
}
// 创建设备节点(会在 /dev/mydrv 生成)
mydrv_device = device_create(mydrv_class, NULL, dev, NULL, "mydrv");
if (IS_ERR(mydrv_device)) {
class_destroy(mydrv_class);
cdev_del(&mydrv_cdev);
unregister_chrdev_region(dev, 1);
return PTR_ERR(mydrv_device);
}
return 0;
}
static void __exit mydrv_exit(void) {
dev_t dev = mydrv_cdev.dev;
device_destroy(mydrv_class, dev); // 删除设备节点
class_destroy(mydrv_class); // 销毁设备类
cdev_del(&mydrv_cdev);
unregister_chrdev_region(dev, 1);
}
3.5 完整驱动框架总结
字符设备驱动的开发流程可归纳为以下步骤,每一步都对应内核提供的 API:
- 定义 file_operations:实现 open/release/read/write/ioctl 等回调。
- 分配设备号:使用
alloc_chrdev_region()动态分配或register_chrdev_region()静态分配。 - 初始化 cdev:
cdev_init()绑定 fops,cdev_add()注册到内核。 - 创建设备节点:使用
class_create()+device_create()自动生成/dev/节点。 - 实现硬件操作:在回调中通过
iowrite32()/ioread32()或 I2C/SPI 总线 API 访问硬件。 - 注销清理:
device_destroy()→class_destroy()→cdev_del()→unregister_chrdev_region()。
调试技巧: 驱动加载后,执行
cat /proc/devices | grep mydrv 查看设备号;执行 ls -l /dev/mydrv 确认设备节点权限。若节点未自动生成,检查 udev 规则或手动 mknod 测试。