驱动框架搭建:file_operations 结构体详解
字符设备驱动的核心,说白了就是 file_operations 这个结构体。它像一张函数跳转表,把用户空间的系统调用和内核空间的驱动函数一一对应起来。我刚开始学驱动时,总觉得这玩意儿很玄乎,后来写多了才发现——它就是驱动和用户程序之间的「翻译官」。
核心认知:file_operations 是字符设备驱动的灵魂。没有它,用户程序调 open/read/write,内核根本不知道找谁干活。
2.1 file_operations 结构体成员
这个结构体定义在 linux/fs.h 里,成员非常多。但实际开发中,我们只用实现需要的几个。我见过有人把全部成员都列出来,结果大部分是 NULL——没必要,反而显得乱。
| 成员名 | 函数原型 | 说明 |
|---|---|---|
owner |
struct module *owner |
指向模块自身,防止模块被卸载时还在用 |
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 *) |
从设备读数据 |
write |
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) |
向设备写数据 |
unlocked_ioctl |
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) |
设备控制命令(新内核用这个) |
我的习惯:每次写驱动,先把 file_operations 结构体定义好,哪怕有些函数先写空实现。这样框架清晰,后面填代码就行。
2.2 核心接口实现
open 和 release
open 是驱动和设备的「握手」环节。我一般在这里做三件事:递增使用计数、初始化硬件状态、分配私有数据。release 就是反着来——递减计数、释放资源。
static int my_open(struct inode *inode, struct file *filp)
{
struct my_dev *dev;
dev = container_of(inode->i_cdev, struct my_dev, cdev);
filp->private_data = dev;
// 递增模块使用计数
try_module_get(THIS_MODULE);
printk(KERN_INFO "Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
module_put(THIS_MODULE);
printk(KERN_INFO "Device released\n");
return 0;
}
我曾经踩过的坑:release 里忘了调用 module_put,结果模块卸载时 refcount 不为 0,rmmod 死活卸不掉。查了半天才发现是计数没减回去。
read 和 write
这两个接口是驱动和用户空间的数据通道。有个关键点:用户空间的指针不能直接在内核态用,必须用 copy_to_user 和 copy_from_user。为什么?因为内核有权限检查,直接访问用户指针会出大问题。
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_dev *dev = filp->private_data;
size_t available = dev->data_len - *f_pos;
size_t to_read = min(count, available);
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_dev *dev = filp->private_data;
size_t to_write = min(count, DEV_BUF_SIZE - *f_pos);
if (to_write == 0)
return -ENOSPC;
if (copy_from_user(dev->data + *f_pos, buf, to_write))
return -EFAULT;
*f_pos += to_write;
return to_write;
}
避坑指南:copy_to_user 和 copy_from_user 一定要检查返回值。非零表示拷贝失败,这时候必须返回 -EFAULT。我见过有人忽略返回值,结果数据没拷过去,驱动还返回成功——用户程序拿到一堆垃圾数据。
unlocked_ioctl
ioctl 是驱动的「瑞士军刀」。读、写、控制、配置,什么都能干。新内核用 unlocked_ioctl 替代了老的 ioctl,区别在于调用时没有大内核锁。你想想看,这其实更合理——每个驱动自己管好自己的并发就行。
static long my_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct my_dev *dev = filp->private_data;
int val;
switch (cmd) {
case MY_IOCTL_SET_VAL:
if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
return -EFAULT;
dev->value = val;
break;
case MY_IOCTL_GET_VAL:
if (copy_to_user((int __user *)arg, &dev->value, sizeof(val)))
return -EFAULT;
break;
default:
return -ENOTTY;
}
return 0;
}
关键点:ioctl 的 cmd 编号要按 Linux 规范来生成,用 _IO/_IOR/_IOW/_IOWR 宏。乱写编号容易和其他驱动冲突,调试时特别痛苦。
2.3 模块加载与卸载
模块的加载和卸载,就像设备的「开机」和「关机」。我习惯把初始化逻辑拆成几步:注册设备号、初始化 cdev、添加设备到系统。卸载就是反着来。
static int __init my_init(void)
{
dev_t dev_num;
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to alloc chrdev region\n");
return ret;
}
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加 cdev 到系统
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
printk(KERN_INFO "My device driver loaded\n");
return 0;
}
static void __exit my_exit(void)
{
dev_t dev_num = MKDEV(major, 0);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "My device driver unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
我曾经犯过的错:cdev_add 失败后,忘了 unregister_chrdev_region。结果设备号被占着,下次 insmod 直接报错。记住:每一步失败都要回滚之前成功的操作。
2.4 整体框架图
下面这张图展示了字符设备驱动的完整调用链路。从用户程序到内核,再到硬件,每一步都清晰可见。
2.5 完整代码模板
把上面这些拼起来,就是一个可用的字符设备驱动框架。我每次写新驱动,都是从这个模板开始改的。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEV_BUF_SIZE 1024
struct my_dev {
char data[DEV_BUF_SIZE];
size_t data_len;
struct cdev cdev;
};
static struct my_dev my_device;
// file_operations 实现
static int my_open(struct inode *inode, struct file *filp) { /* ... */ }
static int my_release(struct inode *inode, struct file *filp) { /* ... */ }
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { /* ... */ }
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { /* ... */ }
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { /* ... */ }
// 定义 file_operations
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
// 模块初始化与退出
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple char device driver");
我的建议:模板写好之后,先编译加载试试。能成功 insmod 和 rmmod,说明框架没问题。然后再一个一个填函数实现,这样调试起来心里有底。
嗯,到这里字符设备驱动的框架就搭好了。你想想看,其实核心就是三件事:定义 file_operations、实现接口函数、写好加载卸载。后面的章节我们会在这个框架上继续加功能,比如设备树匹配、中断处理、DMA 传输等等。但不管多复杂,底子都是今天讲的这些东西。