3. 字符设备驱动框架:file_operations结构体、主次设备号、cdev注册与注销
字符设备是Linux驱动开发中最基础的设备类型,其核心框架围绕file_operations结构体、设备号与cdev对象展开。本章将系统讲解这三要素的协作关系与编程实现。
3.1 字符设备驱动核心架构
字符设备驱动的整体架构分为三层:应用层通过系统调用访问设备文件;VFS层将系统调用映射到驱动注册的file_operations回调;驱动层通过cdev对象与设备号关联,最终操作硬件寄存器。
3.2 file_operations结构体
file_operations是驱动与VFS之间的回调函数表,定义在linux/fs.h中。驱动只需实现需要的回调,未实现的成员设为NULL,VFS会使用默认行为。
| 成员名称 | 函数原型 | 说明 |
|---|---|---|
| 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) |
修改文件读写位置,默认实现default_llseek |
关键提示:用户空间指针
__user不能直接解引用!必须使用copy_to_user/copy_from_user或get_user/put_user系列函数,否则会导致内核崩溃或安全漏洞。
3.2.1 典型file_operations定义示例
// 定义设备结构体
struct my_device {
char buffer[256];
int available;
struct mutex lock;
};
// open回调:初始化设备
static int my_open(struct inode *inode, struct file *filp)
{
struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = dev; // 保存设备结构体到文件私有数据
return 0;
}
// read回调:从设备读取数据
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;
ssize_t ret;
mutex_lock(&dev->lock);
if (*f_pos >= sizeof(dev->buffer)) {
mutex_unlock(&dev->lock);
return 0; // EOF
}
count = min(count, sizeof(dev->buffer) - (size_t)*f_pos);
if (copy_to_user(buf, dev->buffer + *f_pos, count)) {
mutex_unlock(&dev->lock);
return -EFAULT;
}
*f_pos += count;
ret = count;
mutex_unlock(&dev->lock);
return ret;
}
// 定义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,
.llseek = default_llseek,
};
3.3 主次设备号
设备号是内核识别设备的唯一标识,由主设备号和次设备号组成(dev_t类型,32位)。主设备号对应驱动,次设备号对应具体设备实例。
3.3.1 设备号分配方式
| 方式 | 函数 | 适用场景 | 注意事项 |
|---|---|---|---|
| 静态分配 | register_chrdev_region(dev_t first, unsigned int count, const char *name) |
已知主设备号(如I2C、SPI等标准设备) | 需确保主设备号未被占用,否则注册失败 |
| 动态分配 | alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name) |
自定义设备,推荐使用 | 内核自动分配未使用的主设备号,避免冲突 |
3.3.2 设备号分配示例
#include <linux/fs.h>
#include <linux/device.h>
#define DEVICE_NAME "my_char_dev"
#define MINOR_COUNT 4 // 支持4个次设备
static dev_t dev_num; // 存储分配的设备号
// 动态分配设备号
static int __init my_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate chrdev region\n");
return ret;
}
pr_info("Allocated dev_t: major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
// 释放设备号
static void __exit my_exit(void)
{
unregister_chrdev_region(dev_num, MINOR_COUNT);
pr_info("Unregistered chrdev region\n");
}
常见错误:忘记调用
unregister_chrdev_region释放设备号,导致模块卸载后设备号仍被占用,下次加载时动态分配可能失败。务必在exit函数中释放。
3.4 cdev注册与注销
cdev(字符设备对象)是内核中表示字符设备的核心数据结构,通过cdev_init初始化后,使用cdev_add将其注册到系统。注册成功后,VFS就能通过设备号找到对应的file_operations。
3.4.1 cdev生命周期
3.4.2 完整cdev注册示例
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
struct my_device {
struct cdev cdev; // 内嵌cdev对象
char buffer[256];
int available;
struct mutex lock;
};
static struct my_device *my_dev;
static int __init my_init(void)
{
int ret;
dev_t dev_num;
// 1. 分配设备号(动态)
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
// 2. 分配设备结构体内存
my_dev = kzalloc(sizeof(struct my_device), GFP_KERNEL);
if (!my_dev) {
unregister_chrdev_region(dev_num, 1);
return -ENOMEM;
}
// 3. 初始化cdev
cdev_init(&my_dev->cdev, &my_fops);
my_dev->cdev.owner = THIS_MODULE;
// 4. 注册cdev到系统
ret = cdev_add(&my_dev->cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
kfree(my_dev);
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 5. 初始化设备内部数据
mutex_init(&my_dev->lock);
my_dev->available = 1;
pr_info("My device initialized: major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
static void __exit my_exit(void)
{
dev_t dev_num = my_dev->cdev.dev; // 从cdev获取设备号
// 1. 注销cdev
cdev_del(&my_dev->cdev);
// 2. 释放设备号
unregister_chrdev_region(dev_num, 1);
// 3. 释放设备结构体
mutex_destroy(&my_dev->lock);
kfree(my_dev);
pr_info("My device exited\n");
}
module_init(my_init);
module_exit(my_exit);
3.4.3 cdev_add失败处理
cdev_add可能因为设备号冲突、内存不足等原因失败。失败后必须立即回滚已分配的资源,否则会导致资源泄漏。
最佳实践:使用
goto错误处理模式,确保每个错误路径都正确释放资源。内核驱动中goto是标准的错误处理方式,不要滥用但也不要排斥。
static int __init my_init(void)
{
int ret;
dev_t dev_num;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0)
goto err_alloc_region;
my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
if (!my_dev) {
ret = -ENOMEM;
goto err_alloc_dev;
}
cdev_init(&my_dev->cdev, &my_fops);
my_dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&my_dev->cdev, dev_num, 1);
if (ret < 0)
goto err_cdev_add;
mutex_init(&my_dev->lock);
return 0;
err_cdev_add:
kfree(my_dev);
err_alloc_dev:
unregister_chrdev_region(dev_num, 1);
err_alloc_region:
return ret;
}
3.5 设备节点自动创建
传统方式需要手动mknod创建设备节点。现代驱动使用udev/mdev机制,通过class_create和device_create自动创建设备节点。
#include <linux/device.h>
static struct class *my_class;
static struct device *my_device;
static int __init my_init(void)
{
// ... 前面的cdev注册代码 ...
// 创建设备类
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
ret = PTR_ERR(my_class);
goto err_class_create;
}
// 创建设备节点(自动在/dev/下生成)
my_device = device_create(my_class, NULL, dev_num, NULL, "my_device%d", 0);
if (IS_ERR(my_device)) {
ret = PTR_ERR(my_device);
goto err_device_create;
}
return 0;
err_device_create:
class_destroy(my_class);
err_class_create:
cdev_del(&my_dev->cdev);
// ... 其他清理 ...
return ret;
}
static void __exit my_exit(void)
{
device_destroy(my_class, my_dev->cdev.dev);
class_destroy(my_class);
cdev_del(&my_dev->cdev);
// ... 其他清理 ...
}
3.6 本章小结
| 组件 | 作用 | 关键函数/宏 |
|---|---|---|
| file_operations | 定义驱动回调函数表 | .open .read .write .unlocked_ioctl |
| 设备号 | 唯一标识设备 | alloc_chrdev_region MKDEV MAJOR MINOR |
| cdev | 字符设备内核对象 | cdev_init cdev_add cdev_del |
| 设备节点 | 用户空间访问入口 | class_create device_create |
核心口诀:先分配设备号,再初始化cdev,最后cdev_add注册;注销顺序完全相反。设备节点通过class/device机制自动创建,无需手动mknod。