Nuttx - VFS
从伪文件系统 inode 树到设备驱动注册、从 open() 路径查找到 mount() 文件系统接入,完整解析 NuttX 虚拟文件系统的统一 I/O 抽象机制。
本文回答以下问题:NuttX 如何用同一套 open/read/write/close 接口统一访问设备驱动和真实文件系统?伪文件系统的 inode 树是如何组织和查找的?一个 open("/dev/console", O_RDWR) 调用经历了哪些步骤?mount 如何将真实文件系统”嫁接”到伪文件系统上?读完后,你将能够从源码级别追踪任何文件操作的完整路径,并理解如何为 NuttX 编写自己的设备驱动或文件系统。
1. 开篇:VFS 解决什么问题?
嵌入式系统中有多种 I/O 资源:串口、SPI 设备、LED、Flash 文件系统、网络 socket、/proc 状态信息。如果每种资源用不同的 API 访问,应用代码将充斥条件分支和平台耦合。
VFS(Virtual File System)的核心思想是:一切皆文件——所有 I/O 资源都通过统一的 open()/read()/write()/close() 接口访问,用路径名(如 /dev/ttyS0、/mnt/sd/config.txt)区分不同资源。
NuttX 的 VFS 有一个独特设计:根文件系统永远是伪文件系统(pseudo filesystem)。与 Linux 必须有物理块设备作为根不同,NuttX 的根是一棵内存中的 inode 树,不需要任何存储介质。设备驱动和真实文件系统都挂载到这棵树上。
NuttX 官方文档(Documentation/implementation/nuttx_tasking.rst:269-307)对此有描述:
“The NuttX root file system is always a pseudo-file system… Then you can mount real filesystem in the pseudo-filesystem.”
这种设计使得 NuttX 在没有任何存储设备的系统上也能正常启动和运行——所有设备驱动都注册为 /dev/xxx 节点,无需真实文件系统支持。
接下来看这棵伪文件系统树的具体结构。
2. 伪文件系统与 inode 树
路径具有天然的层级结构(/dev/ttyS0 = 根 → dev → ttyS0)。用树结构表示可以:
- 自然支持路径逐段查找
- 中间节点自动充当目录(如
/dev是一个伪目录) - 支持在任意节点挂载真实文件系统(mount point)
2.1 inode 树的三指针结构
文件:include/nuttx/fs/fs.h:401-421
1 | struct inode |
每个 inode 通过 i_parent/i_peer/i_child 三个指针形成一棵排序多叉树:同层兄弟按名字字母序通过 i_peer 串联,子节点通过 i_child 连接。
实例:启动后典型的 inode 树
1 | g_root_inode |
下图展示了典型的 inode 树结构,绿色箭头表示 i_child(进入子层),蓝色箭头表示 i_peer(同层兄弟):

全局根节点:
文件:fs/inode/fs_inodesearch.c:57
1 | FAR struct inode *g_root_inode = NULL; |
g_root_inode 在 fs_initialize() 时被初始化为 / 节点。树的访问受读写信号量 g_inode_lock 保护(读操作并发,写操作互斥)。
2.2 inode 类型
文件:include/nuttx/fs/fs.h:118-130
1 |
类型决定了 VFS 在 open()/read()/write() 时走哪条分发路径。
2.3 路径查找:inode_search()
文件:fs/inode/fs_inodesearch.c:217-392(_inode_search() 内部函数)
路径查找是 VFS 最频繁的操作。算法核心是逐段匹配:
1 | static int _inode_search(FAR struct inode_search_s *desc) |
实例:查找 /dev/console 的过程
1 | Step 1: name = "dev/console" (跳过前导 '/') |
遇到挂载点时:如果路径是 /mnt/sd/dir/file.txt,搜索到 /mnt/sd(MOUNTPT 类型)时停止,desc->relpath = "dir/file.txt" 保存剩余路径。后续由挂载的文件系统(如 FAT)处理相对路径。
理解了 inode 树和路径查找,下面看核心数据结构如何将树节点与实际 I/O 操作连接起来。
3. 核心数据结构
3.1 struct file_operations:驱动的操作函数表
文件:include/nuttx/fs/fs.h:218-250
1 | struct file_operations |
每个设备驱动注册时提供这样一个函数指针表。VFS 通过 inode->u.i_ops 指针调用对应方法。
3.2 struct mountpt_operations:文件系统的操作函数表
文件:include/nuttx/fs/fs.h:285-363
1 | struct mountpt_operations |
关键设计:file_operations 和 mountpt_operations 的 close/read/write/seek/ioctl/poll 方法签名和位置完全相同。VFS 在打开文件后统一通过 inode->u.i_ops->read() 调用——无论底层是设备驱动还是文件系统。区别只在 open() 时:驱动收到 open(filep),文件系统收到 open(filep, relpath, oflags, mode)。
3.3 struct file:打开的文件
文件:include/nuttx/fs/fs.h:457-467
1 | struct file |
每次成功 open() 都创建一个 struct file,它绑定到对应的 inode。后续 read()/write() 通过 file->f_inode->u.i_ops 分发到正确的驱动或文件系统。
3.4 union inode_ops_u:操作指针联合体
文件:include/nuttx/fs/fs.h:380-397
1 | union inode_ops_u |
同一个 inode 根据类型使用不同的联合体成员。VFS 根据 i_flags 判断类型后选择正确的成员访问。
数据结构建立了”谁是谁”的关系。接下来看设备驱动如何将自己注册到 inode 树中。
4. 设备驱动注册:register_driver()
设备驱动必须出现在 inode 树中,用户代码才能通过路径名找到它。register_driver() 的职责是:在 inode 树的指定路径创建一个 DRIVER 类型的节点,并将操作函数表关联上去。
4.1 注册流程
文件:fs/driver/fs_registerdriver.c:67-134
1 | int register_driver_with_size(FAR const char *path, |
inode_reserve()(文件:fs/inode/fs_inodereserve.c:176)负责:
- 调用
inode_search()找到插入位置 - 为路径中不存在的中间目录创建 PSEUDODIR 节点(如
/dev不存在则自动创建) - 为最终节点分配内存(
fs_heap_zalloc(FSNODE_SIZE(namelen))) - 按字母序插入到
i_peer链表中
实例:register_driver("/dev/ttyS0", &uart_ops, 0666, dev) 的效果
1 | Before: |
设备注册完成后,用户代码就可以通过 open("/dev/ttyS0", ...) 找到这个节点。接下来看 open() 的完整路径。
5. open() 完整调用链
open() 需要完成多件事:路径解析(可能跨越伪文件系统和真实文件系统)、权限检查、文件描述符分配、驱动/文件系统的初始化回调。这是整个 VFS 唯一需要区分”驱动”和”文件系统”的地方——之后 read/write/close 走统一路径。
5.1 调用链概览
下图展示了 open("/dev/console", O_RDWR) 从路径查找到驱动调用的完整时序:

1 | User: open("/dev/console", O_RDWR) |
5.2 file_vopen():核心分发逻辑
文件:fs/vfs/fs_open.c:74-246(关键摘录)
1 | static int file_vopen(FAR struct file *filep, FAR const char *path, |
分发逻辑的关键:对于驱动节点(如 /dev/console),直接调用 i_ops->open(filep)。对于挂载点(如 /mnt/sd/file.txt),调用 i_mops->open(filep, "file.txt", oflags, mode)——将挂载点以下的相对路径传给文件系统处理。
5.3 文件描述符分配
文件:fs/inode/fs_files.c:566-633
NuttX 的文件描述符表(struct fdlist)是一个二维数组:
1 | struct fdlist |
fd 值映射为:row = fd / BLOCK_SIZE,col = fd % BLOCK_SIZE(BLOCK_SIZE = CONFIG_NFILE_DESCRIPTORS_PER_BLOCK)。
分配时线性扫描找第一个空位(fl_fds[i][j].f_file == NULL),将 struct file 指针存入该位置,返回 i * BLOCK_SIZE + j 作为 fd。
open() 完成后,后续的 read/write/close 都通过 fd → file → inode → ops 的路径分发。下面看 read/write 如何工作。
6. read()/write() 分发机制
因为 open() 时已经将 inode 绑定到 struct file,而 file_operations 和 mountpt_operations 的 read/write 方法签名完全相同、位于结构体的相同偏移。VFS 只需要 file->f_inode->u.i_ops->read(filep, buf, len)——无论底层是什么。
6.1 read() 调用链
文件:fs/vfs/fs_read.c:158(file_readv() 核心)
1 | static ssize_t file_readv(FAR struct file *filep, |
完整路径:read(fd, buf, n) → file_get(fd) 获取 struct file * → file_readv() → i_ops->read(filep, buf, n)。
6.2 分发路径图
1 | fd (int) |
对比 Linux:Linux VFS 中 read/write 还经过 page cache 层(generic_file_read_iter),而 NuttX 是直通式——没有 page cache,数据直接在驱动/文件系统和用户缓冲区之间传递。这使得 NuttX VFS 延迟更低、更可预测,但不支持 mmap 文件映射(需要 page cache)。
read/write 的简洁性源于 open() 时已完成了所有”类型判断”工作。接下来看更复杂的 mount() 如何将真实文件系统接入 inode 树。
7. mount() 机制:真实文件系统的接入
伪文件系统只能存放设备驱动节点,不能存储持久化数据。要访问 SD 卡、Flash 上的文件,需要将真实文件系统”嫁接”到伪文件系统的某个节点上。mount 就是建立这个嫁接关系的操作。
7.1 文件系统注册表
文件:fs/mount/fs_mount.c:109-195
NuttX 在编译时通过静态数组注册所有支持的文件系统:
1 | /* 基于块设备的文件系统 */ |
mount() 调用时,内核遍历上述三个数组匹配 filesystemtype 字符串(如 "vfat"),找到对应的 mountpt_operations 函数表。如果没有匹配的文件系统类型,返回 -ENODEV。
7.2 mount() 流程
文件:fs/mount/fs_mount.c:286-570(nx_mount() 核心,以下为关键步骤摘录)
1 | mount("/dev/mmcsd0", "/mnt/sd", "vfat", 0, NULL): |
mount 完成后,inode 树中 /mnt/sd 节点的类型从 PSEUDODIR 变为 MOUNTPT。后续对 /mnt/sd/xxx 的 open() 会在路径搜索时被该挂载点”截断”,剩余路径交给 FAT 文件系统处理。
7.3 挂载点如何”截断”路径搜索
回忆 _inode_search() 中的关键判断:
1 | if (*name == '\0' || INODE_IS_MOUNTPT(inode)) |
例如 open("/mnt/sd/photos/img.jpg"):
- 搜索到
/mnt/sd时发现是 MOUNTPT,停止 desc->relpath = "photos/img.jpg"file_vopen()调用fat_operations->open(filep, "photos/img.jpg", oflags, mode)- FAT 文件系统在自己的目录结构中解析
photos/img.jpg
这就是 NuttX 伪文件系统与真实文件系统的”交接”机制。
8. 文件描述符管理
POSIX 要求每个进程有独立的文件描述符空间。在 NuttX 中,struct fdlist 存储在 task_group_s 中——同一任务组(进程)的所有线程共享文件描述符表。
8.1 stdin/stdout/stderr 与 /dev/console
启动时第一个任务(IDLE)打开 /dev/console 三次,获得 fd 0、1、2:
文件:sched/init/nx_start.c:540-545
1 | nx_open("/dev/console", O_RDWR); /* fd 0 = stdin */ |
后续通过 task_create() 或 posix_spawn() 创建的子任务/进程会继承父任务的文件描述符表(group_setuptaskfiles() 中完成)。所有任务默认共享对 /dev/console 的 stdin/stdout/stderr。
8.2 I/O 重定向
如果一个任务关闭 fd 1 然后打开一个新文件,新文件会获得 fd 1(因为分配时取最小空闲 fd)。之后该任务的所有 printf()(内部写 stdout = fd 1)就输出到了新文件——这就是 I/O 重定向的原理。NuttX 的 telnet 服务器就是这样实现的:将 /dev/telnet 设备重定向为子任务的 stdin/stdout。
9. 对比分析
| 特性 | NuttX VFS | Linux VFS | FreeRTOS |
|---|---|---|---|
| 根文件系统 | 伪文件系统(内存树) | 必须是物理块设备 | 无 VFS 概念 |
| inode 结构 | 树(parent/peer/child) | 哈希表 + dentry cache | — |
| Page Cache | 无 | 有(read/write 经过 page cache) | — |
| 文件描述符 | per-task_group 二维数组 | per-process fd_table | per-task 链表(+FAT 扩展) |
| 设备访问 | open(“/dev/xxx”) | open(“/dev/xxx”) | 直接调用驱动 API |
| 挂载点 | inode 类型=MOUNTPT | vfsmount + super_block | ff_mount (FatFs only) |
| 支持的 FS | FAT, ROMFS, LittleFS, TMPFS, PROCFS, NFS… | ext4, btrfs, XFS, NFS… | FatFs (通常只有一个) |
| 路径缓存 | 无(每次全路径搜索) | dentry cache(O(1) 热路径) | — |
| 统一 I/O | open/read/write/close/ioctl | 同 | 无统一接口 |
NuttX VFS 的设计取舍:
- 无 dentry cache——每次
open()都要走完整的树搜索。对嵌入式系统可接受(inode 树通常很浅,几十个节点),但对大型文件系统性能不如 Linux。 - 无 page cache——read/write 直通驱动,延迟低、内存占用小,但无法利用缓存加速重复读取。
- 伪文件系统根——不需要任何存储设备就能启动,特别适合 MCU + 外设的场景。
10. 关键要点
NuttX 的根永远是伪文件系统——一棵内存中的 inode 树,设备驱动和真实文件系统都挂载其上。
inode 树使用 parent/peer/child 三指针结构——同层兄弟按名字字母序排列,路径查找逐段匹配。
file_operations 和 mountpt_operations 共享 read/write/close 签名——open() 之后,VFS 对驱动和文件系统的分发完全统一。
register_driver() 在 inode 树中创建 DRIVER 节点,绑定操作函数表和私有数据。
open() 是 VFS 唯一需要区分类型的地方——驱动收到
open(filep),文件系统收到open(filep, relpath, oflags, mode)。mount() 将伪目录节点转换为 MOUNTPT,后续路径搜索在该节点”截断”,剩余路径交给文件系统。
文件描述符是 per-task_group 的二维数组,子任务继承父任务的 fd 表(包括 stdin/stdout/stderr)。
NuttX VFS 是直通式(无 page cache)——延迟低、确定性好,适合实时系统。
11. 参考文件索引
| 文件路径 | 关键内容 | 引用行号 |
|---|---|---|
include/nuttx/fs/fs.h |
inode, file, file_operations, mountpt_operations, fdlist | 118-505 |
fs/inode/inode.h |
inode_search_s, SETUP_SEARCH 宏 | 50-143 |
fs/inode/fs_inodesearch.c |
g_root_inode, inode_search(), _inode_search(), _inode_compare() | 57-573 |
fs/inode/fs_inodereserve.c |
inode_reserve(), inode_alloc(), inode_insert() | 176-264 |
fs/inode/fs_inodefind.c |
inode_find()(加锁 + 搜索 + 引用计数) | 52-76 |
fs/inode/fs_inode.c |
g_inode_lock, inode_lock/unlock, inode_checkperm() | 49-153 |
fs/inode/fs_files.c |
fdlist_dupfile(), file_allocate(), file_get() | 566-982 |
fs/driver/fs_registerdriver.c |
register_driver(), register_driver_with_size() | 67-134 |
fs/vfs/fs_open.c |
open(), nx_vopen(), file_vopen() | 74-455 |
fs/vfs/fs_read.c |
read(), file_readv() | 158-413 |
fs/vfs/fs_write.c |
write(), file_writev() | 148-467 |
fs/mount/fs_mount.c |
mount(), nx_mount(), g_bdfsmap/g_nonbdfsmap | 109-585 |
fs/fs_initialize.c |
fs_initialize() 启动初始化 | 全文 |
drivers/serial/serial.c |
uart_register(), uart_open(), g_serialops | 139-713 |
Documentation/implementation/nuttx_tasking.rst |
官方 VFS 设计描述 | 269-307 |