文件系统层
AD23N GP-MCU SDK 的虚拟文件系统(VFS)抽象层,通过统一的 struct vfs_operations 操作表屏蔽 FAT、norfs、freefs 等底层文件系统的差异,为上层应用提供挂载、文件读写、目录扫描、ioctl 控制等标准文件操作接口。
Purpose and Scope
本文档讲解文件系统层的核心抽象机制与公共 API,包括:
- VFS 操作表
struct vfs_operations与句柄模型struct imount的设计 - 文件系统驱动的注册方式(
REGISTER_VFS_OPERATIONS段注册) - 挂载(
vfs_mount)、文件打开/读写/关闭、扫描与 ioctl 的完整流程 - 底层驱动(FAT / norfs / freefs)如何通过操作表接入 VFS
以下主题属于兄弟页面,本文不展开:FAT 文件系统内部实现(sdk/app/bsp/common/fs/vfs_fat.c 之外的 fat/ 目录)、norfs / freefs 各自的分区与磨损均衡算法、以及上层应用(如录音、播放列表)对文件系统的具体使用。
概述
在嵌入式 MCU 上,存储介质可能是 SPI NOR Flash、SD 卡或片内 flash,不同介质上的文件系统(FAT、norfs、freefs)接口差异巨大。文件系统层的设计目标是用一张操作函数表 + 一个通用句柄把差异收敛起来:
- 每个文件系统驱动通过
REGISTER_VFS_OPERATIONS宏把自身的struct vfs_operations实例链接进.vfs_operations代码段; vfs_init()遍历该段逐个调用驱动的init;vfs_mount()按fs_type字符串匹配驱动并调用其mount;- 之后所有文件操作(open/read/write/seek/close/delete/ioctl)都经由句柄中保存的
ops表分发到具体驱动。
这套设计让应用层可以完全与底层介质/文件系统解耦:同一份播放器、录音、升级代码既能跑在 FAT(SD 卡/U 盘)上,也能跑在内部 norfs 上,只需在挂载时指定不同的 type。
架构
flowchart TD
subgraph sg_App["应用层"]
App["上层应用<br/>(播放/录音/升级)"]
end
subgraph sg_VFS["文件系统层 (VFS)"]
API["vfs_* 公共 API<br/>vfs_mount / vfs_openbypath / vfs_read<br/>vfs_write / vfs_seek / vfs_ioctl"]
HDL["句柄模型<br/>struct imount {" ops; union pfs/pfile; "}"]
OPS["操作表<br/>struct vfs_operations"]
end
subgraph sg_Drv["底层文件系统驱动"]
FAT["fatfs 驱动<br/>fs_type = \"fat\""]
NORFS["norfs 驱动<br/>fs_type = \"norfs\""]
FREEFS["freefs 驱动<br/>fs_type = \"freefs\""]
end
subgraph sg_Dev["存储介质"]
SD["SD卡 / U盘"]
NOR["SPI NOR Flash"]
IFLASH["片内 Flash"]
end
App -->|"标准文件接口"| API
API --> HDL
HDL -->|"ops 分发"| OPS
OPS -->|"vfs_ops_begin..vfs_ops_end 段"| FAT
OPS --> NORFS
OPS --> FREEFS
FAT --> SD
NORFS --> NOR
FREEFS --> IFLASH
架构要点:
- 操作表(vtable)是核心抽象:
struct vfs_operations(见 vfs.h)定义了 20+ 个函数指针,覆盖挂载、打开(按路径/索引/簇)、创建、读写、定位、删除、属性、扫描、格式化等全部能力。驱动只需实现自己支持的那部分,未实现的指针保持 NULL,VFS 层会返回E_VFS_OPS。 - 句柄统一:
struct imount(见 vfs.h)只包含ops指针和一个联合体pfs/pfile——同一个结构既充当文件系统句柄(存pfs)又充当文件句柄(存pfile),极大简化了资源管理。 - 段注册机制:驱动实例被链接到
.vfs_operations段,vfs.c通过vfs_ops_begin/vfs_ops_end边界遍历,实现"新增驱动不改 VFS 核心代码"的开放-封闭原则。
核心实现分析
1. 操作表与注册机制
struct vfs_operations 是文件系统层的"协议",其关键字段(节选自 vfs.h):
struct vfs_operations {
const char *fs_type; // 文件系统类型名,如 "fat" / "norfs" / "freefs"
void (*init)(void); // 驱动初始化
u32(*mount)(void **ppfs, void *p_device); // 挂载
u32(*openbypath)(void *pfs, void **ppfile, const char *path);
u32(*openbyindex)(void *pfs, void **ppfile, u32 index);
u32(*createfile)(void *pfs, void **ppfile, u32 *pindex);
u32(*read)(void *pfile, void *buff, u32 len);
u32(*write)(void *pfile, void *buff, u32 len);
u32(*seek)(void *pfile, u32 offset, u32 mode);
u32(*close_fs)(void **ppfs);
u32(*close_file)(void **ppfile);
u32(*fdelete)(void *);
int (*fget_attr)(void *, void *attr);
int (*flen)(void *, u32 *parm);
int (*ftell)(void *, u32 *parm);
u32(*name)(void *, void *name, u32 len);
int (*ioctl)(void *, int cmd, int arg);
int (*fscan_interrupt)(struct vfscan *, void *, const char *path, u8 max_deepth, int (*callback)(void));
void (*fscan_release)(struct vfscan *);
int (*fsel)(struct vfscan *, void *, int sel_mode, void **, int);
int (*file_crc)(void *pfile);
int (*format)(void **p_fs_hdl, void *device, u32 clust_size, u8 create_new);
};
驱动注册使用段属性宏(vfs.h):
#define REGISTER_VFS_OPERATIONS(ops) \
const struct vfs_operations ops SEC(.vfs_operations)
Source: vfs.h
设计意图:链接器把所有驱动的操作表按顺序排布在 .vfs_operations 段,vfs.c 用 list_for_each_vfs_operation 宏(vfs.c)在段边界间遍历。相比链表注册,这种段注册零运行时开销、不占 RAM,且天然确定初始化顺序。
2. 初始化与挂载流程
vfs_init() 遍历段内所有操作表,逐个调用 init(vfs.c):
void vfs_init(void)
{
struct vfs_operations *ops;
list_for_each_vfs_operation(ops) {
if (NULL != ops->init) {
log_info("ops_init!!!\n");
ops->init();
}
}
}
vfs_mount() 是挂载的核心入口(vfs.c):
u32 vfs_mount(void **ppvfs, void *device, void *type)
{
if ((void *)NULL == *ppvfs) {
*ppvfs = vfs_hdl_malloc();
if ((void *)NULL == *ppvfs) {
return E_NO_VFS; // 句柄资源不足
}
}
struct imount *pvfs = *ppvfs;
struct vfs_operations *ops;
list_for_each_vfs_operation(ops) {
if (NULL != ops->mount) {
if (NULL != type) {
if (0 != strcmp(ops->fs_type, type)) {
continue; // 指定类型时精确匹配
}
} else {
// 未指定类型时跳过 norfs / freefs,优先尝试 FAT
if (0 == strcmp(ops->fs_type, "norfs")) { continue; }
if (0 == strcmp(ops->fs_type, "freefs")) { continue; }
}
if (0 == ops->mount(&(pvfs->pfs), device)) {
pvfs->ops = ops; // 绑定成功,保存操作表
return 0;
} else {
if (NULL != ops->close_fs) {
ops->close_fs(&(pvfs->pfs)); // 挂载失败需清理
}
}
}
}
*ppvfs = vfs_fhdl_free(*ppvfs); // 全部失败则释放句柄
return E_NO_FS;
}
Source: vfs.c
值得注意的设计细节:
- type 为 NULL 的默认行为:遍历时会跳过
norfs和freefs,即默认挂载优先尝试 FAT——因为 FAT 通常用于可移动介质(SD/U 盘),而内部 flash 文件系统需要显式指定类型。这避免了在无外部介质时误挂内部文件系统。 - 失败回滚:单个驱动
mount失败会调用其close_fs做清理;所有驱动都失败后释放 VFS 句柄并返回E_NO_FS,不留悬挂资源。 - 挂载成功后
pvfs->ops被保存,后续所有文件操作都从这个句柄取操作表。
3. 文件打开与读写分发
vfs_openbypath() 展示了一致的分发模式(vfs.c):
u32 vfs_openbypath(void *pvfs, void **ppvfile, const char *path)
{
if ((void *)NULL == *ppvfile) {
*ppvfile = vfs_hdl_malloc();
if ((void *)NULL == *ppvfile) {
return E_NO_VFS;
}
}
...
p_vfile->ops = p_vfs->ops; // 文件句柄继承文件系统句柄的 ops
ops = p_vfs->ops;
if (NULL != ops->openbypath) {
err = ops->openbypath(p_vfs->pfs, &p_vfile->pfile, path);
} else {
err = E_VFS_OPS; // 驱动未实现该操作
}
__vfs_openbypath:
if (0 != err) {
vfs_file_close(ppvfile); // 失败自动回收文件句柄
}
return err;
}
Source: vfs.c
要点:
- 句柄继承:文件句柄的
ops直接从文件系统句柄复制,因此一次挂载后,打开/读/写/关闭都无需再传文件系统句柄——struct imount的联合体设计让pfile与pfs共用同一内存布局,vfs_read/write/seek/close等接口的入参类型可以完全统一。 - 懒分配与自动回收:句柄在首次使用时才从
vfs_hdl_malloc()资源池分配;任何失败路径都会把已分配的句柄释放掉,上层无需关心清理。 - 同一模式还用于
vfs_openbyindex(按文件序号打开)、vfs_openbyfile(按扩展名匹配打开)和vfs_createfile(新建文件)。
4. 文件属性 / 删除 / 扫描适配层
vfs_fat.c 中的 vfs_get_fsize()、vfs_ftell()、vfs_file_delete() 是 VFS 公共 API 的薄封装,统一通过 ops 分发(vfs_fat.c):
u32 vfs_file_delete(void *pvfile)
{
struct imount *p_vfile = pvfile;
if ((void *)NULL == p_vfile) {
return E_FS_PFILE; // 非法句柄
}
struct vfs_operations *ops;
ops = p_vfile->ops;
if (((void *)NULL != ops) && ((void *)NULL != ops->fdelete)) {
u32 res = E_VFS_OPS;
if (NULL != (p_vfile->pfile)) {
res = ops->fdelete(p_vfile->pfile);
if (0 != res) {
return E_FS_PFILE;
}
}
return res;
}
return E_VFS_OPS; // 驱动未实现删除
}
Source: vfs_fat.c
文件扫描是文件系统层最复杂的部分,参数解析器 __fscan_arg_handler() 支持类似命令行风格的扫描选项(vfs_fat.c):
/*
* -t 文件类型
* -r 包含子目录
* -d 扫描文件夹
* -a 文件属性 r: 读, /: 非
* -s 排序方式, t:按时间排序, n:按文件号排序
*/
static void __fscan_arg_handler(struct vfscan *fs, const char *arg)
{
int step = 0;
char *p;
/*
* fs->attr = F_ATTR_RO: 搜索只读文件
* fs->attr = F_ATTR_ARC: 搜索非读文件
* fs->attr = F_ATTR_ARC | F_ATTR_RO: 搜索所有文件
*/
fs->attr = F_ATTR_ARC | F_ATTR_RO; // 默认扫描所有属性
while (*arg) {
switch (step) {
case 0:
if (*arg == '-') { step = 1; }
break;
case 1:
if (*arg == 't') { step = 2; p = fs->ftype; fs->scan_file = 1; }
...
}
}
}
Source: vfs_fat.c
设计意图:扫描接口 fscan_interrupt 支持中断回调(int (*callback)(void)),便于在扫描大量文件时让出 CPU 或响应按键/低电量事件;fscan_release 负责释放扫描过程中动态分配的资源(如长文件名缓冲),避免长时间扫描导致的内存泄漏。
5. ioctl 命令集
VFS 通过 FS_IOCTL_* 枚举向应用暴露底层能力(vfs.h),常用命令包括:
| 命令 | 用途 |
|---|---|
FS_IOCTL_GET_FILE_NUM | 获取文件总数 |
FS_IOCTL_FILE_CHECK | 文件完整性检查 |
FS_IOCTL_FREE_CACHE | 释放文件系统缓存 |
FS_IOCTL_SET_NAME_FILTER | 设置文件名过滤规则 |
FS_IOCTL_GET_FOLDER_INFO | 获取文件夹序号与文件数 |
FS_IOCTL_SET_LFN_BUF / FS_IOCTL_SET_LDN_BUF | 设置长文件名/长目录名缓冲(512B) |
FS_IOCTL_SET_EXT_TYPE | 设置后缀类型过滤 |
FS_IOCTL_OPEN_DIR / FS_IOCTL_ENTER_DIR / FS_IOCTL_EXIT_DIR | 目录导航 |
FS_IOCTL_MK_DIR | 创建文件夹 |
FS_IOCTL_FILE_SYNC | 文件同步落盘 |
FS_IOCTL_GET_PARTITION_INFO | 获取分区信息(簇大小、容量) |
FS_IOCTL_GET_FREE_SPACE | 获取剩余空间 |
FS_IOCTL_RESET_VFSCAN | 重置扫描状态 |
ioctl 的入参 cmd 直接透传给驱动操作表的 ioctl 函数,因此各文件系统可以在此基础上扩展私有命令,VFS 层无需为每个新能力新增接口。
核心流程
下图展示了从系统启动到一次完整文件读写操作的真实调用链:
sequenceDiagram
participant App as 应用
participant VFS as vfs.c 公共层
participant OPS as struct vfs_operations
participant FAT as fat/norfs/freefs 驱动
participant DEV as 存储设备
App->>VFS: vfs_init()
VFS->>OPS: 遍历 .vfs_operations 段
OPS-->>VFS: 逐个调用 init()
App->>VFS: vfs_mount(&pfs, dev, "fat")
VFS->>VFS: vfs_hdl_malloc() 分配 imount
VFS->>OPS: strcmp(fs_type, "fat") 匹配
OPS->>FAT: mount(&pfs, dev)
FAT->>DEV: 读取分区/目录表
FAT-->>VFS: 成功,绑定 pvfs->ops
App->>VFS: vfs_openbypath(pfs, &pf, "REC/A001.MP3")
VFS->>VFS: vfs_hdl_malloc() 分配文件句柄
VFS->>OPS: pfile->ops = pfs->ops(继承)
OPS->>FAT: openbypath(pfs, &pfile, path)
FAT-->>VFS: 打开成功
App->>VFS: vfs_read(pf, buf, len)
VFS->>OPS: ops->read(pfile, buf, len)
OPS->>FAT: read 数据
FAT->>DEV: 读取扇区/页
FAT-->>VFS: 返回实际读取长度
VFS-->>App: 字节数
App->>VFS: vfs_seek(pf, offset, SEEK_SET)
App->>VFS: vfs_file_close(&pf)
VFS->>OPS: ops->close_file(&pfile)
OPS->>FAT: 关闭并释放资源
流程关键点:
- 初始化阶段:
vfs_init()只做驱动级初始化(如注册设备、准备资源池),不挂载任何文件系统;挂载是应用按需发起的。 - 挂载阶段:
vfs_mount内部完成句柄分配 → 类型匹配 → 驱动挂载 → 绑定 ops 四步;类型不匹配或挂载失败都会继续尝试下一个驱动。 - 打开阶段:文件句柄的
ops从文件系统句柄复制,此后读写调用不再携带文件系统句柄,这是struct imount联合体设计的直接收益。 - 读写阶段:所有读写最终落到驱动实现,VFS 层不做数据缓冲(缓存策略由各驱动决定,如 FAT 的扇区缓存),保证抽象层开销极低。
- 关闭阶段:
vfs_file_close释放文件句柄;句柄资源不足时返回E_NO_VFS,驱动未实现时返回E_VFS_OPS。
使用示例
示例 1:挂载并遍历驱动
应用可通过 vfs_type_name 查询已挂载文件系统的类型名(vfs.c):
void *vfs_type_name(void *p_vfs)
{
if (NULL == p_vfs) {
return NULL;
}
log_info("p_vfs 0x%x", (u32)p_vfs);
struct imount *pvfs = p_vfs;
struct vfs_operations *ops = pvfs->ops;
log_info("ops 0x%x", (u32)ops);
if (NULL == ops) {
return NULL;
}
return (void *)ops->fs_type;
}
Source: vfs.c
示例 2:注册一个新文件系统驱动
驱动只需定义操作表并用段注册宏挂入系统,即可被 vfs_init / vfs_mount 自动发现:
const struct vfs_operations my_fs_ops SEC(.vfs_operations) = {
.fs_type = "myfs",
.init = my_fs_init,
.mount = my_fs_mount,
.openbypath = my_fs_open,
.read = my_fs_read,
.write = my_fs_write,
.close_file = my_fs_close,
};
示例 3:读取文件大小与删除文件
u32 fsize = 0;
vfs_get_fsize(pfile, &fsize); // 经 ops->flen 分发
u32 err = vfs_file_delete(pfile); // 经 ops->fdelete 分发,删除失败返回 E_FS_PFILE
API 参考
公共 API 声明见 vfs.h,实现位于 vfs.c 与 vfs_fat.c。
| 函数 | 签名 | 说明 | 典型返回 |
|---|---|---|---|
vfs_init | void vfs_init(void) | 遍历 .vfs_operations 段调用所有驱动的 init | — |
vfs_mount | u32 vfs_mount(void **ppvfs, void *device, void *type) | 分配句柄、按 fs_type 匹配驱动并挂载;type=NULL 时跳过 norfs/freefs 优先尝试 FAT | 0 成功;E_NO_VFS 句柄不足;E_NO_FS 无可用文件系统 |
vfs_openbypath | u32 vfs_openbypath(void *pvfs, void **ppvfile, const char *path) | 按路径打开文件,句柄继承 pfs 的 ops | 0 成功;E_VFS_HDL 文件系统句柄为空;E_VFS_OPS 未实现 |
vfs_openbyindex | u32 vfs_openbyindex(void *pvfs, void **ppvfile, u32 index) | 按文件序号打开(配合 FS_IOCTL_GET_FILE_NUM 使用) | 同上 |
vfs_openbyfile | u32 vfs_openbyfile(void *pcvfile, void **ppvfile, void *ext_name) | 按扩展名/类型匹配打开(歌词等关联文件) | 同上 |
vfs_createfile | u32 vfs_createfile(void *pvfs, void **ppvfile, u32 *pindex) | 新建文件并返回索引 | 同上 |
vfs_read | u32 vfs_read(void *pvfile, void *buf, u32 len) | 读文件,经 ops->read 分发 | 实际读取字节数 |
vfs_write | u32 vfs_write(void *pvfile, void *buf, u32 len) | 写文件,经 ops->write 分发 | 实际写入字节数 |
vfs_seek | u32 vfs_seek(void *pvfile, u32 offset, u32 mode) | 定位,mode 取 SEEK_SET/SEEK_CUR/SEEK_END | 0 成功 |
vfs_file_close | u32 vfs_file_close(void **ppvfile) | 关闭文件并释放句柄 | 0 成功 |
vfs_fs_close | u32 vfs_fs_close(void **ppvfs) | 卸载文件系统 | 0 成功 |
vfs_file_name | u32 vfs_file_name(void *pvfile, void *name, u32 len) | 取文件名 | 长度 |
vfs_get_attrs | int vfs_get_attrs(void *pvfile, void *pvfs_attr) | 取 struct vfs_attr(attr/fsize/sclust) | 0 成功 |
vfs_get_fsize | int vfs_get_fsize(void *pvfile, void *parm) | 取文件大小 | 大小;句柄为空返回 0 |
vfs_ftell | int vfs_ftell(void *pvfile, void *parm) | 取当前读写位置 | 偏移;句柄为空返回 0 |
vfs_file_delete | u32 vfs_file_delete(void *pvfile) | 删除文件(关闭由上层负责) | 0 成功;E_FS_PFILE 删除失败 |
vfs_ioctl | int vfs_ioctl(void *pvfile, int cmd, int arg) | 透传 FS_IOCTL_* 命令到驱动 | 驱动定义 |
vfs_delete_dir | int vfs_delete_dir(void *pvfs, char *path) | 删除目录 | 0 成功 |
vfs_file_crc | int vfs_file_crc(void *pvfile) | 计算文件 CRC | 校验值 |
vfs_type_name | void *vfs_type_name(void *p_vfs) | 返回已挂载文件系统的 fs_type 字符串 | 类型名;无效句柄返回 NULL |
错误码(errno-base.h 定义):
| 错误码 | 含义 | 触发场景 |
|---|---|---|
E_NO_VFS | VFS 句柄资源不足 | vfs_hdl_malloc() 返回 NULL |
E_NO_FS | 无可用文件系统 | 所有驱动 mount 均失败 |
E_VFS_HDL | 无效文件系统句柄 | vfs_openbypath 传入 NULL pfs |
E_VFS_OPS | 驱动未实现该操作 | 对应 ops 函数指针为 NULL |
E_FS_PFILE | 文件操作失败 | 删除/读写时 pfile 无效 |
失败模式与边界情况
- 挂载失败回滚:
vfs_mount中单个驱动挂载失败会调用其close_fs;全部失败后释放句柄返回E_NO_FS(vfs.c)。上层应据此决定是重试还是切换存储介质。 - 打开失败自动回收:
vfs_openbypath在错误路径调用vfs_file_close释放句柄,防止文件描述符泄漏(vfs.c)。 - 未实现操作:驱动可以只实现部分函数,VFS 层统一返回
E_VFS_OPS。新增能力(如格式化)不会破坏老驱动。 - 删除与关闭分离:
vfs_file_delete只删数据不关句柄(注释明确"SH系列文件关闭在外面应用"),若上层先关闭再删除,驱动内部会处理 pfile 为空的场景。 - 并发与重入:VFS 层本身无锁,句柄分配与释放依赖
vfs_resource的资源池;FAT 驱动内部的缓存/目录表访问互斥由驱动实现保证。扫描接口提供callback中断钩子,避免长扫描阻塞中断上下文(vfs_fat.c)。 - 长文件名内存:LFN/LDN 缓冲需通过
FS_IOCTL_SET_LFN_BUF/FS_IOCTL_SET_LDN_BUF(各 512B)显式配置,扫描长文件名目录前必须设置,否则可能截断(vfs.h)。
性能与运维
- 零拷贝抽象:VFS 层只是函数指针分发,读写数据直接传入驱动,无中间缓冲,适合音频流等大数据量场景。
- 段注册零 RAM 开销:操作表保存在 Flash 的
.vfs_operations段,运行时仅遍历指针,启动初始化耗时与驱动数量线性相关且极小。 - 缓存管理:
FS_IOCTL_FREE_CACHE可在低内存场景(如录音开始前)主动释放 FAT 缓存;FS_IOCTL_FILE_SYNC用于关键数据落盘。 - 句柄资源:
vfs_hdl_malloc/vfs_fhdl_free(vfs.h)基于静态资源池,句柄数量有限,应用应配对使用 open/close,防止E_NO_VFS。
扩展点
- 新增文件系统:实现
struct vfs_operations并用REGISTER_VFS_OPERATIONS注册即可,fs_type需唯一;挂载时通过type参数选择。 - 私有 ioctl:在
FS_IOCTL_*基础上,驱动可扩展自定义 cmd 编号,应用经vfs_ioctl透传。 - 扫描策略:通过
__fscan_arg_handler支持的-t/-r/-d/-a/-s选项组合,可实现按类型、递归、属性、排序等定制扫描。 - 格式化:驱动实现
format函数指针后,应用可对设备执行格式化(指定簇大小与是否新建)。