XV6

在了解的磁盘布局和盘块操作之后,我们可以来讨论索引节点的问题。文件系统中最核心的对象就是索引节点,虽然 inode 没有文件名(文件名是目录的功能),但它代表并管理着文件在磁盘上的数据。目录操作也是建立在索引节点的操作之上的。

用户进行文件读写操作时,使用的是文件逻辑偏移,而磁盘读写使用的是盘块号和盘块内偏移,只有索引节点才真正知道数据存储在哪个盘块上,即文件的物理结构。

索引节点的操作分为两类:

  1. 对索引节点所管理的文件数据的读写。
  2. 对索引节点自身的分配、删除和修改。

1. 索引节点缓存

为了加快索引节点的读写操作,XV6 使用了索引节点的缓存,因此索引节点就呈现出 “磁盘索引节点” 和 “内存索引节点” 两个版本。

索引节点缓存 icache,它是磁盘索引节点在内存中的缓存, 可以加快索引节点的读写操作。磁盘索引节点位于磁盘中,这些索引节点缓存是磁盘索引节点在内存中的影像,并加上动态管理数据,例如引用计数 reficache.inode[NINODE] 中最多可以缓存 NINODE 个磁盘索引节点的信息,其中的元素 inode[] 则是依据磁盘上的 dinode 结构体而填写。

1.1 内存索引节点

icache 缓存的是多个 inode 内存索引节点,它是磁盘索引节点 dinode 的内存形态。

内存索引节点的前半部分是磁盘索引节点所没有的成员变量,后面的 typemajorminornlinksizeaddr[] 则是完全从磁盘 dinode 中拷贝而来。

索引节点缓存根据工作状态,分成已分配 Allocation、被引用 Referencingref>0)、有效 ValidflagsI_VALID 置位)、锁定 LockedflagsI_BUSY 置位)。

iinit() 用于内存索引节点缓存的初始化。在对索引节点缓存的互斥锁完成初始化之后,读入超级块并打印出磁盘布局信息,这就是 XV6 启动时打印的内容之一,如下:

sb: size 1000 nblocks 941 ninodes 200 nlog 30 logstart 2 inodestart 32 bmap start 58

很意外地发现 iinit() 是在 forkret() 中调用的,不过只被调用一次,这是由 first 变量保证的。这是因为 iinit()initlog() 必须在进程环境中进行,因此不能放在 kernelmain() 初始化中执行。

2. 索引节点上的读写操作

进程发出的文件读写操作,会转换到该文件索引节点所管理的盘块上的读写操作。我们先来学习对一个已经存在的文件,如果已经找到它的 inode 索引节点,如何读写该文件的内容数据。

readi()

readi() 用于从 inode 对应的磁盘文件的偏移 off 处,读入 n 个字节到 dst 指向的数据缓冲区 中。如果是设备文件(T_DEV),则使用设备的读操作函数 devsw[ip->major].read() 完成读入操作。否则将执行磁盘文件的读入操作。

磁盘文件需要逐个盘块读入数据,但首先要知道文件偏移量对应的物理盘块号是哪个,这是通过 bmap() 完成的。

确定盘块号之后,将会调用前面讨论过的 bread() 完成磁盘盘块的读入。由于 bread() 将数据读入到块缓存中,因此还需要用 memmove() 将数据拷贝到用户空间缓冲区。

bmap()

由于进程发出的文件读写操作使用的是字节偏移(转换成文件内部的逻辑盘块号 bn), 而磁盘读写 bread()bwrite() 使用的是物理盘块号,因此需要 bmap() 将文件字节偏移对应的逻辑盘块号 bn 转换成物理盘块号。其转换过程需要借助索引节点的 dinode.addr[]inode.addr[],并且需要考虑直接盘块和间接盘块。

如果对应的数据盘块不存在,则 bmap() 会调用 balloc() 分配一个空闲盘块,然后再修改索引,使得 ip->addrs[bn] 指向新分配的盘块;如果该偏移落入间接索引区,则可能还需要分配间接索引盘块,然后才能分配 bn 所对应的数据盘块并建立索引关系。

writei()

writei() 需要逐个盘块写出数据,因为有块缓存的存在,因此会先调用 bread() 完成磁盘盘块的读入到块缓存,然后才是将数据拷贝到块缓存中,最后由 log_write() 向日志系统写出。

如果是设备(T_DEV)则需要通过它自己的读函数 devsw[ip->major].read 其完成。

fs.c

fs.c 的代码涉及三部分内容:

  1. 盘块操作的代码。
  2. inode 操作的代码。
  3. 目录操作的代码。

本小节分析了盘块操作的代码以及 inode 操作中有关 “文件数据” 读写的代码,其余代码将陆续完成分析。

3. 对索引节点的操作

前面是对已有的文件、且已经定位其 inode 索引节点的读写操作。但是文件操作并不仅限于对已有文件的读写操作,还包括创建、删除等其他操作。

XV6 的索引节点操作同时涉及磁盘索引节点 dinode 及其缓存 inode(内存索引节点)。

ialloc()

新创建一个文件时,就需要用 ialloc() 分配一个新的索引节点。ialloc() 用于在 dev 设备上分配一个类型为 type 的索引节点。该函数扫描整个磁盘索引节点区,遍历 1~sb.ninodes 的所有索引节点编号,逐个检查其类型 type 是否为 0,如果为 0 则设置其类型为 type,并用 log_write() 通知日志系统更新磁盘内容。

然后用 iget() 建立相应的索引节点缓存。也就是说 ialloc() 同时完成在磁盘上和内存缓存中的操作。由于是分配空闲的索引节点,因此无需从磁盘读入其 dinode 内容来填写 inode 缓存。

iget()

根据设备号 dev 和索引节点号 inum 在索引节点缓存中查找,返回所匹配的索引节点缓存, 或者分配一个空闲的索引节点缓存。

iget() 工作过程如下:遍历所有 icahce.inode[] 缓存找到 devinum 所指定的 inode 则增 加其 ref 引用计数。若没有找到对应的 inode 缓存,但是有空闲的 inode 则记录在 ip 指针上, 然后填写该 inode 内容并返回。

如果 iget() 不仅没有找到对应的 inode 缓存,且发现没有空闲的缓存(empty==0),则应该回收 inode 缓存。

iupdate()

iupdate()inode 缓存的内容更新到磁盘 dinode 上,最后写出到磁盘中。

idup()

增加索引节点缓存的引用计数。

stati

将索引节点的基本信息读入到 stat 结构体并返回。

itrunc()

将索引节点所管理文件数据的直接块和间接块都释放掉,每个磁盘盘块通过 bfree() 释放(数据盘块的位图清零),实际上数据还在磁盘。