在了解的磁盘布局和盘块操作之后,我们可以来讨论索引节点的问题。文件系统中最核心的对象就是索引节点,虽然 inode 没有文件名(文件名是目录的功能),但它代表并管理着文件在磁盘上的数据。目录操作也是建立在索引节点的操作之上的。
用户进行文件读写操作时,使用的是文件逻辑偏移,而磁盘读写使用的是盘块号和盘块内偏移,只有索引节点才真正知道数据存储在哪个盘块上,即文件的物理结构。
索引节点的操作分为两类:
为了加快索引节点的读写操作,XV6 使用了索引节点的缓存,因此索引节点就呈现出 “磁盘索引节点” 和 “内存索引节点” 两个版本。
索引节点缓存 icache,它是磁盘索引节点在内存中的缓存, 可以加快索引节点的读写操作。磁盘索引节点位于磁盘中,这些索引节点缓存是磁盘索引节点在内存中的影像,并加上动态管理数据,例如引用计数 ref。icache.inode[NINODE]
中最多可以缓存 NINODE 个磁盘索引节点的信息,其中的元素 inode[]
则是依据磁盘上的 dinode
结构体而填写。
icache
缓存的是多个 inode 内存索引节点,它是磁盘索引节点 dinode 的内存形态。
内存索引节点的前半部分是磁盘索引节点所没有的成员变量,后面的 type
、major
、minor
、nlink
、size
和 addr[]
则是完全从磁盘 dinode 中拷贝而来。
索引节点缓存根据工作状态,分成已分配 Allocation
、被引用 Referencing
(ref>0
)、有效 Valid
(flags
的 I_VALID
置位)、锁定 Locked
(flags
的 I_BUSY
置位)。
iinit() 用于内存索引节点缓存的初始化。在对索引节点缓存的互斥锁完成初始化之后,读入超级块并打印出磁盘布局信息,这就是 XV6 启动时打印的内容之一,如下:
sb: size 1000 nblocks 941 ninodes 200 nlog 30 logstart 2 inodestart 32 bmap start 58
很意外地发现 iinit()
是在 forkret() 中调用的,不过只被调用一次,这是由 first
变量保证的。这是因为 iinit()
和 initlog()
必须在进程环境中进行,因此不能放在 kernel
的 main()
初始化中执行。
进程发出的文件读写操作,会转换到该文件索引节点所管理的盘块上的读写操作。我们先来学习对一个已经存在的文件,如果已经找到它的 inode
索引节点,如何读写该文件的内容数据。
readi()
用于从 inode
对应的磁盘文件的偏移 off
处,读入 n 个字节到 dst
指向的数据缓冲区 中。如果是设备文件(T_DEV
),则使用设备的读操作函数 devsw[ip->major].read()
完成读入操作。否则将执行磁盘文件的读入操作。
磁盘文件需要逐个盘块读入数据,但首先要知道文件偏移量对应的物理盘块号是哪个,这是通过 bmap()
完成的。
确定盘块号之后,将会调用前面讨论过的 bread()
完成磁盘盘块的读入。由于 bread()
将数据读入到块缓存中,因此还需要用 memmove()
将数据拷贝到用户空间缓冲区。
由于进程发出的文件读写操作使用的是字节偏移(转换成文件内部的逻辑盘块号 bn
), 而磁盘读写 bread()
和 bwrite()
使用的是物理盘块号,因此需要 bmap()
将文件字节偏移对应的逻辑盘块号 bn
转换成物理盘块号。其转换过程需要借助索引节点的 dinode.addr[]
或 inode.addr[]
,并且需要考虑直接盘块和间接盘块。
如果对应的数据盘块不存在,则 bmap()
会调用 balloc()
分配一个空闲盘块,然后再修改索引,使得 ip->addrs[bn]
指向新分配的盘块;如果该偏移落入间接索引区,则可能还需要分配间接索引盘块,然后才能分配 bn
所对应的数据盘块并建立索引关系。
writei()
需要逐个盘块写出数据,因为有块缓存的存在,因此会先调用 bread()
完成磁盘盘块的读入到块缓存,然后才是将数据拷贝到块缓存中,最后由 log_write()
向日志系统写出。
如果是设备(T_DEV
)则需要通过它自己的读函数 devsw[ip->major].read
其完成。
fs.c
的代码涉及三部分内容:
inode
操作的代码。本小节分析了盘块操作的代码以及 inode
操作中有关 “文件数据” 读写的代码,其余代码将陆续完成分析。
前面是对已有的文件、且已经定位其 inode
索引节点的读写操作。但是文件操作并不仅限于对已有文件的读写操作,还包括创建、删除等其他操作。
XV6 的索引节点操作同时涉及磁盘索引节点 dinode
及其缓存 inode
(内存索引节点)。
新创建一个文件时,就需要用 ialloc()
分配一个新的索引节点。ialloc()
用于在 dev
设备上分配一个类型为 type
的索引节点。该函数扫描整个磁盘索引节点区,遍历 1~sb.ninodes
的所有索引节点编号,逐个检查其类型 type
是否为 0,如果为 0 则设置其类型为 type
,并用 log_write()
通知日志系统更新磁盘内容。
然后用 iget()
建立相应的索引节点缓存。也就是说 ialloc()
同时完成在磁盘上和内存缓存中的操作。由于是分配空闲的索引节点,因此无需从磁盘读入其 dinode
内容来填写 inode
缓存。
根据设备号 dev
和索引节点号 inum
在索引节点缓存中查找,返回所匹配的索引节点缓存, 或者分配一个空闲的索引节点缓存。
iget()
工作过程如下:遍历所有 icahce.inode[]
缓存找到 dev
和 inum
所指定的 inode
则增 加其 ref
引用计数。若没有找到对应的 inode
缓存,但是有空闲的 inode
则记录在 ip
指针上, 然后填写该 inode
内容并返回。
如果 iget()
不仅没有找到对应的 inode
缓存,且发现没有空闲的缓存(empty==0
),则应该回收 inode
缓存。
iupdate()
将 inode
缓存的内容更新到磁盘 dinode
上,最后写出到磁盘中。
增加索引节点缓存的引用计数。
将索引节点的基本信息读入到 stat 结构体并返回。
将索引节点所管理文件数据的直接块和间接块都释放掉,每个磁盘盘块通过 bfree()
释放(数据盘块的位图清零),实际上数据还在磁盘。