数据回写

对于EXT3文件系统而言,在绝大多数情况下,IO请求走到page cache之后就认为这个IO处理已经完成了。用户的IO请求被放入Cache之后,用户操作结束。实际上,此时的IO处境非常的危险,如果系统在此时Crash,那么内存中缓存的数据将会彻底丢失。为了保证数据不丢失,需要有一种机制及时的将缓存中的数据同步(回写)到磁盘。对于2.6.23版本的Linux,采用了Pdflush的数据同步机制;在3.2版本的Linux中,采用了writeback的数据同步机制。这种机制的变革主要考虑了系统性能提升方面的因素。关于这两种机制的对比可以参考文章《Linux 3.2中回写机制的变革》。

Linux3.2中,当一个IO数据被写入Page缓存之后,EXT3文件系统会调用ext3_ordered_write_end函数(注意,在Linux2.6.23中调用aops->commit_write,会有所差别)结束整个过程。在该函数中主要完成pagedirty标志位的设置、日志信息的清除工作。其中,设置page页为dirty的操作就是用来唤醒(或者创建)一个writeback线程去处理缓存中的脏数据。设置pagedirty标志的核心函数是__set_page_dirty(调用关系为:ext3_ordered_write_endàblock_write_endà__block_commit_writeàmark_buffer_dirtyà__set_page_dirty)。__set_page_dirty函数说明如下:

static void __set_page_dirty(struct page *page,
struct address_space *mapping, int warn)
{
spin_lock_irq(&mapping->tree_lock);
if (page->mapping) { /* Race with truncate? */
WARN_ON_ONCE(warn && !PageUptodate(page));
account_page_dirtied(page, mapping);
/* 设置radix tree中的Tag标志 */
radix_tree_tag_set(&mapping->page_tree,
page_index(page), PAGECACHE_TAG_DIRTY);
}
spin_unlock_irq(&mapping->tree_lock);
/* 将inode加入到writeback线程处理的事务队列中 */
__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
}

__set_page_dirty函数中主要完成了两件事情:

1)radix tree中设置需要回写的page页为dirty。我们知道Linux为了加速dirty页的检索,在radix tree中采用了tag机制。Tag本质上就是一种flagradix tree的每层都有tag,上层的tag信息是下层信息的汇总。所以,如果radix tree中没有脏页,那么最顶层的tag就不会被标识成dirty。如果radix tree中有脏页,脏页所在的那条分支tag才会出现dirty flag。显然,采用这种方法可以加速脏页的检索。如下图所示,如果0x00000010地址所在的page页为脏,那么其所在的访问路径会被标识成Dirty,而其他的路径不受影响。因此,文件系统在查询脏页的时候,会节省很多的检索时间。

2)将文件inode标识成dirty。通过__mark_inode_dirty函数将文件inode设置成脏,并且将该inode交给回写线程进行处理。通知回写线程的处理方式比较简单,直接将该inode挂载到writeback线程调度处理的inode链表中。我们知道,每个设备都会有一个writeback线程处理脏页回写过程,每个writeback都会维护一条需要处理的inode链表。当writeback线程被唤醒之后,其会从inode链表中获取需要处理的inode,并且从该inode所在的radix树中获取脏页,然后生成IO将数据写入磁盘。对于EXT3文件系统而言,其一定会架构在一个块设备之上,因此,在mount文件系统的时候,会将底层块设备的writeback对象(bdi)告诉给EXT3文件系统的root_inode。这样文件系统内部的inode需要进行回写数据时,直接将该inode设置成dirty,然后通过root_inode获取writeback,并且将需要处理的inode挂载到任务链表中,最后唤醒writeback线程进行数据回写操作。

EXT3所在设备的writeback线程被唤醒之后会将文件中的dirty page页刷新到磁盘。Linux中处理完成该功能的函数是do_writepageswriteback_single_inodeà do_writepages)。由于EXT3文件系统没有注册自己的writepages方法,因此,直接调用系统提供的generic_writepages处理脏页。刷新处理一个inodedirty page的核心函数是write_cache_pages。该函数的主要功能是搜索inode对应radix tree中的脏页,然后调用EXT3writepage方法将脏页中的数据同步到磁盘。在刷新数据的过程中与块设备层相接口的的方法是submit_bh。每个page页都会对应一个buffer headEXT3想要刷新数据的时候直接调用submit_bhpage页中生成若干个bio,然后转发至底层物理设备。至此,IO旅程从文件系统开始转向块设备层。

通过上述分析,我们可以了解到一个EXT3写操作基本可以分成如下两个步骤:

1)将数据写入page cache,每个文件都会采用一个radix treepage页进行高效检索管理。当数据被写入page页之后,需要将该页标识成dirty,说明该页中有新的数据需要刷新到磁盘。为了提高radix tree检索dirty page的性能,Linux采用了Tag机制。当page页被标识成dirty之后,需要将该inode加入到对应的writeback线程任务处理队列中,等待writeback线程调度处理。Radix treewriteback之间的关系如下图所示。

2)每个块设备都会有一个writeback内核线程处理page cache/buffer的回写任务。当该线程被调度后,会检索对应inoderadix tree,获取所有的脏页,然后调用块设备接口将脏数据回写到磁盘。

通过上述两大步骤,最常见的EXT3文件写操作完成。一个IO从用户态通过系统调用进入内核,然后在radix tree上小住了一段时间,即将踏上新的征程——块设备层。

 

<待续...>