Linux 内存管理之 Rmap 反向映射

文章目录

  • 一、简介
  • [二、struct page](#二、struct page)
    • [2.1 mapping 字段](#2.1 mapping 字段)
      • [2.1.1 页缓存(Page Cache)](#2.1.1 页缓存(Page Cache))
      • [2.1.2 匿名页(Anonymous Pages)](#2.1.2 匿名页(Anonymous Pages))
  • 三、匿名页反向映射
    • [3.1 相关结构体](#3.1 相关结构体)
      • [3.1.1 struct vm_area_struct](#3.1.1 struct vm_area_struct)
      • [3.1.2 struct anon_vma](#3.1.2 struct anon_vma)
      • [3.1.3 struct anon_vma_chain](#3.1.3 struct anon_vma_chain)
    • [3.2 匿名页反向映射全流程](#3.2 匿名页反向映射全流程)
    • [3.3 匿名页反向映射的建立](#3.3 匿名页反向映射的建立)
      • [3.3.1 进程内存分配产生匿名页面](#3.3.1 进程内存分配产生匿名页面)
        • [3.3.1.1 anon_vma_prepare](#3.3.1.1 anon_vma_prepare)
        • [3.3.1.2 alloc_zeroed_user_highpage_movable](#3.3.1.2 alloc_zeroed_user_highpage_movable)
        • [3.3.1.3 page_add_new_anon_rmap](#3.3.1.3 page_add_new_anon_rmap)
      • [3.3.2 父进程fork子进程创建匿名页面](#3.3.2 父进程fork子进程创建匿名页面)
  • 四、文件页反向映射
    • [4.1 文件页反向映射全流程](#4.1 文件页反向映射全流程)
    • [4.2 page_add_file_rmap](#4.2 page_add_file_rmap)
  • 参考资料

一、简介

反向映射是内存管理中的一个核心概念,用于高效地通过物理页找到映射了该页的所有虚拟地址(即页表项)。这对于页面回收、迁移、换出等操作至关重要。

反向映射的发展经历了几个阶段,从最初的匿名页反向映射,到后来加入文件页和交换缓存的反向映射支持。

为什么需要反向映射?

在操作系统中,多个进程的虚拟地址可能映射到同一个物理页(例如共享内存、写时复制等)。当内核需要回收一个物理页时,它必须修改所有映射了该页的页表项,使其无效或指向其他位置。如果没有反向映射,内核将不得不遍历所有进程的页表来寻找映射,这是极其低效的。

一个物理页 → 多个虚拟映射:通过 fork()、KSM 等机制,单个物理页可能被多个进程的虚拟地址映射

反向映射的目标是:给定一个物理页,快速找到所有映射了该页的虚拟地址(即页表项),即快速定位所有映射到某个物理页的虚拟地址和进程。。

正向映射:虚拟地址 → 物理页帧(通过页表实现)

正向映射即内存映射,即从虚拟内存到物理内存的映射。

反向映射:物理页帧 → 所有映射它的虚拟地址

反向映射在已知物理页面(page frame,可能是PFN、可能是指向page descriptor的指针,也可能是物理地址,内核有各种宏定义用于在它们之间进行转换)的情况下,找到映射该物理页面的虚拟地址。

由于一个page frame可以在多个进程之间共享,因此反向映射的任务是把分散在各个进程地址空间中的所有的page table entry全部找出来。

如下图所示:

关键应用场景:

页面回收(回收前需解除所有映射)

内存迁移(迁移前需更新所有页表项)

透明大页分裂(THP split)

NUMA 平衡

比如内存回收:当发生内存回收时,通过反向映射技术在inactive lru中很容易获得物理内存页面并找到所有映射关系进行解映射。

二、struct page

c 复制代码
struct page {
	union {
		struct {	/* Page cache and anonymous pages */
			/* See page-flags.h for PAGE_MAPPING_FLAGS */
			struct address_space *mapping;  //file page cache
			pgoff_t index;		/* Our offset within mapping. */
		};
	}
	union {		/* This union is 4 bytes in size. */
		/*
		 * If the page can be mapped to userspace, encodes the number
		 * of times this page is referenced by a page table.
		 */
		atomic_t _mapcount;
	};
}

物理页面描述符struct page中与反向映射有关的两个关键成员是mapping和__mapcount:

(1)mapping:字段 mapping 用于区分匿名页面和基于文件映射的页面,如果该字段的最低位被置位了,那么该字段包含的是指向 anon_vma 结构(用于匿名页面)的指针;否则,该字段包含指向 address_space 结构的指针(用于基于文件映射的页面)。

mapping:表示页面所指向的地址空间。内核中的地址空间通常有两个不同的地址空间,---个用于文件映射页面,如在读取文件时,地址空间用于将文件的内容数据与装载数据的存储介质区关联起来;另---个用于匿名映射。内核使用一个简单直接的方式实现了"一个指针,两种用途",mapping成员的最低两位用于判断是否指向匿名映射或KSM页面的地址空间。如果指向匿名页面,那么mapping成员指向匿名页面的地址空间数据结构anon_vma。

mapping等于NULL,表示该page frame不再内存中,而是被swap out到磁盘去了。

mapping不为NULL,且第一位置位,该页为匿名页,mapping指向ano_vma结构。

mapping不为NULL,且第一位为0,该页为文件页,mapping指向address_space结构。

(2)index成员指向了该page在整个vm_area_struct中的偏移。

(3)__mapcount:_mapcount表示共享该物理页面的页表现数目,即有多少个进程页表的pte映射到该物理页面。该值初始值为-1,每增减一个pte映射该值+1

即该 page 映射了多少个进程的虚拟内存空间,一个 page 可以被多个进程映射。

2.1 mapping 字段

mapping 字段的双重用途:

struct page 中的 mapping 字段是一个联合体(union),它有两种用途:

对于文件映射页(file-backed pages),它指向文件的 address_space 结构体。

对于匿名页,它包含一个指向 anon_vma 结构体的指针,以及一些标志位。

c 复制代码
// v5.15/source/include/linux/page-flags.h

/*
 * On an anonymous page mapped into a user virtual memory area,
 * page->mapping points to its anon_vma, not to a struct address_space;
 * with the PAGE_MAPPING_ANON bit set to distinguish it.  See rmap.h.
 *
 * On an anonymous page in a VM_MERGEABLE area, if CONFIG_KSM is enabled,
 * the PAGE_MAPPING_MOVABLE bit may be set along with the PAGE_MAPPING_ANON
 * bit; and then page->mapping points, not to an anon_vma, but to a private
 * structure which KSM associates with that merged page.  See ksm.h.
 *
 * PAGE_MAPPING_KSM without PAGE_MAPPING_ANON is used for non-lru movable
 * page and then page->mapping points a struct address_space.
 *
 * Please note that, confusingly, "page_mapping" refers to the inode
 * address_space which maps the page from disk; whereas "page_mapped"
 * refers to user virtual address space into which the page is mapped.
 */
#define PAGE_MAPPING_ANON	0x1
#define PAGE_MAPPING_MOVABLE	0x2
#define PAGE_MAPPING_KSM	(PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)
#define PAGE_MAPPING_FLAGS	(PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)

在 Linux 内核中,struct page 的 mapping 字段是一个巧妙的设计,它通过指针与标志位复用的方式来高效存储不同类型的映射信息:

对于文件映射页:mapping 直接指向文件的 struct address_space

对于匿名页:mapping 低几位存储标志位(如 PAGE_MAPPING_ANON),剩余高位存储 struct anon_vma 的指针。

这种设计允许内核通过简单的位运算同时存储类型信息和指针,无需额外字段,节省了内存空间。

这种指针与标志位复用的设计主要出于两个目的:

节省内存:内核中 struct page 实例数量庞大(通常数百万个),每个实例节省几个字节就能显著减少内存占用

提高性能:通过位运算直接提取信息,避免了条件判断和函数调用的开销

这种设计依赖于两个重要前提:

地址对齐:现代系统的内存地址通常是按页对齐的(例如 4KB 对齐),这意味着指针的低几位总是 0(例如 4KB 对齐时,低 12 位为 0)

标志位占用低位:内核利用了指针低几位永远为 0 的特性,将标志位存储在这些空闲位中

通过这种方式,内核在不使用额外内存的情况下,实现了类型信息与指针的共存。

struct page 的 mapping 字段在页缓存(Page Cache)和匿名页(Anonymous Pages)中的双重作用:

2.1.1 页缓存(Page Cache)

struct address_space是用于管理文件系统中的文件页缓存(page cache):

c 复制代码
struct address_space *page_mapping(struct page *page)
{
	struct address_space *mapping;
	
	//获取复合页(compound page)的头页
	page = compound_head(page);
	
	......
	//匿名页过滤
	mapping = page->mapping;
	if ((unsigned long)mapping & PAGE_MAPPING_ANON)
		return NULL;

	//清除低2位标志(PAGE_MAPPING_FLAGS = 0x3)
	return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
}
EXPORT_SYMBOL(page_mapping);

page_mapping() 用于安全获取与 struct page 关联的 address_space 指针,主要服务于:

文件系统页缓存管理

页面回收(page reclaim)

内存迁移(migration)

反向映射(reverse mapping)

2.1.2 匿名页(Anonymous Pages)

c 复制代码
static inline void *__page_rmapping(struct page *page)
{
	unsigned long mapping;

	mapping = (unsigned long)page->mapping;
	//先对 PAGE_MAPPING_FLAGS 取反(得到一个高地址部分全为 1,标志位部分全为 0 的掩码)
	//然后将这个掩码与 mapping 进行按位与操作,从而清除标志位,只保留高地址部分的指针值
	//对于匿名页:低几位存储标志位,高地址部分存储 anon_vma 结构体指针
	mapping &= ~PAGE_MAPPING_FLAGS;

	//返回anon_vma 结构体(对于匿名页)
	return (void *)mapping;
}

struct anon_vma *page_anon_vma(struct page *page)
{
	unsigned long mapping;

	// 1. 处理复合页(compound page)的情况
	// 复合页是由多个连续物理页组成的大页,这里获取其头部页
	page = compound_head(page);
	
	// 2. 获取 page 结构体中的 mapping 字段并转换为无符号长整型
	mapping = (unsigned long)page->mapping;
	
	// 3. 检查 mapping 字段的标志位,判断是否为匿名页
	if ((mapping & PAGE_MAPPING_FLAGS) != PAGE_MAPPING_ANON)
		return NULL;
	
	// 4. 如果是匿名页,则调用 __page_rmapping() 获取其 anon_vma 指针
	return __page_rmapping(page);
}

(1)复合页处理 (compound_head())

Linux 内核支持将多个连续的物理页合并为一个 "复合页"(Compound Page),用于支持大页(Huge Pages)等特性。每个复合页有一个 "头部页"(head page)和多个 "尾部页"(tail pages),只有头部页包含完整的元数据。compound_head() 函数用于获取复合页的头部页。

(2)mapping 字段的双重用途:

struct page 中的 mapping 字段是一个联合体(union),它有两种可能的用途:

对于文件映射页(file-backed pages),它指向文件的 address_space 结构体

对于匿名页,它包含一个指向 anon_vma 结构体的指针。

通过将 mapping 转换为无符号长整型并与 PAGE_MAPPING_FLAGS 掩码进行按位与操作,可以提取出标志位,从而判断该页是否为匿名页。

标志位检查 (PAGE_MAPPING_ANON)

PAGE_MAPPING_FLAGS 是一个预定义的掩码,用于提取 mapping 字段中的标志位部分。PAGE_MAPPING_ANON 是一个特定的标志值,表示该页是匿名页。如果标志位匹配,则说明该页是匿名页,可以继续处理;否则返回 NULL。

(3)标志位检查 (PAGE_MAPPING_ANON)

PAGE_MAPPING_FLAGS 是一个预定义的掩码,用于提取 mapping 字段中的标志位部分。PAGE_MAPPING_ANON 是一个特定的标志值,表示该页是匿名页。如果标志位匹配,则说明该页是匿名页,可以继续处理;否则返回 NULL。

(4)获取反向映射数据结构 (__page_rmapping())

如果确认是匿名页,则调用 __page_rmapping() 函数来获取该页的 anon_vma 指针。这个函数通常会通过一些内部机制(如指针运算或间接查找)从 mapping 字段中提取出实际的 anon_vma 结构体地址。

page_anon_vma() 用于安全获取与匿名页关联的 anon_vma 结构指针,主要服务于:

反向映射(RMAP)操作

页面回收时的匿名页处理

COW(Copy-On-Write)机制

KSM(Kernel Samepage Merging)

c 复制代码
	if (PageAnon(page)) {
		struct anon_vma *page__anon_vma = page_anon_vma(page);
	}

三、匿名页反向映射

3.1 相关结构体

3.1.1 struct vm_area_struct

c 复制代码
// v5.15/source/include/linux/mm_types.h

struct vm_area_struct {
	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_lock &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */
}

anon_vma_chain通过链表链接了vma。

vma则会有指针指向自己的anon_vma。

内核在匿名页面创建需要建立反向映射的钩子,即建立相关的数据结构。有两个重要的数据结构:struct anon_vma 和 struct anon_vma_chain。

3.1.2 struct anon_vma

c 复制代码
// v5.15/source/include/linux/rmap.h

/*
 * The anon_vma heads a list of private "related" vmas, to scan if
 * an anonymous page pointing to this anon_vma needs to be unmapped:
 * the vmas on the list will be related by forking, or by splitting.
 *
 * Since vmas come and go as they are split and merged (particularly
 * in mprotect), the mapping field of an anonymous page cannot point
 * directly to a vma: instead it points to an anon_vma, on whose list
 * the related vmas can be easily linked or unlinked.
 *
 * After unlinking the last vma on the list, we must garbage collect
 * the anon_vma object itself: we're guaranteed no page can be
 * pointing to this anon_vma once its vma list is empty.
 */
struct anon_vma {
	struct anon_vma *root;		/* Root of this anon_vma tree */
	......
	struct anon_vma *parent;	/* Parent of this anon_vma */
	......
	/* Interval tree of private "related" vmas */
	struct rb_root_cached rb_root;
};

struct anon_vma:匿名线性区描述符,每个匿名vma都会有一个这个结构。

page数据结构中的mapping成员指向匿名页面的anon_vma数据结构。

对于一个页框,若该页为匿名页,则其struct page中的mapping指向 anon_vma。

anon_vma结构体用于管理匿名页对应的所有VMAs:

匿名页找到对应的anon_vma,然后再遍历AV的rb_root查询到该页框所有的anon_vma_chain,然后通过anon_vma_chain获取对应的VMA。

anon_vma为匿名页提供一个 "映射集合" 管理单元,每个匿名页通过 page->mapping 关联到一个 AV。内部通过 rb_root 红黑树存储所有与该匿名页相关的 AVC 节点,实现对多进程映射关系的快速插入、删除和查询。

anon_vma 是匿名页反向映射的核心数据结构,主要解决:

匿名页生命周期管理:跟踪所有映射同一物理页的 VMA(Virtual Memory Area)

COW (Copy-On-Write) 优化:在 fork/mprotect 时高效复制映射关系

内存回收支持:快速找到所有映射页面的进程,以便解除映射或迁移

3.1.3 struct anon_vma_chain

c 复制代码
// v5.15/source/include/linux/rmap.h

/*
 * The copy-on-write semantics of fork mean that an anon_vma
 * can become associated with multiple processes. Furthermore,
 * each child process will have its own anon_vma, where new
 * pages for that process are instantiated.
 *
 * This structure allows us to find the anon_vmas associated
 * with a VMA, or the VMAs associated with an anon_vma.
 * The "same_vma" list contains the anon_vma_chains linking
 * all the anon_vmas associated with this VMA.
 * The "rb" field indexes on an interval tree the anon_vma_chains
 * which link all the VMAs associated with this anon_vma.
 */
struct anon_vma_chain {
	struct vm_area_struct *vma;
	struct anon_vma *anon_vma;
	struct list_head same_vma;   /* locked by mmap_lock & page_table_lock */
	struct rb_node rb;			/* locked by anon_vma->rwsem */
	......
};

anon_vma_chain 是连接虚拟内存区域(VMA)和匿名页反向映射结构(anon_vma)的桥梁。

通过same_vma链表节点,将anon_vma_chain添加到vma->anon_vma_chain链表中;

same_vma 中存储的 anon_vma_chain 对应的 VMA 全都是一样的,链表结构 same_vma 存储了进程相应虚拟内存区域 VMA 中所包含的所有匿名页。

即所有指向相同VMA的 anon_vma_chain 会被链接到一个链表中,链表头就是VMA的anon_vma_chain成员。

通过rb红黑树节点,将anon_vma_chain添加到anon_vma->rb_root的红黑树中;

列表元素 anon_vma_chain 中的 anon_vma 是不一样的。

而一个anon_vma 会管理若干的anon_vma_chain (及管理若干的VMA),所有相关的anon_vma_chain (即VMA(其子进程或者孙进程))都挂入红黑树,根节点就是anon_vma 的rb_root成员。
每个 anon_vma_chain 对应一个映射该物理页的 VMA。

代码如下所示:

c 复制代码
static void anon_vma_chain_link(struct vm_area_struct *vma,
				struct anon_vma_chain *avc,
				struct anon_vma *anon_vma)
{
	avc->vma = vma;				// AVC 指向所属 VMA
	avc->anon_vma = anon_vma;		// AVC 指向所属 anon_vma
	list_add(&avc->same_vma, &vma->anon_vma_chain);		//加入 VMA 的链表
	anon_vma_interval_tree_insert(avc, &anon_vma->rb_root);  //插入 anon_vma 的红黑树
}

如下图所示:


页框与page结构对应,page结构中的mapping字段指向anon_vma,从而可以通过RMAP机制去找到与之关联的VMA;

page找到VMA的路径一般如下:page->anon_vma->anon_vma_chain->vm_area_struct,其中anon_vma_chain起到桥梁作用,至于为何需要anon_vma_chain,主要考虑当父进程和多个子进程同时拥有共同的page时的查询效率。

作为 AV 与 vma 之间的 "桥梁",每个 AVC 同时关联一个 AV 和一个 vma:

通过 avc->anon_vma 指向所属的 AV;

通过 avc->vma 指向对应的虚拟内存区域(vma)。

同时,vma 会通过 vma->anon_vma_chain 维护一个 AVC 链表:同一 vma 可能映射多个匿名页,因此会关联多个 AVC,这些 AVC 以链表形式串联,便于 vma 快速遍历自身关联的所有 AV。

3.2 匿名页反向映射全流程

关键数据结构关系:

c 复制代码
struct page {
	union {
		struct {	/* Page cache and anonymous pages */
			struct address_space *mapping;  	//file page cache
			//struct anon_vma *anon_vma;      	// anonymous pages(带PAGE_MAPPING_ANON标记)
			pgoff_t index;						/* Our offset within mapping. */
		};
	}
}

struct vm_area_struct {
	struct list_head anon_vma_chain;   		//通过链表链接该VMA中所包含的所有anon_vma_chain
	struct anon_vma *anon_vma;				//指向自己所属的anon_vma数据结构
}

struct anon_vma {
	struct anon_vma *root;		/* Root of this anon_vma tree */
	struct anon_vma *parent;	/* Parent of this anon_vma */
	/* Interval tree of private "related" vmas */
	struct rb_root_cached rb_root;    // 红黑树管理anon_vma_chain
};

struct anon_vma_chain {
	struct vm_area_struct *vma;   // 指向自己所属的关联的进程虚拟内存空间
	struct anon_vma *anon_vma;    // 指向自己所属的anon_vma数据结构
	struct list_head same_vma;    //链表节点,通常把anon_vma_chain添加到vma->anon_vma_chain链表中
	struct rb_node rb;			 //红黑树节点,通常把anon_vma_chain添加到anon_vma->rb_root的红黑树
};

(1) 物理页到struct page

c 复制代码
// 通过PFN获取page
struct page *pfn_to_page(unsigned long pfn) 

(2)获取anon_vma

c 复制代码
struct anon_vma *anon_vma = page_anon_vma(page);
static inline struct anon_vma *page_anon_vma(struct page *page)
{
    if (((unsigned long)page->mapping & PAGE_MAPPING_ANON) == 0)
        return NULL;
    return (struct anon_vma *)(page->mapping & ~PAGE_MAPPING_FLAGS);
}

(3)遍历红黑树anon_vma_chain,然后获取vm_area_struct。

c 复制代码
anon_vma_lock_read(anon_vma);  // 加读锁
struct anon_vma_chain *avc;
struct rb_node *rb_node;
for (rb_node = rb_first_cached(&anon_vma->rb_root); rb_node; rb_node = rb_next(rb_node)) {
    avc = rb_entry(rb_node, struct anon_vma_chain, rb);
    vma = avc->vma;
    
    // 计算虚拟地址
    unsigned long vaddr = vma->vm_start + (page->index << PAGE_SHIFT);
    struct task_struct *task = vma->vm_mm->owner;
    pid_t pid = task_pid_nr(task);
    
    // 处理映射关系(我们自定义逻辑)
    handle_mapping(vma, pid, vaddr);
}
anon_vma_unlock_read(anon_vma);  // 释放锁

(1)通过物理地址获取物理页描述符 struct page

物理内存被划分为固定大小的页框(如 4KB),每个页框由 struct page 结构体描述,包含页的状态、映射关系等核心信息。

获取方式:通过物理地址计算页框号(pfn = phys_addr >> PAGE_SHIFT),再通过 pfn_to_page(pfn) 宏将页框号转换为 struct page 指针。

作用:struct page 是连接物理页与虚拟地址映射的枢纽,其成员 mapping 和 index 是反向映射的关键。

(2)利用 page->mapping 获取匿名映射数据(anon_vma)

对于匿名页(如进程堆、栈等无文件关联的内存),page->mapping 指向 struct anon_vma 结构体(简称 AV),而非文件映射中的 address_space。

struct anon_vma 的核心作用:管理所有映射了该匿名页的虚拟内存区域(vma),通过红黑树组织这些映射关系,实现高效查找。

关键成员:rb_root(红黑树的根节点),用于存储 struct anon_vma_chain(简称 AVC)节点,每个 AVC 对应一个 vma 与匿名页的映射关系。

(3)遍历 anon_vma->rb_root 红黑树,处理每个 anon_vma_chain(AVC)

红黑树 anon_vma->rb_root 中的每个节点是 struct anon_vma_chain(AVC),它是连接 anon_vma 与 vma 的桥梁。

解析 anon_vma_chain(AVC)数据

struct anon_vma_chain 包含两个核心成员:

vma:指向映射该匿名页的 struct vm_area_struct(VMA),即进程的虚拟内存区域(描述虚拟地址范围、权限等)。

rb_node:红黑树节点,用于将 AVC 链接到 anon_vma 的红黑树中。

通过遍历红黑树的每个 AVC 节点,可执行以下操作:

获取 VMA 与虚拟地址:

利用 AVC->vma 得到对应的 VMA,VMA 中包含虚拟地址范围(vm_start、vm_end)和所属进程(vma->vm_mm->owner,即 task_struct)。

结合 page->index 计算该物理页在 VMA 中的虚拟地址:vaddr = vma->vm_start + (page->index << PAGE_SHIFT)。

其中 page->index 是该物理页在 VMA 中的偏移量(以页为单位)。

获取进程 PID:通过 vma->vm_mm->owner->pid 从 VMA 所属的内存描述符(mm_struct)中获取进程 PID。

(4)遍历结果:获取所有映射该物理页的进程与虚拟地址

通过遍历 anon_vma 的红黑树,处理每个 AVC 节点,最终可收集到:

所有映射该匿名页的进程 PID(通过 vma->vm_mm->owner)。

每个进程中对应的虚拟地址(通过 vma->vm_start + page->index * PAGE_SIZE)。

对应的 VMA 信息(如虚拟地址范围、访问权限等)。

总结:匿名页反向映射的核心逻辑

匿名页的反向映射通过 物理页(page)→ 匿名映射管理(anon_vma)→ 映射链(anon_vma_chain)→ 虚拟内存区域(vma)→ 进程(task_struct) 的链路,实现了从物理页到所有映射它的虚拟地址及进程的追踪。

如下图所示:

图片来源:https://blog.csdn.net/u010923083/article/details/116456497

3.3 匿名页反向映射的建立

3.3.1 进程内存分配产生匿名页面

进程为自己的进程地址空间VMA分配物理内存时,通常会产生匿名页面。例如:

c 复制代码
malloc() → 用户进程写内存 → 内核发生缺页异常 → do_anonymous_page()

用户态进程访问虚拟内存的时候,发现没有映射到物理内存,页表也没有创建过,才触发缺页异常。进入内核调用 do_page_fault,一直调用到 __handle_mm_fault,既然原来没有创建过页表,于是,__handle_mm_fault 调用 pud_alloc 和 pmd_alloc,来创建相应的页目录项,最后调用 handle_pte_fault 来创建页表项。

c 复制代码
do_page_fault()
	-->__handle_mm_fault()
		-->handle_pte_fault ()
			-->do_anonymous_page()
c 复制代码
handle_pte_fault()
{
	//如果 PTE,也就是页表项,从来没有出现过,那就是新映射的页
	if (!vmf->pte) {
		//如果是匿名页,应该映射到一个物理内存页,调用 do_anonymous_page
		if (vma_is_anonymous(vmf->vma))
			return do_anonymous_page(vmf);
}
c 复制代码
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page;
	
	/* Allocate our own private page. */
	anon_vma_prepare(vma)

	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);

	page_add_new_anon_rmap(page, vma, vmf->address, false);
}
3.3.1.1 anon_vma_prepare
c 复制代码
static inline int anon_vma_prepare(struct vm_area_struct *vma)
{
	if (likely(vma->anon_vma))
		return 0;

	return __anon_vma_prepare(vma);
}
c 复制代码
/**
 * __anon_vma_prepare - attach an anon_vma to a memory region
 * @vma: the memory region in question
 *
 * This makes sure the memory mapping described by 'vma' has
 * an 'anon_vma' attached to it, so that we can associate the
 * anonymous pages mapped into it with that anon_vma.
 *
 * The common case will be that we already have one, which
 * is handled inline by anon_vma_prepare(). But if
 * not we either need to find an adjacent mapping that we
 * can re-use the anon_vma from (very common when the only
 * reason for splitting a vma has been mprotect()), or we
 * allocate a new one.
 *
 * Anon-vma allocations are very subtle, because we may have
 * optimistically looked up an anon_vma in page_lock_anon_vma_read()
 * and that may actually touch the rwsem even in the newly
 * allocated vma (it depends on RCU to make sure that the
 * anon_vma isn't actually destroyed).
 *
 * As a result, we need to do proper anon_vma locking even
 * for the new allocation. At the same time, we do not want
 * to do any locking for the common case of already having
 * an anon_vma.
 *
 * This must be called with the mmap_lock held for reading.
 */
int __anon_vma_prepare(struct vm_area_struct *vma)
{
	struct mm_struct *mm = vma->vm_mm;
	struct anon_vma *anon_vma, *allocated;
	struct anon_vma_chain *avc;

	//1. 分配 AVC 结构
	avc = anon_vma_chain_alloc(GFP_KERNEL);
	if (!avc)
		goto out_enomem;

	//2. 查找可合并的 anon_vma
	anon_vma = find_mergeable_anon_vma(vma);
	allocated = NULL;
	if (!anon_vma) {
		//若找不到可复用的 AV,则分配一个新的 anon_vma 结构。
		anon_vma = anon_vma_alloc();
	}

	//3. 锁定并设置 VMA 的 anon_vma
	anon_vma_lock_write(anon_vma);
	/* page_table_lock to protect against threads */
	spin_lock(&mm->page_table_lock);
	if (likely(!vma->anon_vma)) {
		vma->anon_vma = anon_vma;
		//将 AVC 同时加入 VMA 的链表和 AV 的红黑树。
		anon_vma_chain_link(vma, avc, anon_vma);
		/* vma reference or self-parent link for new root */
		anon_vma->degree++;
		allocated = NULL;
		avc = NULL;
	}

	return 0;
}

__anon_vma_prepare函数的作用是确保一个虚拟内存区域(VMA, vm_area_struct)关联到一个 anon_vma 结构,用于管理匿名页(Anonymous Pages)的逆向映射(Reverse Mapping)。

为给定的 VMA 准备 anon_vma 结构,确保后续分配的匿名页能够正确建立反向映射关系。

(1)分配 AVC 结构

c 复制代码
avc = anon_vma_chain_alloc(GFP_KERNEL);
if (!avc)
    goto out_enomem;
c 复制代码
static inline struct anon_vma_chain *anon_vma_chain_alloc(gfp_t gfp)
{
	return kmem_cache_alloc(anon_vma_chain_cachep, gfp);
}

分配一个 anon_vma_chain 结构,用于后续连接 VMA 和 AV。

(2)查找可合并的 anon_vma

c 复制代码
anon_vma = find_mergeable_anon_vma(vma);
allocated = NULL;
if (!anon_vma) {
    anon_vma = anon_vma_alloc();
    if (unlikely(!anon_vma))
        goto out_enomem_free_avc;
    allocated = anon_vma;
}

a:find_mergeable_anon_vma(vma):尝试查找相邻 VMA 中可复用的 anon_vma(例如因 mprotect() 分割的 VMA 可能共享同一个 AV)。

c 复制代码
/*
 * find_mergeable_anon_vma is used by anon_vma_prepare, to check
 * neighbouring vmas for a suitable anon_vma, before it goes off
 * to allocate a new anon_vma.  It checks because a repetitive
 * sequence of mprotects and faults may otherwise lead to distinct
 * anon_vmas being allocated, preventing vma merge in subsequent
 * mprotect.
 */
struct anon_vma *find_mergeable_anon_vma(struct vm_area_struct *vma)
{
	struct anon_vma *anon_vma = NULL;

	/* Try next first. */
	//优先检查后向相邻 VMA(vm_next)
	if (vma->vm_next) {
		//:判断该相邻 VMA 的 anon_vma 是否可以复用。
		anon_vma = reusable_anon_vma(vma->vm_next, vma, vma->vm_next);
		if (anon_vma)
			return anon_vma;
	}

	/* Try prev next. */
	//检查前向相邻 VMA(vm_prev)
	if (vma->vm_prev)
		//检查前一个 VMA 的 anon_vma 是否可以复用。
		anon_vma = reusable_anon_vma(vma->vm_prev, vma->vm_prev, vma);

	/*
	 * We might reach here with anon_vma == NULL if we can't find
	 * any reusable anon_vma.
	 * There's no absolute need to look only at touching neighbours:
	 * we could search further afield for "compatible" anon_vmas.
	 * But it would probably just be a waste of time searching,
	 * or lead to too many vmas hanging off the same anon_vma.
	 * We're trying to allow mprotect remerging later on,
	 * not trying to minimize memory used for anon_vmas.
	 */
	return anon_vma;
}

该函数用于在分配新的 anon_vma 之前,检查当前 VMA(vm_area_struct)的相邻 VMA,看看是否可以复用它们关联的 anon_vma。

此vma能与前后的vma进行合并,系统就不会为此vma创建anon_vma,而是这两个vma共用一个anon_vma,但是会创建一个anon_vma_chain,如下:

图片来自于:https://www.cnblogs.com/tolimit/p/5398552.html

这种情况,如果新的vma能够与前后相似vma进行合并,则不会为这个新的vma创建anon_vma结构,而是将此新的vma的anon_vma指向能够合并的那个vma的anon_vma。不过内核会为这个新的vma建立一个anon_vma_chain,链入这个新的vma中,并加入到新的vma所指的anon_vma的红黑树中。在这种情况中,匿名页的反向映射就能够找到新的vma。

为什么优先检查 vm_next?

内存局部性:在大多数情况下,mprotect 或 mmap 操作更倾向于向后扩展内存,因此 vm_next 更有可能共享相同的 anon_vma。

anon_vma 复用条件:

两个 VMA 必须属于同一个进程(mm_struct)。

它们的 anon_vma 必须未被其他进程共享(否则需要 COW 处理)。

c 复制代码
static struct anon_vma *reusable_anon_vma(struct vm_area_struct *old, struct vm_area_struct *a, struct vm_area_struct *b)
{
	//检查两个VMA(a和b)是否兼容。
	if (anon_vma_compatible(a, b)) {
		struct anon_vma *anon_vma = READ_ONCE(old->anon_vma);

		//检查anon_vma是否可复用
		if (anon_vma && list_is_singular(&old->anon_vma_chain))
			return anon_vma;
	}
	return NULL;
}

该函数用于检查给定的VMA(old)的anon_vma是否可以安全地被新的VMA(a或b)复用,以避免不必要的anon_vma分配。

list_is_singular(&old->anon_vma_chain):检查old的anon_vma_chain是否只包含一个节点。

意义:如果anon_vma_chain是单例(singular),说明old的anon_vma未被其他进程共享(即未经过fork),可以安全复用。

避免共享anon_vma的复杂情况:

如果anon_vma_chain包含多个节点,说明anon_vma已被多个进程共享(例如通过fork)。

这种情况下复用anon_vma可能导致写时复制(COW)问题,因此必须分配新的anon_vma。

b:若找不到可复用的 AV,则分配一个新的 anon_vma 结构。

c 复制代码
static inline struct anon_vma *anon_vma_alloc(void)
{
	struct anon_vma *anon_vma;

	anon_vma = kmem_cache_alloc(anon_vma_cachep, GFP_KERNEL);
	if (anon_vma) {
		atomic_set(&anon_vma->refcount, 1);
		anon_vma->degree = 1;	/* Reference for first vma */
		anon_vma->parent = anon_vma;
		/*
		 * Initialise the anon_vma root to point to itself. If called
		 * from fork, the root will be reset to the parents anon_vma.
		 */
		anon_vma->root = anon_vma;
	}

	return anon_vma;
}

图片来自于:https://www.cnblogs.com/tolimit/p/5398552.html

之后再次访问此vma中不属于已经映射好的页的其他地址时,就不需要再次为此vma创建anon_vma和anon_vma_chain结构了。

(3)锁定并设置 VMA 的 anon_vma

c 复制代码
anon_vma_lock_write(anon_vma);
spin_lock(&mm->page_table_lock);
if (likely(!vma->anon_vma)) {
    vma->anon_vma = anon_vma;
    anon_vma_chain_link(vma, avc, anon_vma);
    anon_vma->degree++;
    allocated = NULL;
    avc = NULL;
}
spin_unlock(&mm->page_table_lock);
anon_vma_unlock_write(anon_vma);

设置逻辑:

若 VMA 尚未关联 AV(vma->anon_vma == NULL),则:

将新 AV 赋值给 vma->anon_vma。

通过 anon_vma_chain_link() 将 AVC 同时加入 VMA 的链表和 AV 的红黑树。

增加 AV 的引用计数 degree,表示有新的 VMA 关联到该 AV。

3.3.1.2 alloc_zeroed_user_highpage_movable
c 复制代码
// v5.15/source/arch/x86/include/asm/page.h

#define alloc_zeroed_user_highpage_movable(vma, vaddr) \
	alloc_page_vma(GFP_HIGHUSER_MOVABLE | __GFP_ZERO, vma, vaddr)

物理页分配:分配一个用户态物理页,从伙伴系统中申请一个匿名页,并初始化为全零。

3.3.1.3 page_add_new_anon_rmap
c 复制代码
/**
 * page_add_new_anon_rmap - add pte mapping to a new anonymous page
 * @page:	the page to add the mapping to
 * @vma:	the vm area in which the mapping is added
 * @address:	the user virtual address mapped
 * @compound:	charge the page as compound or small page
 *
 * Same as page_add_anon_rmap but must only be called on *new* pages.
 * This means the inc-and-test can be bypassed.
 * Page does not have to be locked.
 */
void page_add_new_anon_rmap(struct page *page,
	struct vm_area_struct *vma, unsigned long address, bool compound)
{
	//如果是复合页(Compound Page,如THP),计算子页数量;否则为1。
	int nr = compound ? thp_nr_pages(page) : 1;
	
	//标记页面为SwapBacked
	__SetPageSwapBacked(page);
	if (compound) {
		/* increment count (starts at -1) */
		atomic_set(compound_mapcount_ptr(page), 0);
	} else {
		// 普通页处理
		/* increment count (starts at -1) */
		atomic_set(&page->_mapcount, 0);
	}
	//更新匿名页统计
	__mod_lruvec_page_state(page, NR_ANON_MAPPED, nr);
	//设置匿名页反向映射
	__page_set_anon_rmap(page, vma, address, 1);
}

该函数用于将新分配的匿名页添加到反向映射(Reverse Mapping, RMAP)系统中,是匿名页内存管理的核心操作之一。其主要作用:
建立反向映射:关联匿名页与对应的VMA(虚拟内存区域)。

更新计数状态:维护页的_mapcount、LRU状态等。

支持大页(THP):透明大页的特殊处理。

(1)标记页为交换支持(Swap-Backed)

c 复制代码
__SetPageSwapBacked(page);

标记该物理页为 "可交换"(swap-backed),即可以被交换到磁盘。

匿名页默认支持交换,与文件映射页(如 mmap 的文件)区分开来。

(2)建立反向映射关系

c 复制代码
__page_set_anon_rmap(page, vma, address, 1);

核心操作:调用 __page_set_anon_rmap 建立物理页与 VMA 的反向映射关系:

设置 page->mapping 指向 VMA 的 anon_vma(AV)。

设置 page->index 为虚拟地址在 VMA 中的偏移量(以页为单位)。

将物理页添加到 AV 的红黑树中(通过 anon_vma_chain),使该物理页与 VMA 关联。

(3)为什么_mapcount初始值为-1?

-1:表示页未被映射。

0:表示被1个进程映射。

N:被N+1个进程映射(如共享内存)。

__page_set_anon_rmap
c 复制代码
/**
 * __page_set_anon_rmap - set up new anonymous rmap
 * @page:	Page or Hugepage to add to rmap
 * @vma:	VM area to add page to.
 * @address:	User virtual address of the mapping	
 * @exclusive:	the page is exclusively owned by the current process
 */
static void __page_set_anon_rmap(struct page *page,
	struct vm_area_struct *vma, unsigned long address, int exclusive)
{
	//获取 VMA 的匿名内存映射对象(anon_vma)
	struct anon_vma *anon_vma = vma->anon_vma;

	BUG_ON(!anon_vma);

	//如果页面已经是匿名页面,则直接返回
	if (PageAnon(page))
		return;

	/*
	 * If the page isn't exclusively mapped into this vma,
	 * we must use the _oldest_ possible anon_vma for the
	 * page mapping!
	 */
	//1(独占):新分配的匿名页,仅属于当前进程(如do_anonymous_page分配的页)。
	if (!exclusive)
		//0(共享):页可能被多个进程共享(如fork后的COW页),需使用anon_vma层次结构的根节点(root)统一管理。
		anon_vma = anon_vma->root;

	/*
	 * page_idle does a lockless/optimistic rmap scan on page->mapping.
	 * Make sure the compiler doesn't split the stores of anon_vma and
	 * the PAGE_MAPPING_ANON type identifier, otherwise the rmap code
	 * could mistake the mapping for a struct address_space and crash.
	 */
	//设置匿名页标识
	//通过将指针值加上PAGE_MAPPING_ANON(为0x1),利用指针的最低有效位作为标记位。
	//这样mapping字段既能存储anon_vma地址,又能标识页类型(匿名页 vs. 文件页)。
	anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;
	WRITE_ONCE(page->mapping, (struct address_space *) anon_vma);
	//计算虚拟地址在VMA中的线性页偏移
	page->index = linear_page_index(vma, address);
}

该函数是匿名页反向映射(Reverse Mapping, RMAP)的核心底层实现,负责将匿名页与对应的虚拟内存区域(VMA)关联起来。其核心功能包括:

设置匿名页标识:通过mapping字段标记页面为匿名页(PAGE_MAPPING_ANON)。

关联anon_vma:将页面的mapping指向VMA所属的anon_vma结构。

记录虚拟地址偏移:通过index字段保存地址在VMA中的位置。

内存屏障的重要性:

代码注释中提到的 page_idle 函数会通过 page->mapping 进行无锁的乐观反向映射扫描

通过 WRITE_ONCE 和特殊指针标记,确保对 page->mapping 的写入是原子的,防止其他代码误判

共享页面的处理:

当页面被多个进程共享时(non-exclusive),使用 anon_vma->root 确保所有进程都能通过根对象找到这个页面

这是处理共享匿名内存(如 fork 后的父子进程共享内存)的关键机制

特殊指针标记:

将 PAGE_MAPPING_ANON 作为指针偏移量,创建一个特殊的 struct address_space* 指针

这种设计允许内核通过检查指针值来区分不同类型的映射(文件映射 vs 匿名映射)

c 复制代码
static inline pgoff_t linear_page_index(struct vm_area_struct *vma,
					unsigned long address)
{
	pgoff_t pgoff;
	if (unlikely(is_vm_hugetlb_page(vma)))
		return linear_hugepage_index(vma, address);
	pgoff = (address - vma->vm_start) >> PAGE_SHIFT;
	pgoff += vma->vm_pgoff;
	return pgoff;
}

该函数用于计算给定虚拟地址 (address) 在虚拟内存区域 (vma) 中的线性页索引(pgoff_t 类型),即确定该地址在 VMA 所映射的文件或匿名内存中的页偏移量。

普通页面的页偏移量计算:

c 复制代码
pgoff = (address - vma->vm_start) >> PAGE_SHIFT;
pgoff += vma->vm_pgoff;

计算虚拟地址相对于 VMA 起始地址的偏移量(address - vma->vm_start)

将字节偏移量右移 PAGE_SHIFT 位,转换为页偏移量(因为 PAGE_SHIFT 通常是 12,表示 4KB 页面)

将结果加上 VMA 的起始页偏移量(vma->vm_pgoff),得到最终的页索引。

vm_pgoff:表示 VMA 映射的文件或设备内存的起始页号(对匿名页通常为 0)。

如下图所示:

3.3.2 父进程fork子进程创建匿名页面

父进程通过fork系统调用创建子进程时,子进程会复制父进程的进程地址空间VMA数据结构作为自己的进程地址空间,并且会复制父进程的PTE页表项内容到子进程的页表中,实现父子进程共享页表。

多个不同子进程中的虚拟页面会同时映射到同一个物理页面,另外多个不相干进程虚拟页面也可以通过KSM机制映射到同一个物理页面。

四、文件页反向映射

4.1 文件页反向映射全流程

同匿名页一样,可能会有多个进程的VMA同时共享一个文件映射页。而进程文件页的反向映射是通过一个与文件相关的结构address_space来进行维护的。

文件页的反向映射通常通过mapping和index来关联到文件的页缓存。当内核需要找到某个物理页对应的文件时,可以通过mapping找到对应的address_space,然后通过index计算出该页在文件中的位置。

c 复制代码
struct page {
	union {
		struct {	/* Page cache and anonymous pages */
			/* See page-flags.h for PAGE_MAPPING_FLAGS */
			struct address_space *mapping;  //file page cache
			pgoff_t index;		/* Our offset within mapping. */
		};
	}
	union {		/* This union is 4 bytes in size. */
		/*
		 * If the page can be mapped to userspace, encodes the number
		 * of times this page is referenced by a page table.
		 */
		atomic_t _mapcount;
	};
}

page->index:表示该页在映射的文件中的偏移量(以页为单位,即page index)。注意, page->index是页偏移,而不是字节偏移。它表示该页在文件中的第几个页(从0开始)。

c 复制代码
struct address_space {
	struct inode		*host;
	......
	struct rb_root_cached	i_mmap;
}

每个文件对应一个 address_space

i_mmap:区间树 (Interval Tree),i_mmap 存储的是所有映射该文件的 vm_area_struct(VMA)结构。一个文件可能会被映射到多个进程的多个VMA中,所有的这些VMA都被挂入到i_mmap指向的区间树 (Interval Tree)中。

c 复制代码
struct vm_area_struct {
	unsigned long vm_start;		/* Our start address within vm_mm. */
	......
	struct mm_struct *vm_mm;	/* The address space we belong to. */
	......
	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
	......
}

vma->vm_pgoff:表示该VMA(虚拟内存区域)在映射的文件中的起始页偏移(同样以页为单位)。即VMA映射的文件部分从文件的第vma->vm_pgoff页开始。

计算页在VMA中的页内偏移:对于给定的页,如果它属于这个VMA,那么它在VMA中的页内偏移为:page->index - vma->vm_pgoff。

计算虚拟地址 :该页在VMA中的虚拟地址为:vma->vm_start + (page->index - vma->vm_pgoff) * PAGE_SIZE。

页表项(PTE):有了虚拟地址(virtual address)和地址空间(vma->vm_mm),我们可以通过查询页表来找到对应的PTE。

当需要查找映射某个文件页的所有进程PTE时:

  1. 通过page->mapping获取address_space
  2. 获取页在文件中的偏移pgoff = page->index
  3. 遍历address_space->i_mmap中的所有VMA(使用vma_interval_tree_foreach宏)。
  4. 对于每个VMA,计算该页在VMA中的虚拟地址:address = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT)
  5. 检查address是否在[vma->vm_start, vma->vm_end)之间。如果不是,跳过。
  6. 如果地址有效,那么我们就得到了该页在进程地址空间中的虚拟地址,以及所属的mm_struct(即vma->vm_mm)。
  7. 然后,通过该虚拟地址和mm_struct,我们可以查询页表找到PTE。

如下图所示:

获取页表项 (PTE):

(1)确定文件内偏移

page->index: 文件内的页偏移(以页为单位)

vma->vm_pgoff: VMA 在文件中的起始页偏移

(2)计算 VMA 内的页偏移

c 复制代码
vma_page_offset = page->index - vma->vm_pgoff

(3)计算虚拟地址

c 复制代码
virtual_address = vma->vm_start + (vma_page_offset << PAGE_SHIFT)
                 = vma->vm_start + (page->index - vma->vm_pgoff) * PAGE_SIZE

(4) 获取页表项 (PTE)

c 复制代码
pte = lookup_pte(vma->vm_mm, virtual_address)

4.2 page_add_file_rmap

文件页反向映射通过page_add_file_rmap来完成:

c 复制代码
/**
 * page_add_file_rmap - add pte mapping to a file page
 * @page: the page to add the mapping to
 * @compound: charge the page as compound or small page
 *
 * The caller needs to hold the pte lock.
 */
void page_add_file_rmap(struct page *page, bool compound)
{
	int i, nr = 1; // nr 用于记录需要增加统计的页数(通常是1,大页时可能更多)

	// 如果 compound 为 true,但 page 不是透明大页,则触发内核 BUG。
	VM_BUG_ON_PAGE(compound && !PageTransHuge(page), page);

	// 获取此页所属内存控制组(memcg)的锁,确保对 memcg 统计的修改是原子的。
	lock_page_memcg(page);

	// 如果 compound 为 true 且 page 确实是一个透明大页 (THP)
	if (compound && PageTransHuge(page)) {
		int nr_pages = thp_nr_pages(page); // 获取大页包含的页数(通常是 512 个 4KB 页)

		// 遍历大页中的每一个子页
		for (i = 0, nr = 0; i < nr_pages; i++) {
			// 增加子页的 _mapcount。如果增加前 _mapcount 是 -1(表示之前无映射),则 atomic_inc_and_test 返回 true。
			// nr 记录有多少个子页是从"无映射"状态变为"有映射"状态的。
			if (atomic_inc_and_test(&page[i]._mapcount))
				nr++;
		}

		// 增加大页的复合映射计数。如果增加后计数不为 0,说明之前已有映射,跳转到 out。
		if (!atomic_inc_and_test(compound_mapcount_ptr(page)))
			goto out;

		// 根据页是否被标记为可换出(SwapBacked),更新不同的 LRU 统计。
		// NR_SHMEM_PMDMAPPED: 共享内存大页映射数
		// NR_FILE_PMDMAPPED: 文件页大页映射数
		if (PageSwapBacked(page))
			__mod_lruvec_page_state(page, NR_SHMEM_PMDMAPPED, nr_pages);
		else
			__mod_lruvec_page_state(page, NR_FILE_PMDMAPPED, nr_pages);
	}
	// 【处理普通页或非 compound 情况】
	else {
		// 【处理复合页但非大页的情况】如果 page 是复合页(如普通大页)且有映射(page_mapping(page) 不为空)
		if (PageTransCompound(page) && page_mapping(page)) {
			struct page *head = compound_head(page); // 获取复合页的头页

			// 调试警告:如果子页被锁定但头页未被锁定,可能有问题。
			VM_WARN_ON_ONCE(!PageLocked(page));

			// 【关键标志】设置头页的 DoubleMap 标志。
			// 这个标志用于优化,表示该复合页被映射了,避免在某些操作(如 munmap)中重复遍历所有子页。
			SetPageDoubleMap(head);

			// 如果子页被 mlock 锁定,则清除头页的 mlock 标志(? 这行逻辑可能需要结合上下文理解,通常 mlock 是针对整个复合页的)
			if (PageMlocked(page))
				clear_page_mlock(head);
		}

		// 【增加普通页的映射计数】
		// 如果增加后 _mapcount 不为 0,说明之前已有映射,跳转到 out。
		if (!atomic_inc_and_test(&page->_mapcount))
			goto out;

		// nr 保持为 1(初始值)
	}

	// 增加"已映射文件页"的全局统计。
	// 这里只在 _mapcount 从 -1 变为 0 时才增加(即从"无映射"到"首次有映射")。
	// nr 的值在大页分支中可能大于 1,在普通页分支中为 1。
	__mod_lruvec_page_state(page, NR_FILE_MAPPED, nr);

out:
	// 释放之前获取的 memcg 锁。
	unlock_page_memcg(page);
}

page_add_file_rmap 函数是 Linux 内核内存管理子系统中的一个核心函数。它的主要作用是增加一个文件页(file-backed page)的映射计数。

当一个进程通过 mmap 或其他方式将一个文件映射到其虚拟地址空间时,内核需要记录这个映射关系。page_add_file_rmap 就是在这个过程中被调用的,它会:

(1)增加 page->_mapcount:这个计数器记录了有多少个不同的虚拟地址(PTEs)映射到了这个物理页框。每次增加一个映射,这个计数就加一。

(2)更新内存统计信息:将该页计入全局的"已映射文件页"统计中(NR_FILE_MAPPED)。

(3)处理大页(THP)和特殊标志:对于透明大页(Transparent Huge Page, THP)或共享内存页,进行额外的计数和标志设置。

调用该函数时,调用者必须持有 PTE 锁(page table entry lock)。这是为了保证在修改页表项(PTE)和更新页的映射计数(_mapcount)这两个操作之间的原子性,防止竞态条件。

核心思想:维护物理页与虚拟地址映射关系的"引用计数",以便在后续操作(如页面回收、写时复制)时,内核能知道这个页被多少个地方使用。

参考资料

Linux 5.15

https://www.cnblogs.com/LoyenWang/p/12164683.html
https://zhuanlan.zhihu.com/p/627558618
https://blog.csdn.net/u010923083/article/details/116456497
https://blog.csdn.net/u012489236/article/details/114734823
https://www.zhihu.com/question/60110786
http://www.wowotech.net/memory_management/reverse_mapping.html
https://zhuanlan.zhihu.com/p/564867734
https://www.cnblogs.com/arnoldlu/p/8335483.html

相关推荐
虾..7 小时前
Linux 软硬链接和动静态库
linux·运维·服务器
Evan芙8 小时前
Linux常见的日志服务管理的常见日志服务
linux·运维·服务器
晨晖28 小时前
单链表逆转,c语言
c语言·数据结构·算法
hkhkhkhkh1239 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
HZero.chen11 小时前
Linux字符串处理
linux·string
张童瑶11 小时前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功12311 小时前
什么是SELinux
linux
石小千11 小时前
Linux安装OpenProject
linux·运维
柏木乃一11 小时前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程
Lime-309011 小时前
制作Ubuntu 24.04-GPU服务器测试系统盘
linux·运维·ubuntu