导引:点亮什么与为什么
002 让内核能「列出」initrd 里的文件了,但这份能力是死的:你只能打印文件名,没法「打开
hello.txt、读它的内容」,更没法对不同的来源(现在是 ramdisk,以后会有磁盘文件系统)用同一套办法操作。这一章,我们在「认得文件」之上搭一层虚拟文件系统(VFS):它定义一个统一的「文件对象」(inode)、一套统一的操作接口、一张「路径 → 文件系统」的挂载表,然后让 ramdisk 实现这套接口挂上来。完成后,内核拿一个路径,就能解析出对应的文件对象、读出内容——不管它背后是 ramdisk 还是别的东西。注意:这一章是内核内部的管线,用户态还碰不到它(那要等 004 接上系统调用)。003 内容多,拆成两篇,这是第一篇。
这一章我们要点亮什么
内核第一次有了「VFS」这个词背后那套结构。具体四块:
第一,一个文件对象的抽象——Inode。它代表「一个文件」(或目录),带着编号、大小、类型,以及一组「怎么操作我」的函数指针。
第二,一套文件系统后端要遵守的接口——FileSystem。任何一个想被 VFS 管理的文件系统(ramdisk、将来的 ext2),都得实现「挂载」和「按名查找」两个方法。
第三,一张挂载表——把「路径前缀」映射到「哪个文件系统」。内核拿到一个绝对路径,靠这张表找到该交给哪个后端处理。
第四,一个**「打开的文件」描述和文件描述符表**——File 记录「这个打开的文件,读写到哪个偏移了」,FDTable 管理一个进程手里的所有打开文件(占位,fd 编号就是它的下标)。
验收点是内核启动时打出的:[VFS] Ramdisk mounted at /——ramdisk 被挂到了根路径 /,之后内核就能通过 VFS 按名找到 initrd 里的文件、读出内容。
为什么现在需要它
为什么紧跟在 002 之后。002 的 ramdisk 只会「解析归档、打印清单」,它的 mount 返回个文件数就完事了,文件内容谁都拿不到。问题出在缺一层抽象:没有「按名字取文件」的接口,没有「把名字变成可读对象」的机制。要往「能加载并运行磁盘上的程序」走,内核必须能 lookup("hello.txt") 拿到一个能读的对象。这一章就是把这层抽象立起来。
为什么要做成「虚拟」文件系统,而不是让 ramdisk 直接提供读写?因为文件来源会变。今天只有 ramdisk(只读、内存里),005 会来 ext2(磁盘上、能读写)。如果上层(系统调用、shell)直接调 ramdisk 的函数,等 ext2 来了就得改两遍。VFS 的价值就在这:它定义一套与后端无关的接口(inode + 操作表),上层只跟 VFS 打交道,后端各自实现这套接口。换个文件系统,上层一行不改。这就是 Linux 也用的那套思路——我们这一章是它的最小形态。
还有一笔设计上的账,关于「怎么实现多态」。一个文件系统后端要提供「读、写、列目录」等操作,而每个文件对象(inode)都得能调到这些操作。最直觉的 C++ 做法是给 inode 加虚函数,但那是每个 inode 一个虚表指针——文件一多,这笔开销不小。这一章用了个更省的手法:把操作做成一个函数指针表(InodeOps),同一类 inode 共用一张静态表,inode 里只存一个指向表的指针。这避免了每对象虚表,代价是函数指针表要手工维护(下面专门讲)。而文件系统这一层(只有 ramdisk、ext2 这么几个后端)反而用了正经的 C++ 虚函数——后端数量少,虚表开销无所谓。这种「对象多用函数指针表、类型少用虚函数」的混搭,是这一章一个值得记住的设计取舍。
作者事后吐槽:是的,这个「值得记住的设计取舍」后来被作者自己推翻了。等到 006 那一串做 ext2 的写入/创建时,操作从三个(read/write/readdir)涨到一长串(create、mkdir、unlink、stat…),
InodeOps索性直接长成了带virtual的类——函数指针表当初省下来的那点内存,在「方法越来越多、还要继承要重写」面前根本不值一提。所以别把这里的取舍当成永恒真理:它只是 003 这个阶段、就这三个方法时的合理选择。这一章我们照 003 的原貌讲(它就是函数指针表),但权当个预告——抽象会演化,别过早把它焊死。