本文所述是针对 linux 引入了虚拟内存管理机制以后所涉及的知识点。linux 中页缓存的本质就是对于磁盘中的部分数据在内存中保留一定的副本,使得应用程序能够快速的读取到磁盘中相应的数据,并实现不同进程之间的数据共享。
因此,linux 中页缓存的引入主要是为了解决两类重要的问题:
- 磁盘读写速度较慢(ms 级别);
- 实现不同进程之间或者同一进程的前后不同部分之间对于数据的共享;
如果没有进程之间的共享机制,那么对于系统中所启动的所有进程,在打开文件的时候都要将需要的数据从磁盘加载进物理内存空间,这样不仅导致加载速度变慢,而且造成了物理内存的浪费。为了解决以上问题,linux 操作系统使用了缓存机制。
在虚拟内存机制出现以前,操作系统使用块缓存机制,但是在虚拟内存出现以后,操作系统管理 IO 的粒度更大,因此采用了页缓存机制。此后,和后备存储的数据交互普遍以页为单位。
页缓存是基于页的、面向文件的一种缓存机制。
以上只是对页缓存的重要性做了介绍。但是,还有三个问题还没有解释:
- 页缓存究竟是如何实现,其和文件系统是如何关联的?
- 页缓存、内存以及文件 IO 之间的关系是怎样的?
- 页缓存中的数据如何实现和后备存储之间的同步?
一、页缓存的实现
既然页缓存是以页为单位进行数据管理的,那么必须在内核中标识该物理页。其实每个真正存放数据的物理页帧都对应一个管理结构体,称之为 struct page,其结构体如下。
struct page {
????unsigned long????flags;
????atomic_t????_count;
atomic_t????_mapcount;
????unsigned long????private;
????struct address_space????*mapping;
????pgoff_t????index;
????struct list_head????lru;
????void*????virtual;
};
| 成员 | 描述 |
|---|---|
| flags | 描述 page 当前的状态和其他信息,如当前的 page 是否是脏页 PG_dirty;是否是最新的已经同步到后备存储的页 PG_uptodate; 是否处于 lru 链表上等 |
| _count | 引用计数,标识内核中引用该 page 的次数。如果要操作该 page,引用计数会 +1,操作完成之后 -1。当该值为 0 时,表示没有引用该 page 的位置,所以该 page 可以被解除映射,这在内存回收的时候是有用的。 |
| _mapcount | 页表被映射的次数。也就是说 page 同时被多少个进程所共享,初始值为 -1,如果只被一个进程的页表映射了,该值为 0。 |
| private | 私有数据指针 |
| mapping | 它有三种含义: 1. 如果 mapping = 0,说明该 page 属于交换缓存(swap cache); 当需要地址空间时会指定交换分区的地址空间 swapper_space; 2. 如果 mapping != 0, bit[0] = 0, 说明该 page 属于页缓存或者文件映射,mapping指向文件的地址空间address_space; 3. 如果mapping != 0, bit[0] != 0 说明该 page 为匿名映射,mapping 指向 struct anon_vma 对象; |
| index | 在映射的虚拟空间(vma_area)内的偏移;一个文件可能只是映射了一部分,假设映射了 1M 的空间,那么 index 指的是 1M 空间内的偏移,而不是在整个文件内的偏移 |
| lru | 当 page 被用户态使用或者是当做页缓存使用的时候,将该 page 连入 zone 中的 lru 链表,供内存回收使用 |
注意区分 _count 和 _mapcount。_mapcount 表示的是被映射的次数,而 _count 表示的是被使用的次数;被映射了不一定被使用,但是被使用之前肯定要先被映射。
页缓存就是将一个文件在内存中的所有物理页所组成的一种树形结构,我们称之为基数树,用于管理属于同一个文件在内存中的缓存内容。
如上所述,一个文件在内存中对应的所有物理页组成了一棵基数树。而一个文件在内存中具有唯一的 inode 结构标识,inode 结构中有该文件所属的设备及其标识符,因而,根据一个 inode 能够确定其对应的后备设备。
为了将文件在物理内存中的页缓存和文件及其后备设备关联起来,linux 内核引入了 address_space 结构体。可以说 address_space 结构体是将页缓存和文件系统关联起来的桥梁,其组成如下:
struct address_space {
????struct inode*????host; /*指向与该address_space相关联的inode节点*/
????struct radix_tree_root????page_tree; /*所有页形成的基数树根节点*/
????spinlock_t????tree_lock; /*保护page_tree的自旋锁*/
????unsigned int????i_map_writable; /*VM_SHARED的计数*/
????struct prio_tree_root????i_map;
????struct list_head????i_map_nonlinear;
????spinlock_t????i_map_lock; /*保护i_map的自旋锁*/
????atomic_t????truncate_count; /*截断计数*/
????unsigned long????nrpages; /*页总数*/
????pgoff_t????writeback_index; /*回写的起始位置*/
????struct address_space_operation*????a_ops; /*操作表*/
????unsigned long????flags; /*gfp_mask掩码与错误标识*/
????struct backing_dev_info*????backing_dev_info; /*预读信息*/
????spinlock_t????private_lock; /*私有address_space锁*/
struct list_head????private_list; /*私有address_space链表*/
struct address_space*????assoc_mapping; /*相关的缓冲*/
}
| 成员 | 描述 |
|---|---|
| host | 指向与该 address_space 相关联的 inode 节点,inode 节点与 address_space 之间是一一对应关系 |
| struct radix_tree_root | 指向的 host 文件在该内存中映射的所有物理页形成的基数树的根节点,参考博客 |
| struct prio_tree_root | 与该地址空间相关联的所有进程的虚拟地址区间 vm_area_struct 所对应的整个进程地址空间 mm_struct 形成的优先查找树的根节点; vm_area_struct 中如果有后备存储,则存在 prio_tree_node 结构体,通过该 prio_tree_node 和 prio_tree_root 结构体,构成了所有与该 address_space 相关联的进程的一棵优先查找树,便于查找所有与该 address_space 相关联的进程 |
下面列出 struct prio_tree_root 和 struct prio_tree_node的结构体。
struct prio_tree_root {
????struct prio_tree_node* prio_tree_root;
????unsigned short index_bits;
};
struct prio_tree_node {
????struct prio_tree_node* left;
????struct prio_tree_node* right;
????struct prio_tree_node* parent;
????unsigned long start;
????unsigned long last;
};
为了便于形成页缓存、文件和进程之间关系的清晰思路,如图。

【说明】:本文章由站长整理发布,文章内容不代表本站观点,如文中有侵权行为,请与本站客服联系(QQ:254677821)!