随着芯片技术的发展,各种处理器的处理能力不断提高,手持智能终端得到极大的普及。嵌入式Linux操作系统在未来的手持智能设备中将扮演着非常重要的角色,使嵌入式Linux的应用和研究不断的深入。
由于Microsoft公司的Windows操作系统占据了桌面操作系统绝大多数份额,而手持智能设备与PC机的数据交换又在所难免,因此,绝大多数的大容量嵌入式智能设备必须采用与PC机兼容的FAT/FAT32文件系统。随着便携式硬盘的应用,FAT32在嵌入式硬盘上已成为主流的格式。
同时,随着CPU处理能力的提高,面向存储的应用需求在手持智能设备上也随着不断增长,文件系统的访问性能将是未来的手持设备非常关键的因素。然而,当硬盘在手持智能设备上应用时,由于硬盘访问的高耗能特性,对于手持设备的设计构成了极大的挑战。而硬盘的能耗又与读写访问的时间成正比,从节能的角度出发,系统设计者同样希望在单位时间内读取更多内容,以减少硬盘访问时间从而达到节能的目的。因此,在Linux上的FAT32的优化实现成为非常迫切的需求。
1 Linux中FAT32文件系统读操作分析
1.1 虚拟文件系统与FAT32[1-2]
Linux系统中的虚拟文件系统VFS(Virtual File System)是一个非常强大的机制,其设计思路是在内核中提供一个文件系统框架,包括接口函数集、管理用的数据结构以及各种缓存机制。VFS提供上下两个方面的接口,上层接口是提供给I/O系统的用户使用的,包括应用程序和内核的其他管理模块,通过该接口可使I/O系统(文件、设备、网络等)完成如打开、关闭、读、写等;下层接口是提供给真实文件系统的,VFS支持的每个真实文件系统都要通过这个接口来实现。通过这种机制,Linux将系统存在的各种真实文件系统(如EXT2/EXT3、FAT/FAT32、JFFS/JFFS2等)以及设备文件都统一到一种操作中,以此来实现系统的管理与调度。
FAT(File Allocation Table)文件系统是Microsoft公司推出的广泛使用在Dos、Windows 9X、Windows 2000以及Windows XP系统中。由于Windows系列的操作系统的普及,其FAT文件系统被人们所广泛熟悉和应用。当前针对大容量硬盘,FAT32文件系统占据了主要的地位。在FAT32文件系统中,以下三个概念与文件的组织密切相关:
扇区(Sector): 数据存取的最小物理单位。
簇(Cluster):文件最小分配单位,与分区大小、文件系统相关。
逻辑扇区(Logic Sector):在文件系统实现中,为了优化和统一设计所定义的读写长度。
1.2 文件读在内核中的实现
以读操作为例,通过Linux系统中VFS的作用,从用户空间对FAT32的操作,系统可以抽象成从fread( )映射到内核函数do_generic_file_read( )来完成具体的文件读操作。在文件/μCLinux/linux-2.4.x/mmnommu/filemap.c中存在这个接口实现的原型。虽然这类接口并不是基本的,但正如大多数文件系统的实现,FAT32就是通过这类接口来实现文件的各种操作。
图1描述了函数do_generic_file_read( )的实现原理。从函数入口处获得目标内容的文件描述指针,从而获得文件入口。通过分析描述符inode以及当前状态,系统获得预读read_ahead的大小,进行相应的计算,获得所需要获取的目标内容Page页索引以及offset偏移量。然后发起预读的指令,并等待获得相应的Page内容后,将其拷贝到buffer中进行组织,并提供上层程序磁盘文件在内存中的映像。
1.3 文件预读机制与Page读[1-4]
在do_generic_file_read的实现中,磁盘读动作实际是在预读read_ahead中完成的,即预读机制。这是由于Linux系统为了获得更高的性能以及充分利用CPU处理能力,VFS设计中做了一层buffer/cache缓冲。当系统发现buffer/cache中有即将要访问的内容缺失时,系统将发起一次预读请求。下层文件系统根据寻找CPU以及总线的空闲状态,执行具体的预读机制。这样,上下层构成一个异步过程来完成系统的任务,以达到充分利用系统资源的目的。
在考察read_ahead( )的实现中可以发现,实际上read_ahead( )函数的主要功能是根据实际需求不断调用文件系统中的readpage( )函数来完成的。这是由于Linux的内存管理都是按照页(Page)模式进行组织的。也就是说,每次从具体的对象数据存储设备(如硬盘)上读取相应的数据时,将严格按照page的大小进行读取动作。根据一般定义,Page采用4 096B为单位。在Linux上的FAT32实现中,将由fat_readpage( )具体应用实例来实现这个功能。
1.4 Block读实现[3-4]
由于不同的硬件设备存在不同的物理结构,在文件系统格式化时,最基本的存储单元Cluster的大小是不同的。如通常能够见到的有512B、1KB等。也就是说,实际文件的存储是按照不同的目标存储设备划分为不同的块来存储的。在文件系统实现中,为了兼容不同的目标系统与硬件设备,在FAT文件系统中的Page读动作的实现中,引入了一个Block概念,即根据具体文件描述,按照Block大小完成整个Page的读命令。
在μCLinux/linux-2.4.x/mmnommu/filemap.c文件中,fat_readpage( )的实现就是根据上述目标进行相应设计的,即通过inode获取相应文件的具体存储信息,然后将Page读转化为按照Block块方式进行读操作。也就是通过反复调用block_read_full_page( )函数来满足最后Page内容的获取。
函数block_read_full_page( )的具体实现过程如图2所示。系统根据传入的参数,获得Block大小,生成相应的缓存空间,然后反复发出Block读的Request,直到完成整个Page的读任务。
如图2所示Block_read_full_page( )的实现机理中,最重要的是根据系统状况,经过计算确切地获得将由多少个Block来组成一个Page。
在Linux实现中,Block大小决定于文件描述符inode中的i_blkbits域。在Linux中的FAT32文件系统设计中,inode->i_blkbits是由FAT32系统中的logic_sector_size决定的,即用/linux-2.4.x/fs/fat/inode.c来实现从FAT32文件系统映射到Linux的inode各项定义。
1.5 系统MAKE_REQUEST[1-4]
经过上述各个步骤的计算,在文件系统实现中,将文件读操作转化为若干个不同的Block读需求,最后向下层驱动程序层发起具体的命令Request。上述的转化,基本上是根据底层配置以及内存管理的需求,将大的/整体的命令细分/拆分为更加细小的动作。
而在实际执行过程中,肯定存在较多的过度拆分的情况,以致于产生过多低效率的命令,因此,在具体实现过程中,为了避免这种情况,在实际发出Request之前,需要对其进行相应的检查,合并相关的Request,以提高系统实现性能。这个过程将由submit_bh来完成。
图3所示是submit_bh函数中的主体调用子函数_make_request的实现过程。在FAT32实现中,_make_request根据获得的Block大小、存储设备的sector number,准备好内存空间后,向IDE发出具体的Request。而具体的Request合并将发生在发出Request之前。其实现原理根据当前队列中Request的地址相关性来判断。
2 优化策略分析
面对提高文件系统访问性能的需求,经过分析系统如何处理用户发起的读命令,观察read( )命令从VFS到具体的文件系统FAT32的实现,转化为具体的每一个Request的整个过程,系统的优化可从以下几个方面进行。
2.1 Block读操作改进
根据1.4节针对block_read_full_page( )的描述,实际上是根据实际文件系统定义的Block大小,将一个page转化为多个Block的读动作。而在FAT32的具体实现中,根据/linux-2.4.x/fs/fat/inode.c文件中的描述,Block size等于logic_sector_size的大小,即逻辑扇区大小。
在FAT文件系统的定义中,逻辑扇区是为了统一不同硬盘的物理扇区而设置的。由于一般物理扇区最小为512B,因此在FAT32普遍实现中,逻辑扇区设置为512B。
而当前大容量的硬盘系统,其物理扇区普遍大于4KB。在这种情形下,根据Linux上的FAT32实现,一个4KB或者以上的物理扇区的读,被人为地划分为8次512B逻辑扇区的读命令。而由于物理原因,可知道物理扇区将是磁盘上最小的寻址单位,也就是说,在最坏的情况下(即下层__make_request没有及时判断出这些buffer是可以合并的),向一个以4KB为扇区的硬盘发出一个page(4KB)的读命令,最后将由8次同一个扇区的读动作来实现。
针对block_read_full_page划分的不合理,可以尝试用重写block_read_full_page来实现,即扩大Block为4KB。这样即可以认为,一个Linux的page读将按照一次Block读来完成。同时由于Linux内存管理都以4KB大小的page作为基本单位,这样在所有文件系统的内部,将以4KB为最小单位进行读取,把跨4KB的特殊情况留给下层驱动来完成拆分(由于大容量硬盘的应用目标,这种情况几乎不会出现)。因此,Block改进就是通过改进Block的大小,进行合并过多的拆分,来达到提高系统的读性能的作用。
2.2 预读机制控制
Linux系统上的FAT32文件系统实现,依然强烈依赖着预读机制来完成实际的读操作。这是由于Linux最初是以PC机为设计目标的,即存在内存交换文件和各种缓冲机制来对有限的资源进行无限的逻辑扩展[5]。
这种多重缓冲的设计机制,非常适合应用程序/控制命令流存储的磁盘管理。然而,在本嵌入式系统设计中,FAT32作为数据存储空间,数据存储相对有序,并且可预测性比较强。因此,这种抽象带来的好处不是特别的明显。同时由于存在多级缓冲,尤其是硬盘系统的多级缓冲,会造成以下几个缺点:
(1)因多次数据搬移,造成性能下降。对于嵌入式系统尤其是消费类设备,由于成本的原因,其总线带宽(包括内存总线与外部总线)都是相对有限的,因此,在这类总线中的数据搬移造成的延迟,是不能忽略的(而PC机的设计中,由于高速的内存吞吐量,往往这个延迟是可以忽略的)。
(2)缓冲和cache的存在,会造成具体动作更多不可预测性,这违反了实时系统的需求。因为嵌入式系统很多层面都有一定的实时性要求;其次,增加了硬盘电源管理的难度,即硬盘状态将频繁切换,减少有机会进入省电的Idle模式及更加省电的Sleep模式,浪费了硬盘自身APM(Advanced Power Management)带来的好处。
因此,在本设计中需要对预读机制进行管理,甚至去除预读机制。实际上是对文件读实现中的do_generic_file_read( )函数进行改造,去除了预读判断机制,采用直接调用方式。
2.3 Page机制改进
整个文件系统的读操作,将以page为单位进行相应的规划,即以4 096B为考虑对象。而在真实的磁盘系统中,由于大容量磁盘的普及,4 096B几乎成了最小的物理扇区。面对这样的磁盘系统,其FAT文件读写具体实现,实际上不能充分利用底层硬件以及驱动程序提供的各种优化措施,如DMA等[6-7]。
针对这样的思路,需要引入多个page读操作的相关性,即在fat_readpage( )之前增加多个page合并的判断。可以借鉴Request合并的方式进行page合并,即通过目标地址判断的方式进行合并部分Page读动作。
3 优化实例
在实际优化中,采用了前面提到的三种优化策略,在某一个实际的系统上进行相应的测试,取得了较好的效果。
图4是一个ARM嵌入式系统的详细测试结果。该测试的物理实施条件是:
ARM7TDMI的系统,CPU频率88MHz,8KB i-cache/no d-cache;硬盘挂接的EMIF为44MHz,16bit位宽;SDRAM为32bit位宽,运行在88MHz下;硬盘为4 200转,20GB;系统采用μCLinux 2.4.18。
测试采用发起read( )用户读操作进行相应的测试。其中每个测试采用不同大小的buffer来观察实际优化前/后的访问速率比较。
从测试结果可以看出,在采用buffer为8KB进行文件读时,可以取得超过50%以上的访问性能的提升。同时在这种测试条件下,也获得了最好的读性能,达到2MB/s以上的测试性能。这个读性能基本上已可以满足很多多媒体系统所需要的数据流要求。
同时在这种优化策略下,应用系统可以有针对性地优化应用程序中的各种读操作。建议采用4KB或者8KB的buffer,使系统运行在最佳的状态。
本文仔细分析了Linux的FAT32实现中读操作的具体实现过程,针对FAT32系统实现的缺陷,提出了多种优化策略,并在某一个嵌入式设备中进行具体的优化和测试,取得了一定的性能提升。最后给出了对应用程序设计的建议。
文件系统优化是一个非常深奥的课题,尤其是嵌入式系统的文件系统设计,针对不同的应用,应有不同的优化目标。本文介绍了初步的优化方法,在某一个具体的嵌入式设备上进行相应的实践,取得了良好的效果。