一、简介
反向映射是内存管理中的一个核心概念,用于高效地通过物理页找到映射了该页的所有虚拟地址(即页表项)。这对于页面回收、迁移、换出等操作至关重要。
反向映射的发展经历了几个阶段,从最初的匿名页反向映射,到后来加入文件页和交换缓存的反向映射支持。
为什么需要反向映射?
在操作系统中,多个进程的虚拟地址可能映射到同一个物理页(例如共享内存、写时复制等)。当内核需要回收一个物理页时,它必须修改所有映射了该页的页表项,使其无效或指向其他位置。如果没有反向映射,内核将不得不遍历所有进程的页表来寻找映射,这是极其低效的。
一个物理页 → 多个虚拟映射:通过 fork()、KSM 等机制,单个物理页可能被多个进程的虚拟地址映射
反向映射的目标是:给定一个物理页,快速找到所有映射了该页的虚拟地址(即页表项),即快速定位所有映射到某个物理页的虚拟地址和进程。。
正向映射:虚拟地址 → 物理页帧(通过页表实现)
正向映射即内存映射,即从虚拟内存到物理内存的映射。
反向映射:物理页帧 → 所有映射它的虚拟地址
反向映射在已知物理页面(page frame,可能是PFN、可能是指向page descriptor的指针,也可能是物理地址,内核有各种宏定义用于在它们之间进行转换)的情况下,找到映射该物理页面的虚拟地址。
由于一个page frame可以在多个进程之间共享,因此反向映射的任务是把分散在各个进程地址空间中的所有的page table entry全部找出来。
如下图所示:
关键应用场景:
页面回收(回收前需解除所有映射)
内存迁移(迁移前需更新所有页表项)
透明大页分裂(THP split)
NUMA 平衡
比如内存回收:当发生内存回收时,通过反向映射技术在inactive lru中很容易获得物理内存页面并找到所有映射关系进行解映射。
二、struct page
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 结构体的指针,以及一些标志位。
// 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):
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)
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)
if (PageAnon(page)) {
struct anon_vma *page__anon_vma = page_anon_vma(page);
}
三、匿名页反向映射
3.1 相关结构体
3.1.1 struct vm_area_struct
// 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
// 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
// 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。
代码如下所示:
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 匿名页反向映射全流程
关键数据结构关系:
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
// 通过PFN获取page
struct page *pfn_to_page(unsigned long pfn)
(2)获取anon_vma
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。
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分配物理内存时,通常会产生匿名页面。例如:
malloc() → 用户进程写内存 → 内核发生缺页异常 → do_anonymous_page()
用户态进程访问虚拟内存的时候,发现没有映射到物理内存,页表也没有创建过,才触发缺页异常。进入内核调用 do_page_fault,一直调用到 __handle_mm_fault,既然原来没有创建过页表,于是,__handle_mm_fault 调用 pud_alloc 和 pmd_alloc,来创建相应的页目录项,最后调用 handle_pte_fault 来创建页表项。
do_page_fault()
-->__handle_mm_fault()
-->handle_pte_fault ()
-->do_anonymous_page()
handle_pte_fault()
{
//如果 PTE,也就是页表项,从来没有出现过,那就是新映射的页
if (!vmf->pte) {
//如果是匿名页,应该映射到一个物理内存页,调用 do_anonymous_page
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf);
}
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
static inline int anon_vma_prepare(struct vm_area_struct *vma)
{
if (likely(vma->anon_vma))
return 0;
return __anon_vma_prepare(vma);
}
/**
* __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 结构
avc = anon_vma_chain_alloc(GFP_KERNEL);
if (!avc)
goto out_enomem;
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
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)。
/*
* 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 处理)。
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 结构。
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
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
// 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
/**
* 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)
__SetPageSwapBacked(page);
标记该物理页为 “可交换”(swap-backed),即可以被交换到磁盘。
匿名页默认支持交换,与文件映射页(如 mmap 的文件)区分开来。
(2)建立反向映射关系
__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
/**
* __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 匿名映射)
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 所映射的文件或匿名内存中的页偏移量。
普通页面的页偏移量计算:
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计算出该页在文件中的位置。
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开始)。
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)中。
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时:
- 通过
page->mapping
获取address_space
。 - 获取页在文件中的偏移
pgoff = page->index
。 - 遍历
address_space->i_mmap
中的所有VMA(使用vma_interval_tree_foreach
宏)。 - 对于每个VMA,计算该页在VMA中的虚拟地址:
address = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT)
。 - 检查
address
是否在[vma->vm_start, vma->vm_end)
之间。如果不是,跳过。 - 如果地址有效,那么我们就得到了该页在进程地址空间中的虚拟地址,以及所属的
mm_struct
(即vma->vm_mm
)。 - 然后,通过该虚拟地址和
mm_struct
,我们可以查询页表找到PTE。
如下图所示:
获取页表项 (PTE):
(1)确定文件内偏移
page->index: 文件内的页偏移(以页为单位)
vma->vm_pgoff: VMA 在文件中的起始页偏移
(2)计算 VMA 内的页偏移
vma_page_offset = page->index - vma->vm_pgoff
(3)计算虚拟地址
virtual_address = vma->vm_start + (vma_page_offset << PAGE_SHIFT)
= vma->vm_start + (page->index - vma->vm_pgoff) * PAGE_SIZE
(4) 获取页表项 (PTE)
pte = lookup_pte(vma->vm_mm, virtual_address)
4.2 page_add_file_rmap
文件页反向映射通过page_add_file_rmap来完成:
/**
* 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