4. 字符设备驱动框架
字符设备是Linux驱动开发中最基础的设备类型,其核心框架围绕file_operations结构体、设备号、cdev接口三大要素构建。本章将系统讲解字符设备驱动的完整骨架,包括设备号的分配与释放、cdev的初始化与添加、以及驱动与VFS(虚拟文件系统)的交互机制。
4.1 字符设备驱动核心架构总览
字符设备驱动的本质是内核模块通过注册cdev结构体,将设备号与file_operations回调函数绑定到VFS层。用户空间的open/read/write/ioctl等系统调用,最终会通过VFS跳转到驱动实现的对应函数。
关键理解:字符设备驱动是内核模块与VFS之间的桥梁。驱动开发者只需实现file_operations中的回调函数,并正确注册cdev,即可让用户空间通过标准文件I/O接口访问硬件。
4.2 file_operations结构体:驱动与VFS的契约
file_operations是字符设备驱动的核心数据结构,定义在<linux/fs.h>中。它是一组函数指针的集合,每个指针对应一个系统调用。驱动开发者根据设备特性实现其中部分或全部回调。
| 成员名称 | 函数原型 | 说明 |
|---|---|---|
| owner | struct module *owner |
通常设为 THIS_MODULE,防止模块被卸载时仍有打开的文件 |
| open | int (*open)(struct inode *, struct file *) |
设备打开时调用,用于初始化硬件或递增使用计数 |
| release | int (*release)(struct inode *, struct file *) |
设备关闭时调用,用于释放资源或递减使用计数 |
| read | ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) |
从设备读取数据,需使用 copy_to_user() 安全拷贝 |
| write | ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) |
向设备写入数据,需使用 copy_from_user() 安全拷贝 |
| unlocked_ioctl | long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) |
设备控制命令接口,替代旧的 ioctl(无大内核锁) |
| llseek | loff_t (*llseek)(struct file *, loff_t, int) |
修改文件读写位置,支持 SEEK_SET/SEEK_CUR/SEEK_END |
| mmap | int (*mmap)(struct file *, struct vm_area_struct *) |
将设备内存映射到用户空间,用于高性能数据交换 |
典型实现示例:以下代码展示了一个最小化的file_operations定义,仅实现open/release/read/write四个基本回调。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_char_dev"
#define BUF_SIZE 1024
static char device_buffer[BUF_SIZE];
static int open_count = 0;
static int my_open(struct inode *inode, struct file *filp) {
open_count++;
printk(KERN_INFO "Device opened %d times\n", open_count);
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
printk(KERN_INFO "Device closed\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
size_t available = BUF_SIZE - *f_pos;
size_t to_read = count < available ? count : available;
if (to_read == 0) return 0;
if (copy_to_user(buf, device_buffer + *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) {
size_t available = BUF_SIZE - *f_pos;
size_t to_write = count < available ? count : available;
if (to_write == 0) return -ENOSPC;
if (copy_from_user(device_buffer + *f_pos, buf, to_write)) {
return -EFAULT;
}
*f_pos += to_write;
return to_write;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
安全注意:在read/write回调中,必须使用
copy_to_user()和copy_from_user()进行用户空间与内核空间的数据拷贝,严禁直接解引用用户空间指针,否则会导致内核崩溃或安全漏洞。
4.3 主次设备号:设备的唯一标识
每个字符设备在Linux内核中由设备号唯一标识。设备号是dev_t类型(32位无符号整数),其中高12位为主设备号,低20位为次设备号。
4.3.1 设备号操作宏
内核提供一组宏用于设备号的编码与解码,避免手动位操作:
| 宏/函数 | 功能 | 示例 |
|---|---|---|
MKDEV(major, minor) |
将主次设备号合并为 dev_t |
dev_t dev = MKDEV(240, 0); |
MAJOR(dev) |
从 dev_t 提取主设备号 |
int major = MAJOR(dev); |
MINOR(dev) |
从 dev_t 提取次设备号 |
int minor = MINOR(dev); |
4.3.2 静态分配 vs 动态分配
| 分配方式 | 函数 | 优点 | 缺点 |
|---|---|---|---|
| 静态分配 | register_chrdev_region() |
设备号固定,便于udev规则匹配 | 需手动指定主设备号,可能冲突 |
| 动态分配 | alloc_chrdev_region() |
内核自动分配未使用的主设备号,避免冲突 | 设备号不固定,需通过/proc/devices查看 |
推荐实践:在正式产品中优先使用动态分配,配合udev规则或设备树自动创建设备节点。静态分配仅用于教学或与旧版内核兼容的场景。
4.4 cdev接口:字符设备的内核抽象
cdev结构体是内核用于管理字符设备的核心对象,定义在<linux/cdev.h>中。每个字符设备在内核中对应一个cdev实例,它封装了设备号与file_operations的绑定关系。
4.4.1 cdev结构体定义
struct cdev {
struct kobject kobj; // 内核对象,用于sysfs和引用计数
struct module *owner; // 所属模块,通常设为 THIS_MODULE
const struct file_operations *ops; // 设备操作函数集
struct list_head list; // 内核内部链表节点
dev_t dev; // 设备号
unsigned int count; // 次设备号数量
};
4.4.2 cdev操作流程
4.4.3 完整注册与注销代码模板
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#define DEVICE_NAME "my_cdev"
#define MINOR_COUNT 1
static dev_t dev_num;
static struct cdev *my_cdev;
/* file_operations 定义(此处省略具体实现) */
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
static int __init my_init(void) {
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Allocated major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
/* 2. 分配并初始化cdev */
my_cdev = cdev_alloc();
if (!my_cdev) {
printk(KERN_ERR "Failed to allocate cdev\n");
unregister_chrdev_region(dev_num, MINOR_COUNT);
return -ENOMEM;
}
cdev_init(my_cdev, &my_fops);
my_cdev->owner = THIS_MODULE;
/* 3. 将cdev添加到内核 */
ret = cdev_add(my_cdev, dev_num, MINOR_COUNT);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
cdev_del(my_cdev);
unregister_chrdev_region(dev_num, MINOR_COUNT);
return ret;
}
printk(KERN_INFO "Character device registered successfully\n");
return 0;
}
static void __exit my_exit(void) {
/* 逆序释放:先删除cdev,再释放设备号 */
cdev_del(my_cdev);
unregister_chrdev_region(dev_num, MINOR_COUNT);
printk(KERN_INFO "Character device unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Minimal character device driver example");
关键点:
cdev_alloc()动态分配cdev,cdev_init()用于静态分配的cdev。两者选其一,不要混用。cdev_add()之后设备立即可用,因此必须在最后一步调用。- 卸载时严格按照逆序释放:先
cdev_del(),再unregister_chrdev_region()。
4.5 设备注册与注销的完整流程
综合以上内容,字符设备驱动的注册与注销遵循对称设计原则:初始化时按顺序执行,清理时逆序执行。
| 阶段 | 步骤 | 函数/操作 | 说明 |
|---|---|---|---|
| 注册 | 1 | alloc_chrdev_region() |
动态分配设备号(推荐) |
| 2 | cdev_alloc() 或 cdev_init() |
分配并初始化cdev结构体 | |
| 3 | 设置 owner 和 ops |
绑定模块所有权和操作函数集 | |
| 4 | cdev_add() |
将cdev注册到内核,设备立即可用 | |
| 注销 | 1 | cdev_del() |
从内核移除cdev,阻止新访问 |
| 2 | unregister_chrdev_region() |
释放设备号 |
常见错误:
- 顺序错误:先调用
cdev_add()再分配设备号,会导致内核崩溃。 - 遗漏注销:模块卸载时未调用
cdev_del(),下次加载时会因设备号冲突而失败。 - 次设备号数量不匹配:
cdev_add()的count参数必须与alloc_chrdev_region()的count一致。
4.6 本章小结
字符设备驱动框架是嵌入式Linux驱动开发的基石。掌握以下核心要点即可快速上手:
- file_operations:驱动与VFS的契约,实现open/read/write/release等回调。
- 设备号:主设备号标识驱动类型,次设备号标识设备实例;优先使用动态分配。
- cdev接口:内核管理字符设备的抽象,通过cdev_init/cdev_add完成注册。
- 对称设计:注册与注销严格逆序,确保资源正确释放。
下章预告:第5章将基于本章的字符设备框架,实现具体的GPIO控制驱动,包括GPIO的申请、配置、读写操作,以及用户空间的测试程序编写。