第十五章--页高速缓存

        磁盘高速缓存是一种软件机制,它允许系统把通常存放在磁盘上的一些数据保留在RAM中,以便对那些数据的进一步访问不用再访问磁盘而尽快得到满足。
        页高速缓存--一种对完整的数据页进行操作的磁盘高速缓存。
一、页高速缓存
        页高速缓存(page cache)是Linux内核所使用的主要磁盘高速缓存。在绝大多数情况下,内核在读写磁盘时都引用页高速缓存。新页被追加到页高速缓存以满足用户态进程的读请求。如果页不在高速缓存中,新页就被加到高速缓存中,然后用从磁盘读出的数据填充它。如果内存有足够的空闲空间,就让该页在高速缓存中长期保留,使其他进程再使用该页时不再访问磁盘。
        同样,在把一页数据写到块设备之前,内核首先检查对应的页是否已经在高速缓存中;如果不在,就要先在其中增加一个新项,并用要写到磁盘中的数据填充该项。I/O数据的传送并不是马上开始,而是要延迟几秒之后才对磁盘进行更新,从而使进程有机会对要写入磁盘的数据做进一步修改(换句话说,就是内核执行延迟的写操作)。
        内核的代码和数据结构不必从磁盘读,也不必写入磁盘,因此,页高速缓存中的页可能是下面的类型:
        * 含有普通文件数据的页。
        * 含有目录的页。
        * 含有直接从块设备文件(跳过文件系统层)读出的数据的页。
        * 含有用户态进程数据的页,但页中的数据已经被交换到磁盘。
        * 属于特殊文件系统文件中的页,如共享内存的进程间通信(Interprocess Communication, IPC)所使用的特殊文件系统shm。
        页高速缓存中的每个页所包含的数据肯定属于某个文件。这个文件(或者更确切地说是文件的索引节点)就称为页的所有者(owner)。
        几乎所有的文件读和写操作都依赖于页高速缓存。只有在O_DIRECT标志被置位而进程打开文件的情况下才会出现例外:此时,I/O数据的传送绕过了页高速缓存而使用了进程用户态地址空间的缓冲区;少数数据库应用软件为了能采用自己的磁盘高速缓存算法而使用了O_DIRECT标志。
        内核设计者实现页高速缓存主要为了满足下面两种需要:
        * 快速定位含有给定所有者相关数据的特定页。为了尽可能充分发挥页高速缓存的优势,对它应该采用高速的搜索操作。
        * 记录在读或写中的数据时应当如何处理高速缓存中的每个页。例如,从普通文件、块设备文件或交换区读一个数据页必须用不同的实现方式,因此内核必须根据页的所有者选择适当的操作。
1.1、address_space对象
        页高速缓存的核心数据结构是address_space对象,它是一个嵌入在页所有者的索引节点对象中的数据结构。高速缓存中的许多页可能属于同一个所有者,从而可能被链接到同一个address_space对象。该对象还在所有者的页和对这些页的操作之间建立起链接关系。
        每个页描述符都包括把页链接到页高速缓存的两个字段mapping和index。mapping字段指向拥有页的索引节点的address_space对象,index字段表示在所有者的地址空间中以页大小为单位的偏移量,也就是在所有者的磁盘映像中页中数据的位置。在页高速缓存中查找页时使用这两个字段。
        页高速缓存可以包含同一磁盘数据的多个副本。例如,可以用下述方式访问普通文件的同一4KB的数据块:
        * 读文件;因此,数据就包含在普通文件的索引节点所拥有的页中。
        * 从文件所在的设备文件(磁盘分区)读取块;因此,数据就包含在块设备文件的主索引节点所拥有的页中。
        因此,两个不同address_space对象所引用的两个不同的页中出现了相同的磁盘数据。
1.2、基树
        为了实现页高速缓存的高效查找,Linux2.6采用了大量的搜索树,其中每个address_space对象对应一颗搜索树。
        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、缓冲区页
        下面是内核创建缓冲区页的两种普通情况:
        * 当读或写的文件页在磁盘块中不相邻时。发生这种情况是因为文件系统为文件分配了非连续的块,或因为文件中有“洞”。
        * 当访问一个单独的磁盘块时(例如,当读超级块或索引节点块时)。
        在第一种情况下,把缓冲区页的描述符插入普通文件的基树;保存好缓冲区首部,因为其中存有重要的信息,即存有数据在磁盘中位置的块设备和逻辑块号。
        在第二种情况下,把缓冲区页的描述符插入基树,树根是与块设备相关的特殊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、向通用块层提交缓冲区首部
        一对submit_bh()和ll_rw_block()函数,允许内核对缓冲区首部描述的一个或多个缓冲区进行I/O数据传送。
2.7.1、submit_bh()函数

2.7.2、ll_rw_block()函数

三、把脏页写入磁盘
        内核不断用包含块设备数据的页填充页高速缓存。只要进程修改了数据,相应的页就被标记为脏页,即把它的PG_dirty标志置位。
        Unix系统允许把脏缓冲区写入块设备的操作延迟执行,因为这种策略可以显著地提高系统的性能。对高速缓存中的页的几次写操作可能只需对相应的磁盘块进行一次缓慢的物理更新就可以满足。此外,写操作没有读操作那么紧迫,因为进程通常是不会由于延迟写而挂起,而大部分情况都因为延迟读而挂起。正是由于延迟写,使得任一物理块设备平均为读请求提供的服务将多于写请求。
        一个脏页可能直到最后一刻(即直到系统关闭时)都一直逗留在主存中。然而,从延迟写策略的局限性来看,它有两个主要的缺点:
        * 如果发生了硬件错误或电源掉电的情况,那么就无法再获得RAM的内容,因此,从系统启动以来对文件进行的很多修改就丢失了。
        * 页高速缓存的大小(由此存放它所需的RAM的大小)就可能要很大----至少要与所访问块设备的大小相同。
        因此,在下列条件下把脏页刷新(写入)到磁盘:
        * 页高速缓存变得太满,但还需要更多的页,或者脏页的数量已经太多。
        * 自从页变成脏页以来已过去太长时间。
        * 进程请求对块设备或者特定文件任何待定的变化都进行刷新。通过调用sync()、fsync()或fdatasync()系统调用来实现。
        缓冲区页的引入使问题更加复杂。与每个缓冲区页相关的缓冲区首部使内核能够了解每个独立缓冲区的状态。如果至少有一个缓冲区首部的BH_Dirty标志被置位,就应该设置相应缓冲区页的PG_dirty标志。当内核选择要刷新的缓冲区页时,它扫描相应的缓冲区首部,并只把脏块的内容有效地写到磁盘。一旦内核把缓冲区的所有脏页刷新到磁盘,就把页的PG_dirty标志清0。
3.1、pdflush内核线程
        根据下面的原则控制pdflush线程的产生和消亡:
        * 必须有至少两个,最多八个pdflush内核线程。
        * 如果到最近的1s期间没有空闲pdflush,就应该创建新的pdflush。
        * 如果最近一次pdflush变为空闲的时间超过1s,就应该删除一个pdflush。
3.2、搜索要刷新的脏页
        当内存不足或用户显式地请求刷新操作时执行wakeup_bdflush()函数,特别是在下述情况下会调用该函数:
        * 用户态进程发出sync()系统调用。
        * grow_buffers()函数分配一个新缓冲区页时失败。
        * 页框回收算法调用free_more_memory()或try_to_free_pages()。
        * mempool_alloc()函数分配一个新的内存池元素时失败。
3.3、回写陈旧的脏页
        内核试图避免当一些页很久没有被刷新时发生饥饿危险。因此,脏页在保留一定时间后,内核就显式地开始进行I/O数据的传输,把脏页的内容写到磁盘。
四、sync()、fsync()和fdatasync()系统调用
        用户应用程序把脏缓冲区刷新到磁盘会用到三个系统调用:
        sync():允许进程把所有的脏缓冲区刷新到磁盘。
        fsync():允许进程把属于特定打开文件的所有块刷新到磁盘。
        fdatasync():与fsync()非常相似,但不刷新文件的索引节点块。
4.1、sync()系统调用

4.2、fsync()和fdatasync()系统调用
        系统调用fsync()强制内核把文件描述符参数fd所指定文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
        系统调用fdatasync()与fsync()非常相似,但是它只把包含文件数据而不是那些包含索引节点信息的缓冲区写到磁盘。

你可能感兴趣的:(linux内核)