文件系统核心数据结构详解
好,咱们今天来啃硬骨头。文件系统移植,说白了就是跟三个数据结构打交道:struct inode、struct mountpt_operations 和 struct 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_ops 和 i_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);
};
你想想看,这个结构体里哪些函数是必须实现的?我个人经验是:bind、unbind、statfs、open、close、read、write 这七个是底线。其他的可以返回 -ENOSYS。
小技巧:写文件系统驱动时,我习惯先把 bind 和 unbind 写好。这两个函数通了,挂载和卸载就没问题。然后再逐步实现读写操作。
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_operations 和 file_operations 搞混了。结果挂载成功了,但读写文件时内核直接 panic。记住:挂载点用 mountpt_operations,文件用 file_operations,别搞反了。
实际移植中的注意事项
嗯,这里分享几个实战经验:
- 引用计数管理:每次
open调用要增加i_crefs,close时减少。我见过有人忘了减引用计数,导致文件系统卸载不了。 - 私有数据指针:
i_private可以指向任何东西。我习惯用它指向文件系统特定的上下文结构体。但记得在unbind时释放内存。 - 错误处理:所有操作函数都要返回标准的错误码。比如
-ENOSYS表示功能未实现,-EIO表示 IO 错误。 - 线程安全:NuttX 是实时操作系统,多个任务可能同时访问同一个文件。如果你的文件系统不支持并发访问,记得加锁。
最后说一句:这三个结构体是 NuttX 文件系统的基石。你花时间把它们搞透,后面移植任何文件系统都会轻松很多。我当年就是在这上面下了功夫,后来移植 SPIFFS、FATFS 都顺风顺水。
下一章咱们讲文件系统注册和挂载的完整流程,到时候你会看到这三个结构体是怎么串起来的。