8.9 代码:Inodes
为了分配新的inode(例如,在创建文件时),xv6调用ialloc
(kernel/fs.c:196)。Ialloc
类似于balloc
:它一次一个块地遍历磁盘上的索引节点结构体,查找标记为空闲的一个。当它找到一个时,它通过将新type
写入磁盘来声明它,然后末尾通过调用iget
(kernel/fs.c:210)从inode缓存返回一个条目。ialloc
的正确操作取决于这样一个事实:一次只有一个进程可以保存对bp
的引用:ialloc
可以确保其他进程不会同时看到inode可用并尝试声明它。
Iget
(kernel/fs.c:243)在inode缓存中查找具有所需设备和inode编号的活动条目(ip->ref > 0
)。如果找到一个,它将返回对该incode的新引用(kernel/fs.c:252-256)。在iget
扫描时,它会记录第一个空槽(kernel/fs.c:257-258)的位置,如果需要分配缓存项,它会使用这个槽。
在读取或写入inode的元数据或内容之前,代码必须使用ilock
锁定inode。Ilock
(kernel/fs.c:289)为此使用睡眠锁。一旦ilock
以独占方式访问inode,它将根据需要从磁盘(更可能是buffer cache)读取inode。函数iunlock
(kernel/fs.c:317)释放睡眠锁,这可能会导致任何睡眠进程被唤醒。
Iput
(kernel/fs.c:333)通过减少引用计数(kernel/fs.c:356)释放指向inode的C指针。如果这是最后一次引用,inode缓存中该inode的槽现在将是空闲的,可以重用于其他inode。
如果iput
发现没有指向inode的C指针引用,并且inode没有指向它的链接(发生于无目录),则必须释放inode及其数据块。Iput
调用itrunc
将文件截断为零字节,释放数据块;将索引节点类型设置为0(未分配);并将inode写入磁盘(kernel/fs.c:338)。
iput
中释放inode的锁定协议值得仔细研究。一个危险是并发线程可能正在ilock
中等待使用该inode(例如,读取文件或列出目录),并且不会做好该inode已不再被分配的准备。这不可能发生,因为如果缓存的inode没有链接,并且ip->ref
为1,那么系统调用就无法获取指向该inode的指针。那一个引用是调用iput
的线程所拥有的引用。的确,iput
在icache.lock
的临界区域之外检查引用计数是否为1,但此时已知链接计数为零,因此没有线程会尝试获取新引用。另一个主要危险是,对ialloc
的并发调用可能会选择iput
正在释放的同一个inode。这只能在iupdate
写入磁盘以使inode的type
为零后发生。这个争用是良性的:分配线程将客气地等待获取inode的睡眠锁,然后再读取或写入inode,此时iput
已完成。
iput()
可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()
这样看起来是只读的调用,也可能最终调用iput()
。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。
iput()
和崩溃之间存在一种具有挑战性的交互。iput()
不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:进程可能仍在读取和写入该文件,因为它已成功打开该文件。但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。
文件系统以两种方式之一处理这种情况。简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。
第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。恢复时,文件系统将释放列表中的任何文件。
Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。