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 函数指针之间的调用关系:

用户态 → VFS → 驱动 file_operations 调用流程 用户态进程 open("/dev/mydev", O_RDWR) | ioctl(fd, CMD, &data) | close(fd) VFS 虚拟文件系统层 根据设备号查找 inode → 获取 file_operations 指针 struct file_operations .open = my_open, .release = my_release, .unlocked_ioctl = my_ioctl .read = my_read, .write = my_write, .llseek = NULL, .poll = NULL (函数指针表:驱动实现所需接口,未实现置 NULL) my_open() 初始化设备、挂载私有数据 my_ioctl() 命令分发、数据拷贝、硬件控制 my_release() 资源释放、计数递减

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 则提供了无限扩展的自定义控制通道。掌握这三个接口的注册与实现,是编写任何字符设备驱动的基础。