《闪存数据库概念与技术》
厦门大学数据库实验室 林子雨 编著
(本网页是第2章内容,全书内容请点击这里访问本书官网,官网提供本书整本电子书PDF下载)
上层应用需要借助于闪存文件系统或者FTL机制,来完成对闪存芯片的直接管理和控制。虽然闪存文件系统和FTL机制的设计并非直接服务于数据库,但是,了解二者的工作机制,对于解决闪存数据库中的诸多问题,具有重要的借鉴作用,因此,本章重点介绍闪存文件系统,并介绍两款典型的闪存文件系统产品JFFS2和Yaffs,然后,在接下来的第3章,将会详细介绍FTL机制。
2.1 闪存文件系统和闪存转换层的比较
闪存存储管理的主要目的就是为上层应用提供硬件抽象,主要包括两种方式,即闪存文件系统和闪存转换层(FTL:Flash Translation Layer)。如图[FTL-JFFS-comparison]所示,前者是直接针对闪存设计的文件系统,比如JFFS2[Woodhouse01](Journalling Flash File System Version 2)和YAFFS[One08],后者则是通过一个中间层来隐藏闪存的底层硬件细节,把闪存设备模拟成一个块设备,上层应用可以和访问磁盘一样的方式来访问闪存设备。闪存文件系统只能针对特殊厂商的闪存设备,无法广泛应用关于各种不同类型的闪存设备,通用性不强;闪存转换层可以将现有的各种常用的磁盘管理技术移植到闪存设备上,可以把各个不同厂商的闪存产品模拟成类似磁盘的块设备,具有广泛的应用范围。闪存文件系统比FTL具有更高的性能,一般用于固定的、非插拔的NAND闪存管理。表[FTL-JFFS-comparison-table]给出了闪存文件系统和闪存转换层的特点比较。
大多数闪存文件系统都借鉴了日志文件系统的设计思想,实践证明,这种设计非常适用于闪存存储设备。在传统的、不是基于日志的文件系统中,为了提高文件系统的效率,通常采用缓冲区来进行数据的读写。但是,这种方式也存在一定的隐患,比如,当发生断电事故时,如果缓冲区中的数据还没有全部刷新写入到磁盘,就会引起部分数据的丢失,而且这种数据丢失是无法恢复的。缓冲区中可能会包含一些关键数据,比如文件系统的管理信息,丢失这些数据会导致文件系统数据组织的混乱,甚至引起文件系统的崩溃。
日志文件系统可以很好地克服上述缺陷,它会在磁盘中维护一个日志文件,所有数据更新都以追加的方式写入到日志中,每个更新操作都对应于日志中的一条记录。日志文件的尾部包含了文件系统中的最新数据。当系统发生断电事故后,只需要扫描日志就可以恢复还原文件。
当前针对闪存文件系统的研究主要包括文件系统日志管理、文件系统快速初始化、文件系统崩溃恢复、垃圾收集和页面分配技术等。
图[FTL-JFFS-comparison] 闪存转换层和闪存文件系统的工作方式区别
表[FTL-JFFS-comparison-table] 闪存转换层和闪存文件系统的特点比较
闪存文件系统 | 闪存转换层 | |
原理 | 直接针对闪存设计的文件系统 | 通过一个中间层把闪存设备模拟成一个块设备 |
通用性 | 通用性不强 | 具有广泛的应用范围 |
代表产品 | JFFS/JFFS2/JFFS3、LFM、YAFFS | 各种FTL机制,比如BAST、LAST |
2.2 闪存文件系统JFFS2
JFFS2是一个典型的基于日志结构的日志文件系统,在嵌入式Linux系统中得到了广泛的应用,帮助上层应用完成对闪存芯片的直接管理和控制。JFFS2文件系统针对闪存的特性,提出了有效的管理策略,从而获得了较好的运行效率。
本节简要介绍闪存文件系统JFFS2,并指出它的不足之处。
2.2.1 JFFS2概述
JFFS2是根据JFFS改进得到的,最初的JFFS是一个纯粹的基于日志的文件系统,包含数据和元数据的节点会被顺序地存放到闪存芯片上,在可用的闪存空间里严格地、按照线性方式使用存储空间。在JFFS当中,在日志中只有一种类型的节点,使用了结构体类型jffs_raw_inode来表示。JFFS2的节点类型更加灵活,可以定义新的节点类型。
JFFS2文件系统主要包括三个功能模块:块分配模块、垃圾回收模块和磨损均衡模块,各个模块的功能如下:
- 块分配模块:负责维护闪存中的可用空闲块,决定可以使用的下一个空闲块;
- 垃圾回收模块:负责跟踪闪存中的处于“无效”状态的块,当闪存空间不足时,启动垃圾回收过程,回收无效块,产生新的空闲块;
- 磨损均衡模块:负责在闪存不同的物理块之间均匀地分布写操作,延长闪存寿命。
JFFS2把闪存块组织成多个链表(如表[JFFS2-block-list]所示),主要包括:干净块链表、脏块链表、空闲块链表。每个链表实际上就是一个按照“先进先出”原则组织的队列。当干净块链表中的某个块中的数据发生更新时,就从干净块链表中摘掉这个块,对块中的相应节点做标记后,把该块再放入到脏块链表的尾部。脏块链表中的块,都是要执行擦除操作的候选块,在执行垃圾回收时,可以选择脏块链表头部的块进行擦除。空闲块链表中的块,则是已经执行过擦除操作的空白块,可以被分配给后续到达的写操作。
表[JFFS2-block-list] JFFS2文件系统的链表类型
链表名称 | 说明 |
clean_list | 干净块链表,块内包含了没有发生更新的有效数据 |
dirty_list | 脏块链表,块内包含了部分已经被更新过的无效数据(或脏数据) |
free_list | 空闲块链表,已经被擦除过的、可用的空闲块 |
JFFS2文件系统被挂载时,会调用一个函数listType()扫描每个块,并根据具体情况把每个块分别放入相应的链表中,即放入到干净块链表、脏块链表或者空闲块链表。
function listType()
{ for each 闪存块b do { if(b是擦除过的可用空闲块)then 把b添加到空闲块链表尾部; else if(b中有过时数据)then 把b添加到脏块链表尾部; else if(b中包含的数据全部都是有效数据)then 把b添加到干净块链表尾部; } } |
随着应用程序对闪存空闲块的不断消耗,可用的空闲块的数量会变得越来越少,当减小到低于某个事先设定的阈值时,JFFS2文件系统会调用garbageCollection()函数,启动垃圾回收过程。如图[JFFS2-garbage-collection]所示,垃圾回收过程开始以后,系统从脏块链表中选择一个可以回收的块,如果该块中包含有效数据,还需要把有效数据复制合并到其他空闲块中,然后对该块执行擦除操作。需要特别指出的是,为了实现块的均衡磨损,garbageCollection()函数在选择要擦除的块时,会以99%的概率从脏块链表中选择一个块,以1%的概率从干净块链表中选择一个块。
function garbageCollection()
{ 产生一个随机数random; if(random mod 100不等于0)then 从脏块链表中选中链表头部的块; else 从干净块链表中选中链表头部的块; 擦除选中的区块; } |
图[JFFS2-garbage-collection] JFFS2文件系统的垃圾回收过程
2.2.2 JFFS2的不足之处
JFFS2文件系统的不足之处包括以下几个方面:
- 具有较长的挂载时间:JFFS2的挂载过程需要从头到尾扫描闪存块,需要耗费较长的时间。
- 磨损平衡具有随机性:JFFS2在选择要擦除的块时,会以99%的概率从脏块链表中选择一个块,以1%的概率从干净块链表中选择一个块。这种概率的方法,很难保证磨损的均衡性。在某些情况下,甚至可能造成对块的不必要的擦除操作,或者引起磨损平衡调整的不及时。
- 可扩展性较差:JFFS2在挂载时,需要扫描整个闪存空间,因此,挂载时间和闪存空间大小成正比。另外,JFFS2对内存的占用量也和闪存块数成正比。在实际应用中,JFFS2最大能用在128MB的闪存上。
此外,JFFS2在NAND闪存上可能无法取得很好的性能,主要原因包括:
- NAND闪存设备通常要比NOR闪存设备的容量大许多,因此,在和闪存管理相关的数据结构方面,前者也会比后者大许多。而且,JFFS2的挂载时间和闪存空间大小成正比,因此,对于容量普遍较大的NAND闪存设备,JFFS2的扫描时间会较长,无法取得好的性能。
- NAND闪存设备是以页为单位访问数据,如果只想访问页中的一部分数据,也必须顺序读取整个页的全部数据,而无法跳过不需要的数据。这就减慢了扫描和挂载的过程。
2.3 闪存文件系统Yaffs
2.3.1 Yaffs概述
Yaffs是一个专门为NAND和NOR闪存设计的开源文件系统[Yaffs],它已经被广泛应用于Linux和RTOSs等操作系统中。Yaffs是Yet Another Flash File System的缩写,是由查尔斯.曼宁(Charles Manning)在2001年提出的。Yaffs的第一个版本是在2001年年末到2002年年初开发的,到了2002年中期的时候,Yaffs开始采用不同的产品。在2002年5月,Yaffs被正式宣布,并产生了更多的衍生项目;6月,开始提供对其他操作系统的支持;9月,Yaffs被宣布可以应用于Linux设备上。
Yaffs代码包括两种版本,即最初版本的Yaffs代码和当前版本的Yaffs2代码。Yaffs2代码支持由最初版本的Yaffs代码提供的功能,并且提供了扩展的功能,可以支持额外的操作模式,这些操作模式对于更大、更现代的NAND闪存而言是必需的。Yaffs2提供了和Yaffs代码的前向兼容性。Yaffs、Yaffs1和Yaffs2三种术语的区分如下:
- Yaffs1是一种更加简单的操作模式,使用了“删除标记位”来记录闪存页的状态。
- Yaffs2是一种更加复杂的操作模式,可以支持更大类型的闪存,这类闪存不能使用删除标记位。
- Yaffs是Yaffs1和Yaffs2这二者的公共操作模式。
Yaffs1作为Yaffs的最初版本,通常只会工作在页大小为512字节的闪存设备上,采用了一个类似于SM卡(SmartMedia)的内存布局,但是,Yaffs1已经被扩展为可以支持更大的闪存页,比如页大小为1K的Intel M18闪存。Yaffs是Yaffs1的升级产品,可以支持更大的、不同的设备。Yaffs很容易进行封装,目前已经被应用到不同的产品中,比如POS机、电话和航空设备等。Yaffs也已经被应用到多个不同的操作系统中,可以支持的操作系统包括Linux、Windows CE和eCOS等。
注:SmartMedia(SM卡)是一种快闪存储器,由Toshiba(东芝)公司在1995年夏季推出,用来对抗MiniCard、CompactFlash和PC Card等存储卡标准。SM卡曾是数码相机普遍支持的存储格式,得到了富士相机和奥林巴斯相机的大力支持,并在2001年左右达到巅峰,当时差不多占据了50%的市场份额。但是,2001之后SM卡开始走下坡路,这其中包含多个方面的原因:(1)无法提供大容量的存储,几乎看不到128MB以上容量的SM卡;(2)随着数码相机尺寸的不断缩小,SM卡的尺寸相对而言就显得太大了;(3)奥林巴斯放弃支持SM卡,转而支持SD卡。总之,由于缺少更多的新设备支持SM卡,SM卡从市场上消失只是时间问题。 |
Yaffs被设计成可以在多种环境下工作,这就催生了对可移植性的需求。对可移植性的支持,也改进了测试工作,因为,在应用环境中要比在一个操作系统内核中更加容易进行代码的开发和测试工作。可移植性使得Yaffs可以在具有更少资源的情况下,获得更快的发展。Yaffs改进可移植性的主要策略包括:
- 在主体代码中不使用与操作系统相关的特性;
- 在主体代码中不使用与编译器相关的特性;
- 使用抽象类型和函数来支持Unicode和ASCII操作。
为了增强鲁棒性,便于集成和开发,Yaffs采用了尽可能简单的设计理念,主要策略是:
- Yaffs采用单线程模型;Yaffs的加锁粒度是分区,这要比在更低粒度(比如块或页)上使用锁更加简单。
- 基于日志结构的文件系统使得垃圾回收更加简单。
在Yaffs中,空间分配的单元是“厚片”(chunk)。通常,一个厚片和一个NAND闪存页是一致的,但是,厚片更加灵活,可以包含多个页,这种灵活性使得系统可以根据需要进行配置。许多个厚片构成一个块,一个块是执行擦除操作的基本单元。NAND闪存可能在出厂时就有坏块,而且在设备使用过程中也会出现坏块,因此,为了可以探测到坏块,Yaffs必须对坏块进行标记和检测。为了实现这个目的,NAND闪存通常会使用一些错误探测机制和错误纠正码。Yaffs可以使用闪存自带的ECC或者使用自己的方法,来实现闪存错误检测。
2.3.2 Yaffs体系架构
图[Yaffs-structure]给出了Yaffs体系架构图,从中可以看出Yaffs文件管理系统在整个应用环境中所处的位置,以及Yaffs文件系统、实时操作系统(RTOS)、闪存设备、Yaffs接口(Yaffs Direct Interface)和POSIX之间的关系。使用Yaffs接口,可以把Yaffs移植到一个嵌入式环境或者实时操作系统中。
图[Yaffs-structure] Yaffs体系架构图
2.3.3 Yaffs1如何存储文件
Yaffs1和Yaffs2在存储文件的方式上,存在一些差别。Yaffs2采用了真正的日志结构,即任何时候都只能顺序追加写入日志记录。而Yaffs1则采用了修改过的日志结构,使用了“删除标记位”,破坏了顺序写入日志的规则,而Yaffs2则不使用删除标记位。
Yaffs1并没有给任何文件分配指定的数据存储区域,因此,每个文件的数据并非被写入到与该文件相关的区域,而是以顺序日志的形式写入闪存。日志中的每条日志记录,都占用闪存的一个厚片(注意,如果“厚片”这个概念不好理解的话,可以直接把“厚片”理解成“页”)。Yaffs1把厚片分成两种不同的类型:
(1)数据厚片:包含了常规数据内容,也就是文件中保存的数据;
(2)对象头部:是关于一个对象的描述符,包括父目录的标识符、对象名称等。Yaffs中的每个对象可以是一个常规文件、目录、硬链接、符号链接或特定链接。对于一个文件对象而言,当文件创建、文件裁减或文件关闭时,都可能会创建一个对象头部,通过对象头部中存储的元数据,就可以获得对象名称和文件长度等信息。
每个厚片都有和它关联的标签,包括以下字段:
- 对象编号:用来确定一个厚片属于哪个对象;
- 厚片编号:用来确定一个厚片属于文件的哪个部分;厚片编号是0,表示这个厚片存储了一个对象头部。厚片编号是1,表示这个厚片是文件中的第一个厚片,厚片编号是2,表示这是文件中的下一个厚片,依此类推。
- 删除标记位:只在Yaffs1中存在,Yaffs2中不存在,表示这个厚片不再有用。
- 字节计数:如果这是一个数据厚片,就表示数据的字节数。
- 序列号:只在Yaffs1中存在,Yaffs2中不存在,当几个厚片具有相同的对象编号和厚片编号时,就可以使用序列号加以区分。
为了更好地理解Yaffs1的文件存储方式,这里给出一个简单的实例。这里假设每个块中包含了4个厚片,开始的时候,所有的厚片都是可用的。下面分成六步执行各种操作,在每步结束后,这里都会给出各个块中的各个厚片的状态,并给予简单描述。
第一步:创建一个文件。文件系统为新文件分配了第1块第0厚片,用来存储新文件的对象头部,该厚片的“删除标记位”是“有效”,说明厚片中包含的数据当前是有效的,没有被删除。新文件的对象编号是300,由于当前文件中不包含任何数据,因此,对象头部中关于文件长度的元数据的值是0。由于第1块第0厚片存储的是对象头部,因此,厚片编号是0。
块 | 厚片 | 对象编号 | 厚片编号 | 删除标记位 | 备注 |
1 | 0 | 300 | 0 | 有效 | 文件的对象头部 (长度是0) |
第二步:写一些数据到文件中。这里连续写入数据到三个厚片中,因此,第1块的第1、2、3厚片,都是数据厚片,保存了刚写入的数据,这些厚片的“删除标记位”是“有效”。新写入的这三个厚片的数据,都属于对象编号为300的文件,因此,这三个厚片的“对象编号”值都是300。第1块第1厚片是文件的第一个数据厚片,因此,厚片编号是1;第1块第2厚片是文件的第二个数据厚片,因此,厚片编号是2;第1块第3厚片是文件的第三个数据厚片,因此,厚片编号是3。
块 | 厚片 | 对象编号 | 厚片编号 | 删除标记位 | 备注 |
1 | 0 | 300 | 0 | 有效 | 文件的对象头部 (长度是0) |
1 | 1 | 300 | 1 | 有效 | 第一个数据厚片 |
1 | 2 | 300 | 2 | 有效 | 第二个数据厚片 |
1 | 3 | 300 | 3 | 有效 | 第三个数据厚片 |
第三步:关闭文件。由于在第二步中为文件写入了新数据,因此,在关闭文件时,必须为这个文件创建一个新的对象头部,使得它能够反映这种变化。由于第1个块只能容纳4个厚片,已经满了,因此,需要在第2块第0厚片存储这个新的对象头部,并把“对象编号”值设置为300,删除标记位设置为“有效”,并且要在对象头部中记录新的文件长度n。原来保存在第1块第0厚片中的对象头部,就会废弃,因此,把这个厚片的删除标记位设置为“删除”。由于第2块第0厚片存储的是对象头部,因此,厚片编号是0。
块 | 厚片 | 对象编号 | 厚片编号 | 删除标记位 | 备注 |
1 | 0 | 300 | 0 | 删除 | 废弃的对象头部(长度是0) |
1 | 1 | 300 | 1 | 有效 | 第一个数据厚片 |
1 | 2 | 300 | 2 | 有效 | 第二个数据厚片 |
1 | 3 | 300 | 3 | 有效 | 第三个数据厚片 |
2 | 0 | 300 | 0 | 有效 | 新的对象头部(长度为n) |
第四步:打开文件进行读写,把文件中的第一个数据厚片(即第1块第0厚片)进行重写,然后关闭文件。由于闪存采用异地更新,因此,重写操作并非直接对第1块第0厚片的数据进行修改,而是把重写后的新数据写入到新分配的第2块第1厚片中,并把原来旧的第1块第0厚片废弃掉,即把它的删除标记位设置为“删除”。由于第2块第1厚片存储的是文件的第一个厚片,因此,厚片编号是1。此外,由于文件发生了更新,因此,必须为这个文件创建一个新的对象头部,使得它能够反映这种变化。这里在第2块第2厚片中写入新的对象头部,原来旧的对象头部所在的厚片(第2块第0厚片),就会被废弃掉,即把该厚片标记为“删除”。
块 | 厚片 | 对象编号 | 厚片编号 | 删除标记位 | 备注 |
1 | 0 | 300 | 0 | 删除 | 废弃的对象头部 |
1 | 1 | 300 | 1 | 删除 | 废弃的第一个数据厚片 |
1 | 2 | 300 | 2 | 有效 | 第二个数据厚片 |
1 | 3 | 300 | 3 | 有效 | 第三个数据厚片 |
2 | 0 | 300 | 0 | 删除 | 废弃的对象头部 |
2 | 1 | 300 | 1 | 有效 | 新的第一个数据厚片 |
2 | 2 | 300 | 0 | 有效 | 新的对象头部(长度为n) |
第五步:把文件的大小变为0,方法是:使用O_TRUNC打开文件,然后关闭文件。由于文件长度发生了变化,必须为这个文件创建一个新的对象头部,这里在第2块第3厚片中写入新的对象头部,原来旧的对象头部所在的厚片(第2块第2厚片),就会被废弃掉,即把该厚片标记为“删除”。由于文件已经被裁减成长度为0,不包含任何数据内容,因此,需要把几个数据厚片(即第1块第2厚片,第1块第3厚片,第2块第1厚片),都废弃掉,即把它们的删除标记为设置为“删除”。通过这种方式,文件就实现了裁减,数据厚片中的数据就被裁减掉了。
块 | 厚片 | 对象编号 | 厚片编号 | 删除标记位 | 备注 |
1 | 0 | 300 | 0 | 删除 | 废弃的对象头部 |
1 | 1 | 300 | 1 | 删除 | 废弃的第一个数据厚片 |
1 | 2 | 300 | 2 | 删除 | 第二个数据厚片 |
1 | 3 | 300 | 3 | 删除 | 第三个数据厚片 |
2 | 0 | 300 | 0 | 删除 | 废弃的对象头部 |
2 | 1 | 300 | 1 | 删除 | 删除的第一个数据厚片 |
2 | 2 | 300 | 0 | 删除 | 废弃的对象头部 |
2 | 3 | 300 | 0 | 有效 | 新的对象头部(长度为0) |
第六步:这个时候,第1块中的所有厚片都已经被标记为“删除”,这就意味着第1块不再包含有用的信息,因此,现在可以擦除第1块并且重用空间。这里可以重命名文件,为了完成这个事情,可以在第3块第0厚片中为这个文件写入一个新的对象头部,并把原来旧的第2块第3厚片中的对象头部废弃掉。
块 | 厚片 | 对象编号 | 厚片编号 | 删除标记位 | 备注 |
1 | 0 | 擦除 | |||
1 | 1 | 擦除 | |||
1 | 2 | 擦除 | |||
1 | 3 | 擦除 | |||
2 | 0 | 300 | 0 | 删除 | 废弃的对象头部 |
2 | 1 | 300 | 1 | 删除 | 删除的第一个数据厚片 |
2 | 2 | 300 | 2 | 删除 | 废弃的对象头部 |
2 | 3 | 300 | 0 | 删除 | 废弃的对象头部 |
3 | 0 | 300 | 0 | 有效 | 新的对象头部显示了新的文件名称 |
通过观察可以发现,第2块现在只包含标记为“删除”的厚片,因此,也可以被擦除和重用。需要指出的是,厚片中的标签告诉我们一些非常有用的信息,包括哪个厚片属于哪个对象、厚片在文件中的位置、哪个厚片是当前的等等。具备这些信息以后,我们就可以在系统挂载的时候通过扫描各个厚片来重新创建文件的状态。这就意味着,当系统失败的时候(失败可能发生在顺序写入的任何时间点),我们就可以把系统恢复到失败发生之前的那个点。
2.3.4 垃圾回收
由于闪存采用异地更新,这导致一个闪存块中会同时包含一些有效厚片和删除厚片。随着系统的运行,被标记为删除的厚片会越来越多,导致可用的闪存空间会越来越少。由于这些删除厚片分散在不同的闪存块中,而且和有效厚片夹杂在一起,因此,必须采用有效的垃圾回收机制来回收这些被删除厚片占据的闪存空间。垃圾回收对于闪存文件系统而言,通常是影响性能的决定性因素。许多闪存文件系统,需要在一次垃圾回收过程中做许多工作。Yaffs在每次只需要处理一个块,这就减少了在一次垃圾回收过程中所需要完成的工作量,因此,减少了系统阻塞时间。
Yaffs垃圾回收过程如下:
- 利用启发式算法寻找一个值得回收的块,如果没有找到,就退出垃圾回收过程;
- 遍历块中的每个厚片,如果这个厚片正在使用,就为这个厚片创建一个新的副本,并且删除原来的厚片。修改RAM数据结构,从而反映这种变化。
一旦块中的所有厚片都已经被删除,这个块就可以被擦除重用。注意,上面的步骤(2)中,虽然在对某个厚片执行复制以后删除了原来厚片,但是,实际上没有必要执行真正的删除操作,因为,整个块会被执行擦除操作。
为了改善系统整体性能,Yaffs会尽可能延迟垃圾回收过程,从而减少垃圾回收的次数。Yaffs用来确定一个块是否值得回收的启发式算法如下:
- 如果还存在许多可用的擦除块,那么,Yaffs就不需要竭尽全力执行垃圾回收过程,而是只会尝试对那些只包含很少有效厚片的块执行垃圾回收,这个过程称为“被动垃圾回收”,它把一个块的垃圾回收工作分摊到多个垃圾回收过程中。
- 当擦除块已经很少时,Yaffs就需要竭尽全力执行垃圾回收过程,来回收更多的空间,这时会对那些包含较多有效厚片的块也进行回收,这个过程称为“积极垃圾回收”,整个块就会在单个垃圾回收过程中被收回。
2.3.5 Yaffs1序列号
Yaffs1在发生文件修改时,经常需要执行“删除旧厚片和写入新厚片”的操作,如果这个执行过程发生故障,就会导致系统中同时存在多个具有相同标签值得厚片,那么,该如何判断哪个厚片是旧厚片,哪个厚片是当前厚片呢?Yaffs1采用序列号解决这个问题。
为了说明序列号的重要作用,现在假设没有序列号,看看会发生什么问题。首先考虑一个文件的重命名操作,也就是说,要把一个对象头部厚片用一个新的厚片(包含新名称)来代替。为了完成这个重命名操作,Yaffs需要完成以下工作:
- 删除旧的厚片;
- 写一个新的厚片;
如果上述执行过程中系统部发生任何故障,整个过程可以顺利执行结束,重命名操作就可以顺利完成。但是,这里假设系统在完成上述工作的过程中会发生故障,在删除操作执行后,文件系统失败了,这时,系统就会缺少相应的厚片与相应的标签值对应,这就可能会引起文件丢失。为了避免这个问题,我们必须在删除旧的厚片之前写入新的厚片。但是,这种做法仍然无法彻底解决问题,这种做法的操作序列如下:
- 写入一个新的厚片;
- 删除旧的厚片;
再次假设系统在完成上述工作的过程中会发生故障,当写入一个新的厚片这个操作完成以后,发生了系统失败,这时,系统中就会存在两个厚片和同一个标签值对应,同样无法判定哪个厚片是当前值。
为了解决上述问题,Yaffs1引入了序列号。在Yaffs1中,每个块都被标记为一个2位的序列号,每次当一个具有相同标签的厚片的值被更新替换时,序列号都会递增,可以通过检查序列号来确定哪个是当前的厚片。由于序列号只会在删除旧的厚片之前增加1,一个2位的序列号就足够了。表[old-current-chunk]给出了有效的<旧厚片,当前厚片>配对情况。
表[old-current-chunk] 旧厚片序列号和当前厚片序列号配对情况
旧厚片序列号 | 当前厚片序列号 |
00 | 01 |
01 | 10 |
10 | 11 |
11 | 00 |
因此,根据表[old-current-chunk],就可以通过附加在旧厚片和新厚片上面的序列号,来确定哪个是旧的厚片,哪个是新的厚片,就可以删除旧的厚片。
2.3.6 Yaffs2 NAND模型
Yaffs2是Yaffs的一个扩充,用来实现一个新的目标集合,包括:
第一,在一个块内执行顺序厚片写操作。更多现代NAND闪存的可靠性规范,通常都假设顺序写。由于Yaffs2没有写入删除标记,一个块内部的厚片写操作是严格顺序执行的。
第二,零重写。Yaffs1和Yaffs2的很大区别就是,前者有删除标记位,而后者没有,后者是真正的顺序日志结构。虽然Yaffs1只需要在厚片的“带外区域”修改一个字节,就可以设置删除标记位,但是,更多现代闪存设备都不允许重写,因此,为了更好地支持更现代、更大的闪存设备,Yaffs2根本就不执行重写,真正实现了“零重写”。不过,在Yaffs2中仍然会使用厚片删除的概念,只不过被删除的厚片会在内存数据结构中设置删除标记,这个删除标记不会被写入到闪存中。但是,这里有一个问题,如果闪存中的厚片没有删除标记位,那么Yaffs2在执行扫描时是怎样识别哪个厚片是当前的、哪个厚片是已经被删除的呢?为了能够区分当前厚片和旧厚片,Yaffs2提供了另外一种机制,具体如下:
(1)顺序号。Yaffs2的顺序号和Yaffs1的序列号不是一个概念。随着每个块被分配,Yaffs2文件系统的顺序号会递增,块中的每个厚片都会被标记为该顺序号。顺序号可以确定日志的时间先后顺序,帮助Yaffs恢复文件系统状态,这是通过以时间的顺序后向扫描日志来实现的,即从最高顺序号扫描到最低顺序号。由于采用后向扫描,最近被写入的“对象编号:厚片编号”配对,会被首先扫描,所有后续的相应厚片一定是废弃的,将会被视为删除。在对象头部中的文件长度,可以被用来跟踪重新修改文件长度后的文件。如果一个对象头部已经被读取,那么,那些超出这个文件长度的后续的数据厚片,很显然就是废弃的,会被视为删除。
(2)缩减头部标志。用来标记对象头部,写入这种标志后,可以缩减文件大小。后面会详细阐述这个问题。缩减头部标记的用途,解释起来会有点绕。缩减头部标记是用来表明一个文件的长度已经发生缩减,可以防止这些对象头部被垃圾回收过程删除。为了更好地理解这个问题,这里假设如果不存在缩减头部标记的话,将会发生什么情况?
为了简化起见,这里假设在下面操作执行过程中没有发生垃圾回收,这种假设是完全可以保证做到的。
现在考虑一个文件,它经历了下面的操作:
第1步:f = open(“xmu”,O_CREAT| O_RDWR, S_IREAD|S_IWRITE); /*创建文件*/
第2步:write(f,data,6*1024*1024); /*写入6 MB数据*/
第3步:truncate(f,2*1024*1024); /*把文件长度裁减到2MB */
第4步:lseek(f,3*1024*1024, SEEK_SET); /*把文件访问位置设置到3MB的位置*/
第5步:write(f,data,1024*1024); /* 写入1MB数据*/
第6步:close(f);/*关闭文件*/
经过上述操作以后,文件的长度将会是4MB,但是,在2MB和3MB之间会存在一个“洞”,其中包含的数据是已经被裁减掉的,不应该被读取,根据POSIX标准,这个洞应该总是被读取为0。
注:POSIX表示可移植操作系统接口(Portable Operating System Interface,缩写为 POSIX 是为了读音更像UNIX)。POSIX标准最初是由美国电气和电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)最初开发的,目的是为了提高 UNIX 环境下应用程序的可移植性。然而,POSIX 并不局限于 UNIX。许多其它的操作系统,例如DEC OpenVMS也支持POSIX标准。 |
Yaffs2在NAND闪存中处理上面的文件操作过程时,会产生下面的厚片序列:
- 文件创建后的对象头部(文件长度是0);
- 6MB的数据厚片(0到6MB);
- 发生文件裁减后的对象头部(文件长度是2MB);
- 1MB的数据厚片(3MB到4MB);
- 文件关闭后的对象头部(文件长度是4MB)。
在上面这些厚片中,只有下面这些厚片包含了当前最新值:
- 在上面第2步中创建的第1个1MB数据;
- 在上面的第4步中创建的1MB数据;
- 在上面的第5步中创建的对象头部。
Yaffs在读取文件内容时,应该只去读取包含当前数据的厚片,而不能去读取“洞”中的数据,否则,就会得到不正确的结果。为了避免读取“洞”中的数据从而获得正确的结果,Yaffs必须记住在第3步中发生的文件裁减操作。为此,当创建了一个洞的时候,Yaffs会首先写入一个“缩减头部”,用来表明洞的开始,并且写入一个常规对象头部,用来表明洞的结束。“缩减头部标记”改变了垃圾回收过程的行为,从而确保这些缩减头部不会被垃圾回收过程给擦除掉,直到确认安全时才会被擦除。这对于那些大量使用带洞的文件系统而言,这样做可能会带来负面性能影响。此外,缩减头部也可以表明一个文件已经被裁减,但是文件中被裁减部分的记录没有丢失。
2.3.7 坏块和NAND错误的处理
NAND闪存被设计成具有很低的代价和很高的密度。为了获得更高的产出,从而减小产品成本,NAND闪存通常都会在出厂时包含一些坏块,此外,在闪存设备使用过程中,也会出现一些新的坏块。因此,对于任何闪存文件系统而言,如果不具备坏块处理机制,就不适合用来管理闪存。
Yaffs1使用SM卡类型的坏块标记,它会检查闪存页的带外区域的第6个字节(字节5),如果是一个好块,这个字节应该是0XFF,如果一个块在出厂时已经被标记为坏块,这个字节应该是0X00。当闪存设备在后续使用过程中出现坏块时,Yaffs1会使用自己的方式对坏块进行标记,从而和工厂标记的坏块加以区分。当读或写操作失败的时候,或者当三个ECC错误被探测到的时候,Yaffs就会把一个块标记为“坏块”。ECC可以通过硬件实现,也可以通过软件驱动实现,或者在Yaffs自身内部实现。需要再次强调的是,任何缺乏有效ECC处理机制的闪存文件系统,是不适合作为闪存管理的。如果Yaffs1确定一个块是坏块,它就会把该块标记为0X59(’Y’)。一个块被标记为坏块以后,就会退出使用,从而改进了文件系统的鲁棒性。
Yaffs2模式被设计成支持更大范围的设备和带外区域布局。因此,Yaffs2没有确定带外区域的哪个字节作为标记位,而是调用驱动函数来确定一个块是否是坏块以及标记它为坏块。Yaffs2没有提供内置的ECC,而是需要由驱动程序提供ECC。
2.3.8 内存数据结构
在理论上,只需要很少的内存数据结构就可以实现一个基于日志结构的文件系统,但是,这会降低系统性能。因此,需要设计有效的内存数据结构,来存储足够的信息,从而提供较高的性能。
在介绍具体的内存数据结构之前,需要首先指出Yaffs中的各种内存数据结构到底服务于什么目的。主要的内存数据结构是在yaffs_guts.h中定义的,它们的主要目的是:
- 设备/分区:名称是yaffs_dev,用来维护和Yaffs分区或挂载点相关的信息,可以允许Yaffs同时支持多个分区和不同类型的分区。实际上,几乎其他内存数据结构都是这个数据结构的一部分,或者通过这个数据结构来访问。
- NAND块信息:名称是yaffs_block_info,维护了闪存块的当前状态。每个yaffs_dev都有一组这类信息。
- NAND厚片信息:这是一个附加在yaffs_ dev上面的“位字段”,维护了系统中每个厚片的当前使用情况。分区中的每个厚片存在与之对应的一个位。
- 对象:名称是yaffs_obj,维护了一个对象的状态。在文件系统中,每个对象都有一个yaffs_obj,每个对象可以是一个常规文件、目录、硬链接、符号链接或特定链接。每个对象通常使用对象编号进行唯一标识。yaffs_obj的主要功能是,存储绝大多数的对象元数据。这就意味着,关于一个对象的绝大多数信息,都可以被立即访问,而不需要读取闪存。元数据包括对象编号、指向父目录的父指针、短名称、对象类型和授权等。特别需要说明的是,短名称具有很好的用途,如果对象名字足够短,就可以放入一个小的固定尺寸的数组中,不需要每次在请求的时候都从闪存中读取出来。
- 文件结构:对于每个文件结构,Yaffs会为其维护一棵树,可以定位一个文件中的数据厚片的位置。树中包含了称为yaffs_tnode的节点。
- 目录结构:目录结构允许对象采用名称进行查找,提供了一种快速的对象查找机制。
- 对象编号哈希表:它提供了一种根据对象编号来寻找对象的机制,在垃圾回收、扫描和类似操作中,都需要用到这个特性。每个yaffs_dev使用一个哈希表,哈希表有256个桶,每个对象编号会被使用一个哈希函数来找到与之对应的哈希桶。每个哈希桶都有一个属于该桶的对象的双向链表,每个哈希桶同时包含了该桶中对象数目的计数器。Yaffs会尽量采用合理的策略来分配对象编号,从而使得每个哈希桶中的对象数量尽可能低,这样可以防止任何哈希桶中的双向链表过长。
- 缓存:Yaffs提供了一个读/写缓存,它可以明显改进短操作的性能。缓存的尺寸可以在运行时进行设定。
下面的内容会重点介绍两种内存数据结构的使用方法,即目录结构和文件对象。
2.3.8.1 目录结构
目录结构的目的,就是通过名字快速访问一个对象。对于执行文件操作而言,比如打开、重命名,这是必须的。Yaffs目录结构是采用一个对象树来组织的。每个yaffs_obj对象,都有一个称为兄弟的双向链表节点,它会把一个目录中的所有兄弟节点链接到一起。每个Yaffs分区都有一个根目录,根目录下面可以创建子目录。每个Yaffs分区也都有一些特定目的的“伪目录”,它不会被保存到NAND闪存中,而是在挂载的时候创建的:
- “丢失+查找”目录:这个目录是用来存储任何丢失的文件部分,它们已经不能被保存到正常的目录中。
- “解链(unlinked)和删除”目录:这些都是伪目录,不会出现在目录树中。在这些目录中放置对象,表明它们是处于解链和删除的状态。
每个对象都有一个名称,可以通过对象名称在目录中查找一个对象。因此,一个目录中的对象名称必须是唯一的。这个规则对于已经解链或删除的伪目录中的对象不适用,因为,我们从来不会使用对象名称来查询解链或删除的文件(可能会存在许多名称相同的、已经被删除的文件)。
可以通过两种方式来加速名称的解析:
- 把短名称保存到yaffs_obj的目录中,从而不必从闪存中加载。
- 每个对象都有一个“名称概要”,这是一个关于对象名称的简单的哈希结果,可以加速匹配的过程。因为,比较哈希结果要比比较对象名称来得快。
图[Yaffs-directory-tree] 一个Yaffs目录结构的实例
图[Yaffs-directory-tree]给出了Yaffs目录结构的一个实例,它对应的目录详细信息如表[Yaffs-directory]所示。可以看出,根目录下存在两个一级子目录A和B,其中,子目录A是空目录,不包含任何子目录和文件,目录B下存在一个二级子目录C和两个数据文件D,E,目录C可能会继续包含更多的子目录,但是,为了简化起见,图中没有给出。
表[Yaffs-directory] 一个Yaffs目录结构的实例
/ | 根目录 |
/A | 目录A是空目录 |
/B | 目录名称是B |
/lost+found | 空目录 |
/B/C | 目录名称是C |
/B/D | D是数据文件 |
/B/E | E是数据文件 |
2.3.8.2 文件对象
每个文件对象都有一棵tnode树,它是一个分层的索引结构,用来提供从文件逻辑位置到实际的NAND闪存厚片物理地址的映射。文件对象存储了下面的主要信息:
- 文件尺寸;
- topLevel:Tnode树的深度;
- top:指向Tnode树顶端的指针。
top和topLevel一起工作来控制tnode树。
图[yaffs-tnode-tree] Yaffs文件对象的tnode树
如图[yaffs-tnode-tree]所示,tnode树是由tnode节点的层次结构构成的,每个tnode节点都保存了以下信息:
- 在第0层及其以上的层:这些层上的tnode节点也被称为内部节点,一个内部节点具有8个指针,指向其它在它下面层的tnode节点;
- 在第0层:一个tnode具有16个NAND闪存厚片编号,用来确定RAM内存中的厚片位置。
当文件刚被创建的时候,它只会被分配一个低层的tnode。当文件的内容超出了单个tnode节点可以容纳的范围的时候,系统会为它分配第二个tnode节点,同时创建一个内部节点指向这两个tnode节点。随着文件尺寸的增加,更多的tnode节点会被增加进来,当一个层满时,会产生更多的层。如果文件被裁减,或者如果一个文件中的厚片被替换(即数据被重写或者被垃圾回收过程复制走了),那么,tnode节点树必须被更新,从而反映这种变化。
2.3.9 不同机制是如何工作的
前面内容已经详细介绍了Yaffs的数据结构,现在来阐述Yaffs的一些工作机制。
2.3.9.1 块和厚片的管理
2.3.9.1.1 块状态
Yaffs会跟踪操作中的每个块和厚片的状态,这些信息首先是在扫描过程中(或从检查点恢复中)被构建的。表[yaffs-block-state]给出了一个块可能处于的各种状态。
表[yaffs-block-state] Yaffs中块的各种状态
状态 | 说明 |
UNKNOWN | 块的状态还是未知的 |
NEEDS_SCANNING | 在预扫描过程中,已经确定这个块上面包含一些需要的东西,需要被扫描 |
SCANNING | 这个块当前正在被扫描 |
EMPTY | 这个块上面什么都没有(已经擦除过的),可以用来分配。当块被擦除过以后,就可以进入这种状态 |
ALLOCATING | 这个块当前正被厚片分配器用来进行分配 |
FULL | 这个块中的所有厚片都已经被分配,至少一个厚片还包含有用信息,并且没有被删除 |
DIRTY | 块中的所有厚片,都已经被分配,所有都已经被删除。在这种状态之前,这个块可能已经在FULL或者COLLECTING状态。这个块现在可以被擦除,并且返回到EMPTY状态 |
CHECKPOINT | 这个块中包含了检查点数据,当检查点失效的时候,这个块就可以被擦除,并且返回到空状态 |
COLLECTING | 这个块正在被垃圾回收,只要所有有效数据已经被检查到,这个块就变成DIRTY状态 |
DEAD | 这个块已经退休,被标记为坏块,这是一个块的最终状态 |
图[yaffs-block-state-transform] Yaffs块的状态转换图
图[yaffs-block-state-transform]给出Yaffs块的状态转换图。常规的运行时状态是:EMPTY,ALLOCATING,FULL,COLLECTING,DIRTY。COLLECTING状态是为了给块设置检查点。DEAD状态是块的最终状态,或者损坏,或者发生了错误。
2.3.9.1.2 块和厚片的分配
所有可用的厚片,必须在重写之前被分配。厚片的分配机制是由yaffs_AllocatedChunk()提供的,非常简单。每个分区都有一个当前块用来进行分配,这个块被称为“分配块”。厚片是从分配块中顺序进行分配的。当厚片被分配的时候,块和厚片的管理信息会被更新。当块被分配完毕的时候,另外一个空块就会被选择成为分配块。
2.3.9.1.3 关于磨损均衡
闪存块只能执行有限次数的擦除操作,一个闪存文件系统需要确保不让少数块被过分擦除。这对一个文件系统(比如FAT)来说是至关重要的,因为,这些文件系统只会用有限数量的逻辑块来存储文件分配表条目,而这些条目会被频繁地修改。如果FAT采用了静态的逻辑到物理块的映射,那么,用来存储文件分配表的块,会磨损得更频繁。因此,对于FAT而言,有一点是非常重要的,那就是,需要适当变化逻辑到物理的映射,从而使得文件分配表被写入到不同的物理块中,以此来达到均衡磨损的目的。
磨损均衡对于基于日志结构的文件系统而言,通常不是个问题。因为,基于日志结构的文件系统总是把日志记录顺序地追加写入到尾部,因此就不需要总是磨损同一个块。
通常有两种方式可以获得磨损均衡:
- 采用一个特定函数集合来执行磨损均衡;
- 把磨损均衡作为其他操作的副产品,即其他操作完成的过程也同时提供了一定程度的均衡磨损。
Yaffs采用了第二种方案。首先,由于是基于日志的结构,Yaffs通过顺序写入日志就已经内在地实现了磨损均衡。其次,块是从分区中已经擦除的块中顺序地分配的,因此,擦除的过程和分配块的过程,也提供了一定程度的磨损均衡。因此,在Yaffs中,即使没有代码来执行专门的磨损均衡策略,磨损均衡也是作为其他操作的副产品发生了。这种策略执行地很好,已经在模拟器和真实的NAND闪存上都表现出了较好的性能。
2.3.9.2 内部缓存
在实际应用场景中,一些应用会执行许多小数据量的读和写操作,比如一次读或写几个字节的操作。这种操作行为对于闪存性能而言会产生严重的负面影响,而且会影响闪存寿命。因为,闪存写操作是以页为单位,小数据量的写操作会浪费大量的闪存空间,导致闪存空间利用率很低,而且,小数据量的写操作也会增加垃圾回收过程的开销,因为,需要进行大量的擦除操作。
Yaffs内部缓存的主要目的就是减少应用对NAND闪存的访问次数。这种缓存机制非常简单,主要是为那些不怎么复杂的、又缺乏自身文件缓存层的操作系统而设计的。
Yaffs缓存被保存在一个缓存条目数组中,缓存条目的数量是在yaffs_Device配置中设置的,当缓存条目数量为0时,就意味着让缓存失效。缓存管理算法也很简单,很可能无法扩展到较大的数量,最好保持5到20个缓存条目。每个缓存条目保存了以下信息:(1)对象编号,厚片编号,被缓存的厚片的标签信息;(2)实际的数据类型;(3)缓存状态(计数器)。
在Yaffs缓存中找到一个可用的缓存条目是一个非常简单的操作。我们只需要遍历缓存条目,找到一个不忙的条目即可。如果所有的缓存条目都是忙的,那么就会执行一个缓存驱逐操作,把一个已有的缓存条目中的数据驱逐出缓冲区,腾出空间,Yaffs使用LRU算法来选择LRU缓存条目。一旦一个缓存条目被访问,它的计数器就会增加1,这个计数器用来给LRU缓存替换算法提供参考信息。已经被修改过的、并且还没有被写回闪存的缓存条目,会被添加一个脏标记,从而告诉我们在刷新或驱逐缓存页的时候,是否需要把页写回到闪存中。这种缓存替换策略非常简单,却提供了很好的性能。
2.3.9.3 扫描
当挂载一个Yaffs分区时,需要通过扫描来建立状态信息,这个过程需要耗费一定的时间,因为需要从系统中所有有效的厚片中读取标签值。对于Yaffs1和Yaffs2而言,二者的扫描过程是不同的。
2.3.9.3.1 Yaffs1扫描
Yaffs1进行扫描工作时,会首先从分区中的所有厚片中读取标签值,然后据此判定厚片是否有效,如果没有设置删除标记位,那么它就是有效的,扫描过程中各个块的读取顺序是不重要的,可以采用任何顺序来读取块。对于任何有效的厚片,根据厚片的类型不同(可能是数据厚片或对象头部),可以采用以下方式把它添加到文件系统中:
- 如果厚片是一个数据厚片(厚片编号大于0),那么,这个厚片必须被增加到相应的文件对象的tnode树中;
- 如果厚片是一个对象头部(厚片编号等于0),那么,对象必须被创建;
- 如果在扫描过程中的任何时刻,发现正在读取具有相同的“对象编号:厚片编号”的另一个厚片,那么可以使用2位的序列号来处理冲突。
2.3.9.3.2 Yaffs2扫描
Yaffs1采用了删除标记位,因此,它的扫描过程相对Yaffs2而言就显得更加简单。但是,Yaffs2没有删除标记位,这就使得扫描的过程更加复杂。Yaffs2进行扫描工作时需要做的第一件事情是,对块进行预扫描,从而确定块的顺序号。块的链表会根据顺序号进行排序,形成一个按照时间顺序排列的链表。然后,Yaffs会对这些块进行后向扫描(也就是反向时间顺序),因此,第一个遇到的“对象编号:厚片编号”配对,就是当前的厚片,后面的具有相同标签值的厚片都是过期的数据。
Yaffs会读取每个块的每个厚片中的标签,如果它是一个数据厚片(即厚片编号大于0),那么:
- 如果此前有一个具有相同的“对象编号:厚片编号”的厚片已经被找到,那么,现在这个厚片肯定不是当前数据,删除这个厚片;
- 否则,如果这个厚片被标记为删除或污染,就删除它;
- 否则,如果对象还不存在,那么这个厚片是从上次对象头部写入之后写入的,因此,这个厚片一定是文件中最后写入的厚片,这种情形一定是在一个“不彻底关闭(unclean shutdown)”之前发生的,而这时文件仍然是保持打开的。我们可以创建这个文件,然后使用厚片编号和厚片标签信息来设置文件内容;
- 否则,如果对象确实存在,厚片大小超过了对象文件的扫描长度,那么,它就是一个裁减后的厚片,可以被删除;
- 否则,把它放入到文件的tnode树中。
如果它是一个对象头部,即厚片编号等于0,那么:
- 如果这个对象在被删除的目录里,那么就把这个对象标记为删除,并且把这个对象的缩减长度设置为0;
- 否则,如果之前还没有发现这个对象的对象头部,那么,现在这个对象头部就是当前的对象头部,并且保留了当前的名称。如果之前还没有发现针对这个厚片的数据厚片,那么,在这个对象头部中的长度就是文件的长度,这个文件长度用来确定文件的扫描长度;
- 否则,如果文件长度比当前的扫描长度更小,就只利用文件长度作为扫描长度;
- 否则,这是一个废弃的对象头部,可以删除。
这里需要对文件扫描长度做更多的说明,它的目的是确定应该被忽略的数据厚片,这是由于文件裁减引起的。
2.3.9.3.3 检查点
挂载扫描需要耗费大量时间,也减慢了挂载的过程。检查点是一种加速挂载的机制,它通过获取Yaffs在卸载时的运行时状态的一个快照,然后在再次挂载时重新构建运行时状态。有了检查点机制以后,只有在以下情形中需要执行扫描工作:(1)挂载一个Yaffs分区时,如果不存在检查点数据;(2)挂载过程被告知忽略检查点数据。
实际的检查点机制非常简单。一系列的数据会被写入到一个块的集合,这个块集合被标记为专门用来保存检查点数据,重要的运行时状态会被写入到数据流。并非所有的状态都需要被保存,只有需要用来重构运行时数据结构的状态才需要被保存。例如,文件元数据就不需要保存,因为,很容易通过“延迟加载”进行加载。
检查点采用以下顺序存储数据:开始标记(包括检查点格式编号)、Yaffs_Device设备信息、块信息、厚片标记、对象(包括文件结构)、结尾标记、校验和。
检查点的有效性是通过下面的机制来保证的:
- 采用任何可用的ECC机制来存储数据;
- 开始标记包含了一个检查点的版本信息,从而使得一个废弃的检查点(如果这个检查点代码发生了变化),不会被读取;
- 数据被写入到数据流,作为结构化的记录,每个记录都具有类型和大小;
- 当需要的时候,结束标记必须被读取;
- 在整个检查点数据集合中,需要维护两个校验和。
如果任何检查失败,检查点就会被忽略,状态必须被重新扫描。这里需要注意写检查点块时的块状态变化,那么,如何才能正确地重建块状态呢?这是通过下面的机制来实现的:
- 当写检查点的时候,会给检查点块的分配一个EMPTY块。当正在写检查点时,Yaffs并不会改变块的状态,因此,它们被写入到检查点的状态为EMPTY或CHECKPOINT,至于到底是哪个状态,其实并不是很重要;
- 在读取检查点之后,就可以知道哪个块已经被用来存储检查点数据,因此,可以更新这些块来反映CHECKPOINT状态。
任何修改(写或擦除)都可以让检查点失效,因此,任何修改路径都会检查是否存在一个检查点,如果存在就擦除它。常规的Yaffs块分配器和垃圾收集程序,也必须知道检查点的正确大小,从而确保有足够的空间可以保存检查点。
2.4 本章小结
本章内容首先对闪存文件系统和闪存转换层进行了特点比较,并指出了二者在工作方式上的差异;然后介绍了闪存文件系统JFFS2,它是一个典型的基于日志结构的日志文件系统,在嵌入式Linux系统中得到了广泛的应用,帮助上层应用完成对闪存芯片的直接管理和控制;最后,介绍了闪存文件系统Yaffs,详细阐述了它的体系架构、文件存储、垃圾回收、坏块和NAND错误处理、内存数据结构及其各种工作机制。
2.5 习题
- 阐述闪存文件系统和闪存转换层的各自特点。
- 说明闪存文件系统JFFS2的不足之处。
- 说明闪存文件系统Yaffs的各种内存数据结构及其作用。
- 阐述闪存文件系统Yaffs的垃圾回收过程。
- 阐述闪存文件系统Yaffs的检查点机制的工作原理。