XV6 文件系统主要涉及文件数据的组织形式和访问方法,只在底层数据盘块的读写操作上才与具体设备有交互,因此是相对独立的一个系统。
对比 Linux 上的文件系统,由于 Linux 的 VFS 使用了页缓存和文件映射页,因此还会和虚存管理紧密相连。XV6 与虚存无关,因此相对要简单的多。
无论是 XV6 还是 Linux 上的文件系统,都将设备、管道当作文件来统一处理,体现万物皆文件的概念。
XV6 文件系统和其他 Unix 类的文件系统相似,例如熟悉的 Linux EXT2 文件系统。学习过程中有需要注意两个领域的知识:
XV6 文件系统的设计是层次性的,分为 7 层,分别实现不同的抽象
层 | 抽象接口 |
---|---|
6 | 文件描述符 |
5 | 路径名 |
4 | 目录 |
3 | 节点 |
2 | 日志 |
1 | 缓存块 |
0 | 磁盘 |
讨论和学习 XV6 的文件概念时,首先将文件分成两个层面,一个是用户进程的文件逻辑抽象,另一个是文件在磁盘上的物理形态。
用户进程体验到的文件逻辑结构是一个线性可寻址的字节集合,只要给出具体的偏移量就可以访问到文件的任意内容。作为对比,数据库文件系统的逻辑结构则可能是可查询的记录集合,而不是无格式的字节顺序集合。
其次文件还有很多属性,例如名字、访问权限、创建修改日期等等。而所有的文件构成一个磁盘文件系统,而且这些文件组织成层次性的树形目录结构。
首先来看逻辑结构。XV6 使用 file 结构体来描述这个逻辑文件,因此其成员 off
用于指出当前读写的字节偏移位置。
磁盘是块设备,每次读写最小单位是块。如果文件逻辑结构的长度超过一个块的大小则需要用多个盘块来存储。如果一个文件的所有盘块是连续地存放在磁盘上的,那么将会有更快的访问速度,但是也会引起严重的 “零头” 浪费,这是连续分配方法的固有问题。因此大多数文件系统都是不排斥盘块的连续存放,但从机制上允许离散的分布。
上述机制需要一个映射(索引)表,同时文件的逻辑结构也切割为逻辑盘块(与物理盘块大小相同)。这时读写位置的偏移量从字节偏移,变为逻辑盘块号和块内偏移量。在划分逻辑盘块时,文件结尾处的数据可能会不足一个盘块大小。
文件中的每一个逻辑盘块,通过一个索引表将映射到一个物理盘块上,这个索引表构成了该文件的索引节点的重要内容。下面我们来讨论索引节点的概念。
将文件的多个逻辑盘块映射到离散的物理盘块上需要一些索引信息,XV6 中将文件的这种索引信息称为索引节点,使用 dinode 结构体来记录,其中 addrs[]
就是索引。这个索引就是操作系统原理课程中的提到的文件索引 (混合索引方式),XV6 的索引包括直接索引和一个间接索引,在后面 bmap() 函数中还将具体讨论这两种索引。另外为了加快索引节点的访问,XV6 还定义了内存中的索引节点缓存 inode 结构体,其主要内容来源于 dinode
。
索引节点不是文件数据本身,而属于管理性的信息。索引节点的信息和文件数据本身都存放在磁盘上,因此磁盘中的盘块就分成两种用途:
这就引出磁盘布局的问题,XV6 将磁盘的盘块按一定方式划分,一部分用于文件系统的管理信息,另一部分用于保存文件自身的数据,形成如下表的布局。
boot | super | inodes | bit map | data | log |
---|---|---|---|---|---|
1 块 | 1 块 | n 块 | n 块 | n 块 | n 块 |
XV6 文件系统的超级块给出了磁盘布局信息,超级块由 superblock 结构体描述。其中 size
是文件系统的盘块总数,nblocks
是用于文件数据的盘块数量,ninodes
是索引节点总数,nlog
是日志区的盘块总数, logstart
是日志区起始盘块号,inodestart
是索引节点区的起始盘块号,bmapstart
是位图区的起始盘块号。
对照上表的 XV6 文件系统布局,可以知道 superblock
各成员和图中的对应关系。 可以看出 XV6 磁盘文件系统的前面都是管理性的信息,后面是数据区和日志区,具体布局如下:
ninodes/BSIZE
(BSIZE=512
,是盘块大小)项,每一项(内含对应文件的索引)可以管理一个文件的物理盘块,因此整个文件系统最多有 ninodes/BSIZE
个文件。nlog
个盘块。目录不是必要的,如果我们对文件进行编号使用(不使用文件名和目录路径),则上面索引节点给出的功能就足够了:给出待读写的文件索引节点编号,我们读入索引节点内容,然后根据其索引就可以读写磁盘盘块。不过这样使用并不方便,我们更习惯的是使用层次性路径名的、树形的目录结构。
为了记录目录的层次结构,每一个目录都必须记录自己所管理的文件和子目录。目录本身也是文件,也就是说 /
目录的目录项是保存在不同的索引节点的磁盘文件上的。
进入 XV6 后,执行 ls
指令,可以看到根目录下的所有文件,前几项如下:
. 1 1 512
.. 1 1 512
README.md 2 2 59
cat 2 3 13616
echo 2 4 12628
第一列是文件名字,第二列是文件类型,第三列是索引节点,第四列是文件大小(单位:字节)。
执行 mkdir mydir
指令,可生成一个目录 mydir
,执行 ls mydir
查看具体信息如下:
$ ls mydir
. 1 19 32
.. 1 1 512
整个过程呈现出一个递推过程,归结为以下几个步骤:
经过多次递推的过程,就可以遍历目录树上的任何一个节点。这就需要一个起点条件:/
根目录的索引节点号是固定为 1,因此无需依赖上级目录而直接获得。 这里再次提醒读者,从原理上说即使没有目录,也可以正常对文件内容进行读写,前提是知道该文件的索引节点号。
进程使用文件并不使用 file
结构体或 dinode
结构体,而是提供文件的路径名来打开文件并获得一个文件描述符,后续将使用文件描述符来指代这个打开的文件。XV6 中的每个进程都有一个文件描述符表 ofile[],每个文件描述符直接指向一个 file
结构体(系统管理的已打开的文件)。
有了前面的基本概念,我们来继续深入了解索引节点和目录项的细节,并将两者整合联系起来。
索引节点代表文件自身,但是索引节点并不包含文件名(文件名是通过目录项给出的)。索引节点 由 dinode 结构体描述,它给出了文件的管理信息(元数据 meta data)。当索引节点代表磁盘文件时,dinode
将给出本文件的物理结构, 文件的盘块按照混合索引方式组织,类似于 Linux 的 EXT 文件系统上的 inode
。由 dinode
的 addrs[NDIRECT+1] 成员指出文件磁盘盘块所在位置,其中 0~NDIRECT-1
项都是直接块,最后一项是间接索引块。XV6 的磁盘文件的大小由 size 成员记录。
由于 dinode
有可能代表的设备或管道,因此需要 type
成员来区分普通磁盘文件、管道和设备。T_DIR 表示目录、T_FILE
表示磁盘文件、T_DEV
代表设备。如果 inode
类型代表设备,则还需要用 major
和 minor
来区分设备号。
由于 XV6 的文件系统支持硬链接,因此还需要有 nlink
来记录有多少个目录项指向该 inode
节点。
与 Unix 类文件系统相似,XV6 文件系统中也将目录当作文件,只是其中的内容被解释成目录项。 每个目录项将联系文件名和索引节点。目录项使用 dirent 结构体来表示,只有两个成员:name[DIRSIZ]
用于记录文件名(DIRSIZ=14
字节),inum
用于对应文件的索引节点。根目录 /
的索引号是固定的,ROOTINO 设置为 1,也就是说根目录是不需要经过目录项解释的。
fs.h
中主要定义了超级块 superblock
、索引节点 dinode
和目录项 dirent
,它们在前面刚讨 论过。其次是几个文件系统中的常量:
IPB
(inode per block
)是每个盘块上可以记录的 inode
数量。
IBLOCK(i,sb)
用来计算第 i
个 inode
位于哪个盘块。
BPB
(bitmap bits per block
)是一个盘块的位数。
BBLOCK(b,sb)
用于计算第 b
个数据盘块所对应的位图 bit 所在的盘块号。
DIRSIZ
(directory size
)是目录项中文件名字符串的大小。
fcntl.h
中定义了打开文件时的访问模式(不是文件自身的可访问权限),包括读模式、写模式、读写模式和创建模式。