3. file_operations 结构体:open、release、read、write 函数原型与实现
在 Linux 字符设备驱动中,file_operations 结构体是驱动与 VFS(虚拟文件系统)之间的核心契约。用户空间对设备文件执行 open()、close()、read()、write() 等系统调用时,VFS 会通过该结构体找到驱动对应的回调函数。本章节将详细解析这四个关键接口的原型、参数含义及标准实现模式。
核心要点: file_operations 结构体是驱动面向 VFS 的“方法表”,每个成员函数指针对应一个系统调用。驱动开发者只需填充需要的回调,未使用的成员可设为 NULL。
3.1 file_operations 结构体概述
该结构体定义在 <linux/fs.h> 中,包含数十个操作成员。对于基础字符设备驱动,最常实现的是以下四个:
| 成员名称 | 对应系统调用 | 典型用途 | 返回值 |
|---|---|---|---|
open |
open() |
初始化设备、递增使用计数、分配私有数据 | 0 成功,负值错误码 |
release |
close() |
清理资源、递减使用计数、释放私有数据 | 0 成功,负值错误码 |
read |
read() |
从设备拷贝数据到用户空间 | 读取字节数,负值错误码 |
write |
write() |
从用户空间拷贝数据到设备 | 写入字节数,负值错误码 |
编程提示: 在 Linux 5.x/6.x 内核中,
read 和 write 的原型已改为 ssize_t (*read)(struct file *, char __user *, size_t, loff_t *),其中用户空间指针使用 __user 宏标记,禁止直接解引用。
3.2 open 函数原型与实现
函数原型:
int (*open)(struct inode *inode, struct file *filp);
参数说明:
inode:VFS inode 指针,包含设备主次编号(iminor(inode)、imajor(inode))。filp:文件对象指针,驱动可将私有数据结构挂载到filp->private_data。
标准实现模式:
static int my_open(struct inode *inode, struct file *filp)
{
struct my_device *dev;
/* 1. 从 inode 获取设备结构体(通常通过 container_of 或全局数组) */
dev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = dev;
/* 2. 递增模块使用计数(可选,现代内核推荐用 THIS_MODULE 自动管理) */
try_module_get(THIS_MODULE);
/* 3. 初始化硬件状态(如复位 FIFO、使能中断) */
// my_device_reset(dev);
printk(KERN_INFO "my_device opened, minor=%d\n", iminor(inode));
return 0;
}
避坑指南: 不要在 open 中分配大块内存或执行耗时操作,因为 open 可能被频繁调用。资源分配建议延迟到第一次 read/write 时进行,或使用引用计数管理。
3.3 release 函数原型与实现
函数原型:
int (*release)(struct inode *inode, struct file *filp);
参数说明: 与 open 相同,inode 和 filp 分别指向 inode 和文件对象。
标准实现模式:
static int my_release(struct inode *inode, struct file *filp)
{
struct my_device *dev = filp->private_data;
if (!dev)
return -ENODEV;
/* 1. 清理硬件状态(如关闭中断、停止 DMA) */
// my_device_stop(dev);
/* 2. 递减模块使用计数 */
module_put(THIS_MODULE);
/* 3. 释放私有资源(如果是在 open 中分配的) */
// kfree(dev->buffer);
printk(KERN_INFO "my_device released\n");
return 0;
}
注意: release 并不等同于“设备被删除”,它只表示当前文件描述符关闭。如果多个进程打开了同一个设备,release 会在最后一个文件描述符关闭时被调用(前提是驱动正确实现了引用计数)。
3.4 read 函数原型与实现
函数原型:
ssize_t (*read)(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
参数说明:
| 参数 | 方向 | 说明 |
|---|---|---|
filp |
输入 | 文件对象指针,通过 filp->private_data 获取设备结构体 |
buf |
输出 | 用户空间缓冲区指针,禁止直接解引用,必须使用 copy_to_user() |
count |
输入 | 用户请求读取的字节数 |
f_pos |
输入/输出 | 文件读写位置偏移量,驱动可修改以支持 lseek |
标准实现模式:
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;
size_t available;
/* 1. 检查设备状态 */
if (!dev->data_ready)
return -EAGAIN; // 无数据可读,非阻塞时返回
/* 2. 计算可读数据量(不超过用户请求) */
available = min(count, dev->data_len - *f_pos);
if (available == 0)
return 0; // EOF
/* 3. 使用 copy_to_user 安全拷贝到用户空间 */
if (copy_to_user(buf, dev->buffer + *f_pos, available)) {
return -EFAULT; // 拷贝失败
}
/* 4. 更新文件位置和返回值 */
*f_pos += available;
ret = available;
/* 5. 触发下一次数据准备(如启动下一次 ADC 转换) */
// my_device_start_read(dev);
return ret;
}
关键规则: 永远不要直接解引用用户空间指针
buf。必须使用 copy_to_user() 或 copy_from_user(),这些函数会检查地址合法性并处理缺页异常。返回值是实际传输的字节数,错误时返回负值(如 -EFAULT、-EAGAIN)。
3.5 write 函数原型与实现
函数原型:
ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
参数说明: 与 read 对称,buf 为 const char __user *,表示用户空间传入的数据。
标准实现模式:
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_device *dev = filp->private_data;
ssize_t ret;
size_t to_write;
/* 1. 检查设备是否可写 */
if (dev->buffer_full)
return -ENOSPC; // 缓冲区满
/* 2. 计算可写入量(不超过设备缓冲区剩余空间) */
to_write = min(count, dev->buf_size - *f_pos);
if (to_write == 0)
return -ENOSPC;
/* 3. 使用 copy_from_user 安全拷贝到内核空间 */
if (copy_from_user(dev->buffer + *f_pos, buf, to_write)) {
return -EFAULT;
}
/* 4. 更新文件位置和返回值 */
*f_pos += to_write;
ret = to_write;
/* 5. 触发硬件处理(如启动 DAC 输出、发送数据到外设) */
// my_device_write_hw(dev, to_write);
return ret;
}
3.6 完整 file_operations 结构体实例化
将上述四个函数注册到 file_operations 结构体中:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = NULL, // 使用默认偏移行为
.unlocked_ioctl = NULL, // 后续章节实现
};
最佳实践: 使用
const 修饰 file_operations 结构体,将其放入只读数据段,防止意外修改。未实现的回调设为 NULL,VFS 会返回 -EINVAL 给用户空间。
3.7 常见错误与调试技巧
| 错误场景 | 现象 | 根因 | 解决方案 |
|---|---|---|---|
| 直接解引用用户指针 | 内核 Oops,系统可能崩溃 | 用户地址未映射到内核空间 | 使用 copy_from_user / copy_to_user |
| open 中未设置 private_data | read/write 中 filp->private_data 为 NULL | 驱动未保存设备结构体指针 | 在 open 中赋值 filp->private_data = dev |
| read 返回 0 但仍有数据 | 用户空间读到 EOF | 未正确更新 *f_pos 或判断条件错误 |
检查偏移量计算逻辑 |
| write 未检查缓冲区满 | 数据覆盖或丢失 | 缺少流控机制 | 实现环形缓冲区或返回 -ENOSPC |
| release 中重复释放资源 | 内核 double-free 崩溃 | 引用计数管理错误 | 使用 kref 或原子变量跟踪引用 |
调试建议: 在 open/release 中添加
printk 打印进程 PID 和设备次编号,使用 cat /proc/devices 确认设备注册状态。对于 read/write 数据流问题,可在 copy_to_user 前后打印内核缓冲区内容(使用 print_hex_dump)。