本文是参加2025春夏季开源操作系统训练营时对第二阶段文档第6章做的笔记。
文档:https://learningos.cn/rCore-Camp-Guide-2025S
完整版文档:https://rcore-os.cn/rCore-Tutorial-Book-v3
本章将实现一个简单的文件系统 – easyfs,能够对持久存储设备和I/O资源进行管理,持久存储设备即计算机断电后数据不会消失,但是速度比较慢,容量比较大的设备。
文件可代表很多种不同类型的I/O资源,在用户的角度,文件就是一些字节序列,可能是常规文件、目录,也可能标准输出、标准输入、管道等I/O资源的抽象。目录也可以看作一种文件,它保存着一些目录项(directory entry),可以看作映射,目录和文件可以一起看做一棵多叉树(目录树),目录树中的每个目录和文件都可以用它的绝对路径来进行索引和定位。
常规文件和目录都是实际保存在持久存储设备中的。持久存储设备仅支持以扇区(或块)为单位的随机读写,文件系统负责中间转换,能够将同一个逻辑上目录树结构转化为一个不同的持久存储设备上的扇区布局。之后,在一台计算机中,可能同时有多个持久存储设备,可能是不同文件系统格式存储的,一般操作系统内核中会有一层VFS(虚拟文件系统),规定了逻辑上目录树结构的通用格式及相关操作的抽象接口,便于统一管理。
rCore的实现中对目录树结构做了很多简化:
接下来介绍一些系统调用:
open
系统调用打开一个文件,支持一些标志位,并得到它的文件描述符。1 | bitflags! { |
pub fd_table: Vec<Option<Arc<dyn File + Send + Sync>>>
保存在TCB(正常应该是PCB,rCore里还没有改名)中,dyn
关键字是支持运行时多态的一种关键字,运行时多态需要在运行时查一种类似于 C++ 中的虚表。close
系统调用关闭一个文件,并释放它的文件描述符。read/write
调用,可以对文件进行顺序读写。文件系统 easy-fs 被从内核中分离出来,分成两个不同的 crate :
easy-fs
是简易文件系统的本体;easy-fs-fuse
是能在开发环境中运行的应用程序,用于将应用打包为 easy-fs 格式的文件系统镜像,也可以用来对 easy-fs
进行测试。块设备接口层(BlockDevice)定义了设备驱动需要实现的块读写接口,实际上这是需要由文件系统的使用者(比如操作系统内核或直接测试 easy-fs
文件系统的 easy-fs-fuse
应用程序)提供并接入到 easy-fs
库的。其中一个块和一个扇区同为 512 字节。
块缓存层实际上就是把内存作为磁盘的缓存,在读写的情况比较多时,可以将缓冲区统一管理起来。要读写一个块的时候,首先去全局管理器中查看这个块是否已被缓存到内存缓冲区中,然后类似其它场景中(比如CPU中)的缓存操作。块缓存 BlockChche
中保存了一个字节数组,对应的磁盘中的块的编号,底层块设备的引用,和 modified
位。
1 | pub struct BlockCache { |
然后,在块缓存全局管理器 BlockCacheManager
中使用队列维护一些块缓存。
磁盘数据结构层的代码在 layout.rs
和 bitmap.rs
中,即一块磁盘中存储的数据的数据结构,磁盘布局按照块编号从小到大顺序地分成 5 个不同属性的连续区域:
索引节点位图和数据块位图的结构是一样的,每个位图都由若干个块组成,每个块大小为 512 bytes,即 4096 bits。每个 bit 都代表一个索引节点/数据块的分配状态,位图所要做的就是进行索引节点/数据块的分配和回收。分配和回收的思路就是遍历整个区域,对应的函数中使用了一些闭包语法,闭包是一种可以捕获其环境中的变量的匿名函数,参数和返回类型可以自动推导。
1 | pub fn modify<T, V>(&mut self, offset: usize, f: impl FnOnce(&mut T) -> V) -> V { |
这个闭包的参数就是由 f(self.get_mut(offset))
这行代码自动传入的,闭包不是自己“找”参数的,是调用者 modify
送进去的参数。可以把闭包理解成一种更强大的函数指针。
索引节点 DiskInode
如下,每个文件/目录在磁盘上均以一个 DiskInode
的形式存储。其中的 indirect1 和 indirect2 是为文件较大的情况下准备的。
1 |
|
每个保存内容的数据块都只是一个字节数组,目录项有特殊的格式:
1 |
|
上一节介绍了磁盘布局和数据的组织方式,将它们整合起来就是文件系统的职责,可以看做一个磁盘块管理器。这一节介绍的内容是放在内存中的。
1 | // easy-fs/src/efs.rs |
EasyFileSystem 可以通过将块设备编号为 0 的块作为超级块读取进来,就可以知道 easy-fs 的磁盘布局,由此可以构造 efs
实例。
EasyFileSystem 可以管理磁盘的布局,但是作为文件系统的使用者,我们是不关心布局的,我们只想看到逻辑上的目录和文件,因此设计了索引节点 Inode
暴露给文件系统的使用者,直接对文件和目录进行操作。这里的 Inode
是存放在内存中的,上一节的 DiskInode
放在磁盘块中。
1 | // easy-fs/src/vfs.rs |
block_id
和 block_offset
记录该 Inode
对应的 DiskInode
保存在磁盘上的具体位置;fs
是指向 EasyFileSystem
的一个指针,对 Inode
的种种操作实际上都通过它来完成。
目前 EasyFileSystem 仅支持绝对路径,对于任何文件/目录的索引都必须从根目录开始向下逐级进行。要获取根目录,只要读取 id 为 0 的块即可。因为根目录对应于文件系统中第一个分配的 inode ,因此它的 inode_id
总会是 0 。之后,文件的索引、创建、读写就都很简单了,调用块缓存层的函数,修改对应的块即可。
在实现了文件系统之后,可以将应用打包到 easy-fs 镜像中放到磁盘中,当我们要执行应用的时候只需从文件系统中取出ELF 加载到内存中执行即可,这样就避免了内核体积过度膨胀和浪费内存资源,easy-fs-fuse
的主体实现了这个功能。
之前已经介绍了 easy-fs
文件系统内部的层次划分。要将 easy-fs
接入到内核中,还需要再分几层抽象:
easy-fs
所需的 BlockDevice
Trait 。easy-fs
层:它接受一个块设备 BlockDevice
,并可以在上面打开文件系统,进而获取 Inode
核心数据结构,进行各种文件系统操作。easy-fs
提供的 Inode
进一步封装成 OSInode
,以表示进程中一个打开的常规文件。OSInode
是文件的内核内部表示,因此需要为它实现 File
Trait 从而将它放入到进程文件描述符表中并通过 sys_read/write
系统调用进行读写。rCore主要运行在 qemu 上,在 qemu 上,使用 VirtIOBlock
访问 VirtIO 块设备,并将它全局实例化为 BLOCK_DEVICE
,使内核的其他模块可以访问。
1 | // os/src/drivers/block/mod.rs |
在启动qemu时,还需要添加一些参数:
1 | -drive file=$(FS_IMG),if=none,format=raw,id=x0 \ |
这里为虚拟机添加一块虚拟硬盘,内容为之前通过 easy-fs-fuse
工具打包的包含应用 ELF 的 easy-fs 镜像,并命名为 x0
。然后将硬盘 x0
作为一个 VirtIO 总线中的一个块设备接入到虚拟机系统中。virtio-mmio-bus.0
表示 VirtIO 总线通过 MMIO 进行控制,且该块设备在总线中的编号为 0 。之后在创建内核地址空间时添加对硬盘的映射。
OS 中的索引节点 OSInode
只是对 Inode
的简单包装,表示进程中一个被打开的常规文件或目录,readable/writable
分别表明该文件是否允许通过 sys_read/write
进行读写。