磁盘高速缓存是一种软件机制,它允许系统把通常存放在磁盘上的一些数据保留在RAM中,以便对那些数据的进一步访问不用再访问磁盘而尽快得到满足。
一、页高速缓存
页高速缓存(page cache)是Linux内核所使用的主要磁盘高速缓存。在绝大多数情况下,内核在读写磁盘是都引用页高速缓存。新页被追加到页高速缓存以满足用户态进程的读请求。如果页不在高速缓存中,新页就被加到高速缓存中,然后用从磁盘读出的数据填充它。如果内存有足够的空闲空间,就让该页在高速缓存中长期保留,使其他进程再使用该页时不再访问磁盘。
内核的代码和数据结构不必从磁盘读,也不必写入磁盘,因此,页高速缓存中的页可能是下面的类型:
1、含有普通文件数据的页。
2、含有目录的页。
3、含有直接从块设备文件(跳过文件系统层)读出的数据的页。
4、含有用户态进程数据的页,但页中的数据已经被交换到磁盘。
5、属于特殊文件系统文件中的页,如果共享内存的进程间通信(Interprocess Communication, IPC)所使用的特殊文件系统shm。
内核设计者实现页高速缓存主要为了满足下面两种需要:
1、快速定位含有给定所有者相关数据的特定页。为了尽可能充分发挥页高速缓存的优势,对它应该采用高速的搜索操作。
2、记录在读或写中的数据时应当如何处理高速缓存中的每个页。例如,从普通文件、块设备文件或交换区读一个数据页必须用不同的实现方式,因此内核必须根据页的所有者选择适当的操作。
1.1、address_space对象
页高速缓存的核心数据结构是address_space对象,它是一个嵌入在页所有者的索引节点对象中的数据结构。高速缓存中的许多页可能属于同一个所有者,从而可能被链接到同一个address_space对象。该对象还在所有者的页和对这些页的操作之间建立起链接关系。
每个页描述符都包括把页链接到页高速缓存的两个字段mapping和index。mapping字段指向拥有页的索引节点的address_space对象,index字段表示在所有者的地址空间中以页大小为单位的偏移量,也就是在所有者的磁盘映像中页中数据的位置。在页高速缓存中查找页时使用这两个字段。
1.2、基树
address_space对象的page_tree字段是基数(radix tree)的根,它包含指向所有者的页描述符的指针。给定的页索引表表示页在所有者磁盘映像中的位置,内核能够通过快速搜索操作来确定所需要的页是否在页高速缓存中。当查找所需要的页时,内核把页索引转换为基树中的路径,并快速找到页描述符所(或应当)在的位置。如果找到,内核可以从基树获得页描述符,而且还可以很快确定所找到的页是否是脏页(也就是应当被刷新到磁盘的页),以及其数据的I/O传送是否正在进行。
线性地址最高20位分成两个10位的字段:第一个字段是页目录中的偏移量,而第二个字段是某个页目录项所指向的页表中的偏移量。
1.3、页高速缓存的处理函数
1.3.1、查找页
1.3.2、增加页
函数add_to_page_cache()把一个新页的描述符插入到页高速缓存。它接收的参数有:页描述符的地址page、address_space对象的地址mapping、表示在地址空间内的页索引的值offset和为基树分配新节点时所使用的内存分配标志gfp_mask。
1.3.3、删除页
1.3.4、更新页
函数read_cache_page()确保高速缓存中包括最新版本的指定页。它的参数是指向address_space对象的指针mapping、表示所请求页的偏移量的值index、指向从磁盘读页数据的函数的指针filler(通常是实现地址空间readspace方法的函数)以及传递给filler函数的指针data(通常为NULL)。
1.4、基树的标记
为了能快速搜索脏页,基树中的每个中间节点都包含一个针对每个孩子节点(或叶子节点)的脏标记,当有且只有至少有一个孩子节点的脏标记被置位时这个标记被设置。最底层节点的脏标记通常是页描述符的PG_dirty标志的副本。通过这种方式,当内核遍历基树搜索脏页时,就可以跳过脏标记为0的中间节点的所有子树;中间节点的脏标记为0说明其子树中的所有页描述符都不是脏的。
二、把块存放在页高速缓存中
2.1、块缓冲区和缓冲区首部
每个快缓冲区都有buffer_head类型的缓冲区首部描述符。该描述符包含内核必须了解的、有关如何处理块的所有信息。因此,在对所有块操作之前,内核检查缓冲区首部。
2.2、管理缓冲区首部
缓冲区首部有它们自己的slab分配器高速缓存,其描述符kmem_cache_s存在变量bh_cachep中。alloc_buffer_head()和free_buffer_head()函数分别用于获取和释放缓冲区首部。
缓冲区首部的b_count字段是相应的块缓冲区的引用计数器。在每次对块缓冲区进行操作之前递增计数器并在操作之后递减它。除了周期性地检查保存在页高速缓存中的块缓冲区之外,当空闲内存变得很少时也要对它进行检查,只有引用计数器等于0的块缓冲区才可以被回收。
2.3、缓冲区页
下面是内核创建缓冲区页的两种普通情况:
1、当读或写的文件页在磁盘块中不相邻时。发生这种情况是因为文件系统为文件分配了非连续的块,或因为文件中有“洞”。
2、当访问一个单独的磁盘块时(例如,当读超级块或索引节点块时)。
在第一种情况下,把缓冲区页的描述符插入普通文件的基树;保存好缓冲区首部,因为其中存有重要的信息,即存有数据在磁盘中位置的块设备和逻辑块号。
在第二种情况下,把缓冲区页的描述符插入基树,树根是与块设备相关的特殊bdev文件系统中索引节点的address_space对象。这种缓冲区页必须满足很强的约束条件,就是所有的块缓冲区涉及的块必须是在块设备上相邻存放的。
在一个缓冲区页内的所有块缓冲区大小必须相同,因此,在80x86体系结构上,根据块的大小,一个缓冲区页可以包括1~8个缓冲区。
2.4、分配块设备缓冲区页
当内核发现指定块的缓冲区所在的页不在页高速缓存中时,就分配一个新的块设备缓冲区页。特别是,对块的查找操作会由于下述原因而失败:
1、包含数据块的页不在块设备的基树中:这种情况下,必须把新页的描述符加到基树中。
2、包含数据块的页在块设备的基树中,但这个页不是缓冲区页:在这种情况下,必须分配新的缓冲区首部,并将它链接到所属的页,从而把它变成块设备缓冲区页。
3、包含数据块的缓冲区页在块设备的基树中,但页中块的大小与所请求的块大小不相同:这种情况下,必须释放旧的缓冲区首部,分配经过重新赋值的缓冲区首部并将它链接到所属的页。
2.5、释放块设备缓冲区页
2.6、在页高速缓存中搜索块
2.6.1、__find_get_block()函数
2.6.2、__getblk()函数
2.6.3、__bread()函数
2.7、向通用块层提交缓冲区首部
2.7.1、submit_bh()函数
2.7.2、ll_rw_block()函数
三、把脏页写入磁盘
内核不断用包含块设备数据的页填充页高速缓存。只要进程修改了数据,相应的页就被标记为脏页,即把它的PG_dirty标志置位。
Unix系统允许把脏缓冲区写入块设备的操作延迟执行,因为这种策略可以显著地提高系统的性能。对高速缓存中的页的几次写操作可能只需对相应的磁盘块进行一次缓慢的物理更新就可以满足。此外,写操作没有读操作那么紧迫,因为进程通常是不会由于延迟写而挂起,而大部分情况都因为延迟读而挂起。正是由于延迟写,使得任一物理块设备平均为读请求提供的服务将多于写请求。
一个脏页可能直到最后一刻(即直到系统关闭时)都一直逗留在主存中。然而,从延迟写策略的局限性来看,它有两个主要的缺点:
1、如果发生了硬件错误或电源掉电的情况,那么就无法再获得RAM的内容,因此,从系统启动以来对文件进行的很多修改就丢失了。
2、页高速缓存的大小(由此存放它所需的RAM的大小)就可能要很大----至少要与所访问块设备的大小相同。
因此,在下列条件下把脏页刷新(写入)到磁盘:
1、页高速缓存变得太满,但还需要更多的页,或者脏页的数量已经太多。
2、自从页变成脏页以来已过去太长时间。
3、进程请求对块设备或者特定文件任何待定的变化都进行刷新。通过调用sync()、fsync()或fdatasync()系统调用来实现。
3.1、pdflush内核线程
根据下面的原则控制pdflush线程的产生和消亡:
1、必须有至少两个,最多八个pdflush内核线程。
2、如果到最近的1s期间没有空闲pdflush,就应该创建新的pdflush。
3、如果最近一次pdflush变为空闲的时间超过1s,就应该删除一个pdflush。
3.2、搜索要刷新的脏页
当内存不足或用户显式地请求刷新操作时执行wakeup_bdflush()函数,特别是在下述情况下会调用该函数:
1、用户态进程发出sync()系统调用。
2、grow_buffers()函数分配一个新缓冲区页时失败。
3、页框回收算法调用free_more_memory()或try_to_free_pages()。
4、mempool_alloc()函数分配一个新的内存池元素时失败。
3.3、回写陈旧的脏页
内核试图避免当一些页很久没有被刷新时发生饥饿危险。因此,脏页在保留一定时间后,内核就显式地开始进行I/O数据的传输,把脏页的内容写到磁盘。
四、sync()、fsync()和fdatasync()系统调用
用户应用程序把脏缓冲区刷新到磁盘会用到三个系统调用:
sync():允许进程把所有的脏缓冲区刷新到磁盘。
fsync():允许进程把属于特定打开文件的所有块刷新到磁盘。
fdatasync():与fsync()非常相似,但不刷新文件的索引节点块。
4.1、sync()系统调用
4.2、fsync()和fdatasync()系统调用
系统调用fsync()强制内核把文件描述符参数fd所指定文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
系统调用fdatasync()与fsync()非常相似,但是它只把包含文件数据而不是那些包含索引节点信息的缓冲区写到磁盘。
第十五章--页高速缓存,布布扣,bubuko.com
第十五章--页高速缓存
原文:http://blog.csdn.net/apple_guet/article/details/22163935