- 分配inode:新的常规文件需要使用一个新的inode 结构进行保存。因此在创建文件时需先为新的文件分配新的inode 结构。
这一步骤需要在inode 分配器的位图(bitmap)中查找空闲的inode,并将其对应的比特位标记为1,标记该inode 已被使用。
-
初始化inode:在分配并得到inode 之后,文件系统需要对该inode 进行初始化操作,即将新文件的信息保存在inode 结构之中。
-
增加目录项:最后,需要在父目录中添加新的目录项,保存新文件的文件名和inode 号。
情况1 只有分配inode 的操作保存在设备中,增加目录项的操作并没有保存在存储设备中
新创建的文件无法在文件系统中被观测到。但是由于对应的inode 在分配信息中已经被标记为占用,而实际上该inode 并未被任何文件所使用,该inode 将无法被释放,造成inode空间的泄漏。如果inode 空间被多次泄漏,文件系统中所能保存的文件越来越少。这种情况实际上违反了inode 分配信息与inode 结构的实际使用之间的一致性。
情况2 只有初始化inode 的操作保存在设备中,增加目录项的操作并没有保存在存储设备中
新创建的文件无法在文件系统中被观测到。同时,由于inode 的分配信息未被修改,后续的创建文件操作依然可以使用到该inode 结构,因此并未产生空间泄漏。这种情况的出现并不会造成文件系统的一致性问题。
情况3 只有增加目录项的操作保存在设备中
由于增加目录项的操作被持久化在存储设备中,当文件系统遍历该目录时,能够看到新文件对应的文件名和inode 号。
然而由于该inode 结构中的数据未被初始化,文件系统尝试访问该inode 时,会访问到未初始化的数据从而造成错误。同时,由于该inode 号对应的分配信息未被持久化,在后续的文件操作中,该 inode 可能会被再次分配给其他文件,从而导致该inode 错误地被两个不同的文件共享,造成数据丢失、泄漏和被篡改等问题。
这种情况违反了inode 分配信息、inode 结构与目录项中所保存的inode 号之间的一致性,即所有目录项中所保存的inode 号皆应被分配且初始化。
情况4 只有分配inode 的操作未保存在设备中
该情况下,由于分配inode 的信息未被持久化,会造成情况3中错误地指向未分配inode 结构的情况,从而造成正确性和安全性的问题。
情况5 只有初始化inode 的操作未保存在设备中
与情况3相似,如果只有inode 的初始化操作未被持久化,则文件系统在后续访问该文件时,会访 问到未初始化的数据,从而产生错误。
creat(“a”); fd = creat(“b”); write(fd,…); crash
持久化/Durable: 哪些操作可见 a和b都可以
原子性/Atomic: 要不所有操作都可见,要不都不可见 要么a和b都可见,要么都不可见
有序性/Ordered: 按照前缀序(Prefix)的方式可见 如果b可见,那么a也应该可见
- 原子更新技术
- 日志
- 写时复制
- Soft updates
创建:在使用日志之前,需要先进行日志的创建和初始化操作。在此过程中,文件系统会为日志分配内存和存储空间,并初始化维护日志所需的元数据。此后,日志进入到写入阶段。
写入:在写入阶段,文件系统可以将要进行的操作或操作影响到的数据及其所在位置写入到日志中。举例来说,如果文件系统想要将位置0x50处的数据A改成B,其需要在日志中记录“位置0x50上的数据从A修改为B”。 需要注意的是,由于操作数量较多或者操作的数据量较大,在写入阶段记录的操作信息(即日志内容)往往超出存储设备的原子写入大小,因而无法原子地写入到存储设备中。因此,在崩溃后,存储设备中处于写入阶段的日志内容可能是不完整的。为了防止这些不完整的日志内容造 成一致性问题,在进行恢复时,处于写入阶段的日志的内容会被直接丢弃。换句话说,在写入阶段的日志内容一般不会马上生效。这些日志内容的生效需要等到日志的提交阶段。
提交:在提交阶段中,文件系统需要将此前在日志中记录的操作信息原子地标记为有效。在日志提交成功后,日志中所记录的信息在进行崩溃恢复时才会发挥效用。具体来说,在进行提交前,文件系统需要将日志内容以固定的格式,持久且完整地保存在存储设备中。在保证所有日志内容均已持久化在存储设备中之后,日志系统将在存储设备上原子地标记日志为已提交状态。在日志提交完成后,日志进入完成阶段。
完成:在完成阶段,文件系统可以将实际的修改写回到存储设备之中。由于这些写回的位置及其信息均在日志中有所记录,若在此阶段发生崩溃,在进行恢复时,会使用日志中记录的内容进行恢复,保证日志中所记录操作的原子性。
销毁:日志的记录需要占用一定的内存和存储资源。日志无效后,文件系统可以对日志所占用的这些资源进行回收,即销毁日志。一般情况下,这一过程可以通过批量化的形式延迟完成。
- 每个操作都要写磁盘,内存缓存的优势被抵消
- 每个修改都要拷贝新数据到日志
- 相同块的多个修改被记录多次
缺点:
- 丢的多
- 依然要写两次磁盘
- 定期触发
- 每一段时间(如5s)触发一次
- 日志达到一定量(如500MB)时触发一次
- 用户触发
- 例如:应用调用fsync()时触发
Journal:日志,由文件或设备中某区域组成
Handle:原子操作,由需要原子完成的多个修改组成
Transaction:事务,多个批量在一起的原子操作
writeback: 在该模式下,Ext4 对于文件数据部分的修改不进行日志记录,且对文件数据的写回没有特定的要求。在这种模式下,文件的数据块修改可以在任何时候写入到磁盘中进行持久化。这种模式的一致性保证比较差。在发生崩溃的时候,有更大的概率发生不一致的情况。但是 由于对于写回顺序没有特殊要求,这种模式的性能往往是最优的。
ordered: 在该模式下,Ext4 对于文件数据部分的修改同样不进行日志记录,但是对于一个文件的数据和元数据修改,需要保证文件数据部分的修改在文件元数据被持久化前持久化到设备中。这一顺序的保证,可以减少数据不一致的情况出现,在一定程度上增强文件系统对一致性的保 证。该模式是Ext4 中的默认模式,也是一种性能和一致性保证之间的权衡。
journal: 在该模式下,Ext4 文件中的数据和元数据修改均使用日志进行保护。这是一种一致性保证很强的模式。但是它要求文件数据在日志中和原位置上进行两次写入。因此,对于产生大量文件数据修改的场景来说,这种模式会带来不小的性能开销和不必要的写入操作。
日志是一种保证原子更新和崩溃一致性的常用方法。通过日志记录,可以保证任意位置任意数量操作的原子完成。然而,使用日志同样会带来一些问题。首先,日志要求所有的修改先在日志中进行记录,再将修改应用到其原有位置上。这样所有的修改都执行了两次:一次在日志中,另外一次在原位置上。因此,对于数据修改较多的场景,使用日志保证原子性可能会带来很大的写入开销。另外,日志的原子性保证依赖于日志恢复,因此在发生崩溃并重启后,需要首先进行日志的恢复。只有日志恢复完毕之后,文件系统才能开始处理新的请求。若日志恢复时间较长,整个文件系统将长时间处于不可用状态,进而影响到操作系统或应用程序的启动时间。
在树形数据结构中,写时拷贝往往需要更新到根节点才会停止。我们修改节点C 和E 后,需要继续修改节点B、D 和A(根节点)。这种修改一些节点后,还需要进一步修改数据结构中其他节点以保证原子更新的问题,便称为更新传递问题。
解决:
解决更新传递问题,一种常见的方法是提前进行原子更新。对于树形数据结构来说,并非只有指向树根的指针才可以被原子更新;在上述例子中,原子更新的粒度为一个节点。若在更新传递的过程中,所有要进行的修改被一个原子更新粒度所覆盖,则可以直接原地进行修改,无需继续传递更新。例如在图13.3中,在节点A 中保存了指向节点B 和D 的指针。由于节点A 可以被原子更新,我们可以通过一次原地更新操作,原子地修改A 中指向节点B 和D的指针,让它们指向节点B’和D’,从而避免继续使用写时拷贝技术,停止更新传递。
写放大问题是指实际修改数据量大于用户要修改的数据量的情况。如在进行以4KB 内存页为粒度的写时拷贝技术时,若想要修改某个页中的1 个字节,我们不得不拷贝整个4KB 内存页中的所有数据,因此修改量从1 个字节被放大到4KB。写时拷贝技术中的更新传递,会使写放大问题更加严重。
解决: 当要修改的数据覆盖了完整的原子更新粒度时,可以无需拷贝原有数据,直接写入新的数据,从而在一定程度上缓解写放大问题。例如在图13.3中,由于原子更新粒度为一个节点,且E 节点中所有的数据均需要被修改,在分配E’后,即使我们将E 中的数据拷贝到E’,这些数据也会被新数据全部覆盖掉。因此,在这种情况下,可以省略写时拷贝中的拷贝操作,从而减少写入操作。
我们会将数据块C 和数据块D进行拷贝,并在拷贝副本上进行修改。由于指向数据块C 的指针保存在inode中,而指向数据块D 的指针保存在索引块中,我们需要继续对索引块进行写时拷贝。最终,需要修改的指向数据块C 的指针和指向索引块的指针均保存在inode 结构中。由于inode 结构通常小于存储设备的块大小,通过原子更新inode 结构,我们可以原子地持久化文件数据块C 和数据块D 上的数据修改。由于所有文件数据访问均从inode 结构出发,因此一个文件中的任何位置上的修改,均可以通过写时拷贝技术进行原子更新和持久化。
写时拷贝技术对数据结构有一定的要求,当所有的修改最终能够变成一个原子修改时,才可以使用写时拷贝技术。此外,写时拷贝技术的使用与原子更新粒度有关。当修改的数据量远大于原子更新粒度时,往往只有数据头部和尾部需要真正进行拷贝操作,而中间部分可以根据前文提到的方法省略拷贝操作,直接使用新数据写入新分配出来的节点。这种情况下,写时拷贝技术产生的写放大相对较小。而当数据修改量小于原子更新粒度时,写时拷贝技术造成的写放大会非常严重,即使每次仅修改一个字节,也需要按照原子更新粒度进行数据拷贝。因此,是否适合使用写时拷贝,需要结合原子更新粒度和数据修改量综合考虑。
规则1 不要指向一个未初始化的结构。如:目录项指向一个inode 之前,该inode 结构应该先被初始化。 规则2 一个结构被指针指向时,不要重用该结构。如:当一个inode 指向了一个数据块时,这个数据块不应该被重新分配给其他结构。 规则3 对于一个仍有用的结构,不要修改最后一个指向它的指针。如:重命名文件时,在写入新的目录项前,不应删除旧的目录项。