文件系统核心数据结构详解

好,咱们今天来啃硬骨头。文件系统移植,说白了就是跟三个数据结构打交道:struct inodestruct mountpt_operationsstruct file_operations。这三个东西,我当年刚接触 NuttX 时也绕了很久。今天咱们一个一个拆开讲。

struct inode:文件系统的"身份证"

先看 struct inode。它代表一个文件或目录节点。你可以把它想象成每个文件在内存里的"身份证"。

struct inode {
    FAR struct inode *i_peer;     /* 兄弟节点链表 */
    FAR struct inode *i_child;    /* 子节点链表 */
    int16_t           i_crefs;    /* 引用计数 */
    uint16_t          i_flags;    /* 标志位 */
    mode_t            i_mode;     /* 文件类型和权限 */
    FAR void         *i_private;  /* 私有数据指针 */
    FAR const struct file_operations *i_ops;  /* 文件操作函数表 */
    FAR const struct mountpt_operations *i_mops; /* 挂载点操作函数表 */
    char              i_name[1];  /* 文件名(变长) */
};

这里有几个关键点,我一个个说。

i_peer 和 i_child:这两个指针构成了树形结构。每个目录下的文件通过 i_child 指向第一个子节点,兄弟节点之间用 i_peer 串起来。嗯,说白了就是个链表套链表。

i_crefs:引用计数。这个字段我吃过亏。有一次我写了个驱动,打开文件后忘了释放,结果引用计数一直不为零,文件删不掉。排查了半天才发现是这里的问题。

i_private:私有数据指针。这个字段特别灵活。你可以用它指向任何自定义数据结构。比如在块设备驱动里,我习惯用它指向设备描述符结构体。

核心要点i_opsi_mops 是互斥的。普通文件用 i_ops,挂载点用 i_mops。两者不会同时存在。

struct mountpt_operations:挂载点的"操作手册"

这个结构体定义了文件系统挂载后的所有操作。说白了,就是告诉内核:"我这个文件系统能干什么"。

struct mountpt_operations {
    int (*open)(FAR struct file *filep);
    int (*close)(FAR struct file *filep);
    ssize_t (*read)(FAR struct file *filep, FAR char *buffer, size_t buflen);
    ssize_t (*write)(FAR struct file *filep, FAR const char *buffer, size_t buflen);
    off_t (*seek)(FAR struct file *filep, off_t offset, int whence);
    int (*ioctl)(FAR struct file *filep, int cmd, unsigned long arg);
    int (*sync)(FAR struct file *filep);
    int (*dup)(FAR const struct file *oldp, FAR struct file *newp);
    int (*fstat)(FAR const struct file *filep, FAR struct stat *buf);
    int (*truncate)(FAR struct file *filep, off_t length);
    int (*opendir)(FAR struct inode *mountpt, FAR const char *relpath, FAR struct fs_dirent_s *dir);
    int (*closedir)(FAR struct inode *mountpt, FAR struct fs_dirent_s *dir);
    int (*readdir)(FAR struct inode *mountpt, FAR struct fs_dirent_s *dir);
    int (*rewinddir)(FAR struct inode *mountpt, FAR struct fs_dirent_s *dir);
    int (*bind)(FAR struct inode *blkdriver, FAR const void *data, FAR void **handle);
    int (*unbind)(FAR void *handle);
    int (*statfs)(FAR struct inode *mountpt, FAR struct statfs *buf);
    int (*unlink)(FAR struct inode *mountpt, FAR const char *relpath);
    int (*mkdir)(FAR struct inode *mountpt, FAR const char *relpath, mode_t mode);
    int (*rmdir)(FAR struct inode *mountpt, FAR const char *relpath);
    int (*rename)(FAR struct inode *mountpt, FAR const char *oldrelpath, FAR const char *newrelpath);
    int (*stat)(FAR struct inode *mountpt, FAR const char *relpath, FAR struct stat *buf);
};

你想想看,这个结构体里哪些函数是必须实现的?我个人经验是:bindunbindstatfsopenclosereadwrite 这七个是底线。其他的可以返回 -ENOSYS

小技巧:写文件系统驱动时,我习惯先把 bindunbind 写好。这两个函数通了,挂载和卸载就没问题。然后再逐步实现读写操作。

struct file_operations:普通文件的"操作手册"

这个结构体跟 mountpt_operations 很像,但它是给普通文件用的。比如设备文件、socket 文件等。

struct file_operations {
    int (*open)(FAR struct file *filep);
    int (*close)(FAR struct file *filep);
    ssize_t (*read)(FAR struct file *filep, FAR char *buffer, size_t buflen);
    ssize_t (*write)(FAR struct file *filep, FAR const char *buffer, size_t buflen);
    off_t (*seek)(FAR struct file *filep, off_t offset, int whence);
    int (*ioctl)(FAR struct file *filep, int cmd, unsigned long arg);
    int (*poll)(FAR struct file *filep, struct pollfd *fds, bool setup);
    int (*unlink)(FAR struct inode *inode);
    int (*truncate)(FAR struct file *filep, off_t length);
};

这里有个细节要注意:poll 函数。它用于实现 select/poll 系统调用。如果你写的设备驱动需要支持多路复用,这个函数必须实现。

我曾经写一个串口驱动,忘了实现 poll,结果上层应用用 select 等待数据时,永远返回不了。排查了两天才发现是这里的问题。

三个结构体的关系

这三个结构体怎么配合?我画个简单的逻辑图给你看:

场景 使用的结构体 示例
挂载文件系统 mountpt_operations FATFS、NFS 挂载
读写普通文件 file_operations /dev/ttyS0 读写
目录操作 mountpt_operations opendir、readdir
设备文件 file_operations /dev/null、/dev/zero

避坑指南:我曾经在移植一个文件系统时,把 mountpt_operationsfile_operations 搞混了。结果挂载成功了,但读写文件时内核直接 panic。记住:挂载点用 mountpt_operations,文件用 file_operations,别搞反了。

实际移植中的注意事项

嗯,这里分享几个实战经验:

  • 引用计数管理:每次 open 调用要增加 i_crefsclose 时减少。我见过有人忘了减引用计数,导致文件系统卸载不了。
  • 私有数据指针i_private 可以指向任何东西。我习惯用它指向文件系统特定的上下文结构体。但记得在 unbind 时释放内存。
  • 错误处理:所有操作函数都要返回标准的错误码。比如 -ENOSYS 表示功能未实现,-EIO 表示 IO 错误。
  • 线程安全:NuttX 是实时操作系统,多个任务可能同时访问同一个文件。如果你的文件系统不支持并发访问,记得加锁。

最后说一句:这三个结构体是 NuttX 文件系统的基石。你花时间把它们搞透,后面移植任何文件系统都会轻松很多。我当年就是在这上面下了功夫,后来移植 SPIFFS、FATFS 都顺风顺水。

下一章咱们讲文件系统注册和挂载的完整流程,到时候你会看到这三个结构体是怎么串起来的。