XV6

XV6 文件系统主要涉及文件数据的组织形式和访问方法,只在底层数据盘块的读写操作上才与具体设备有交互,因此是相对独立的一个系统。

对比 Linux 上的文件系统,由于 Linux 的 VFS 使用了页缓存和文件映射页,因此还会和虚存管理紧密相连。XV6 与虚存无关,因此相对要简单的多。

无论是 XV6 还是 Linux 上的文件系统,都将设备、管道当作文件来统一处理,体现万物皆文件的概念。

XV6 文件系统和其他 Unix 类的文件系统相似,例如熟悉的 Linux EXT2 文件系统。学习过程中有需要注意两个领域的知识:

  1. 一个是磁盘上的文件系统格式及其细节。涉及各种超级块、索引节点、目录项等数据结构,偏向于静态概念。
  2. 另一个是用户进程通过系统调用访问这些数据的过程及其所使用的代码。涉及文件请求接口、文件相关的系统调用和文件读写的实现操作,偏向于动态概念。

XV6 文件系统的设计是层次性的,分为 7 层,分别实现不同的抽象

抽象接口
6 文件描述符
5 路径名
4 目录
3 节点
2 日志
1 缓存块
0 磁盘

1. 概述

讨论和学习 XV6 的文件概念时,首先将文件分成两个层面,一个是用户进程的文件逻辑抽象,另一个是文件在磁盘上的物理形态。

1.1 文件的逻辑结构

用户进程体验到的文件逻辑结构是一个线性可寻址的字节集合,只要给出具体的偏移量就可以访问到文件的任意内容。作为对比,数据库文件系统的逻辑结构则可能是可查询的记录集合,而不是无格式的字节顺序集合。

其次文件还有很多属性,例如名字、访问权限、创建修改日期等等。而所有的文件构成一个磁盘文件系统,而且这些文件组织成层次性的树形目录结构。

首先来看逻辑结构。XV6 使用 file 结构体来描述这个逻辑文件,因此其成员 off 用于指出当前读写的字节偏移位置。

1.2 文件的物理结构

磁盘是块设备,每次读写最小单位是块。如果文件逻辑结构的长度超过一个块的大小则需要用多个盘块来存储。如果一个文件的所有盘块是连续地存放在磁盘上的,那么将会有更快的访问速度,但是也会引起严重的 “零头” 浪费,这是连续分配方法的固有问题。因此大多数文件系统都是不排斥盘块的连续存放,但从机制上允许离散的分布。

上述机制需要一个映射(索引)表,同时文件的逻辑结构也切割为逻辑盘块(与物理盘块大小相同)。这时读写位置的偏移量从字节偏移,变为逻辑盘块号和块内偏移量。在划分逻辑盘块时,文件结尾处的数据可能会不足一个盘块大小。

文件中的每一个逻辑盘块,通过一个索引表将映射到一个物理盘块上,这个索引表构成了该文件的索引节点的重要内容。下面我们来讨论索引节点的概念。

1.3 索引节点

将文件的多个逻辑盘块映射到离散的物理盘块上需要一些索引信息,XV6 中将文件的这种索引信息称为索引节点,使用 dinode 结构体来记录,其中 addrs[] 就是索引。这个索引就是操作系统原理课程中的提到的文件索引 (混合索引方式),XV6 的索引包括直接索引和一个间接索引,在后面 bmap() 函数中还将具体讨论这两种索引。另外为了加快索引节点的访问,XV6 还定义了内存中的索引节点缓存 inode 结构体,其主要内容来源于 dinode

索引节点不是文件数据本身,而属于管理性的信息。索引节点的信息和文件数据本身都存放在磁盘上,因此磁盘中的盘块就分成两种用途:

  1. 存放文件数据本身(例如文本或图片)。
  2. 索引节点这样的管理数据(称为元数据 meta data)。

这就引出磁盘布局的问题,XV6 将磁盘的盘块按一定方式划分,一部分用于文件系统的管理信息,另一部分用于保存文件自身的数据,形成如下表的布局。

boot super inodes bit map data log
1 块 1 块 n 块 n 块 n 块 n 块

1.4 磁盘布局

XV6 文件系统的超级块给出了磁盘布局信息,超级块由 superblock 结构体描述。其中 size 是文件系统的盘块总数,nblocks 是用于文件数据的盘块数量,ninodes 是索引节点总数,nlog 是日志区的盘块总数, logstart 是日志区起始盘块号,inodestart 是索引节点区的起始盘块号,bmapstart 是位图区的起始盘块号。

对照上表的 XV6 文件系统布局,可以知道 superblock 各成员和图中的对应关系。 可以看出 XV6 磁盘文件系统的前面都是管理性的信息,后面是数据区和日志区,具体布局如下:

  1. 启动扇区(0 号块),后面接着超级块自身(1 号块)。
  2. 紧接着超级块的是 “索引节点区(表)”,共 ninodes/BSIZEBSIZE=512,是盘块大小)项,每一项(内含对应文件的索引)可以管理一个文件的物理盘块,因此整个文件系统最多有 ninodes/BSIZE 个文件。
  3. 在索引节点表的后面是 bit map 位图,每个盘块都在位图中有一个 bit 管理,0 表示盘块空闲未用,1 表示已经分配使用。也就是说 bit map 中的一个字节就要管理 8 个盘块,经过简单计算就可以将磁盘盘块号和位图中的 bit 对应起来。在 mkfs.c 创建 XV6 文件系统时,这些管理信息所对应的盘块都标记为 1,表示不可分配使用,只有数据区中还未分配使用的盘块所对应的位才为 0。
  4. 在 bit map 位图之后是数据盘块,用于保存文件的数据。后面会看到,目录文件和普通文件一样,都是使用数据区的盘块。
  5. 最后是日志区,共有 nlog 个盘块。

1.5 目录

目录不是必要的,如果我们对文件进行编号使用(不使用文件名和目录路径),则上面索引节点给出的功能就足够了:给出待读写的文件索引节点编号,我们读入索引节点内容,然后根据其索引就可以读写磁盘盘块。不过这样使用并不方便,我们更习惯的是使用层次性路径名的、树形的目录结构。

为了记录目录的层次结构,每一个目录都必须记录自己所管理的文件和子目录。目录本身也是文件,也就是说 / 目录的目录项是保存在不同的索引节点的磁盘文件上的。

进入 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. 当前目录自身的索引节点号。
  2. 读入目录文件内容。
  3. 查找下一级的文件(或目录)的索引节点号
  4. 读写下一级文件(或目录)内容。

经过多次递推的过程,就可以遍历目录树上的任何一个节点。这就需要一个起点条件:/ 根目录的索引节点号是固定为 1,因此无需依赖上级目录而直接获得。 这里再次提醒读者,从原理上说即使没有目录,也可以正常对文件内容进行读写,前提是知道该文件的索引节点号。

1.6 文件描述符

进程使用文件并不使用 file 结构体或 dinode 结构体,而是提供文件的路径名来打开文件并获得一个文件描述符,后续将使用文件描述符来指代这个打开的文件。XV6 中的每个进程都有一个文件描述符表 ofile[],每个文件描述符直接指向一个 file 结构体(系统管理的已打开的文件)。

2. 索引节点与目录项

有了前面的基本概念,我们来继续深入了解索引节点和目录项的细节,并将两者整合联系起来。

2.1 磁盘上的索引节点

索引节点代表文件自身,但是索引节点并不包含文件名(文件名是通过目录项给出的)。索引节点 由 dinode 结构体描述,它给出了文件的管理信息(元数据 meta data)。当索引节点代表磁盘文件时,dinode 将给出本文件的物理结构, 文件的盘块按照混合索引方式组织,类似于 Linux 的 EXT 文件系统上的 inode。由 dinodeaddrs[NDIRECT+1] 成员指出文件磁盘盘块所在位置,其中 0~NDIRECT-1 项都是直接块,最后一项是间接索引块。XV6 的磁盘文件的大小由 size 成员记录。

由于 dinode 有可能代表的设备或管道,因此需要 type 成员来区分普通磁盘文件、管道和设备。T_DIR 表示目录、T_FILE 表示磁盘文件、T_DEV 代表设备。如果 inode 类型代表设备,则还需要用 majorminor 来区分设备号。

由于 XV6 的文件系统支持硬链接,因此还需要有 nlink 来记录有多少个目录项指向该 inode 节点。

2.2 目录项

与 Unix 类文件系统相似,XV6 文件系统中也将目录当作文件,只是其中的内容被解释成目录项。 每个目录项将联系文件名和索引节点。目录项使用 dirent 结构体来表示,只有两个成员:name[DIRSIZ] 用于记录文件名(DIRSIZ=14 字节),inum 用于对应文件的索引节点。根目录 / 的索引号是固定的,ROOTINO 设置为 1,也就是说根目录是不需要经过目录项解释的。

3. 相关代码

fs.h

fs.h 中主要定义了超级块 superblock、索引节点 dinode 和目录项 dirent,它们在前面刚讨 论过。其次是几个文件系统中的常量:

IPBinode per block)是每个盘块上可以记录的 inode 数量。

IBLOCK(i,sb) 用来计算第 iinode 位于哪个盘块。

BPBbitmap bits per block)是一个盘块的位数。

BBLOCK(b,sb) 用于计算第 b 个数据盘块所对应的位图 bit 所在的盘块号。

DIRSIZdirectory size)是目录项中文件名字符串的大小。

fcntl.h

fcntl.h 中定义了打开文件时的访问模式(不是文件自身的可访问权限),包括读模式、写模式、读写模式和创建模式。