引言
为什么要损耗平衡(wear-leveling)?
Flash上的每一位(bit)可以被写操作置成逻辑0。 可是把逻辑 0
置成逻辑 1 却不能按位(bit)来操作,而只能按擦写块(erase block)为单位进行擦写操作。一般来说,“NOR
flash擦写块的大小是128K,Nand
flash擦写块的大小是8K”【2】。从上层来看,擦写所完成的功能就是把擦写块内的每一位都重设置(reset)成逻辑 1。
Flash的使用寿命是有限的。“具体来说,flash的使用寿命由擦写块的最大可擦
写次数决定。超过了最大可擦写次数,这个擦写块就成为坏块(bad
block)了。因此为了避免某个擦写块被过度擦写,以至于它先于其他的擦写块达到最大可擦写次数,我们应该在尽量小的影响性能的前提下,擦写操作均匀的
分布在每个擦写块上。这个过程叫做损耗平衡(wear leveling)”【1】。按照现在的技术水平,一般来说NOR
flash擦写块的最大可擦写次数在十万次左右,NAND flash擦写块的最大可擦写次数在百万次左右。
而传统文件系统对于损耗平衡,以及嵌入式系统的掉电保护都没有很好的考虑,所以需要专门的文件系统来读写FLASH。
现有的嵌入式文件系统主要有哪些?
JFFS2(The Journaling Flash File System
2), YAFFS2 (Yet Another Flash File System
2)等,对于其文件系统的具体分析,不再过多叙述,有兴趣的话可以看看我的参考文献,上面均有详细的分析。本文也就这两种算法的损耗算法进行分析。
正文
JFFS2的损耗平衡算法:
在JFFS2中,Flash被分成一个个擦写块。NOR flash
读/写操作的基本单位是字节;而 NAND flash 又把擦写块分成页(page), 页是写操作的基本单位,一般一个页的大小是 512 或
2K 字节,另外每一页还有16 字节的备用空间(SpareData, OOB)。
JFFS2
维护了几个链表来管理擦写块,根据擦写块上内容的不同情况,擦写块会在不同的链表上。具体来说,当一个擦写块上都是合法(valid)的节点时,它会在
clean_list 上;当一个擦写块包含至少一个过时(obsolete)的节点时,它会在 dirty_list
上;当一个擦写块被擦写完毕,并被写入 CLEANMARKER 节点后,它会在 free_list 上。下表表示了具体链表名:
表1 JFFS2的链表中擦除块名称一览表【3】
链表 |
链表中擦除块的性质 |
clean_list |
只包含有效数据结点 |
very_dirty_list |
所含数据结点大部分都已过时 |
dirty_list |
至少含有一个过时数据结点 |
erasable_list |
所有的数据结点都过时需要擦除。但尚未“调度”到erase_pending_list |
erasable_pending_wbuf_list |
同erase_pending_list,但擦除必须等待wbuf冲刷后 |
erasing_list |
当前正在擦除 |
erase_pending_list |
当前正等待擦除 |
erase_complete_list |
擦除已完成,但尚未写入CLEANMARKER |
free_list |
擦除完成,且已经写入CLEANMARKER |
bad_list |
含有损坏单元 |
bad_used_list |
含有损坏单元,但含有数据 |
现在开始分析具体源代码,源程序文件路径为:uClinux-dist/linux-2.6.x/fs/jffs2
>jffs2/fs.c
在挂载jffs2文件系统时,在jffs2_do_fill_super函数的最后创建并启动GC内核线程,相关代码如下:
if (!(sb->s_flags & MS_RDONLY))
jffs2_start_garbage_collect_thread(c);
return 0;
如果jffs2文件系统不是以只读方式挂载的,就会有新的数据实体写入flash。而
且jffs2文件系统的特点是在写入新的数据实体时并不修改flash上原有数据实体,而只是将其内核描述符标记为“过时”。系统运行一段时间后若空白
flash擦除块的数量小于一定阈值,则GC被唤醒用于释放所有过时的数据实体【3】。
为了尽量均衡地使用flash分区上的所有擦除块,在选择有效数据实体的副本所写入的擦除块时需要考虑Wear Leveling算法,jffs2_find_gc_block()即Wear Leveling算法的关键函数:
>jffs2/gc.c
static struct jffs2_eraseblock *jffs2_find_gc_block(struct jffs2_sb_info *c)
{
struct jffs2_eraseblock *ret;
struct list_head *nextlist = NULL;
int n = jiffies % 128; //0~127的随机数
/* Pick an eraseblock to garbage collect next. This is where we'll
put the clever wear-levelling algorithms. Eventually. */
/* We possibly want to favour the dirtier blocks more when the
number of free blocks is low. */
if (!list_empty(&c->bad_used_list) && c->nr_free_blocks > c->resv_blocks_gcbad) {
D1(printk(KERN_DEBUG "Picking block from bad_used_list to GC next\n"));
nextlist = &c->bad_used_list; //含有损坏单元,但含有数据的块中先回收
} else if (n < 50 && !list_empty(&c->erasable_list)) {
/* Note that most of them will have gone directly to be erased.
So don't favour the erasable_list _too_ much. */
D1(printk(KERN_DEBUG "Picking block from erasable_list to GC next\n"));
nextlist = &c->erasable_list;// 如果有已过时,但尚未擦除的块,50/128的概率回收 } else if (n < 110 && !list_empty(&c->very_dirty_list)) {
/* Most of the time, pick one off the very_dirty list */
D1(printk(KERN_DEBUG "Picking block from very_dirty_list to GC next\n"));
nextlist = &c->very_dirty_list;//如果有大部分数据已经过时的块,110/128的概率回收 } else if (n < 126 && !list_empty(&c->dirty_list)) {
D1(printk(KERN_DEBUG "Picking block from dirty_list to GC next\n"));
nextlist = &c->dirty_list;//如果有部分数据已经过时的块,126/128的概率回收
} else if (!list_empty(&c->clean_list)) {
D1(printk(KERN_DEBUG "Picking block from clean_list to GC next\n"));
nextlist = &c->clean_list;//回收只含有有效数据的块
} else if (!list_empty(&c->dirty_list)) {
D1(printk(KERN_DEBUG "Picking block from
dirty_list to GC next (clean_list was empty)\n"));
nextlist = &c->dirty_list;//回收有部分数据已经过时的块
} else if (!list_empty(&c->very_dirty_list)) {
D1(printk(KERN_DEBUG "Picking block from very_dirty_list to GC next (clean_list and dirty_list were empty)\n"));
nextlist = &c->very_dirty_list;//回收大部分数据已经过时的块
} else if (!list_empty(&c->erasable_list)) {
D1(printk(KERN_DEBUG "Picking block from
erasable_list to GC next (clean_list and {very_,}dirty_list were
empty)\n")
); nextlist = &c->erasable_list;//回收已过时,但尚未擦除的块
} else {
/* Eep. All were empty */
D1(printk(KERN_NOTICE "jffs2: No clean, dirty _or_ erasable blocks to GC from! Where are they all?\n"));
return NULL;//已经无可调度的块
}
ret = list_entry(nextlist->next, struct jffs2_eraseblock, list);
list_del(&ret->list);
c->gcblock = ret;
ret->gc_node = ret->first_node;
if (!ret->gc_node) {
printk(KERN_WARNING "Eep. ret->gc_node for block at 0x%08x is NULL\n", ret->offset);
BUG();
}
/* Have we accidentally picked a clean block with wasted space ? */
if (ret->wasted_size) {
D1(printk(KERN_DEBUG "Converting wasted_size %08x to dirty_size\n", ret->wasted_size));
ret->dirty_size += ret->wasted_size;
c->wasted_size -= ret->wasted_size;
c->dirty_size += ret->wasted_size;
ret->wasted_size = 0;
}
D1(jffs2_dump_block_lists(c));
return ret;
}
由上可见,JFFS2的处理方式是以
50/128的概率回收erasable_list,以110/128概率回收 very_dirty_list,以126/128概率回收
dirty_list,剩下概率回收 clean_list。当然从程序中也可以看出,回收后一条链表块,是要在一定概率以及前面的块链表为空的情况下。
JFFS2 对损耗平衡是用概率的方法来解决的,这很难保证损耗平衡的确定性。在某些情况下,可能造成对擦写块不必要的擦写操作;在某些情况下,又会引起对损耗平衡调整的不及时。
“据说后来,JFFS2有改进的损耗平衡补丁程序,这个补丁程序的基本思想是,记录每
个擦写块的擦写次数,当flash上各个擦写块的擦写次数的差距超过某个预定的阀值,开始进行损耗平衡的调整。调整的策略是,在垃圾回收时将擦写次数小的
擦写块上的数据迁移到擦写次数大的擦写块上。这样一来我们提高了损耗平衡的确定性,我们可以知道什么时候开始损耗平衡的调整,也可以知道选取哪些擦写块进
行损耗平衡的调整”【1】。
现在,JFFS3也正在开发中,在垃圾回收一章中,作者有
这样的话:“JFFS3 Garbage Collection is currently under development and this
chapter may be changed. Any suggestions and ideas are
welcome.”【4】,还没有发布具体的设计(05年写得啊。。。)。我期待Artem B.
Bityutskiy能设计出更加巧妙而高效的平衡算法。
YAFFS2的损耗平衡算法:
YAFFS是专门针对Nand flash的文件系统,YAFFS1只能支持每页为512B。而YAFFS2则另外也支持2K的页面, 其他方面变化见网页介绍:。
YAFFS
对文件系统上的所有内容(比如正常文件,目录,链接,设备文件等等)都统一当作文件来处理,每个文件都有一个页面专门存放文件头,文件头保存了文件的模
式、所有者id、组id、长度、文件名、Parent Object ID
等信息。因为需要在一页内放下这些内容,所以对文件名的长度,符号链接对象的路径名等长度都有限制。
前面说到对于Nand
flash上的每一页数据,都有额外的空间用来存储附加信息,通常Nand flash
驱动只使用了这些空间的一部分,YAFFS正是利用了这部分空间中剩余的部分来存储文件系统相关的内容。以512+16B为一个PAGE 的Nand
flash芯片为例, YAFFS 文件系统数据的存储布局如下所示:
表2 YAFFS的存储布局【5】
0-511 |
Data |
512-515 |
YAFFS TAG |
516 |
Data status byte |
517 |
Block status byte |
518-519 |
YAFFS TAG |
520-522 |
后256字节的ECC |
523-524 |
YAFFS TAG |
525-527 |
前256字节的ECC |
可以看到在这里YAFFS 一共使用8个BYTE 用来存放文件系统相关的信息(yaffs_Tags)。这8 个Byte 的具体使用情况按顺序如下:
表3 yaffs_Tags的具体使用情况【5】
位 |
属性 |
20 |
ChunkID,该page 在一个文件内的索引号 |
2 |
2 bits serial number |
10 |
ByteCount 该page 内的有效字节数 |
18 |
ObjectID 对象ID 号,用来唯一标示一个文件 |
12 |
Ecc, Yaffs_Tags 本身的ECC 校验和 |
2 |
保留 |
其中Serial Number 在文件系统创建时都为0,以后每次写具有同一ObjectID 和ChunkID 的page 的时候都 加1,
因为YAFFS 在更新一个PAGE 的时候总是在一个新的物理Page 上写入数据,再将原先的物理Page 删除,所以该serial
number 可以在断电等特殊情况下,当新的page 已经写入但老的page
还没有被删除的时候用来识别正确的Page,保证数据的正确性。这对保存数据的完整性是很有用的。
YAFFS按顺序分配当前块中的页。当该块中所有的页都用光后,另外一个干净的块将会
被选择分配。至少保留2到3个块用于垃圾块收集。当没有足够的干净块的时候,脏块(比如只包含丢弃数据)将会被擦除成干净块。如果没有脏块,那些特别脏的
块(含有丢弃数据特别多的块)也会被垃圾块回收算法选中。
作者也指出,YAFFS的损耗平衡是块分配策略的副产品,数据总是被写在下一个空闲块
中,所有的块都是被平等对待的。但是包含那些永久数据的块却不会再回收到空闲块中,所以他所说的损耗平衡也仅仅指那些已经空闲或者有可能变空闲的块。假设
在一块8M的NAND器件上,有2M空间用来存放相对固定,不经常修改的数据文件,则经常修改的文件只能在剩下的6M空间上重复擦写。只在这6M空间上做
到均匀损耗,而没有包括剩余的2M。“对整个器件来说,系统并没有合适的搬移策略对固定文件进行搬移,整个器件做不到均匀损耗,在实时记录信息量比较大的
应用环境中,应编写相应的搬移策略函数,对固定文件进行定期的搬移,以确保整个NAND器件的均匀损耗”【7】。
下面开始分析源代码,其公司的主页上可以下到最新源代码:
可以看下它回收算法的关键函数yaffs_CheckGarbageCollection():
>fs/yaffs/yaffs_guts.c
/*
如果可用的擦除块很少,我们就采用“主动模式”来收集垃圾块,否则我们就采用“被动模式”。“主动模式”中将会查看整个Flash的块,就算只有少量过时数据的块也会被回收。“被动模式”只会查收少量区域,然后回收那些过时数据特别多的块。
*/
static int yaffs_CheckGarbageCollection(yaffs_Device * dev)
{
int block;
int aggressive;
int gcOk = YAFFS_OK;
int maxTries = 0;
int checkpointBlockAdjust;
if (dev->isDoingGC) {
return YAFFS_OK;
}
do {
maxTries++;
checkpointBlockAdjust = (dev->nCheckpointReservedBlocks - dev->blocksInCheckpoint);
if(checkpointBlockAdjust < 0)
checkpointBlockAdjust = 0;
if (dev->nErasedBlocks < (dev->nReservedBlocks + checkpointBlockAdjust)) {
/* 跟需要保留的块数和自定义校准块的和作比较,如果少于这个值,就直接选择主动模式*/
aggressive = 1;
} else {
/* 否则,选择被动模式 */
aggressive = 0;
}
/*根据模式查找,回收相应的块*/
block = yaffs_FindBlockForGarbageCollection(dev, aggressive);
if (block > 0) {
dev->garbageCollections++;
if (!aggressive) {
dev->passiveGarbageCollections++;
}
T(YAFFS_TRACE_GC,
(TSTR
("yaffs: GC erasedBlocks %d aggressive %d" TENDSTR),
dev->nErasedBlocks, aggressive));
gcOk = yaffs_GarbageCollectBlock(dev, block);
}
if (dev->nErasedBlocks < (dev->nReservedBlocks) && block > 0) {
T(YAFFS_TRACE_GC,
(TSTR
("yaffs: GC !!!no reclaim!!! erasedBlocks %d after try %d block %d"
TENDSTR), dev->nErasedBlocks, maxTries, block));
}
} while ((dev->nErasedBlocks < dev->nReservedBlocks) && (block > 0)
&& (maxTries < 2));
return aggressive ? gcOk : YAFFS_OK;
}
从上面的算法也可以看出,其实YAFFS的损耗平衡算法也没什么可讲的,主要是由于设计的原因,导致其平等地回收每一个空闲块。如果空闲块比较少,就加紧回收稍微有空闲的块。因为设计简单,所以效率还是不错的,下表是其测试结果,结果还是相当不错:
表4 Times for 1MB of garbage collection at 50% dirty (单位1uS).【6】
Operation |
YAFFS1 |
YAFFS2(512b pages) |
YAFFS2 (2kB pages) |
YAFFS2(2kB pages, x16) |
Delete 1MB |
558080 |
128000 |
16000 |
16000 |
Write 0.5MB |
337920 |
337920 |
168960 |
112640 |
Total |
896000 |
465920 |
184960 |
128640 |
MB/s |
1.1 |
2.1 |
5.4 |
7.7 |
Relative speed |
1 |
1.9 |
4.9 |
7 |
总结及展望:
通过阅读JFFS2和YAFFS的文献的代码,我觉得谁优谁劣很难讲。JFFS的文件
系统开发要比YAFFS要早得多,现在应用范围也比YAFFS广。可以说YAFFS就是相当于JFFS的“另一种文件系统”,所以YAFFS一开始就是从
借鉴JFFS开始的,避免了JFFS启动速度过慢和损耗不平衡等明显的缺点,当然后来JFFS2也进行了改进,如把数据移动到页的末尾(而YAFFS是放
在页的前面),而不再是链表式在页中随便乱放,导致装载速度巨慢。在YAFFS公司网页的开发内容中也可以看到,YAFFS和JFSS2的开发人员经常进
行交流。从我个人的角度来看,YAFFS2要比JFFS2好些,当然这是仅限于Nand
flash来说。在阅读YAFFS2的源代码时我发现,最新的更新日期是07年3月,而就我所看到的JFFS2代码是04年5月,而JFFS3的设计草案
【4】的发布日期也是05年11月。两种文件系统的维护速度可见一斑。
以上的分析我也只能定性得说下,如果有Flash芯片可以测试,那就可以定量的分析了。如果有时间,我想再深入看下两类文件系统的源代码,顺便推荐下,参考文献【3】确实写得很认真,值得一看。
参考文献:
【1】、JFFS2文件系统及新特性介绍:
【2】、David Woodhouse, JFFS : The Journalling Flash File System
【3】、shrek2(的一个ID),《JFFS2源代码情景分析(Beta2)》
【4】、Artem B. Bityutskiy, JFFS3 design issues(Version 0.32 (draft))
【5】、Raymond(),《Yaffs文件系统结构及应用》
【6】、YAFFS Development Notes:
【7】、胡一飞,徐中伟,谢世环,《NAND flash上均匀损耗与掉电恢复在线测试》