#### 概述

文件系统看起来更像是一种约定，约定的是数据在磁盘上的存储形式。磁盘上的文件系统是自恰的，即包含了全部的信息，所以任何操作系统都可以读取到它。操作系统依据这个约定来管理磁盘上的信息，所以操作系统里的那个“文件系统”应该叫文件管理系统，或者说是文件系统的软件层面。

xv6-public和xv6-riscv所用的文件系统差别很小，唯一的差别就在于xv6-riscv的超级块里增加了一个标示自己的魔数。这点差别是微不足道的，对于软件层面的操作几乎没有影响。但在文件管理系统的实现上，xv6-public和xv6-riscv就有比较明显的差别了。虽然它们都划分了七层，但xv6-public是上了一个大锁，而xv6-riscv是真正的多核并行，所以在每层的实现上xv6-riscv都多了些锁的结构。

所以，本文要说的是文件管理系统。XV6的文件管理系统被划分成了七层，但个人认为核心的层只有三层：缓冲区缓存层，索引结点层，文件描述符层。

| 每层名称     | 对应文件          | 核心变量 |
| ------------ | ----------------- | -------- |
| 硬盘层       | virtio_disc.c     |          |
| 缓冲区缓存层 | bio.c             | bcache   |
| 日志层       | log.c             | log      |
| 索引结点层   | fs.c              | icache   |
| 目录层       | fs.c              |          |
| 路径名层     | fs.c              |          |
| 文件描述符层 | file.c, sysfile.c | ftable   |

#### 零、磁盘上的文件系统

磁盘整体上可以分为三个部分。

1. 第一个部分是启动块。它占用一个块的大小，不属于文件系统的一部分。
2. 第二个部分存放的是元数据，具体又可以分为四个区。它占用少量的块，是文件系统的一部分。
   - 超级块：记录了整个文件系统的基本信息。是其它三个区的索引。占用一个块。
   - 日志区：用于保证硬件层面和软件层面的一致性。占用30个块，本区内第一个块是本区内其它块的索引，本区内其它块是记录了具体信息的块。
   - 索引结点区：一个索引结点就是一个文件。占用13个块，最多可以存放200个索引结点。每个索引结点都是对用户数据的索引，采用一级和二级混合索引的方式，可知文件最大不能超过268KB（12+1024/4 个块）。
   - 位图区：对用户数据里空闲块的索引。占用1个字节。
3. 第三个部分存放的是用户数据。它占用了大量的块，是文件系统的一部分。整个镜像给指定的块数是1000，那么这部分的块数就是954（1000－46）。

#### 一、硬盘层

硬盘层实际上就是磁盘驱动程序。

它向缓冲区缓存提供的接口是`virtio_disk_rw`。

- 作用：在缓冲区和磁盘之间交换数据。

- `*b`：缓冲区的指针
- `write`：1把缓冲区的内容写入到磁盘，0把磁盘的内容读取到缓冲区

#### 二、缓冲区缓存层

缓冲区缓存(buffer cache)`bcache.buf[]`里的每一个元素都是双向链表`bcache.head`的元素，链表里的每一个结点都代表一个缓冲区(buffer)。上层文件系统要读磁盘的一个块，先要把块的内容保存到缓冲区，再从缓冲区读出来。上层文件系统要写磁盘的一个块，先要写缓冲区，再把缓冲区的内容写到磁盘上。

上层可能用到如下接口：

`balloc`

- 作用：在磁盘的位图区找一个空闲的块，并返回这个块的块号。
- `dev`：设备号

`bfree`

- 作用：在磁盘的位图区把指定块标记为空闲。
- `dev`：设备号
- `b`：块号
- 执行过程：

`bread`

- 作用：为给定的扇区分配缓冲区，并且把扇区的内容读取到缓冲区。如果那个扇区的内容已经被缓存，则直接返回对应缓冲区的指针。如果那个扇区的内容还没有被缓存，则从缓冲区缓存里找一个引用计数为0的缓冲区，并把硬盘内容读入到缓冲区再返回。相当于在双向链表里通过值查询位置。
- `dev`：设备号
- `blockno`：块号

`bwrite`

- 作用：把缓冲区的内容写入到磁盘。
- `*b`：缓冲区的指针。

`brelse`

- 作用：给定缓冲区的引用计数减1。当引用计数为0时把这个缓冲区从双向链表中删除，再把它插入到链表头的前面。这样做的好处是，当双向链表向后查时都是在用的缓冲区，当双向链表向前查时都是空闲的缓冲区。相当于修改双向链表里指定元素的值，并依据值的结果删除再插入一个元素。
- `*b`：缓冲区的指针。

`bpin`

- 作用：给定缓冲区的引用记数加1。相当于修改双向链表里指定元素的值。
- `*b`：缓冲区的指针。

`bunpin`

- 作用：给定缓冲区的引用记数减1。相当于修改双向链表里指定元素的值。
- `*b`：缓冲区的指针。

#### 三、日志层

日志层是为了解决系统崩溃后，磁盘上的文件系统一致性被破坏的问题。它的思想是在磁盘上建立一个日志区，所有对磁盘的写操作先写到日志区，然后再把日志区的数据写到数据区。如果在写日志区的时候系统崩溃了，那么系统重启后什么都不做；如果在日志区写数据区的时候崩溃了，系统会重新执行一遍日志区写数据区的工作。这样就保证了磁盘上文件系统的一致性。

日志层里的函数是要结合起来用的，一次事务用`begin_op`开头，用`end_op`结尾，中间是对磁盘的操作。

上层可能用到如下接口：

`begin_op`

- 作用：给`log.outstanding`执行加1操作，以表示日志区被引用了1次
- 没有参数，没有返回值

`end_op`

- 作用：给`log.outstanding`执行减1操作。如`log.outstanding`为0则调用`commit`把一次事务写入磁盘。
- 没有参数，没有返回值

`initlog`

- 作用：初始化结构体`log`，并执行`recover_from_log()`。`recover_from_log()`的作用是，如果上次不正常关机，可能会有一些没有完成的写磁盘的内容，这些内容就会放在磁盘的日志区，这个函数会把日志区的内容写入到磁盘，并把对日志区第一个块的修改写回到日志区的第一个块。
- `dev`：设备号。
- `*sb`：超级块在内存中的缓存。
- 返回值(void)

`log_write`

- 作用：在硬盘的日志区为指定的缓冲区分配一个编号，这样随后的`commit()`或`write_log()`函数就会统一把日志区记录的更改写入到硬盘中。
- `*b`：缓冲区的指针
- 返回值(void)

#### 四、索引结点层

到了这一层终于有文件的概念了，一个索引结点其实就对应了一个文件。索引结点(inode)的英文原意应该是index node，它是把属于同一个文件的所有的块索引到一起而形成一个结点。有两种类型的索引结点：磁盘上的索引结点对应的数据结构是`dinode`，超级块的索引结点区可以看作是索引结点组成的顺序表；文件管理系统里的索引结点对应的数据结构是`inode`，所有缓存到内存中的索引结点都放在顺序表`icache.inode[]`里。顺序表`icache.inode[]`里的元素通过磁盘块号和`bcache.buf[]`里的元素对应起来。

上层可能用到如下接口：

`bmap`

- 作用：返回索引结点里一个文件的块号所对应的磁盘上的块号。如果文件块号对应的磁盘块号为0，则会调用`balloc()`分配一个磁盘块号。

`ialloc`

- 作用：分配一个新的`inode`。相当于按值查询顺序表。
- `dev`：设备号
- `type`：文件类型。1目录，2文件，3设备。
- 返回值(`struct inode *`)：新分配的`inode`的指针
- 执行过程：遍历磁盘上索引结点区，查找type值为0的索引结点（表明它是空闲的），调用iget函数分配指针。

`iget`

- 作用：在`icache.inode[]`里备份一个磁盘上的索引结点，并返回其指针。相当于先按值查找顺序表，再修改顺序表某个元素的值。
- `dev`：设备号
- `inum`：磁盘上索引结点在索引区的编号
- 返回值(`struct inode *`)：请求到的索引结点的指针。
- 执行过程：首先会在`icache.inode[]`里遍历所有已缓存在内存里的索引结点，如果请求的索引结点已经缓存则直接返回，否则会记录一个引用计数为0的磁盘上的索引结点。接下来，如果还没有返回则表明还没有缓存，就把引用计数为0的那个索引结点保存到内存后返回。

`iput`

- 作用：释放一个到`inode`的指针。相当于读取并修改顺序表的值，并依据这个值执行一些函数。
- `*ip`：准备释放的索引结点。
- 返回值(void)
- 执行过程：如果这个指针引用计数为1，也没有到它的连接，则执行释放的过程。否则，仅执行引用计数减1的操作。具体的释放过程是，先调用`itrunc()`清空索引结点所指向的所有的块，再调用`iupdate()`把内存里对索引结点的更新写入到磁盘上。

`ilock`

- 作用：给定的索引结点加上睡眠锁。如果磁盘上索引结点的内容还没有缓存到内存里，则缓存之。
- `*ip`：准备加锁的索引结点。

`iunlock`

- 作用：给定的索引结点释放睡眠锁。
- `*ip`：准备释放锁的索引结点。

`readi`

- 作用：把索引结点所指向的文件的内容读取到指定内存地址。相当于读取并修改顺序表的值，并依据这个值执行一些函数。
- `*ip`：索引结点的指针，就是从这个索引结点读取数据的
- `user_dst`：1索引结点的数据是要复制到用户空间，0索引结点的数据是要复制到内核空间
- `dst`：目标地址，用于保存从索引结点读取的数据
- `off`：要读取的数据在文件里的位置
- `n`：要读取的数据有多少个字节
- 返回值(int)：实际读取了多少个字节
- 执行过程：把索引结点里的数据逐块的复制到目标地址

`writei`

- 作用：通过索引结点把对文件的修改写入到磁盘上。相当于读取并修改顺序表的值，并依据这个值执行一些函数。
- `*ip`：索引结点的指针，就是向这个索引结点写入数据的
- `user_src`：1要写入磁盘的数据来自于用户空间，0要写入磁盘的数据来自于内核空间
- `src`：源地址，存放的是要保存到磁盘的数据
- `off`：要写入的数据在文件里的位置
- `n`：要写入的数据有多少个字节
- 返回值(int)：实际写入了多少个字节
- 执行过程：逐块地把数据写入到索引结点里

`stati`

- 作用：把`inode`的信息赋值给`struct stat`。相当于读取顺序表里元素的值。

#### 五、目录层

目录层的作用是把索引结点组织起来，形成一个树状结构，树根就是根目录。这样只要知道根目录在哪里，就能定位到所有的文件了。在这一层里索引结点有名字了，即通常所说的文件名。

根目录在索引结点区的第一个条目上，这在文件系统创建的时候就决定了。

目录层向上提供了如下接口：

`dirlookup`

- 作用：通过名字来查找目录里的条目。
- `*dp`：要查询的目录的inode。
- `*name`：要查询的文件名。
- `*poff`：如果所查询的条目存在，这个条目的位移就保存在`*poff`里。
- 返回值(`struct inode *`)：如果为0，则没有匹配的条目。如果非0，则是该条目的inode的指针。
- 执行过程：依次读取该目录文件下的所有条目，如果要查询的名字和文件名相等，则找到所需的条目，获取它的位移并返回它的inode指针。如果查遍了所有条目并没有找到名字相符的，说明该目录下没有这样的条目，返回0.

`dirlink`

- 作用：向一个目录里写入一个新的目录条目。
- `*dp`：将要写入新条目的目录
- `*name`：新目录的名称
- `inum`：新目录的索引结点号
- 返回值(int)：0执行成功，-1同名目录已存在
- 执行过程：在该目录下找到一个空闲的条目，修改该条目的索引结点号和名称，把该条目的信息写入该目录的inode

#### 六、路径名层

在目录层里文件被组织成树状的结构，而路径名就是这个树状结构的一条路径，是这个树状结构的具现形式。在路径名层里，可以定位到路径所指向的那个结点，以及那个结点所指向的目录。

路径名层内部的函数：

`skipelem`

- 作用：把路径分为“目录”和“名称”两部分。路径里第一级的是“目录”，除了“目录”其余部分被称作“名称”。如果路径里只有一级目录，则“目录”即为路径本身，“名称”为空字符串。如果路径是空字符串或是根目录，则“目录”为数字0.

- `*path`：路径
- `*name`：“名称”
- 返回值：“目录”

`namex`

作用：遍历路径里的每一个目录，依据变量`nameiparent`的值，来决定是返回最后一级的索引结点，还是返回倒数第二级的索引结点

- `*path`：路径
- `nameiparent`：0返回的是路径里最后一级的索引结点，1返回的是路径里倒数第二级的索引结点
- `*name`：所要查询的索引结点的名字
- 返回值：所要查询的索引结点的指针

路径名层向上提供了如下接口：

`namei`

- 作用：查询路径所代表的文件的索引结点
- `*path`：路径
- 返回值：所要查询索引结点的指针
- 执行过程：

`nameiparent`

- 作用：查询路径所代表的文件所在目录的索引结点
- `*path`：路径
- `*name`：查询路径所代表的文件的名称
- 返回值：所要查询索引结点的指针

#### 七、文件描述符层

广义的文件出现在这一层里，就是“一切皆文件”里的那个文件。在这里，文件可能是设备、管道或索引结点。

其实是把所有打开的文件组成一个顺序表`ftable.file[]`，最重要的字段就是`struct inode *ip`，所有对文件的操作实际上就是对`*ip`的操作，其它的字段往往就是起条件限制的作用。

本层提供的函数有：

- `filealloc`

  作用：遍历顺序表`ftable.file[]`，查找引用计数`f->ref`为0的文件，将引用计数置1并返回该文件的指针。

  > 相当于对顺序表进行按值查找，即`LocateElem`的功能。

- `filedup`：对给定文件的引用计数执行递增操作。

  > 相当于修改顺序表指定元素的值。

- `fileclose`：对给定的文件执行引用计数递减操作`--f->ref`。如果递减之后`f->ref`非0，说明有其它进程还在使用此文件，直接返回。否则`f->ref`为0，说明没有进程使用此文件，释放该文件对应的资源，如果是管道则调用`pipeclose()`，如果是普通文件或设备则调用`iput()`。

  > 相当于修改顺序表指定元素的值。并根据值的结果执行一些函数。

- `filestat`：读取索引结点的信息。通过调用`stati()`实现此功能。

  > 相当于输出顺序表指定元素的值。

- `fileread`：从文件里读取数据。不同的文件类型调用不同的底层接口，如果是管道则调用`pipewrite()`，如果是设备则调用`devsw[].read()`，如果是普通文件则调用`readi`。

  > 相当于读取顺序表指定元素的值。并根据值的结果执行一些函数。

- `filewrite`：向文件写入数据。不同的文件类型调用不同的底层接口，如果是管道则调用`pipewrite()`，如果是设备则调用`devsw[].write()`，如果是普通文件则调用`writei`。

  > 相当于读取顺序表指定元素的值。并根据值的结果执行一些函数。

#### 附、系统调用

系统调用不属于文件系统，它是连接文件系统和用户的纽带，从中可以看到用户视角下的文件操作是怎么通过文件系统提供的接口实现的。文件系统是个工具箱，系统调用是在使用这个工具箱，通过分析系统调用才可以对文件系统融汇贯通。