第四章:注册与挂载机制
好,咱们今天聊聊文件系统最核心的环节——注册与挂载。说白了,就是让NuttX认识你的文件系统,然后把它挂到某个目录下,让应用程序能读写文件。
我在做嵌入式项目时,经常遇到有人问:「为什么我实现了文件系统接口,但mount()就是失败?」嗯,多半是注册这步没走对。咱们一步步拆解。
4.1 文件系统类型表
NuttX内核里维护着一张全局表,叫mountpt_operations。这张表记录了所有已注册的文件系统类型。每个类型对应一个struct inode_operations_s结构体。
先看看这个结构体长什么样:
struct inode_operations_s
{
int (*create)(FAR struct inode *parent, FAR const char *name,
mode_t mode, FAR struct inode **result);
int (*mkdir)(FAR struct inode *parent, FAR const char *name,
mode_t mode);
int (*rmdir)(FAR struct inode *parent, FAR const char *name);
int (*rename)(FAR struct inode *old_parent, FAR const char *old_name,
FAR struct inode *new_parent, FAR const char *new_name);
int (*unlink)(FAR struct inode *parent, FAR const char *name);
};
你想想看,每个文件系统都得实现这些操作。NuttX用inode来统一管理所有文件系统节点。不管是FAT、ROMFS还是你自研的,都走同一套接口。
核心要点:文件系统类型表本质上是一个链表,每个节点包含文件系统名称和对应的操作函数指针。注册就是往这个链表里加节点。
4.2 注册一个新文件系统
注册函数是register_filesystem()。我个人习惯在系统初始化阶段调用它。比如在board_initialize()里,或者专门的fs_initialize()中。
函数原型很简单:
int register_filesystem(FAR const char *name,
FAR const struct mountpt_operations *mops);
第一个参数是文件系统名字,比如"fatfs"、"romfs"、"procfs"。第二个参数就是你的操作函数集合。
举个例子,注册一个自定义的"myfs":
#include <nuttx/fs/fs.h>
static const struct mountpt_operations g_myfs_operations =
{
.open = myfs_open,
.close = myfs_close,
.read = myfs_read,
.write = myfs_write,
.ioctl = myfs_ioctl,
.sync = myfs_sync,
.dup = myfs_dup,
.opendir = myfs_opendir,
.closedir= myfs_closedir,
.readdir = myfs_readdir,
.rewinddir= myfs_rewinddir,
.bind = myfs_bind,
.unbind = myfs_unbind,
.stat = myfs_stat,
.statfs = myfs_statfs,
};
int myfs_initialize(void)
{
int ret = register_filesystem("myfs", &g_myfs_operations);
if (ret < 0)
{
ferr("ERROR: Failed to register myfs: %d\n", ret);
return ret;
}
return OK;
}
我的经验:注册时名字别太长,NuttX内部有长度限制。我曾经用了个32字节的名字,结果注册失败,查了半天才发现是名字截断了。建议控制在16字节以内。
4.3 mount() 系统调用流程
mount()是用户态调用的入口。咱们看看它内部是怎么一步步走到你的文件系统代码的。
调用链大致是这样的:
- 用户调用:
mount("/dev/sda1", "/mnt", "myfs", 0, NULL); - 系统调用入口:
sys_mount()做参数校验 - 查找文件系统类型:遍历类型表,找到名字匹配的"myfs"
- 调用bind():文件系统的bind函数被调用,完成底层设备绑定
- 创建inode节点:在挂载点"/mnt"下创建新的inode
- 挂载完成:返回成功
关键代码在mount()的实现里:
int mount(FAR const char *source, FAR const char *target,
FAR const char *filesystemtype, unsigned long mountflags,
FAR const void *data)
{
FAR struct inode *mountpt_inode;
FAR struct inode *blkdriver_inode;
int ret;
/* 1. 查找文件系统类型 */
FAR const struct mountpt_operations *mops;
ret = find_mountpt_operations(filesystemtype, &mops);
if (ret < 0)
{
return ret;
}
/* 2. 打开块设备(如果有) */
if (source)
{
ret = open_blockdriver(source, mountflags, &blkdriver_inode);
if (ret < 0)
{
return ret;
}
}
/* 3. 调用文件系统的bind函数 */
ret = mops->bind(blkdriver_inode, data, &mountpt_inode);
if (ret < 0)
{
close_blockdriver(blkdriver_inode);
return ret;
}
/* 4. 将inode插入挂载点 */
ret = insert_mountpoint(mountpt_inode, target);
if (ret < 0)
{
mops->unbind(mountpt_inode);
close_blockdriver(blkdriver_inode);
return ret;
}
return OK;
}
注意:bind()函数里要完成文件系统元数据的初始化。比如FATFS的bind会读取引导扇区、解析文件分配表。如果bind失败,mount会回滚所有操作,不会留下半残的挂载点。
4.4 挂载点的管理
NuttX用mountpoint结构体来管理每个挂载点。这个结构体记录了挂载路径、对应的inode、文件系统操作表等信息。
我记得有一次调试,发现同一个目录被挂载了两次,第二次挂载直接覆盖了第一次。这是因为NuttX允许重复挂载,但不会报错。所以写代码时最好检查一下目标路径是否已经被挂载。
/* 检查挂载点是否已存在 */
bool is_mountpoint(FAR const char *path)
{
FAR struct inode *node;
int ret = find_inode(path, &node);
if (ret == OK && INODE_IS_MOUNTPT(node))
{
return true;
}
return false;
}
4.5 实战:注册一个伪文件系统
咱们来个实战。假设你要实现一个简单的"memfs",所有文件都存在内存里。注册流程如下:
/* memfs_register.c */
#include <nuttx/fs/fs.h>
#include <nuttx/kmalloc.h>
static int memfs_bind(FAR struct inode *blkdriver,
FAR const void *data,
FAR struct inode **result)
{
/* 分配文件系统私有数据 */
FAR struct memfs_state *state;
state = kmm_zalloc(sizeof(struct memfs_state));
if (!state)
return -ENOMEM;
/* 初始化根目录 */
state->root = memfs_create_dir(NULL, "/");
/* 创建inode */
FAR struct inode *node = inode_alloc();
node->i_private = state;
node->i_ops = &g_memfs_operations;
*result = node;
return OK;
}
static int memfs_unbind(FAR struct inode *mountpt)
{
FAR struct memfs_state *state = mountpt->i_private;
memfs_destroy_all(state->root);
kmm_free(state);
return OK;
}
static const struct mountpt_operations g_memfs_operations =
{
.bind = memfs_bind,
.unbind = memfs_unbind,
/* 其他操作... */
};
int memfs_initialize(void)
{
return register_filesystem("memfs", &g_memfs_operations);
}
避坑指南:我曾经在unbind里忘了释放私有数据,结果每次卸载memfs都泄漏内存。跑了一周后系统OOM了。记住:bind里分配的资源,unbind里一定要释放。
4.6 调试技巧
如果mount失败,别急着怀疑文件系统代码。先检查这几步:
- 注册了吗? 调用
mount前确认register_filesystem已经执行成功 - 名字匹配吗? 注册名和mount参数里的名字要完全一致,大小写敏感
- 设备存在吗? 如果是块设备文件系统,确保设备节点已经创建
- 权限对吗? 有些文件系统需要特定的挂载标志
NuttX提供了mount命令,可以在shell里手动测试:
nsh> mount -t myfs /dev/sda1 /mnt
nsh> ls /mnt
nsh> df
如果mount成功,df应该能看到你的文件系统信息。
4.7 小结
注册和挂载是文件系统移植的第一步,也是最容易出问题的一步。记住三个关键点:
- 注册就是往类型表里加记录,挂载就是调用bind创建实例
- bind/unbind要成对出现,资源管理要对称
- 调试时从注册开始排查,别跳过基础检查
下一章咱们聊聊文件读写操作的具体实现。到时候我会分享一个我踩过的坑——关于缓存一致性的问题,挺有意思的。
核心公式: 注册 + 挂载 = 文件系统可用。缺一步都不行。
公众号:蓝海资料掘金营,微信deep3321