如果应用层发起的是一个Word文档的写操作请求,那么通过上述分析,IO会走到sys_write的地方,然后执行file->f_op->write方法。对于EXT3 ,该方法注册的是do_sync_write。Do_sync_write的实现如下:
ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos) { struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len }; struct kiocb kiocb; ssize_t ret; init_sync_kiocb(&kiocb, filp); kiocb.ki_pos = *ppos; kiocb.ki_left = len; for (;;) { ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos); if (ret != -EIOCBRETRY) break; wait_on_retry_sync_kiocb(&kiocb); } if (-EIOCBQUEUED == ret) ret = wait_on_sync_kiocb(&kiocb); *ppos = kiocb.ki_pos; return ret; }
该方法会直接调用非阻塞写处理函数,然后等待IO完成。对于具体EXT3文件读写过程函数调用关系可以参考《Ext3文件系统读写过程分析》。对EXT3文件写操作主要考虑两种情况,一种情况是DIRECT IO方式;另一种情况是page cache的写方式。Direct IO方式会直接绕过缓存处理机制,page cache缓存方式是应用中经常采用的方式,性能会比Direct IO高出不少。对于每一个EXT3文件,都会在内存中维护一棵Radix tree,这棵radix tree就是用来管理来page cache页的。当IO想往磁盘上写入的时候,EXT3会查找其对应的radix tree,看是否已经存在与写入地址相匹配的page页,如果存在那么直接将数据合并到这个page 页中,并且结束一次IO过程,直接结束应用层请求。如果被访问的地址还没有对应的page页,那么需要为访问的地址空间分配page页,并且从磁盘上加载数据到page页内存,最后将这个page页加入到radix tree中。
对于一些大文件,如果不采用radix tree去管理page页,那么需要耗费大量的时间去查找一个文件内对应地址的page页。为了提高查找效率,Linux采用了radix tree的这种管理方式。Radix tree是通用的字典类型数据结构,radix tree又被称之为PAT位树(Patricia Trie or crit bit tree)。Radix tree是一种多叉搜索树,树的叶子节点是实际的数据条目。下图是一个radix tree的例子,该radix tree的分叉为4,树高为4,树中的每个叶子节点用来快速定位8位文件内偏移地址,可以定位256个page页。例如,图中虚线对应的两个叶子节点的地址值为0x00000010和0x11111010,通过这两个地址值,可以很容易的定位到相应的page缓存页。
通过radix tree可以解决大文件page页查询问题。在Linux的实现过程中,EXT3写操作处理函数会调用generic_file_buffered_write完成page页缓存写过程。在该函数中,其实现逻辑说明如下:
ssize_t generic_file_buffered_write(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos, loff_t *ppos, size_t count, ssize_t written) { do { /* 找到缓存的page页 */ page = __grab_cache_page(mapping,index,&cached_page,&lru_pvec); /* 处理ext3日志等事情 */ status = a_ops->prepare_write(file, page, offset, offset+bytes); /* 将用户数据拷贝至page页 */ copied = filemap_copy_from_user(page, offset, buf, bytes); /* 设置page页为dirty */ status = a_ops->commit_write(file, page, offset, offset+bytes); } }
第一步通过radix tree找到内存中缓存的page页,如果page页不存在,重新分配一个。第二步通过ext3_prepare_write处理EXT3 日志,并且如果page是一个新页的话,那么需要从磁盘读入数据,装载进page页。第三步是将用户数据拷贝至指定的page页。最后一步将操作的Page页设置成dirty。便于writeback机制将dirty页同步到磁盘。
Page cache会占用Linux的大量内存,当内存紧张的时候,需要从page cache中回收一些内存页,另外,dirty page在内存中聚合一段时间之后,需要被同步到磁盘。应该在3.0内核之前,Linux采用pdflush机制将dirty page同步到磁盘,在之后的版本中,Linux采用了writeback机制进行dirty page的刷新工作。有关于writeback机制的一些源码分析可以参考《writeback机制源码分析》。总的来说,如果用户需要写EXT3文件时,默认采用的是writeback的cache算法,数据会首先被缓存到page页,这些page页会采用radix tree的方式管理起来,便于查找。一旦数据被写入page之后,会将该页标识成dirty。Writeback内核线程或者pdflush线程会周期性的将dirty page刷新到磁盘。如果,系统出现内存不足的情况,那么page回收线程会采用cache算法考虑回收文件系统的这些page缓存页。
EXT3文件系统设计要点EXT3文件系统是Linux中使用最为广泛的一个文件系统,其在EXT2的基础上发展起来,在EXT2的基础上加入了日志技术,从而使得文件系统更加健壮。考虑一下,设计一个文件系统需要考虑哪些因素呢?根据我的想法,我认为设计一个文件系统主要需要考虑如下几个方面的因素:
1)文件系统使用者的特征是什么?大文件居多还是小文件居多?如果基本都是大文件应用,那么数据块可以做的大一点,使得元数据信息少点,减少这方面的开销。
2)文件系统是读应用为主还是写应用为主?这点也是很重要的,如果是写为主的应用,那么可以采用log structured的方式优化IO pattern。例如在备份系统中,基本都是以写请求,那么对于这样的系统,可以采用log structured的方式使得底层IO更加顺序化。
3)文件系统的可扩展性,其中包括随着磁盘容量的增长,文件系统是否可以无缝扩展?例如以前的FAT文件系统由于元数据的限制,对支持的容量有着很强的限制。
4)数据在磁盘上如何布局?数据在磁盘上的不同布局会对文件系统的性能产生很大的影响。如果元数据信息离数据很远,那么一次写操作将会导致剧烈的磁盘抖动。
5)数据安全性如何保证?如果文件系统的元数据遭到了破坏,如何恢复文件数据?如果用户误删了文件,如何恢复用户的数据?这些都需要文件系统设计者进行仔细设计。
6)如何保证操作事务的一致性?对于一次写操作设计到元数据更新和文件数据的更新,这两者之间的操作次序如何设计?既能保证很好的性能,又能在系统crash的时候保证文件系统的一致性?在很多设计中采用数据先于元数据的方式,并且通过日志机制保证事务一致性。
7)文件系统元数据占用多少系统内存?如果一个文件系统占用太多的系统内存,那么会影响整个系统的性能。
在设计一个文件系统的时候,需要考虑很多方面的问题,上述仅仅是列出了一些基本点,针对不同的文件系统,会有很多的具体问题。例如对于淘宝的TFS集群文件系统,由于系统前端采用了大量的缓存,从而使得TFS的输入全是大文件,因此可以采用大文件设计思想对其进行优化。比如减少目录层次,这样在检索一个文件的时候,不需要花费太多的时间,这种处理可以提升系统性能。另一个例子是中科蓝鲸的BWFS在非线编领域的应用。BWFS是一个带外的集群存储系统,是一种SAN集群文件系统。这种文件系统架构的最大好处是可扩展性极高,由于元数据和数据IO流分离,所以,在很多应用中可以得到很高的集群性能。但是,这种架构也有一个问题,当应用文件以小文件为主的时候,元数据服务器会成为整个系统的性能瓶颈。因为,Client在访问一个文件的时候首先需要访问元数据服务器获取具体的块地址信息。
为了解决这个问题,文件系统设计人员需要对小文件这一个问题进行优化,其采用的策略是在Client进行元数据缓存。通过元数据缓存可以减少元数据访问的频率,自然就解决了这个系统性能瓶颈点问题。
从上述分析可以看出,文件系统设计是一个非常复杂的过程,其需要考虑很多应用的特征,特别是IO的Pattern和应用模式。很多文件系统的优化都是在深入分析了应用模式之后才得出的解决方案。
对于EXT3这样一个通用文件系统而言,其主要应用领域是个人桌面应用,个人以为可扩展性、性能并不是第一位的,而可靠性是比较重要的。要理解EXT3文件系统,我以为最好的方式是需要理解文件系统在磁盘上的布局,一旦理解了数据布局之后,就可以比较容易的去理解Linux中的很多ext3算法、策略了。
EXT3将整个磁盘空间划分成多个块组,每个块组都采用元数据信息对其进行描述。块组内的具体格式可以描述如下:
从上图我们可以看出一个块组内的基本信息包括:
1)Superblock(超级块)。Superblock描述了整个文件系统的信息。为了保证可靠性,可以在每个块组中对superblock进行备份。为了避免superblock冗余过多,可以采用稀疏存储的方式,即在若干个块组中对superblock进行保存,而不需要在所有的块组中都进行备份。
2)组描述符表。组描述符表对整个组内的数据布局进行了描述。例如,数据块位图的起始地址是多少?inode位图的起始地址是多少?inode表的起始地址是多少?块组中还有多少空闲块资源等。组描述符表在superblock的后面。
3)数据块位图。数据块位图描述了块组内数据块的使用情况。如果该数据块已经被某个文件使用,那么位图中的对应位会被置1,否则该位为0。
4)Inode位图。Inode位图描述了块组内inode资源使用情况。如果一个inode资源已经使用,那么对应位会被置1。
5)Inode表(即inode资源)和数据块。这两块占据了块组内的绝大部分空间,特别是数据块资源。
通过上述分析,我们了解了EXT3在磁盘上的数据布局。而一个文件是由inode进行描述的。一个文件占用的数据块是通过inode管理起来的。在inode结构中保存了直接块指针、一级间接块指针、二级间接块指针和三级间接块指针。对于一个小文件,直接可以采用直接块指针实现对文件块的访问。对于一个大文件,需要采用间接块指针实现对文件块的访问。
在理解数据布局之后,想要实现EXT3文件系统并非难事了。最主要的问题是结合Linux提供的页缓存机制实现page cache,另外通过设备的writeback机制同步dirty page。