2. Linux内核文件操作结构

在 Linux 内核中,一切皆文件。字符设备、块设备、网络套接字乃至普通文件,都通过统一的文件操作接口与用户空间交互。理解 struct file、struct inode 和 file_operations 这三个核心结构体,是编写设备驱动的基础。它们分别代表了打开的文件实例、文件的元数据以及操作函数表。

核心概念速览:
  • struct file:每个 open() 系统调用创建一个实例,保存当前读写位置、标志位、私有数据等。
  • struct inode:每个文件(设备节点)只有一个,保存权限、大小、设备号等静态信息。
  • file_operations:函数指针集合,驱动通过它向 VFS 注册 read、write、poll、ioctl 等回调。

2.1 struct file:打开的文件实例

struct file 是内核中表示一个已打开文件描述符的核心结构。每当用户进程调用 open() 时,内核就会分配一个新的 struct file 实例。该实例的生命周期从 open() 开始,到 close() 结束。多个进程可以同时打开同一个设备文件,每个进程拥有独立的 struct file 实例。

关键字段解析:
struct file {
    struct path                 f_path;         // 文件路径信息
    struct inode                *f_inode;       // 指向关联的 inode
    const struct file_operations *f_op;         // 文件操作函数表(驱动核心)
    unsigned int                f_flags;        // 文件打开标志(O_RDONLY, O_NONBLOCK 等)
    loff_t                      f_pos;          // 当前读写偏移量(对设备驱动通常无意义)
    void                        *private_data;  // 私有数据指针(驱动最常用字段)
    struct address_space        *f_mapping;     // 地址映射(用于页缓存)
    atomic_long_t               f_count;        // 引用计数
    // ... 其他字段
};

在非阻塞 I/O 驱动开发中,f_flags 和 private_data 是最常使用的两个字段:

  • f_flags:驱动通过检查 filp->f_flags & O_NONBLOCK 来判断用户空间是否以非阻塞模式打开设备。这是实现非阻塞 read/write 的关键。
  • private_data:驱动通常在 open() 方法中将自定义的设备结构体指针赋值给 filp->private_data,后续的 read、write、poll 等操作通过该指针访问设备状态。
struct file 实例与进程关系图 进程 A (fd=3) 进程 B (fd=5) 进程 C (fd=7) struct file 实例 A f_flags=O_RDWR | O_NONBLOCK struct file 实例 B f_flags=O_RDONLY struct file 实例 C f_flags=O_RDWR struct inode i_rdev = MKDEV(240, 0) i_mode = S_IFCHR | 0666 i_fop = &my_fops i_private = &my_dev i_count = 3 i_ino = 123456 三个进程各自拥有独立的 struct file 实例,但共享同一个 struct inode。每个 file 的 f_flags 可以不同。

2.2 struct inode:文件的元数据

struct inode 是 VFS 中代表文件本身的数据结构。与 struct file 不同,inode 在文件系统中是唯一的——同一个设备节点无论被打开多少次,都只对应一个 inode 实例。它存储了文件的静态属性,如权限、大小、设备号、操作函数表等。

驱动开发常用字段:
struct inode {
    umode_t             i_mode;         // 文件类型与权限(S_IFCHR | 0666)
    kdev_t              i_rdev;         // 设备号(主设备号 + 次设备号)
    loff_t              i_size;         // 文件大小(设备文件通常为 0)
    const struct file_operations *i_fop; // 默认文件操作(open 时复制到 file->f_op)
    void                *i_private;     // 私有数据指针(驱动常用)
    unsigned long       i_ino;          // inode 编号
    atomic_t            i_count;        // 引用计数
    struct list_head    i_devices;      // 设备链表
    // ... 其他字段
};

在字符设备驱动中,inode 最重要的作用是获取设备号和关联私有数据:

  • i_rdev:通过 imajor(inode) 和 iminor(inode) 宏提取主次设备号,用于区分同一驱动下的不同设备实例。
  • i_private:驱动可以在 open() 中通过 container_of 或直接赋值,将自定义设备结构体挂载到 inode 上,实现设备实例的查找。
struct file 与 struct inode 对比
对比维度 struct file struct inode
生命周期 open() 创建,close() 销毁 文件创建时生成,删除时销毁
实例数量 每个打开操作一个实例 每个文件只有一个实例
存储内容 动态状态(位置、标志、私有数据) 静态元数据(设备号、权限、大小)
驱动关注点 f_flags(非阻塞标志)、private_data i_rdev(设备号)、i_private
引用方式 通过 fd 查找 通过 dentry 或 file->f_inode 访问

2.3 file_operations:驱动行为函数表

file_operations 是驱动与 VFS 之间的契约。它是一个包含大量函数指针的结构体,驱动通过填充这些指针来注册自己的 read、write、poll、open、release 等回调函数。当用户空间调用系统调用时,VFS 会通过 file->f_op 找到对应的驱动函数并执行。

核心函数指针声明:
struct file_operations {
    struct module *owner;           // 通常设为 THIS_MODULE
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    // ... 其他可选函数指针
};

对于非阻塞 I/O 和 poll 机制,以下三个函数指针最为关键:

  • open:初始化设备,分配资源,设置 filp->private_data。
  • read / write:实现数据读写,必须检查 filp->f_flags & O_NONBLOCK 来决定是否阻塞。
  • poll:实现 poll/select/epoll 机制的核心,返回设备当前可读/可写状态,并将等待队列加入 poll_table。
file_operations 驱动回调流程 用户空间 系统调用层 (VFS) file_operations .owner = THIS_MODULE .open = my_open .read = my_read .write = my_write .poll = my_poll .release = my_release .unlocked_ioctl = my_ioctl .mmap = my_mmap 驱动回调实现 my_open(): 初始化硬件 my_read(): 读取数据 my_write(): 写入数据 my_poll(): 检查状态 my_release(): 释放资源 my_ioctl(): 控制命令 my_mmap(): 内存映射 VFS 通过 file->f_op 找到函数表,再通过函数指针跳转到驱动实现。驱动只需填充表,无需关心系统调用细节。

2.4 三者协作:从 open 到 poll 的完整链路

理解三个结构体如何协作,是掌握驱动开发的关键。以下是一个典型的非阻塞 I/O 场景中,从用户空间调用 open() 到 poll() 的完整链路:

  1. 用户调用 open("/dev/mydev", O_RDWR | O_NONBLOCK):VFS 根据路径找到 dentry 和 inode。
  2. VFS 分配 struct file 实例:将 f_flags 设置为 O_RDWR | O_NONBLOCK,f_inode 指向设备 inode。
  3. VFS 调用 inode->i_fop->open(inode, filp):驱动在 my_open() 中初始化设备,并将设备结构体指针存入 filp->private_data。
  4. 用户调用 poll(fd, &fds, timeout):VFS 通过 filp->f_op->poll(filp, poll_table) 调用驱动的 my_poll()。
  5. 驱动在 my_poll() 中:检查设备状态(如缓冲区是否有数据),调用 poll_wait() 将等待队列加入 poll_table,返回 POLLIN | POLLRDNORM 等掩码。
常见陷阱:
  • 混淆 file 与 inode:不要在 inode 中存储每个打开实例的动态状态(如当前读写位置),应该使用 file->private_data。
  • 忽略 f_flags 检查:在 read() 和 write() 中必须检查 filp->f_flags & O_NONBLOCK,否则非阻塞模式会退化为阻塞行为。
  • f_op 赋值时机:字符设备驱动通常在 cdev_init() 时设置 fops,但也可以在 open() 中动态替换 filp->f_op(如实现设备节点差异化)。

2.5 典型驱动模板:open 中的初始化模式

以下是一个标准的字符设备驱动 open() 实现,展示了 inode 和 file 的典型用法:

// 自定义设备结构体
struct my_device {
    struct cdev cdev;
    spinlock_t lock;
    wait_queue_head_t read_wait;
    char buffer[1024];
    int data_ready;
};

// open 回调
static int my_open(struct inode *inode, struct file *filp)
{
    // 从 inode 中获取设备结构体(通过 container_of 或 cdev 宏)
    struct my_device *dev = container_of(inode->i_cdev,
                                         struct my_device, cdev);

    // 将设备结构体指针存入 file 的私有数据区
    filp->private_data = dev;

    // 检查非阻塞标志(可选,用于记录日志)
    if (filp->f_flags & O_NONBLOCK)
        pr_info("Device opened in non-blocking mode\n");

    return 0;
}

// file_operations 定义
static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .read    = my_read,
    .write   = my_write,
    .poll    = my_poll,
    .release = my_release,
};

通过这种模式,驱动在 open() 中建立了 inode → 设备结构体 → file 私有数据 的完整链路,后续所有操作都可以通过 filp-private_data 快速访问设备状态。