浅学文件系统4(页面缓存)

页面缓存简述

页面缓存(Page Cache ): Linux内核管理的一块物理内存, 页面缓存------就是缓存 文件磁盘"数据块" 对应的物理页。

为什么叫"页面缓存" , 不叫"文件缓存" 呢?

答:一个文件包含多个数据块(4KB), 每个数据块加载到物理内存时,需要一个物理页(page), 来存储数据,也就是说,页面缓存的单位就是:物理页(4KB page)

页面缓存的目的: 减少硬盘或磁盘的 I/O 次数

  1. inode散列表: 缓存__元数据信息
  2. dcache散列表: 缓存__目录的目录项数据信息
  3. 页面缓存: 缓存__文件的数据块信息

从图中我们可以知道,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 文件中

相关推荐
peixiuhui15 小时前
Iotgateway技术手册-1. 项目概述
linux·网关·iot·modbus·数据采集网关·iotgateway·采集软件
wdfk_prog15 小时前
[Linux]学习笔记系列 -- [fs]sysfs
linux·笔记·学习
AllFiles15 小时前
Linux 网络故障排查:如何诊断与解决 ARP 缓存溢出问题
linux·后端
pps-key15 小时前
Afrog漏洞扫描器:从入门到入狱......边缘的摇摆记录(pps-key黑化版)
linux·计算机网络·安全·网络安全
学Linux的语莫16 小时前
linux的root目录缓存清理
linux·运维·服务器
oMcLin16 小时前
如何在 SUSE Linux Enterprise Server 15 上部署并优化 K3s 集群,提升轻量级容器化应用的资源利用率?
linux·运维·服务器
L_090716 小时前
【Linux】进程概念
linux
Ghost Face...16 小时前
深入解析YT6801驱动模块架构
linux·运维·服务器
比奇堡派星星16 小时前
Linux 杂项设备驱动框架详解
linux·arm开发·驱动开发