第四章:注册与挂载机制

好,咱们今天聊聊文件系统最核心的环节——注册与挂载。说白了,就是让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()是用户态调用的入口。咱们看看它内部是怎么一步步走到你的文件系统代码的。

调用链大致是这样的:

  1. 用户调用mount("/dev/sda1", "/mnt", "myfs", 0, NULL);
  2. 系统调用入口sys_mount() 做参数校验
  3. 查找文件系统类型:遍历类型表,找到名字匹配的"myfs"
  4. 调用bind():文件系统的bind函数被调用,完成底层设备绑定
  5. 创建inode节点:在挂载点"/mnt"下创建新的inode
  6. 挂载完成:返回成功

关键代码在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 小结

注册和挂载是文件系统移植的第一步,也是最容易出问题的一步。记住三个关键点:

  1. 注册就是往类型表里加记录,挂载就是调用bind创建实例
  2. bind/unbind要成对出现,资源管理要对称
  3. 调试时从注册开始排查,别跳过基础检查

下一章咱们聊聊文件读写操作的具体实现。到时候我会分享一个我踩过的坑——关于缓存一致性的问题,挺有意思的。

核心公式: 注册 + 挂载 = 文件系统可用。缺一步都不行。


公众号:蓝海资料掘金营,微信deep3321