页面缓存简述
页面缓存(Page Cache ): Linux内核管理的一块物理内存, 页面缓存------就是缓存 文件磁盘"数据块" 对应的物理页。
为什么叫"页面缓存" , 不叫"文件缓存" 呢?
答:一个文件包含多个数据块(4KB), 每个数据块加载到物理内存时,需要一个物理页(page), 来存储数据,也就是说,页面缓存的单位就是:物理页(4KB page)

页面缓存的目的: 减少硬盘或磁盘的 I/O 次数 
inode散列表: 缓存__元数据信息dcache散列表: 缓存__目录的目录项数据信息页面缓存: 缓存__文件的数据块信息
从图中我们可以知道,Linux内核为了优化 I/O 做了很多,搞了 inode散列表、dcache散列表、页面缓存
页面缓存中的数据结构
address_space(地址空间)
address_space 数据结构 用来建立 缓存数据__与__它自己来源之间的关联
上图举例:文件a的_第四个数据块在页面缓存中的位置, 文件a的: inode + 数据块偏移量
(inode , offset) ------ (address_sapce, offset)
当然,Linux为了让页面缓存更通用,把 inode 用 address_space 来代替。 当然它们是有关联的。
inode(索引节点)、file(文件) 这些结构体里面都有 address_space 变量定义
ini
struct address_space {
inode *host; 所属问题
radix_tree_root page_tree; 维护所有缓存页(基数树)
long nrpages; 总共缓存page个数
rb_root i_mmap; 和内存映射相关
void *private_data; 私有数据
address_space_operations *a_ops; 方法操作
.......
}
address_space_operations(操作方法)
c
struct address_space_operations {
/* ① 读:磁盘 → 页面缓存 */
/* 单页同步读:磁盘内容填到 page,返回 0/-EIO 等 */
int (*readpage)(struct file *filp, struct page *page);
/* 批量读:一次性把 pages 链表里的 nr_pages 页全部读入 */
int (*readpages)(struct file *filp, struct address_space *mapping, struct list_head *pages, unsigned nr_pages);
/* ② 写:页面缓存 → 磁盘(回写) */
/* 单页回写:由 flusher 线程调用,把脏页写回磁盘 */
int (*writepage)(struct page *page, struct writeback_control *wbc);
/* 批量回写:扫描 mapping 的整棵 radix/xarray 树,把脏页写回 */
int (*writepages)(struct address_space *mapping, struct writeback_control *wbc);
/* ③ 标记脏页 */
/* 把页设为脏,通常由 __set_page_dirty_buffers 调用 */
int (*set_page_dirty)(struct page *page);
/* ④ 直接 I/O:绕过页面缓存 */
/* 用户态 Direct-IO 读写入口,数据直接落盘/从盘读 */
ssize_t (*direct_IO)(struct kiocb *iocb, struct iov_iter *iter);
/* ⑤ 准备 & 提交写(mmap/缓冲写通用) */
/* 写前准备:为 [pos, pos+len) 分配/获取缓存页,返回 locked 页 */
int (*write_begin)(struct file *file, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata);
/* 写后提交:把 page 标记脏、解锁、更新 i_size;返回实际写入字节 */
int (*write_end)(struct file *file,struct address_space *mapping, loff_t pos, unsigned len, unsigned copied, struct page *page, void *fsdata);
........
};
Page(页)
arduino
struct page {
/* ===== 1. 页面缓存相关 ===== */
struct address_space *mapping; 指向所属缓存容器(文件 inode 或 swap 地址空间)。 最低位为 1 时表示该页在 swap 缓存而非文件缓存
pgoff_t index; 页在 mapping 内的逻辑下标(以页为单位,非字节)
/* ===== 2. 物理页框管理 ===== */
页标志位集合:PG_locked、PG_dirty、PG_writeback、PG_uptodate、PG_referenced、PG_swapbacked ...
unsigned long flags;
union {
atomic_t _refcount; 页引用计数, 0 表示空闲, <0为BUG
struct { SLUB 等子系统复用空闲页的字段
unsigned int inuse;
unsigned int objects;
};
};
/* ===== 3. LRU 链表 / 回收 ===== */
struct list_head lru; /* 把页挂到 LRU、swap、inode 回收等各种链表 */
unsigned long private; /* 与页状态联动的私有数据: 1. 缓冲区头指针(块设备页) 2. swap entry 值(swap 缓存页) 3. 其他文件系统私有指针 */
struct mem_cgroup *mem_cgroup; /* 该页归属的 memory cgroup,用于资源统计与回收 */
/* ===== 4. 反向映射 ===== */
atomic_t _mapcount; /* 页表项(PTE)映射计数: -1 表示无映射, 0 表示只有 1 个映射(匿名页或单进程文件映射), >0 表示多进程共享 */
/* ===== 5. 复合页 / 巨型页 ===== */
unsigned int _compound_tail; 复合页(HugeTLB)时,标记本页在巨型页中的序号
struct page *first_page; 指向巨型页的首页
/* ===== 6. 调试 / 热区 ===== */
#ifdef WANT_PAGE_VIRTUAL
void *virtual; 高端内存页的永久内核映射地址(kmap)
#endif
};
页面缓存对外提供的接口方法
Linux内核中针对 页面缓存 封装了很多接口方法,下面简单介绍一些
1. 查找一个页(page)
c
/**
* find_get_page - 在页面缓存里查找并引用一个页
* @mapping: 要搜索的 address_space(即"这个文件"的缓存容器)
* @offset: 页在文件内的逻辑下标(以页为单位,非字节偏移)
*
* 在 radix/xarray 树中查找 <mapping, offset> 对应的 slot。
* 如果命中,则把 page->_refcount 加 1 后返回该页;
* 如果未命中,返回 NULL。
*
* 调用者必须在持有 RCU 读锁或 mapping->tree_lock 的前提下使用,
* 但本函数内部已自行加锁,因此外部无需额外同步。
*/
static inline struct page *find_get_page(struct address_space *mapping, pgoff_t offset){
直接调用内部实现,后两个 0 表示不强制分配(FGP_CREAT=0)、不标记为正在回写(FGP_WRITEBACK=0)
return pagecache_get_page(mapping, offset, 0, 0);
}
2.增加一个页(page)
c
/**
* add_to_page_cache - 把"刚分配好"的一页塞进页面缓存
* @page: 新页,此时未上锁、未在缓存树中
* @mapping: 目标 address_space(即"这个文件"的缓存容器)
* @offset: 页在文件内的逻辑下标(以页为单位)
* @gfp_mask: 分配 radix/xarray 节点时使用的内存标志
*
* 与 add_to_page_cache_locked() 的区别:
* 本函数假定页是"全新"的,因此可以安全地直接加锁。
* 如果插入失败(如 offset 处已存在页),则自动清除锁并返回错误码。
*
* 成功返回 0,失败返回 -EEXIST 等负错误码。
*/
static inline int add_to_page_cache(struct page *page, struct address_space *mapping, pgoff_t offset, gfp_t gfp_mask){
int error;
__SetPageLocked(page); /* 新页,直接加锁,避免并发 */
/* 真正插入 radix/xarray 树,失败时返回负错误码 */
error = add_to_page_cache_locked(page, mapping, offset, gfp_mask);
if (unlikely(error))
__ClearPageLocked(page); /* 插入失败,回滚锁状态 */
return error;
}
3.删除一个页(page) 下面给出符合 Linux 内核编码风格、宽度 80 列的格式化结果,可直接贴回源码树使用。
(仅调整空白与折行,零逻辑变更)
c
/**
* delete_from_page_cache - 把页从页面缓存里摘除
* @page: 要删除的页,调用前必须保证:
* 1) 页已在缓存中 (page->mapping != NULL)
* 2) 页处于 Locked 状态
*
* 函数只做两步:
* ① 从 radix/xarray 树中移除该索引槽位;
* ② 把 page->mapping 置为 NULL,表示页已脱离缓存。
*
* 注意:函数不会释放页本身,也不会把页放进空闲链表------
* 调用者必须已经持有页引用 (refcount),并在后续自行 put_page()。
*/
void delete_from_page_cache(struct page *page){
struct address_space *mapping = page->mapping; /* 所属缓存容器 */
void *shadow = NULL;
int nr = 1; /* 默认只删 1 个条目 */
/* 1. 从 xarray 中删除该索引,同时拿到 shadow 条目(若存在)*/
xa_lock_irq(&mapping->i_pages);
shadow = xa_erase(&mapping->i_pages, page->index);
xa_unlock_irq(&mapping->i_pages);
/* 2. 更新缓存计数 */
if (xa_is_shadow(shadow))
nr = 0; /* shadow 不算真实页 */
__dec_zone_page_state(page, NR_FILE_PAGES);
if (PageSwapBacked(page))
__dec_zone_page_state(page, NR_SHMEM);
mapping->nrpages -= nr;
/* 3. 彻底断开页与缓存容器的关联 */
page->mapping = NULL;
/* 清除页标志,避免后续误用 */
page_remove_rmap(page, false);
/* 如果页还在 LRU 链表上,这里会把它摘下来 */
if (PageLRU(page))
lru_cache_del(page);
}
再看文件读写(简略)流程

什么是脏页?

脏页数据写回到磁盘的方式

再看内存映射
c
#include <unistd.h>
#include <sys/mman.h>
/*
* mmap ------ 把文件或匿名页映射进当前进程地址空间
* 返回:成功 → 映射区首地址;失败 → MAP_FAILED((void *)-1),errno 被设置
*/
void *mmap(void *start, /* 期望映射的起始虚拟地址,通常传 NULL 让内核自动挑选 */
size_t length, /* 待映射字节数,必须是页大小(4K)整数倍 */
int prot, /* 映射区权限位组合:PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE */
int flags, /* 映射属性 + 行为标志,常用组合:
* MAP_SHARED -- 对映射区的写入会回写文件(共享内存/文件落盘)
* MAP_PRIVATE -- 写时复制,不影响原文件(仅本进程可见)
* MAP_ANONYMOUS -- 不关联 fd,直接得一块全 0 匿名内存
* MAP_FIXED -- 强制使用 start 地址(危险,需对齐)
*/
int fd, /* 要映射的文件描述符;若 MAP_ANONYMOUS 通常给 -1 */
off_t offset); /* 文件内起始偏移,也必须按页对齐 */

内存映射涉及的数据结构

c
struct vm_area_struct {
unsigned long vm_start; /* 内存映射区域起始地址 */
unsigned long vm_end; /* 内存映射区域结束地址 */
pgprot_t vm_page_prot; /* 访问权限 */
struct vm_area_struct *vm_next, *vm_prev; /* 挂入 mm->mmap 双向链表 */
struct rb_node vm_rb; /* 挂入 mm->mm_rb 红黑树 */
unsigned long vm_pgoff; /* 以页为单位的文件内偏移 */
struct file *vm_file; /* 被映射的文件(MAP_ANONYMOUS 时 NULL) */
const struct vm_operations_struct *vm_ops; /* vma 操作钩子 */
struct anon_vma *anon_vma; /* 匿名页反向映射 */
/* --- 以下来自 anon_vma 内部,图中混贴,已拆出 --- */
struct rb_node rb; /* anon_vma->rb_root 上的节点 */
unsigned long rb_subtree_last; /* 子树中最大地址 */
#ifdef CONFIG_ANON_VMA
unsigned long shared; /* 引用计数或共享标志 */
#endif
};
如需完整内核定义,请直接参考 linux/mm_types.h。
内存映射涉及的大体流程

再看缺页异常大体流程

按需调页
我们的软件程序一般都被打包到 ELF 文件中 

