作者: WuYJ@263.net.cn
摘要:设计一种能够在典型嵌入式环境下应用的线性文件系统,为嵌入式系统Flash空间的管理提供一种非常有效的手段。它包装和通用文件系统类似的API接口,设计的实现独立于实时操作系统(RTOS)和具体的Flash典型,可方便移植到不同的嵌入式应用中。
在嵌入式系统中,为了便于对闪存(Flash)空间进行管理,会采用文件的形式来访问Flash。目前,可以购买到的Flash文件系统一般都是兼容DOS的文件系统(Flash File System,FFS),这对需要一个具有复杂的目录层次,并且DDS文件兼容的系统来说是必要的;但是对大多数的嵌入式应用来说,这种文件系统太过奢侈。笔者在参与嵌入式系统项目的时候,设计了一种线性文件系统,它适用于大多数的嵌入式应用对Flash文件系统的需求。
线性文件系统设计基于三个目标:一是提供给应用程序通过文件名而不是物理地址访问系统Flash的能力;二是文件系统的设计独立于实时操作系统(RTOS),这样可以很容易移植到不同的嵌入式应用中;三是设计统一的底层接口,适应不同的Flash类型。本文设计的线性文件系统为典型的嵌入式系统提供了所需的类文件系统能力。需要注意的是,本文件系统不支持复杂的Flash扇区擦写次数均衡算法,没有目录层次,并且和其它的文件系统不兼容。
1 线性文件系统
线性文件系统的设计思路是这样的:文件分为文件头和文件数据区两个部分,每个文件按照顺序存放在Flash中,以单向链表来链接文件。文件的起始部分是文件头,包含文件的属性、指向下一个文件头的指针、文件头和文件数据区的32位循环冗余校验和(CRC32)等。文件头用一个32位的字来表示文件属性,每位表示一种属性,如数据文件或者是可执行文件,是否已删除的文件等,具体可以根据应用的需要来定义文件的属性;文件头和文件数据区维护独立的CRC32校验,使文件系统能更精确检测文件的完整性。文件的起始地址没有特殊需求,分配给文件系统的Flash大小限制了文件的大小。另外,线性文件系统作为嵌入式系统的一个功能模块,它为应用程序提供与标准文件系统类似的API接口,如:read()、write()、open()、close()、stat()和seek()等。对于同时在多片Flash的系统而言,每片Flash相当于一个目标,文件都可存储在任何一片中(当然受物理空间限制),但不能跨片存储。
图1 Flash文件系统空间
在第一个文件创建之前,必须进行初始化,将所有分配给文件系统的Flash空间擦除。当创建第一个文件时,起始位置从文件系统的起始地址开始,文件头指针指向下一个空文件的起始位置(链表尾部);第二个文件的位置从当前的链表尾部开始,同时文件头中的链表指针指向新的尾部。删除文件时,仅仅是简单地把文件头的标识位中的活动文件标识位置0,表示删除。这样,在经过多次删除之后,就有必要运行碎片整理模块来进行文件系统Flash空间的碎片整理。碎片整理模块还需要在文件系统Flash空间尾部留一个扇区来数据备份,以便当碎片整理被打断时(如下电或者复位)可以恢复文件系统。这个保留的扇区称空闲扇区。它必须放在文件系统空间之后,这样可以保证文件系统的所有文件在所占用的Flash空间是连续的。整个文件空间的分配如图1所示。
阴影部分是文件头,数据结构如下:
struct hdr{
unsigned short hdrsize; /*文件头字节数*/
long filsize; /*文件头版本*/
long filsize; /*文件大小*/
long flags; /*描述文件的标识*/
unsigned long filcrc; /*文件数据的CRC32的值*/
unsigned long hdrcec; /*文件的最后修改时间*/
struct hdr *next; /*指向下一个文件头的指针*/
char name[NAMESIZE]; /*文件名*/
char info[INFOSIZE]; /*文件描述信息*/
};
碎片整个记录区包含两种数据类型:碎片整理文件头信息表defraghdr和文件区扇区整理前后的CRC值备份表sectorcre。具体的地址分配从空闲扇区的起始地址减1开始,往前分配文件系统扇区数乘以4字节作为sectorcrc的空间;从sectorcrc起始地址减1开始,往前分配活动文件个数乘以64字节作为碎片整理文件头信息表。这两个结构定义如下:
struct defraghdr{
struct hdr *ohdr; /*文件头的原始位置指针*/
struct hdr *nextfile; /*指向下一个文件的指针*/
long filsize; /*文件大小*/
unsigned long crc; /*这个头的CRC32值*/
unsigned long ohdrcrc; /*原始文件头CRC32值的拷贝*/
long idx; /*碎片整理表头的索引*/
long nesn; /*新的文件尾的扇区号*/
long neso; /*新的文件尾的扇区偏移量*/
char *nda; /*新的文件起始地址*/
char fname[NAMESIZE]; /*文件名*/
};
struct sectorcrc{
unsigned long precrc; /*碎片整理前扇区数据CRC32的值*/
unsigned long postcrc; /*碎片整理后扇区数据CRC32的值*/
};
从上面介绍可知,除了文件数据之外,文件系统还需要如下4种额外的开销。
①文件头:这是每个文件必须的开销,如果文件名和信息域各24字节,那么整个文件头共76字节。
②碎片整理文件头信息表:每个活动(非删除)的文件在进行碎片整理时在这个表里创建一个表项,每个表项64字节。
③碎片整理前后的扇区CRC32值表:保存文件整理前后的CRC32值,总的字节数约为文件所占扇区数的4倍。
④空闲块:用来在碎片整理过程中备份当前整理扇区数据。它必须不小于文件系统其它所有扇区。
可以用下面方程计算系统开销的总和:
overhead=(FTOT*(HDRSIZE+64))+SPARESIZE+(SECTORCOUNT*8)
其中:
FTOT是总的文件数;
HDRSIZE是文件头字节数(目前为76字节);
SPARESIZE是空闲块的大小;
SECTORCOUNT是分配给文件系统的Flash扇区数,不包括空闲块。
图2 文件碎片整理
2 碎片整理
创建新文件需要占用文件系统空间;但是,由于Flash的底层技术不允许Flash中的任意地址空间被删除,而是按照扇区为单位删除,为此在删除一个文件的时候,暂时没有把整个文件所占的空间删除,仅仅是在文件头的标识里作一个删除标识,并保留在Flash中。这样,被删除文件积累到一定的数量时,就会占用相当大的空间。因此,需要整理文件系统Flash空间,使被删除文件占用的空间重新使用。图2显示了碎片整理过程。文件F1、F2和F5已经被删除,并且在碎片整理之后从Flash中被清除。
进行碎片整理的方法可以有多种。对于嵌入式系统来说,选择哪种方法,衡量的依据是复杂性和功能之间的平衡。下面讨论两种不同的方法:第一种方法相当简单,但是有缺陷;第二种方法功能强大得多,笔者在线性文件实现中即采用这种方法。当然,存在更加复杂的解决办法,但通常的情况是,所添加的复杂性会使整个文件系统的实现更加复杂。目标是保持文件存储的简单和线性,保证所有的文件都是以连续的空间存储在Flash中。
最简单的方法是将活动的文件备份在RAM中,删除分配给文件系统的Flash空间,然后将RAM中备份的所有文件拷贝回Flash。这种方法很简单,并且不需要分配一个扇区作为空闲区;但问题是,需要有一整块和分配给文件系统的空间一样大的RAM来完成这项工作。更糟的是,如果此时系统被复位,或者在删除扇区内容却还没有将文件拷贝回Flash的时候被断电,文件系统将会崩溃。因为RAM中的内容会随之选择,文件内容会被破坏掉。
我们在文件系统实现设计了一种碎片整理方法,可以防止在碎片整理过程中系统复位导致文件崩溃的情况。采用这种方法,不需要大块的RAM,但是需要预选先分配给碎片整理过程一个Flash扇区作为备份区。这个扇区的字节数不小于任何分配给文件系统的扇区。在整个文件系统中,这个扇区位于分配给文件系统最后一个扇区的下一个扇区。因为扇区可能比需要分配给非删除文件的备份的空间要小,所以它必须逐个扇区进行处理,而不是一下就把所有的碎片整理完。采用备份扇区的好处是,在碎片整理过程中,无论断电或者复位都不会破坏文件系统。当下次系统重新恢复时,会根据在碎片整理前记录的每个扇区碎片整理前后CRC值,来判断当前的文件碎片整理状态。如果上次文件整理没有完成,就会继续上次的整理。这种技术的一个缺陷是空闲扇区的擦写次数会较多。这样空闲扇区就可能因为达到擦写寿命而失败。达到这一点的关键依赖于使用的Flash、所分配给文件系统的扇区数、文件删除和重建的频率。一个可行的解决办法采用电池备份的RAM来替换空闲扇区,可以增加Flash的整体寿命,但是对那些预算紧张的应用来说太过奢移。