参考教材:
Operating Systems: Three Easy Pieces
Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
在线阅读:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 认为课本应该是免费的。
————————————————————————————————————————
这是专业必修课《操作系统原理》的复习指引。
在本文的最后附有复习指导的高清截图。需要掌握的概念在文档截图中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
操作系统原理不是语言课,本复习指导对用到的编程语言的语法的讲解也不会很细致。如果不知道代码中的一些关键字或函数的具体用法,你应该自行查找相关资料。
1、最早的UNIX文件系统的结构是这样的:
S区域记录了整个文件系统的基本信息,包括卷(volume)大小、索引节点数、指向空闲区域开头的指针,等等。索引节点区包括全部文件的索引节点。数据区则记录文件内容。
这个文件系统结构简单,而且为文件与目录的层次性的实现打下了基础。但是,这种结构的文件系统的性能很糟。因为它并没有考虑机械盘的结构,当磁盘读取索引节点后,可能需要寻道至较远的位置才能读到文件。硬盘使用久了以后,还会产生许多碎片(fragment),使得性能问题雪上加霜。
产生碎片的机理有:
(1)硬盘尽可能在距离磁头最近的空白位置开始写入文件,写入完毕后磁头会悬停,不会主动移动到另一片空白区域。
(2)文件被删除以后,释放出来的空间不会与其它空闲空间合并。
(3)当一段空闲空间写满后,如果文件尚未写入完毕,必须寻找另外的空闲空间继续写入。
可见,随着磁盘的使用,磁盘碎片将会越来越多:
磁盘碎片整理(defragmentation)程序能够将文件的多个不连续部分尽量合并,从而减少碎片、恢复磁盘性能。
此外,最初的文件系统的块大小很小(512 Bytes),数据传输速率受到影响。
2、后来,Berkeley的一个研究小组研发了快速文件系统(Fast File System,FFS)。FFS充分考虑的磁盘的物理结构,以求尽量提升性能。FFS保持相关API(open()、read()、write()、close()等)不变,而大改内部结构。
FFS引发了文件系统研究的新纪元。事实上,所有的现代文件系统都保持接口的不变来确保兼容性,而在内部结构的改进上下功夫。
3、FFS将磁盘划分为大量的柱面(cylinder)。一个柱面指的是不同盘面上相同位置(离圆心的距离相同)的磁道的集合。若干个连续的柱面又归为一个柱面组。
当然,磁盘不会向文件系统透露太多的信息。文件系统不知道一个柱面到底怎样被使用。磁盘只是简单地将逻辑地址报告给文件系统,而隐去了盘片的几何细节。因此,现代文件系统(例如ext2、ext3、ext4)将磁盘划分为块组(block group)。一个块组包含许多个块,对应磁盘上的一段连续空间。如果两个文件存放在同一个柱面组或块组中,FFS就能保证依次访问它们不用进行长时间的寻道。
4、每个柱面组的结构是这样的:
与最初的UNIX文件系统一样,S区为文件系统的装载所需。而且这个区域有多个备份,如果一个副本损坏了,由于还有其它副本,挂载和访问文件系统不受影响。
在组内,FFS需要追踪每个索引节点和数据块。索引节点位映像(inode bitmap,ib)和数据位映像(data bitmap,db)负责这个功能。位映像是管理空闲空间的优秀方式,原因是:通过位映像,可以迅速找到一个大块的空闲区域,并将其分配给一个文件。这种方法也许还可以避免在老式文件系统上的空闲列表本身的碎片问题。
5、以创建一个空文件为例,我们来说明文件系统用到的数据结构如何更新。
虽然文件是空的,但是它依然需要占用一个块(一般为4 KB)。由于是新创建的文件,所以需要一个新的索引节点。索引节点区和索引节点位映像区都需要更新。文件本身也有一些数据,这些写入数据区,数据位映像也要修改。到这里为止,创建一个文件需要在所在的柱面组进行4次写入(这些写入操作可能先存入缓冲区里)。
光是这些还没完,当创建新文件时,必须将其写入正确的层级,即目录树需要正确更新。文件所在的目录中,必须添加新文件的相关信息;这个更新可能在父目录所在的数据块内就可以完成,也可能需要申请新的数据块,并更新位映像。父目录的索引节点也要更新,因为目录的长度和修改日期都改变了。
6、FFS放置文件、目录和元数据的原则是:将相关联的信息尽量靠近。于是,具有相关关系的信息尽可能被放在同一个块组;不具有相关性的内容则不放在同一块组。为了实现这个目标,FFS应用了一些启发式算法。
对于目录,FFS尽量找到一个已分配目录较少、空闲的索引节点较多的柱面组,将目录数据和索引节点放入。当然,也可以运用其它的算法(比如考虑空闲数据块的数量)。
对于文件,FFS做两件事。首先,(一般情况下)确保将数据块和索引节点分配在同一组,避免访问索引节点和数据时需要长时间寻道。其次,它将同一目录下的所有文件都放在目录所在的柱面组中。
以下是一种分配 /、/a、/b、/a/c、/a/d、/a/e、/b/f的方式:
与之相对的是将索引节点尽量分散的分配方式,这种方式使得一个柱面组的索引节点表不会很快被写满:
访问一个目录时,一并访问目录下的文件是很常见的:想一想编译、链接的过程。一个目录下的一些代码文件被编译,在同一目录下生成若干目标文件,然后再把这些目标文件链接起来。因此,很多时候FFS都能提升性能,因为在访问具有相关性的文件时避免了要到很远的位置去寻道。
7、在FFS中,如果存储的文件比较大,那么文件很容易填满数据区,造成无法将相关联的文件写入同一个柱面组:
将大段连续文件打散成许多大块,分散在不同的柱面组中,可以解决此问题:
这种思想称为均摊(amortization),在计算机科学中十分常见。不过,这种存储方式会降低顺序读取的性能。如果需要减小性能削弱的幅度,就要把大块的大小增加。
不过,FFS不使用这种方法,而是将前12个数据块与索引节点放在同一个柱面组,每个块都跟一个间接块,一个间接块指向一个不同的柱面组,在这个柱面组继续存储着这个大文件。
近年来,机械硬盘的容量增长得非常快,传输速率亦然;但是受到磁头臂的制约寻道时间则进步缓慢,因此寻道时间在总的读写时间中的占比持续上升。为了减小寻道次数,两次寻道之间要尽量传输更多的数据。
8、大量事实证明,计算机中的大多数文件都是小文件,而且不到2 KB。而块大小通常不小于4 KB,于是存储小文件时,大量的磁盘空间就被内部碎片浪费了。FFS引入了子块(sub-block)解决此问题。在4 KB块大小的基础上,将其进一步划分为8个512 Bytes的子块。如果文件或文件的一部分占用空间不到4 KB,当文件增大但又没有填满一个块时,文件系统就继续分配子块,直至用到块内的最后一个子块。
在前面的VSFS的讨论中,相信你已经发现了:这样做是很低效的,因为大量的IO并没有花费在数据读写本身。FFS修改了libc库(C标准库),使得通过缓存机制尽量将子块的读写凑成块。
在一条磁道上,如果将相邻的扇区相邻编号,那么在顺序读取的时候会出现这样的情况:例如读取完扇区0之后准备读取相邻的扇区1,但是扇区1在读取扇区0期间已经随着盘片旋转而移走了,于是要读取扇区1只能再等盘片转完差不多一圈。解决方案是将下图左侧的编号方式换成右侧的:两个相邻编号的扇区之间至少隔开一个扇区。在划分扇区时,FFS会根据磁盘的性能参数确定划分的具体方式。
你可能在想:这样子不是会降低性能吗?毕竟在盘片旋转一圈的时间内不是所有的时候都在读取。不过,这个问题已经有了合适的对策:现代的磁盘会先一次性读取整条磁道,放到磁盘缓存(磁道缓冲区)里,然后把缓冲区中的内容按照扇区号递增的顺序整理好再传送给DMA控制器。于是,文件系统就不用考虑这些低级层面的细节了。
9、FFS还是第一个能够支持长文件名的文件系统。此外,FFS还引进了原子性的rename()操作。
10、与内存中的内容不同,对文件系统中的数据的持久性要求更高。在长时间的存放后,这些数据必须要保留下来;而断电或系统故障发生后,文件系统也要尽量确保数据安全。
以在上一章的VSFS下为一个文件追加4 KB内容为例,如果写入过程未完毕就发生了故障,那么可以分为以下六种情况:
(1)只写入了数据区。从一致性的角度来看,这种情况没有问题。但是写入依然是失败的,因为没有相应的索引节点和位映像说明数据已经在此处写入。
(2)只写入了索引节点。这种情况符合不一致性(inconsistency),因为索引节点表明这里发生了写入,但是位映像却表明这里未写入。如果在重新写入之前就读取了索引节点指向的位置,那么就会读到错误的数据(这个位置上存在的旧内容)。
(3)只写入了位映像。这种情况同样符合不一致性。如果不修复错误,就会导致空间泄漏:由于位映像标记此处为已分配,因此这个块将永远不会被其它程序写入。
(4)只写入了索引节点和位映像。满足一致性,但没有真正把数据写入数据区,所以一旦在修复前读取了原先要写入的块,就会读到错误的数据。
(5)只写入了索引节点和数据块。不满足一致性,并且如果有程序申请分配新的磁盘空间,那么这个数据块写入的数据可能会被覆盖。
(6)只写入了位映像和数据块。不满足一致性,并且我们不知道这一块的数据属于哪个文件,因为没有索引节点。
11、fsck(file system checker)是一个UNIX工具,用于查找文件系统中的不一致性并予以修复。其它系统也会有类似的工具(比如Windows下的chkdsk)。需要强调,这类工具并不能修复所有问题:例如上面所说的情况(2)和(4)。毕竟它们不知道你尝试在一个块写入的数据到底是什么。
fsck在被检查的文件系统装载前运行,这样就没有任何其它文件系统活动干扰检查与修复。
·fsck首先检查Superblock是否合理,比如已使用空间是否大于所有已分配块的空间之和。当发现问题后,fsck用另一个副本替换出现问题的超级块。
·接下来,fsck检查索引节点和间接指针块、二重间接指针块等,确定哪些块已被使用。这一步可以修正位映像的问题。
之后检查的是索引节点的内部结构。如果索引节点的结构出现问题,那么这个索引节点会被清除(索引节点位映像也要相应修改)。
·再之后检查索引节点链接。fsck检验每个文件的全部链接是否都正确且与引用计数匹配。为了验证链接数量的正确性,fsck扫描整个目录树,并将目录树临时重新构建一遍,比对引用计数是否不同。引用计数不正确时,将其修正;如果一个索引节点对应的目录没有找到,那么它就被移动到lost+found文件夹。
·然后,fsck检查是否有多个索引节点指向同一个块。如果是,清除多余的节点,或将指向的块复制一个副本。
·下一步检查坏块。当一个指针指向它应该指向的范围之外的内容,就被认为已损坏,比如一个指针指示的位置比分区总大小还大。这种情况下,fsck不能做什么,除了将错误的指针移除。
·最后是目录检查。fsck不知道用户文件的内容,然而fsck知道系统创建的那部分记录怎样才是正确的。fsck检查每个目录的记录条目的完整性,比如.和…一定是前2项,每一项中记录的索引节点都已分配,等等。
fsck有一个问题:它太慢了。当卷的容量很大时,运行一次fsck常常需要几十分钟乃至数小时。而且fsck执行起来的花销比较大。比如如果只在写入几个块的过程中断电,为了找出存在问题的部分,就要对整个分区运行一次fsck。就好像你在体育馆掉了钥匙,结果得找遍整个馆。为了解决fsck开销过长的问题,研究人员与从业者研发了新的方案。
12、预写入日志(write-ahead logging),这个方案似乎是从DBMS那边“偷”来的。在文件系统中,由于历史原因,一般称为journaling。第一个引入日志机制的文件系统是Cedar。Linux ext3、ext4,ReiserFS,IBM的JFS,SGI的XFS,Windows的NTFS都采用了这个机制。
预写日志的基本思想是:在写入之前,先在日志中写入相应记录。当写入意外中止时,可以从日志中找到相关信息,然后重试未完成的步骤。虽然记录日志会使得总的写入量大一点,但是预写日志大大降低了重试写入操作的成本。
13、接下来我们学习ext3的预写日志。ext3的许多结构和ext2一样,都是把磁盘空间分成很多个组:
ext3多了一个区域专门用于预写日志(有时候可能将日志写入专门的文件,或者单独的设备):
日志记录的结构是这样的:
TxB即事务开始(transaction begin),TxE即事务结束(transaction end)。TxB包含了准备进行的对文件系统的更新,以及事务标识符(TID)。中间三个块包含准备写入的内容,这种记录称为物理日志(physical logging);也有另一种方案,就是逻辑记录(logical logging),更简略地记下准备更新的内容,例如“更新数据块Db至文件X”。这种记法复杂些,但省空间。TxE标记结束,也包含TID。
于是,写入过程有两步:
(1)写入日志。把事务写在日志区或日志文件里。
(2)检查点(checkpoint)。将待写入的数据和元数据正式写入到磁盘。
那么,如果在预写日志期间就断电了,会出现什么情况呢?为了防止写入错误的日志,可以把日志记录的这几个块一个一个写入,上一个写完了再写下一个;但是,这很慢。我们希望这5块内容一次性并发地写完。不过,如果并发地写入,可能会这样:例如,磁盘将写入请求调整了顺序,先写入了1、2、3、5块。在日志中写入待更新数据之前,系统断电了。重启后,文件系统并不能识别出第4块没有正确写入,从而直接按照日志记录把未进行的真实写入执行。如果写入的是关键数据,那就糟了:比如写入了错误的超级块,这会导致文件系统无法挂载。
文件系统引入的应对方法是:将TxE块在其它块之后写入。这一步由磁盘来保证,最终文件系统执行写入动作分成了三步:日志写入、日志提交(commit)、检查点。
14、写入屏障(write barrier)能确保在及屏障之前的写入操作都被正式执行,而不是先报告写入完毕,再于随后补完所有的写入动作。许多机器都对磁盘操作的正确性提出了相当高的要求,但不幸的是,一些硬盘厂商所谓的“高性能”硬盘忽略了全部的写入屏障。这种做法虽然让磁盘看起来更快,但是也带来了数据可靠性方面的风险。似乎快的东西总是能打败慢的,即便快意味着出错。
15、在预写日志期间,先写入其它块,后写入结束块,可能会导致性能问题:通常需要额外等待磁盘再转一圈(想一想,为什么)。Vijayan Prabhakaran解决了这个问题:在向预写日志一次性写入事务时,一并写入一个校验和(checksum)。预写日志期间若发生意外,在系统恢复后读取预写日志时,就检验其校验和;若不符合,就意味着日志写入错误,此次更新将被跳过。
这个方法引起了Linux文件系统开发人员的注意,后来被引入到了ext4文件系统中。现在,这种方法应用于至少数百万台Linux(包括Android)机器上。
15、当日志写入错误时,系统在重启后可以放弃更新;如果日志写入正确,可以将被打断的更新重做(redo)。如果是在正式写入的过程中遭遇故障,也可以将这次写入重做。这种额外的写入是小概率事件,因为故障是小概率的;而且,为了保持数据的正确性,这点成本不值一提。
16、为了防止频繁的少量数据写入严重拖累性能,一些文件系统(如ext3)一般会将更新请求屯起来,把它们统一写入到在内存中暂存的预写日志里。正式存盘的时候,这一项日志才被执行。
17、如果不断地写入日志而不释放空间,那么记录日志的空间迟早会用完。一种常见方法是将日志的结构改成循环(circular)的(想一想循环队列)的:当空间写满之后,自动从头开始继续写,就覆盖了之前的日志。于是写入操作分成了四步:预写日志、提交日志、检查点、释放空间(删除相应条目也好,直接循环写入也好)。
18、实现了预写日志功能后,从故障中恢复数据是快了,但是通常的写入就慢了,因为每次写入数据都需要先写入相应的日志。为了减少这种影响,人们尝试了不同的方法。比如可以只在写入用户数据之前写入日志(ext3)。但另一个更简单、更通用的方法是:元数据日志(metadata journaling),也称ordered journaling。当需要写入的数据非常长的时候,这种方法十分有效:日志中不包含数据的部分(注意:目录文件中记录的内容也被视为元数据!)。
不过问题又来了。因为是先写入日志再写入数据的,如果在存盘途中断电了,会怎么样?当系统恢复时,由于数据内容不在日志中,文件系统最多只会重写索引节点及位映像。于是,索引节点又指向不正确的数据了。
解决办法是将数据区在写入日志之前就先行写入。也就是说,一次完整的写入过程分为5步:写入数据区、写入仅包含元数据的日志、提交日志、检查点、释放空间。
NTFS和XFS都实现了这种日志方式。而ext3可以让用户选择不同的日志方式,包括data,ordered和unordered三种。unordered模式下,数据区的写入不一定在写入日志之前。不过,对数据区的写入必须在日志提交之前完成,否则还是有可能会出现上面所说的问题:索引节点指向错误的数据。
19、Linux ext3引入了一种新的日志,称为废除(revoke)记录。删除文件时,会先写入相应的日志。系统恢复时,先扫描这种日志,使得原先写入被删除的数据的日志不会被用于重新执行写入,确保被删除的文件不会被意外恢复,导致覆盖了其它有用的文件。
20、基于后向指针的一致性(backpointer-based consistency,BBC)是一种新的检验一致性的技术。BBC为每一个块都配备一个后向指针。当访问文件时,文件系统可以检查索引节点或直接指针指向的数据块是否有后向指针指向索引节点或直接指针本身。如果没有,则意味着写入未完成,返回一个错误。
还有一个新方法optimistic crash consistency,这个方法通过通用化形式的事务校验和(transaction checksum)尽可能一次进行更多的磁盘写入,并应用了几项新技术来探测不一致性。对于一些负载,这种方法能够提升一个数量级的性能。但是为了发挥好作用,磁盘接口需要做一点改变。详见Vijay Chidambaram所著SOSP’13论文《Optimistic Crash Consistency》。
21、1990年代初,一个Berkeley的研究组(John K. Ousterhout和Mendel Rosenblum带队)开发了一种新型文件系统——日志结构文件系统(log-structured file system,LFS)。设计LFS主要是出于以下原因:
·系统内存容量正在增长。这使得更多数据能够在内存中缓存,也使得越来越多的数据要写入磁盘。因此,写入速率对文件系统性能的影响越来越大
·随机IO和顺序IO性能差距大。硬盘容量密度在增加,但是旋转延迟和寻道时间降低缓慢。制造又小又便宜的高速电机很困难。
·当时已有的文件系统在通用的负载下表现很差。例如FFS在创建不到4 KB的小文件的时候需要进行大量的写入:索引节点及其位映像、所在目录本身及其的索引节点、数据块及其位映像。这个过程就会引发多次寻道,并伴随着旋转延迟。
·文件系统未考虑RAID。例如RAID-4和RAID-5都在写入小文件时非常慢。当时的文件系统并未尽力去避免这种情况。
22、当写入磁盘时,LFS会先将全部更新(包括元数据)都暂存在内存的一个段中;当段写满后,就将整个段一次性写入磁盘。LFS从不覆写已有数据,而总是将段写入空闲位置。因为段比较大,因此写入效率较高,文件系统的性能尽可能被发挥出来。
23、LFS的基本思想很简单:把需要写入的数据尽量写在一起。例如写入数据块时要更新索引节点,就把它们连续放置:
(注意:索引节点的大小远小于数据块。这里为了作图方便,画成一样大的块。)
当然,思想倒是说得简单,实现起来并不容易,因为要注意很多细节。
假设在某时刻请求写入块A,过了很短的时间,又请求在块A + 1处写入。但这时候,A + 1已经转到别的位置去了。要写入这个块,得等盘片再转回来。这反而会降低写入速率。
LFS解决这个问题的策略也是老生常谈的写入缓存技术。“段”这个词都给用烂了,不过用在这里还是比较合适:代表一大段连续的数据。LFS凑出来的一个段大约有几MB。
24、在旧的UNIX文件系统中,查找索引节点很容易,因为它们被放在固定的位置。而且,只需要知道索引节点号×每个索引节点的大小,就可以在索引节点区定位到需要的索引节点。如果是在FFS中,查找索引节点也只是稍复杂一点:FFS将索引节点表分成很多个部分,放到了不同的柱面组里。但在LFS中,查找索引节点就困难了:由上图可见,它们被分散在磁盘的各个位置。更糟糕的是,因为LFS从不覆写,因此如果一个索引节点有更新,更新版本的索引节点会被写到其它位置去,而不是覆盖原有的索引节点。
LFS引入了索引节点映射(inode map,imap)。向imap输入索引节点号,可以查到该索引节点的最新版本的地址。imap可以用一个线性表实现,每一项都存放了一个磁盘指针。每次写入索引节点时,索引节点映射都要更新。
如果将索引节点映射放在固定的位置,那么在写入时,当写完其它区域以后,又要经过寻道来到这个区域更新映射。这对性能较为不利。因此,LFS在更新完数据和索引节点后,就将索引节点映射的一部分写在后面:
索引节点映射分散在磁盘的各个角落,怎么找到它们呢?其实大家应该也能想到了:总要留一些固定的区域来专门指示其位置。LFS称其为检查点区(checkpoint region,CR)。CR包含了指向最新版本的索引节点映射的指针。CR区仅定期更新(比如每隔30秒),所以对总体性能影响不太大。使用LFS的磁盘分区的大致结构就是下面这样子(实际上,LFS可能含有2个CR):
25、下面讲述LFS读取文件的过程。一开始内存里什么都没有,然后LFS的CR最先被读取,接着在磁盘上找到imap的每一个部分,将它们都读入内存。之后,根据文件的索引节点号,LFS在索引节点映射中查找索引节点。索引节点可以包含直接指针、间接指针或二重间接指针,这一点与典型UNIX文件系统相同。一般来说,LFS在读取文件的过程产生的IO数与典型的文件系统相同。
26、LFS存储目录的方式是这样的:
索引节点映射包含了目录文件(索引节点号dir)和新建文件(索引节点号k)的信息。当访问该目录下的文件时,先在索引节点映射中查找目录的索引节点的存储位置,然后通过读取目录的索引节点读到目录文件的位置,然后读取目录文件并根据要访问的文件的名称找到其索引节点号,然后再到索引节点映射中查到文件的索引节点的位置,最终读到需要访问的文件的数据。
27、LFS总是将更新(包括修改、追加)后的文件在新的空白位置写入。旧版本怎么办?也许可以一直保留,方便用户恢复(特别是在不小心覆盖一个文件时)。这种文件系统称为版本式文件系统(versioning file system)。不过LFS不这样做。它总是只保留最新版本,于是它会在后台周期性地搜寻旧版本的文件,连同其索引节点和其它结构一起清除。这个过程称为垃圾收集(garbage collection)。
清除旧版本文件时,LFS按照分段的基本策略来执行。为了判断一段数据是否最新,LFS在每一段开头都添加了段摘要块(segment summary block)。这一块包括了每个数据块的索引节点号、文件内偏移等信息。判断期间,对每一个块,从所在段的段摘要块中查找其索引节点号和偏移。下一步,在索引节点映射(通过检查点区找到)中查找该索引节点号并读取其指向的内容(如果这部分信息在内存那就更好了)。最后,通过偏移,查找索引节点(或间接指针块)内部,看看到底将这个偏移指向哪个位置。如果正好指向该数据块,就认为它是最新版本,不能清除。如果没有指向这个数据块,就能判断出它是旧版本,已经不再需要。
LFS还使用了别的方法来加速判定。当一个文件被删除时,LFS也会增加其版本号并在索引节点映射中记录新版本的信息。如果段内也记录了这个信息,那么当读到这个段时,就可以比对段内的版本号和索引节点映射中的版本号来判定这个段是否为最新版本。
28、LFS中,决定何时清理旧版本很容易:周期性清理,或者空闲时清理,或者磁盘空间即将耗尽时清理均可。而选择清理哪些块就比较具有挑战性了,很多研究论文就是围绕这一点的。在LFS原本的论文中,作者介绍了一个区分热段和冷段的方法。一个热段是时常被更新的段,而一个冷段的大多数内容基本保持不变。因此,冷段需要先被清理。当然,这个策略并不完美。后来的研究也给出了更佳的方法。
29、万一在LFS写入的过程中,系统发生致命错误了,怎么办?
通常的操作中,LFS会缓冲一个段的写入动作,稍后(比如段满了,或者等待时间到了)再统一执行。LFS也会把这些写入操作记录在日志中,比如检查点区指向头段和尾段,每一段都指向下一个要写入的段(最后一段指向下一段的指针为空)。LFS会周期性更新检查点区。系统失败在这些动作期间都有可能发生。
对于CR的更新,LFS实际上维护两个CR,每个在磁盘的一端,交替写入。LFS也实现了一个很谨慎的协议,用于通过指向imap和其它信息的最新指针更新CR。具体而言,先写入一个头(带有时间戳(timestamp)),然后写入CR的主体,再写入最后一个块(一样带有时间戳)。如果在CR更新期间断电了或者OS炸了,由于最后的时间戳没有写入,LFS就能知道CR写入过程出现了失败。LFS总是选择使用时间戳一致的CR,这确保了CR的一致性。
LFS定期写入CR,所以上一个成功的更新可能比较久了。所以在重启时,LFS能够轻易通过读取检查点区、检查点区指向的索引节点映射片段、相应的文件和目录来进行恢复。不过,在上一次的成功存盘到致命错误之前的这段时间的更新就丢失了。
为了改进这一点,LFS引入了数据库中的常用技术——roll forward。其基本思路是:从上一次更新完毕的检查点区开始,找到日志记录的末尾(在CR里),然后通过日志不断读取下一个数据段,考察是否有有效更新。如果有,LFS就更新文件系统,于是许多在上一次检查点区更新以后才更新的数据和元数据就得以恢复。详情请查阅Mendel Rosenblum的获奖毕业论文《Design and Implementation of the Log-structured File System》。