上一章我们写了一个 SimpleFS,可以创建文件、读写内容。但现在有个问题:open() 系统调用直接调的是 fs_createfs_read 这些 SimpleFS 专属函数。

如果哪天要支持 FAT32,就得去改系统调用的代码。这显然不对。

这一章加一层 VFS(Virtual File System,虚拟文件系统),把系统调用和具体文件系统隔开。


先把概念搞清楚

间接层解耦

软件里有句老话:任何问题都可以通过加一层间接层解决。VFS 就是这层间接层。

用户进程
   ↓  open / read / write
  VFS(统一接口)
   ↓              ↓
SimpleFS         FAT32

系统调用只跟 VFS 说话,VFS 再转发给具体 FS。新增一种文件系统,只需要实现 VFS 要求的那几个函数,不动系统调用层。

file_operations:C 语言的多态

VFS 要求每种文件系统提供一张函数指针表

typedef struct {
    int (*read) (...);
    int (*write)(...);
    uint32_t (*lookup)(...);
    uint32_t (*create)(...);
} FileOps;

VFS 调 fops->read(...) 时,实际执行的是哪个函数,取决于挂载时注册的是哪张表。这是 C 语言实现"多态"的标准手法,Linux 内核里到处都是这个模式。

vnode:VFS 的通用 inode

具体 FS 有自己的 inode,VFS 层包一层 vnode,里面放着具体 FS 的 inode 号和对应的函数指针表。对上层完全屏蔽了底层差异。

文件描述符(fd)

进程操作文件用的是 fd(一个小整数,比如 3、4、5)。从 fd 到数据的完整链路:

fd(整数)
 → 进程文件表
 → VFile(打开文件上下文:vnode + 当前偏移 offset)
 → vnode(inum + fops)
 → 具体 FS 函数

offset 存在 VFile 里,不在 vnode 里。这样同一个文件被 open 两次,两个 fd 的读写位置互相独立。


设计

三个结构体,各司其职:

结构体 职责
FileOps 函数指针表,每种 FS 实现一份
VNode VFS 层的通用 inode,挂 inum 和 fops
VFile 每次 open 分配一个,存 vnode 指针和当前 offset

对外接口:vfs_init / vfs_open / vfs_read / vfs_write / vfs_close


核心实现

注册文件系统

挂载时把 SimpleFS 的函数填进 FileOps 表,传给 vfs_init

static FileOps simplefs_ops = {
    .read   = fs_read,
    .write  = fs_write,
    .lookup = dir_lookup,
    .create = fs_create,
};

vfs_init(root_inum, &simplefs_ops);

以后要支持 FAT32,就再写一张 fat32_ops 表,vfs_init 的调用方式完全一样。

open:路径 → fd

int vfs_open(const char *path) {
    const char *name = (*path == '/') ? path + 1 : path;

    uint32_t inum = vfs_fops->lookup(vfs_root_inum, name);
    if (!inum)
        inum = vfs_fops->create(vfs_root_inum, name);  // 不存在就创建
    if (!inum) return -1;

    VNode *vn = alloc_vnode(inum);
    return alloc_fd(vn);   // 返回 fd 整数
}

read/write:fd → offset 自动推进

int vfs_read(int fd, void *buf, uint32_t len) {
    VFile *f = &fd_table[fd];
    int n = f->vnode->fops->read(f->vnode->inum, f->offset, buf, len);
    if (n > 0) f->offset += n;   // 自动推进偏移
    return n;
}

write 同理。调用方不需要手动管理偏移,这就是 fd 抽象的价值之一。


用起来

int fd = vfs_open("/hello.txt");
vfs_write(fd, "Hello VFS!\n", 11);
vfs_close(fd);

fd = vfs_open("/hello.txt");
char buf[64] = {0};
vfs_read(fd, buf, 63);
vfs_close(fd);

kprintf("read: %s", buf);
// 输出: read: Hello VFS!

调用方只看到 vfs_open / vfs_read / vfs_write,完全不知道底层是 SimpleFS。


这里省掉了什么

功能 真实做法
多级路径解析 / 分割,逐级 lookup
挂载点(mount) 某个目录可以挂载另一个 FS 的根
文件权限检查 open 时检查 mode + uid/gid
page cache read 先查内存缓存,miss 才读磁盘
vnode 引用计数 多个 fd 引用同一 vnode 时不能提前释放

Linux 的 VFS 层(struct file / struct inode / struct file_operations)和这里的结构一一对应,只是每个字段复杂得多。


下一章

有了 VFS,open/read/write 已经是干净的通用接口了。下一章做 Shell——实现一个最简命令行,让用户能敲命令操作文件,把前十二章的成果串起来。

源码:github.com/tongpengfei/learn-with-ai