页缓存是Linux内核一种重要的磁盘高速缓存,它通过软件机制实现。但页缓存和硬件cache的原理基本相同,将容量大而低速设备中的部分数据存放到容量小而快速的设备中,这样速度快的设备将作为低速设备的缓存,当访问低速设备中的数据时,可以直接从缓存中获取数据而不需再访问低速设备,从而节省了整体的访问时间。
页缓存以页为大小进行数据缓存,它将磁盘中最常用和最重要的数据存放到部分物理内存中,使得系统访问块设备时可以直接从主存中获取块设备数据,而不需从磁盘中获取数据。
在大多数情况下,内核在读写磁盘时都会使用页缓存。内核在读文件时,首先在已有的页缓存中查找所读取的数据是否已经存在。如果该页缓存不存在,则一个新的页将被添加到高速缓存中,然后用从磁盘读取的数据填充它。如果当前物理内存足够空闲,那么该页将长期保留在高速缓存中,使得其他进程再使用该页中的数据时不再访问磁盘。写操作与读操作时类似,直接在页缓存中修改数据,但是页缓存中修改的数据(该页此时被称为Dirty Page)并不是马上就被写入磁盘,而是延迟几秒钟,以防止进程对该页缓存中的数据再次修改。
页缓存的设计需求
页缓存至少需要满足以下两种需求。首先,它必须可以快速定位含有给定数据的特定页。其次,由于页高速缓存中的数据来源不同,比如普通文件、块设备等,内核必须根据不同的数据来源来选择对页缓存的适当操作。
内核通过抽象出address_space数据结构来满足上述两种设计需求。
address_space结构
address_space结构是页高速缓存机制中的核心数据结构,该结构并不是对某一个页高速缓存进行描述,而是以页高速缓存的所有者(owner)为单位,对其所拥有的缓存进行抽象描述。页高速缓存中每个页包含的数据肯定属于某个文件,该文件对应的inode对象就称为页高速缓存的所有者。
页缓存与文件系统和内存管理都有联系。每个inode结构中都嵌套一个address_space结构,即inode字段中的i_data;同时inode中还有i_maping字段指向所嵌套address_spaces结构。而address_space结构通过host字段反指向页高速缓存的所有者。页缓存的本质就是一个物理页框,因此每个页描述符中通过mmaping和index两个字段与高速缓存进行关联。mmaping指向页缓存所有者中的address_space对象。index表示以页大小为单位的偏移量,该偏移量表示页框内数据在磁盘文件中的偏移量。
address_space结构中的i_mmap字段指向一个radix优先搜索树。该树将一个文件所有者中的所有页缓存组织在一起,这样可以快速搜索到指定的页缓存。内核中关于radix树有一套标准的使用方法,它不与特定的数据联系(与内核双联表类似),这样使得使用范围更加灵活。具体操作如下:
radix_tree_lookup():在radix树中对指定节点进行查找;
radix_tree_insert():在radix树中插入新节点;
radix_tree_delete():在radix树中删除指定节点;
此外,该结构中的a_ops字段指向address_space_operations结构,该结构是一个钩子函数集,它表明了对所有者的页进行操作的标准方法。比如writepage钩子函数表示将页中的数据写入到磁盘中,readpage表示从磁盘文件中读数据到页中。通常,这些钩子函数将页缓存的所有者(inode)和访问物理设备的低级驱动程序关联起来。该函数集使得内核在上层使用统一的接口与页缓存进行交互,而底层则根据页缓存中数据的来源具体实现。
通过上面的描述,可以看到address_space结构中的优先搜索树和钩子函数集解决了页高速缓存的两个主要设计需求。
内核对页缓存的操作函数
内核对页缓存的基本操作包含了在一个页缓存所形成的radix树中查找,增加和删除一个页缓存。基于radix的基本操作函数,页高速缓存的处理函数如下:
page_cache_alloc():分配一个新的页缓存;
find_get_page():在页高速缓存中查找指定页;
add_to_page_cache():把一个新页添加到页高速缓存;
remove_from_page_cache():将指定页从页高速缓存中移除;
read_cache_page():确保指定页在页高速缓存中包含最新的数据;
参考:
1. 深入理解Linux内核 第三版
2.深入Linux内核架构