3. 文件操作接口
在 Linux 内核中,设备驱动程序通过 struct file_operations 结构体向 VFS(虚拟文件系统)暴露其操作能力。该结构体是一组函数指针的集合,每个指针对应一个用户态可以通过文件描述符调用的系统调用。对于字符设备驱动而言,open、release 和 ioctl 是最核心的三个接口,分别负责设备的打开、关闭以及自定义控制命令的收发。
3.1 struct file_operations 结构体概述
struct file_operations 定义在 <linux/fs.h> 中,是驱动与 VFS 层之间的契约。驱动只需注册自己需要实现的函数指针,未实现的字段置为 NULL,内核会使用默认行为。
| 函数指针 | 对应系统调用 | 典型用途 |
|---|---|---|
open |
open(2) |
设备初始化、权限检查、分配私有数据 |
release |
close(2) |
资源释放、计数器递减、设备关闭 |
ioctl |
ioctl(2) |
自定义命令、寄存器读写、参数配置 |
read |
read(2) |
从设备读取数据到用户空间 |
write |
write(2) |
从用户空间写入数据到设备 |
llseek |
lseek(2) |
修改文件读写位置 |
mmap |
mmap(2) |
设备内存映射到用户空间 |
poll |
poll(2) / select(2) |
设备可读/可写状态查询 |
关键认知:
struct file_operations 是驱动与内核 VFS 层之间的“虚函数表”。驱动开发者只需填充自己关心的函数指针,未实现的接口由内核提供默认行为(通常返回 -EINVAL 或 -ENOTTY)。
3.2 open 函数指针:设备初始化入口
当用户态调用 open("/dev/mydevice", O_RDWR) 时,内核 VFS 层根据设备号找到对应的驱动,并调用 open 方法。原型如下:
int (*open)(struct inode *inode, struct file *filp);
- inode:指向设备文件的 inode 结构,包含设备号等信息。
- filp:指向文件实例结构,驱动可将私有数据挂载到
filp->private_data。 - 返回值:0 表示成功,负值表示错误码(如
-ENODEV、-EBUSY)。
典型实现模式:
static int my_open(struct inode *inode, struct file *filp) {
struct my_device *dev;
dev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = dev; // 将设备结构体挂到文件实例
// 设备打开计数、硬件初始化、权限检查等
if (!atomic_add_unless(&dev->available, -1, 0))
return -EBUSY;
return 0;
}
常见陷阱: 不要在
open 中执行耗时操作(如等待硬件就绪),这会阻塞用户态进程。若必须等待,应使用 O_NONBLOCK 标志区分阻塞与非阻塞模式。
3.3 release 函数指针:资源清理与关闭
当用户态调用 close(fd) 或进程退出时,内核调用 release 方法。原型:
int (*release)(struct inode *inode, struct file *filp);
- 与
open对称,负责释放open中分配的资源。 - 递减设备打开计数、释放锁、关闭硬件中断、释放 DMA 缓冲区等。
- 返回值通常为 0,内核会忽略非零返回值。
典型实现:
static int my_release(struct inode *inode, struct file *filp) {
struct my_device *dev = filp->private_data;
if (!dev)
return -ENODEV;
atomic_inc(&dev->available); // 恢复设备可用计数
// 关闭硬件、释放中断、清理等待队列等
return 0;
}
注意:
release 与 flush 不同。flush 在每次 close 系统调用时都会调用,而 release 仅在文件描述符引用计数降为 0 时调用一次。对于 dup/fork 共享的文件描述符,release 只会在最后一个引用关闭时触发。
3.4 ioctl 函数指针:自定义控制中枢
ioctl 是驱动实现自定义设备控制的核心接口,允许用户态传递命令码以及可选的数据缓冲区。原型:
long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);
- filp:文件实例指针,可通过
filp->private_data获取设备结构体。 - cmd:命令码,由
_IO/_IOR/_IOW/_IOWR宏构造,包含幻数、序号、方向、数据大小。 - arg:用户态传入的参数,可能是一个整数、指针(需要
copy_from_user/copy_to_user转换)。 - 返回值:非负值表示成功(可返回数据),负值表示错误码(如
-ENOTTY、-EINVAL)。
典型实现框架:
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
struct my_device *dev = filp->private_data;
int ret = 0;
if (_IOC_TYPE(cmd) != MY_MAGIC) return -ENOTTY;
if (_IOC_NR(cmd) >= MY_MAX_CMD) return -EINVAL;
switch (cmd) {
case MY_CMD_GET_STATUS: {
int status = dev->status;
if (copy_to_user((void __user *)arg, &status, sizeof(status)))
return -EFAULT;
break;
}
case MY_CMD_SET_CONFIG: {
struct my_config cfg;
if (copy_from_user(&cfg, (void __user *)arg, sizeof(cfg)))
return -EFAULT;
// 应用配置到硬件
dev->config = cfg;
break;
}
default:
return -ENOTTY;
}
return ret;
}
安全红线: 永远不要直接解引用用户态指针
arg!必须使用 copy_from_user/copy_to_user 或 get_user/put_user 系列函数,否则会导致内核崩溃或安全漏洞。
3.5 文件操作接口整体架构图
下图展示了用户态系统调用、VFS 层、驱动 file_operations 函数指针之间的调用关系:
3.6 函数指针注册完整示例
将上述三个函数指针注册到 struct file_operations 并传递给内核:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.unlocked_ioctl = my_ioctl,
.read = my_read,
.write = my_write,
.llseek = NULL, // 不支持文件定位
.poll = NULL, // 不支持轮询
};
// 在模块初始化时注册字符设备
static int __init my_init(void) {
int ret;
dev_t devno = MKDEV(major, minor);
ret = register_chrdev_region(devno, 1, "my_device");
if (ret < 0) return ret;
cdev_init(&my_cdev, &my_fops); // 绑定 file_operations
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, devno, 1);
if (ret) {
unregister_chrdev_region(devno, 1);
return ret;
}
return 0;
}
最佳实践: 将
file_operations 声明为 static const,确保函数指针表在编译时只读,防止运行时被篡改。同时,.owner = THIS_MODULE 字段必须赋值,用于管理模块引用计数,防止模块在使用中被卸载。
3.7 三接口职责对比
| 接口 | 调用时机 | 主要职责 | 典型错误码 |
|---|---|---|---|
open |
用户态 open() 系统调用 |
设备初始化、权限验证、私有数据挂载、打开计数 | -ENODEV、-EBUSY、-EACCES |
release |
文件描述符引用计数归零时 | 资源释放、硬件关闭、计数恢复、等待队列清理 | 通常返回 0 |
unlocked_ioctl |
用户态 ioctl() 系统调用 |
命令解析、用户态数据拷贝、硬件寄存器读写、状态查询 | -ENOTTY、-EINVAL、-EFAULT |
内核版本差异: 在 Linux 2.6.36 之前,
ioctl 函数指针名为 .ioctl,且持有 BKL(大内核锁)。现代内核使用 .unlocked_ioctl,驱动需自行保证并发安全。若需兼容旧版,可同时实现 .compat_ioctl 用于 32 位兼容模式。
通过 struct file_operations 结构体,驱动开发者以函数指针的方式向内核注册设备操作能力。open 和 release 构成了设备生命周期的开始与结束,而 ioctl 则提供了无限扩展的自定义控制通道。掌握这三个接口的注册与实现,是编写任何字符设备驱动的基础。