上一章我们写了一个 SimpleFS,可以创建文件、读写内容。但现在有个问题:open() 系统调用直接调的是 fs_create、fs_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——实现一个最简命令行,让用户能敲命令操作文件,把前十二章的成果串起来。