页高速缓存Cache和页回写
页高速缓存(cache)是Linux内核实现磁盘缓存。它主要用来减少对磁盘的I/O操作。
具体的讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。这一章将讨论页高速缓存和页回写(将页高速缓存中的变更数据刷新回磁盘的操作)。
磁盘高速缓存之所以在任何现代操作系统中尤为重要源自两个因素:第一,访问磁盘的速度要远远低于(差好几个数量级)访问内存的速度—-ms和ns的差距,因此,从内存访问数据比磁盘访问速度更快,若从处理器的L1和L2高速缓存访问则更快。
第二,数据一旦被访问,就很有可能在短期内再次被访问到。这种在短时期内集中访问同一片数据的原理称作临时局部原理。临时局部原理能保证:如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓冲命中(访问到高速缓冲中的数据)。正是由于内存访问要比磁盘访问快得多,再加上数据一次被访问后更可能再次被访问的特点,所以磁盘的内存缓存将给系统存储性能带来质的飞跃。
缓存手段
页高速缓存是由内存中的物理页面组成的,其内容对应磁盘上的物理块。页高速缓存大小能动态调整—-它可以通过占用空闲内存以扩张大小,也可以自我收缩以缓解内存使用压力。我们称正被缓存的存储设备为后备存储,因为缓存背后的磁盘无疑才是所有缓存数据的归属。当内核开始一个读操作(比如,进程发起一个read()系统调用),它首先会检查需要的数据是否在页高速缓存中。如果在,则放弃访问磁盘,而直接从内存中读取。这个行为称作缓存命中。如果数据没有在缓存中,称为缓存未命中,那么内核必须调度块I/O操作从磁盘去读取数据。然后内核将读来的数据放入页缓存中,于是任何后续相同的数据读取都可命中缓存了。注意,系统并不一定要将整个文件都缓存。缓存可以持有某个文件的全部内容,也可以存储另一些文件的一页或者几页。到底该缓存谁取决于谁被访问到。
写缓存
上面解释了在读操作的过程中页高速缓存的作用,那么在进程写磁盘时,比如执行write()系统调用,缓存如何被使用呢?通常来讲,缓存一般被实现成下面三种策略之一:第一种策略称为不缓存(nowrite),也就是说高速缓存不去缓存任何写操作。当对一个缓存中的数据片进行写时,将直接跳过缓存,写到磁盘,同时也使缓存中的数据失效。那么如果后续读操作进行时,需要再重新从磁盘中读取数据。不过这种策略很少使用,因为该策略不但不去缓存写操作,而且需要额外费力气去使缓存数据失效。
第二种策略,写操作将自动更新内存缓存,同时也更新磁盘文件。这种方式,通常称为写透缓存(write-through cache),因为写操作会立刻穿透缓存到磁盘中。这种策略对保持缓存一致性很有好处—-缓存数据时刻和后备存储保持同步,所以不需要让缓存失效,同时它的实现也最简单。
第三种策略,也是Linux所采用的,称为“回写”。在这种策略下,程序执行写操作直接写到缓存中,后端存储不会立刻直接更新,而是将页高速缓冲中被写入的页面标记成“脏”,并且被加入到脏页链表中。然后由一个进程(回写进程)周期行将脏页链表中的页写回到磁盘,从而让磁盘中的数据和内存中最终一致。最后清理“脏”页标识。注意这里“脏页”这个词可能引起混淆,因为实际上脏的并非页高速缓存中的数据(它们是干干净净的),而是磁盘中的数据(它们已经过时了)。也许更好的描述应该是“未同步”吧。尽管如此,我们说缓存内容是脏的,而不是说磁盘内容。回写策略通常认为要好于写透策略,因为通过延迟写磁盘,方便在以后的时间内合并更多的数据和再一次刷新。当然,其代价是实现复杂度高了许多。
缓存回收
缓存算法最后涉及的重要内容是缓存中的数据如何清除;或者是为更重要的缓存项腾出位置;或者是收缩缓存大小,腾出内存给其他地方使用。这个工作,也就是决定缓存中什么内容将被清除的策略,称为缓存回收策略。Linux的缓存回收是通过选择赶紧页(不脏)进行简单替换。如果缓存中没有足够的干净页面,内核将强制地进行回写操作,以腾出更多的干净可用页。最难的事情在于决定什么页应该回收。理想的回收策略应该是回收那些以后最不可能使用的页面。当然要知道以后的事情你必须是先知。也正是这个原因,理想的回收策略称为预测算法。但这种策略太理想了,无法真正实现。
最近最少使用
缓存回收策略通过所访问的数据特性,尽量追求预测效率。最成功的算法(特别是对于通用目的的页高速缓存)称作最近最少使用算法,简称LRU。LRU回收策略需要跟踪每个页面的访问踪迹(或者至少按照访问时间为序的页链表),以便能回收最老时间戳的页面(或者回收排序链表头所指的页面)。该策略的良好效果源自于缓存的数据越久未被访问,则越不大可能近期再被访问,而最近被访问的最有可能被再次访问。但是,LRU策略并非是放之四海而皆准的法则,对于许多文件被访问一次,再不被访问的情景,LRU尤其失败。将这些页面放在LRU链的顶端显然不是最优,当然,内核并没有办法知道一个文件只会被访问一次,但是它却知道过去访问了多少次。
双链策略
Linux实现的是一个修改过的LRU,也称为双链策略。和以前不同,Linux维护的不再是一个LRU链表,而是维护两个链表:活跃链表和非活跃链表。处于活跃链表上的页面被认为是“热”的且不会被换出,而在非活跃链表上的页面则是可以被换出的。在活跃链表中的页面必须在其被访问时就处于非活跃链表中。两个链表都被都被伪LRU规则维护:页面从尾部加入,从头部移除,如同队列。两个链表需要维持平衡—-如果活跃链表变得过多而超过了非活跃链表,那么活跃链表的头页面将被重新移回到非活跃链表中,以便能在被回收。双链表策略解决了传统LRU算法中对仅一次访问的窘境。而且也简单的实现了伪LRU语义。这种双链表方式也称作LRU/2。更普遍的是n个链表,故称LRU/n。
我们现在知道页缓存如何创建(通过读和写),如何在写时被同步(通过回写)以及旧数据如何被回收来容纳新数据(通过双链表)。现在让我们看看真实世界应用场景中,页高速缓存如何帮助系统。假定你在开发一个很大的软件工程(比如Linux内核),那么你将有大量的源文件被打开,只要你打开读取源文件,这些文件就将被存储在页高速缓存中。只要数据被缓存,那么从一个文件跳到另一个文件将瞬间完成。当你编辑文件时,存储文件也会瞬间完成,因为写操作只需要写到内存,而不是磁盘。当你编译项目时,缓存的文件将使得编译过程更少访问磁盘,所以编译速度也就更快了。如果整个源码树太快了,无法一次性放入内存,那么其中一部分必须被回收—-由于双链表策略,任何回收的文件都将处于非活跃链表,而且不大可能是你正在编译的文件。幸运的是,在你没编译的时候,内核会执行页回写,刷新你所修改文件的磁盘副本。由此可见,缓存将极大地提高系统性能。为了看到差别,对比一下缓存冷(cache cold)时(也就是说重启后,编译你的大型软件工程的时间)和缓存热(cache warm)时的差别吧。
Linux页高速缓存
从名字可以看出,页该高速缓存缓存的是内存页面。缓存中的页来自对正规文件、块设备文件和内存映射文件的读写。如此一来,页高速缓存就包含了最近被访问的文件的数据块。在执行一个I/O操作前(read()操作),内核会检查数据是否已经在页高速缓存中了,如果所需要的数据确实在高速缓存中,那么内核可以从内存中迅速地返回需要的页,而不再需要从相对较慢的磁盘上读取数据。
address_space对象
在页高速缓存中的页可能包含了多个不连续的物理磁盘块。也正是由于页面中映射的磁盘块不一定连续,所以在页高速缓存中检查特定数据是否已经被缓存是件颇为困难的工作。因为不能用设备名称和块号来做页高速缓存中的数据的索引,要不然这将是最简单的定位办法。
另外,Linux页高速缓存对被缓存的页面范围定义非常宽泛。实际上,在最初的System V Release 4引入页高速缓存时,仅仅只用作缓存文件系统数据,所以SVR4的页高速缓存使用它的等价文件对象(称为vnode结构体)管理页高速缓存。Linux页高速缓存的目标是缓存任何基于页的对象,这包含各种类型的文件和各种类型的内存映射。
虽然Linux页高速缓存可以通过扩展inode结构体支持页I/O操作,但这种做法会将页高速缓存局限于文件。为了维持页高速缓存的普遍性(不应该将其绑定到物理文件或者inode结构体),Linux页高速缓存使用了一个新对象管理缓存项和页I/O操作。这个对象是address_space结构体。该结构体是第15章介绍的虚拟地址vm_area_struct的物理地址对等体。当一个文件可以被10个vm_area_struct结构体标识(比如有5个进程,每个调用mmap()映射它两次),那么这个文件只能有一个address_space数据结构—-也就是文件可以有多个虚拟地址,但是只能在物理内存有一份。与Linux内核中其他结构一样,address_space也是文不对题,也许更应该叫它page_cache_entity或者physical_pages_of_a_file。
该结构体定义在文件<linux/fs.h>中,下面给出具体形式:
1 | struct address_space { |
其中i_mmap字段是一个优先搜索树,它的搜索范围包含了在address_space中所有共享的与私有的映射页面。优先搜索树是一种巧妙地将堆与radix树结合的快速检索树。回忆早些提到的:一个被缓存的文件只和一个address_space结构体相关联,但它可以有多个vm_area_struct结构体—-一物理页到虚拟页是个一对多的映射。i_map字段可帮助内核高效地找到关联的被缓存文件。
address_space页总数由nrpages字段描述。
address_space结构往往会和某些内核对象关联。通常情况下,它会与一个索引节点(inode)关联,这时host域就会指向该索引节点:如果关联对象不是一个索引节点的话,比如address_space和swapper关联时,host域会被置为NULL。
address_space操作
a_ops域指向地址空间对象中的操作函数表,这与VFS对象及其操作表关系类似,操作函数表定义在文件<linux/fs.h>中,由address_space_operations结构体来表示:
1 | struct address_space_operations { |
这些方法指针指向那些为指定缓存对象实现的页I/O操作。每个后备存储都通过自己的address_space_operation描述自己如何与页高速缓存交互。比如ext3文件系统在文件fs/ext3/inode.c
中定义自己的操作表。这些方法提供了管理页高速缓存的各种行为,包括最常用的读页到缓存、更新缓存数据。这里面readpage()和writepage()两个方法最为重要。我们下面就来看看一个页面的读操作会包含哪些步骤。首先Linux内核试图在页高速缓存中找到需要的数据;find_get_page()负责完成这个检查动作。一个address_space对象和一个偏移量会传给find_get_page()方法,用于在页高速缓存中搜索需要的数据:
1 | page = find_get_page(mapping,index); |
这里mapping是指定的地址空间,index是文件中的指定位置,以页面为单位。如果搜索的页并没在高速缓存中,find_get_page()将会返回一个NULL,并且内核将分配一个新页面,然后将之前搜索的页加入到页高速缓存中。
1 | struct page *page; |
最后,需要的数据从磁盘读入,再被加入页高速缓存,然后返回给用户:
1 | error = mapping->a_ops->readpage(file,page); |
写操作和读操作有少许不同。对于文件映射来说,当页被修改了,VM仅仅需要调用:
1 | SetPageDirty(page); |
内核会在晚些时候通过writepage()方法把页写出。对特定文件的写操作比较复杂,它的代码在文件mm/filemap.c
中,通常写操作路径要包含以下各步:
1 | page = __grab_cache_page(mapping,index,&cached_page,&lru_pvec); |
首先,在页高速缓存中搜索需要的页。如果需要的页不在高速缓存中,那么内核在高速缓存中新分配一空闲项;下一步,内核创建一个写请求;接着数据被从用户空间拷贝到了内核缓冲;最后将数据写入磁盘。
因为所有的页I/O操作都要执行上面这些步骤,这就保证了所有的页I/O操作必然都是通过页高速缓存进行的。因此,内核也总是试图通过页高速缓存来满足所有的读请求。如果在页高速缓存中未搜索到需要的页,则内核将从磁盘读入需要的页,然后将该页加入到页高速缓存中;对于写操作,页高速缓存更像是一个存储平台,所有要被写出的页豆都要加入页高速缓存中。
基树
因为在任何页I/O操作前内核都要检查页是否已经在页高速缓存中了,所以这种频繁进行的检查必须迅速、高效,否则搜索和检查页高速缓存的开销可能抵消页高速缓存带来的好处(至少在缓存命中率很低的时候,搜索的开销足以抵消以内存代替磁盘进行检索数据带来的好处)。
正如在16.2.2节所看到的,页高速缓存通过两个参数address_space对象加上一个偏移量进行搜索。每个address_space对象都有唯一的基树(radix tree),它保存在page_tree结构体中。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的页。页高速缓存的搜索函数find_get_page()要调用函数radix_tree_lookup(),该函数会在指定基树中搜索指定页面。
基树核心代码的通用形式可以在文件lib/radix-tree.c
中找到。另外,要想使用基树,需要包含头文件<linux/radix_tree.h>。
以前的页散列表
在2.6版本以前,内核页高速缓存不是通过基树检索,而是通过一个维护了系统中所有页的全局散列表进行检索。对于给定的一个键值,该散列表会返回一个双向链表的入口对应于这个所给定的值。如果需要的页储存在缓存中,那么链表中的一项就会与其对应。否则,页就不在页面高速缓存中,散列函数返回NULL。
全局散列表主要存在四个问题:
- 由于使用单个的全局锁保护散列表,所以即使在中等规模的机器中,锁的争用情况也会相当严重,造成性能受损。
- 由于散列表需要包含所有页高速缓存中的页,可是搜索需要的只是和当前文件相关的那些页,所以散列表包含的页面相比搜索需要的页面要大得多。
- 如果散列表搜索失败(也就是给定的页不在页高速缓存中),执行速度比希望的要慢得多,这是因为检索必须遍历指定散列键值对应的整个链表。
- 散列表比其他方法会消耗更多内存。
2.6版本内核中引入基于基树的页高速缓存来解决这些问题。
缓冲区高速缓存
独立的磁盘块通过块I/O缓冲也要被存入页高速缓存。一个缓冲是一个物理磁盘块在内存里的表示。缓冲的作用就是映射内存中的页面到磁盘块,这样一来页高速缓存在块I/O操作时也减少了磁盘访问,因为它缓存磁盘块和减少块I/O操作。这个缓存通常称为缓冲区高速缓存,虽然实际上它没有作为独立缓存,而是作为页高速缓存的一部分。
块I/O操作一次操作一个单独的磁盘块。普遍的块I/O操作是读写i节点。内核提供了bread()函数实现从磁盘读一个块的底层操作。通过缓存,磁盘块映射到它们相关的内存页,并缓存到页高速缓存中。
缓冲和页高速缓存并非天生就是统一的,2.4内核的主要工作之一就是统一它们。在更早的内核中,有两个独立的磁盘缓存:页高速缓存和缓冲区高速缓存。前者缓存页面,后者缓存缓冲区,这两个缓存并没有统一。一个磁盘块可以同时存于两个缓存中,这导致必须同步操作两个缓冲区中的数据,而且浪费了内存,去存储重复的缓存项。今天我们只有一个磁盘缓存,即页高速缓存。虽然如此,内核仍然需要在内存中使用缓冲来表示磁盘块,幸好,缓冲是用页映射块的,所以它正好在页高速缓存中。
flusher线程
由于页高速缓存的缓存作用,写操作实际上会被延迟。当页高速缓存中的数据比后台存储的数据更新时,该数据就称作脏数据。在内存中累积起来的脏页最终必须被写回磁盘。在以下3种情况发生时,脏页被写回磁盘:
- 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘以便释放内存,因为只有干净(不脏的)内存才可以被回收。当内存干净后,内核就可以从缓存清理数据,然后收缩缓存,最终释放出更多的内存。
- 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘,以确保脏页不会无限期地驻留在内存中。
- 当用户进程调用sync()和fsync()系统调用时,内核会按要求执行回写动作。
上面三种工作的目的完全不同。实际上,在旧内核中,这是由两个独立的内核线程分别完成的。但是在2.6内核中,由一群内核线程(flusher线程)执行这三种工作。
首先,flusher线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新写回磁盘。该后台回写例程的目的在于—-在可用内存过低时,释放脏页以重新获得内存。这个特定的内存阈值可以通过dirty_background_ratio sysctl系统调用设置。当空闲内存比阈值dirty_background_ratio还低时,内核便会调用函数flusher_threads()唤醒一个或多个flusher线程,随后flusher线程进一步调用函数bdi_writeback_all()开始将脏页写回磁盘。该函数需要一个参数—-试图写回的页面数目。函数连续地写出数据,知道满足以下两个条件:
- 已经有指定的最小数目的页被写回到磁盘。
- 空闲内存数已经回升,超过了阈值dirty_background_ratio。
上述条件确保了flusher线程操作可以减轻系统中内存不足的压力。回写操作不会在达到这两个条件前停止,除非刷新者线程写回了所有的脏页,没有剩下的脏页可再被写回了。
为了满足第二个目标,flusher线程后台例程会被周期性唤醒(和空闲内存是否过低无关),将那些在内存中驻留时间过长的脏页写出,确保内存中不会有长期存在的脏页。如果系统发生崩溃,由于内存处于混乱之中,所以那些在内存中还没来得及写回磁盘的脏页就会丢失
,所以周期性同步页高速缓存和磁盘非常重要。在系统启动时,内核初始化一个定时器,让它周期地唤醒flusher线程,随后使其运行函数wb_writeback()。该函数将把所有驻留时间超过dirty_expire_interval ms的脏页写回。然后定时器将再次被初始化为dirty_expire_centisecs 秒后唤醒flusheri线程。总而言之,flusher线程周期性地被唤醒并且把超过特定期限的脏页写回磁盘。
系统管理员可以在/proc/sys/vm
中设置回写相关的参数,也可以通过sysctl系统调用设置它们。下表列出了于pdflush相关的所有可设置变量。
页回写设置
变量 | 描述 |
---|---|
dirty_background_ratio | 占全部内存的百分比。当内存中空闲页达到这个比例时,pdflush线程开始回写脏页。 |
dirty_expire_interval | 该数值以百分之一秒为单位,它描述超时多久的数据将被周期性执行的pdflush线程写出。 |
dirty_ratio | 占全部内存百分比,当一个进程产生的脏页达到这个比例时,就开始被写出。 |
dirty_writeback_interval | 该数值以百分之一秒为单位,它描述pdflush线程的运行频率。 |
laptop_mode | 一个布尔值,用于控制膝上型计算机模式。 |
flusher线程的实现代码在文件mm/page-writeback.c
和mm/backing-dev.c
中,回写机制的实现代码在文件fs/fs-writeback.c
中。
膝上型计算机模式
膝上型计算机模式是一种特殊的页回写策略,该策略主要意图是将硬盘转动的机械行为最小化,允许硬盘尽可能长时间地停滞,以此延长电池供电时间。该模式可以通过/proc/sys/vm/laptop_mode
文件进行配置。通常,上述配置文件内容为0,也就是说膝上型计算机模式关闭,如果需要启用膝上型计算机模式,则向配置文件中写入1。
膝上型计算机模式的页回写行为与传统方式相比只有一处变化。除了当缓存中的页面太旧时要执行回写脏页之外,flusher还会找准磁盘运转的时机,把所有其他的物理磁盘I/O、刷新脏缓冲等通通写回到磁盘,以便保证不会专门为了写磁盘而去主动激活磁盘运行。
上述回写行为变化要求dirty_expire_interval和dirty_writeback_interval两阈值必须设置得更大,比如10分钟。因为磁盘运转并不很频繁,所以用这样长的回写延迟就能保证膝上型计算机模式可以等到磁盘运转机会写入数据。因为关闭磁盘驱动器是节电的重要手段,膝上模式可以延长膝上计算机依靠电池的续航能力。其坏处则是系统崩溃或者其他错误会使得数据丢失。
多数Linux发布版会在计算机接上电池或拔掉电池时,自动开启或禁止膝上型计算机模式以及其他需要的回写可调节开关。因此机器可在使用电池电源时自动进入膝上型计算机模式,而在插上交流电源时恢复到常规的页回写模式。
历史上的bdflush、kupdated和pdflush
在2.6版本前,flusher线程的工作分别由bdflush和kupdated两个线程共同完成。
当可用内存过低时,bdflush内核线程在后台执行脏页回写操作。类似flusher,它也有一组阈值参数,当系统中空闲内存消耗到特定阈值以下时,bdflush线程就被wakeup_bdflush()函数唤醒。
bdflush和当前的flusher线程之间存在两个主要区别。第一个区别是系统中只有一个bdflush后台线程,而flusher线程的数目却是根据磁盘数量变化的;第二个区别是bdflush线程基于缓冲,它将脏缓冲写回磁盘,相反,flusher线程基于页面,它将整个脏页写回磁盘。当然,页面可能包含缓冲,但是实际I/O操作对象是整页,而不是块。因为页在内存中是更普遍和普通的概念,所以管理页相比管理块要简单。
因为只有在内存过低和缓冲数量过大时,bdflush例程才刷新缓冲,所以kupdated例程被引入,以便周期地写回脏页。它和pdflush线程的wb_writeback()函数通过同样的服务。
在2.6内核中,bdflush和kupdated已让路给pdflush线程—-page dirty flush的缩写。pdflush线程的执行和今天的flush线程类似。其主要区别在于,pdflush线程数目是动态的,默认是2个到8个,具体多少取决于系统I/O的负载。pdflush线程与任何任务都无关,它们是面向系统所有磁盘的全局任务。这样做的好处是实现简单,可带来的问题是,pdflush线程很容易在拥塞的磁盘上绊住,而现代硬件发生拥塞更是家常便饭。采用每个磁盘一个刷新线程可以使得I/O操作同步执行,简化了拥塞逻辑,也提升了性能。flusher线程在2.6.32内核系列中取代了pdflush线程(针对每个磁盘独立执行回写操作是其和pdflush的主要区别)。
避免拥塞的办法:使用多线程
使用bdflush线程最主要的一个缺点就是,bdflush仅仅包含了一个线程,因此很有可能在页回写任务很重时,造成拥塞。这是因为单一的线程有可能堵塞在某个设备的已拥塞请求队列(正在等待将请求提交给磁盘的I/O请求队列)上,而其他设备的请求队列却没法得到处理。如果系统有多个磁盘和较强的处理能力,内核应该能使得每个磁盘都处于忙状态。不幸的是,即使还有许多数据需要回写,单个的bdflush线程也可能会堵塞在某个队列的处理上,不能使所有磁盘都处于饱和的工作状态,原因在于磁盘的吞吐量是非常有限的。正是因为磁盘的吞吐量很有限,所以如果只有唯一线程执行页回写操作,那么这个线程很容易苦苦等待对一个磁盘上的操作。为了避免出现这种情况,内核需要多个回写线程并发执行,这样单个设备队列的拥塞就不会成为系统瓶颈了。
2.6内核通过使用多个flusher线程上解决上述问题。每个线程可以相互独立地将脏页刷新回磁盘,而且不同的flusher线程处理不同的设备队列。pdflush线程策略中,线程数是动态变化的。每一个线程试图尽可能忙地从每个超级块的脏页链表中回收数据,并且写回磁盘。pdflush方式避免了因为一个忙磁盘,而使得其余磁盘饥饿的状况。通常情况下这样是不错的,但是如果每个pdflush线程在同一个拥塞的队列上挂起了又该如何呢?在这种情况下,多个pdflush线程可能并不比一个线程更好,就浪费的内存而言就要多许多。为了减轻上述影响,pdflush线程采用了拥塞回避策略:它们会主动尝试从那些没有拥塞的队列回写页。从而,pdflush线程将其工作调度开来,防止了仅仅欺负某一个忙碌设备。
这种方式效果确实不错,但是拥塞回避并不完美。在现代操作系统中,因为I/O总线技术和计算机其他部分相比发展要缓慢的多,所以拥塞现象时常发生—-处理器发展速度遵循摩尔定律,但是硬盘驱动器则仅仅比20年前快一点点。要知道,目前除了pdflush之外,I/O系统中还没有其他地方使用这种拥塞回避处理。不过在很多情况下,pdflush确实可以避免向特定盘回写的时间和期望时间相比太久。当前flusher线程模型(自2.6.32内核系列以后采用)和具体块设备关联,所以每个给定线程从每个给定设备的脏页链表收集数据,并写回到对应磁盘。回写于是更趋于同步了,而且由于每个磁盘对应一个线程,所以线程也不需要采用复杂的拥塞避免策略,因为一个磁盘就一个线程操作。该方法提高了I/O操作的公平性,而且降低了饥饿风险。
因为使用pdflush以及后来的flusher线程提升了页回写性能。2.6内核系列相比早期内核可让磁盘利用更饱和。在系统I/O很重的时候,flusher线程可以在每个磁盘上都维护更高的吞吐量。
小结
本章中我们看到了Linux的页高速缓存和页回写。了解了内核如何通过页缓存执行页I/O操作以及这些页高速缓存(通过存储数据在内存中)可以利用减少磁盘I/O,从而极大地提升系统的性能。我们讨论了通过称为“回写缓存”的进程维护在缓存中的更新页面—-具体做法是标记内存中的页面为脏,然后找时机延迟写到磁盘中。flusher内核线程将负责处理这些最终的页回写操作。
通过最近几章的学习,你应该已经对内存与文件系统有了深刻认识,那么接下来我们将进入模块专题,去学习Linux的驱动以及内核如何被模块化、在运行时插入和删除内核代码的动态机制。