Linux 内存管理之 Rmap 反向映射(二)
文章目录
- 前言
- 一、父进程fork子进程
- 1.1 dup_mmap
- 1.2 anon_vma_fork
- 1.3 anon_vma_clone
- anon_vma 复用
- 1.4 示例
- 二、子进程发生写时复制COW
- 2.1 wp_page_copy
- 2.2 page_add_new_anon_rmap
- 2.3 __page_set_anon_rmap
- 参考资料
前言
这篇文章描述了Linux 内存管理之 Rmap 反向映射:Linux 内存管理之 Rmap 反向映射(一),本篇文章主要描述父进程fork子进程创建匿名页面时 Rmap 反向映射机制。
(1)linux 2.6.34以前父子进程fork Rmap 反向映射如下所示:
vma1是父进程的一个匿名映射的vma,a和b都已经分配了page frame,而其他的page都还都没有分配物理页面。在fork之后,子进程copy了vma1,当然由于采用了COW技术,这时候父子进程的匿名页面会共享,同时在父子进程地址空间对应的pte entry中标注write protect的标记,如下图所示:
在父进程fork子进程之后,父子进程对该页面执行写操作之前,父子进程的匿名页是共享的,所以这些page frame指向同一个anon_vma。当然父子进程一旦有write操作就会产生异常,并在异常处理中分配page frame,解除父子进程匿名页面的共享,具体如下图所示:
这时候由于写操作,父子进程原本共享的page frame已经不再共享,然而,这两个page却仍然指向同一个anon_vma,即使父子进程的VMA共享一个anon_vma。可以看到上图 page1、2、3、4都共享一个anon_vma。
在这种情况下,我们看看unmap page 1会发生什么。page1对应的struct page的mapping成员指向了上图中的anon_vma,遍历anon_vma会获取到vma1和vma2,这里面vma2是子进程的VMA,不属于父进程,我们这里umap的是父进程的page1,本来就不应该匹配到。如果anon_vma的链表没有那么长,那么整体性能也OK。然而,在有些网路服务器中,系统非常依赖fork,某个服务程序可能会fork巨大数量的子进程来处理服务请求,在这种情况下,系统性能严重下降。一个具体的示例:系统中有1000进程,都是通过fork生成的,每个进程的VMA有 1000个匿名页。根据目前的软件架构,anon_vma链表中会有1000个vma 的节点,而系统中有一百万个匿名页面属于同一个anon_vma。
当系统中的一个CPU在执行try_to_unmap_anon函数的时候,需要遍历VMA链表,这时会持有anon_vma->lock这个自旋锁。由于anon_vma存有了很多根本无关的VMA,通过,page table的检索过程,你就会发现这个VMA根本和准备unmap的page无关,因此只能scan下一个VMA,整个过程需要消耗大量的时间,延长了临界区(复杂度是O(N))。与此同时,其他CPU在试获取这把锁的时候,基本会被卡住,这时候整个系统的性能可想而知了。更加糟糕的是内核中并非只有unmap匿名页面的时候会上锁、遍历VMA链表,还有一些其他的场景也会这样(例如page_referenced函数)。想象一下,一百万个页面共享这一个anon_vma,对anon_vma->lock自旋锁的竞争那是相当的激烈啊。
这个问题的核心在于 anon_vma 的共享粒度太粗:所有通过 fork 产生的、曾经共享过页面的进程,其 VMA 都被链接到了同一个 anon_vma 链表上,即使它们后来通过 COW 分离了物理页面。
(2)linux 2.6.34以及之后父子进程fork Rmap 反向映射如下所示:
为了解决这个“大一统 anon_vma”带来的性能问题,Linux 内核引入了更精细的反向映射数据结构,其核心是 anon_vma_chain 结构和 anon_vma 的层次化(树状)管理。
AVC的作为主要就是为了建立父进程和子进程之间的联系。通过AVC父子进程枢纽,父进程可以找到子进程的VMA(因为AVC_父子进程枢纽 插入AV_p的红黑树中),而子进程也可以找到父进程的AV(因为AVC_父子进程枢纽 插入VMA_c的链表中),这对父子进程共享一个物理页的情况反向映射效率有很大帮助。
一、父进程fork子进程
父进程通过fork系统调用创建子进程时,子进程会复制父进程的进程地址空间VMA数据结构作为自己的进程地址空间,并且会复制父进程的PTE页表项内容到子进程的页表中,实现父子进程共享页表。
多个不同子进程中的虚拟页面会同时映射到同一个物理页面,另外多个不相干进程虚拟页面也可以通过KSM机制映射到同一个物理页面。
SYSCALL_DEFINE0(fork)-->kernel_clone()-->copy_process()-->copy_mm()-->dup_mm()-->dup_mmap()-->anon_vma_fork()
1.1 dup_mmap
dup_mmap 在 fork() 系统调用期间被调用,负责为新创建的进程(子进程)复制其父进程的整个虚拟内存布局。
dup_mmap 的主要目标是:
复制 VMA 结构:为子进程创建一套与父进程结构完全相同的 vm_area_struct (VMA)。
建立页表映射:为这些新的 VMA 建立初始的页表项(PTEs)。
我这里只关注处理匿名内存 (Anonymous Memory) 和写时复制 (COW):
static __latent_entropy int dup_mmap(struct mm_struct *mm, // 新进程的 mm_structstruct mm_struct *oldmm) // 父进程的 mm_struct
{struct vm_area_struct *mpnt, *tmp, *prev, **pprev;......// 遍历父进程的所有 VMAfor (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {// 创建一个新的 VMA 结构 (tmp),复制 mpnt 的大部分内容tmp = vm_area_dup(mpnt);// 将新 VMA 关联到新进程tmp->vm_mm = mm;// 处理匿名内存 (Anonymous Memory) 和写时复制 (COW) 的核心if (tmp->vm_flags & VM_WIPEONFORK) {// 如果设置了 VM_WIPEONFORK,子进程的这块内存会被清零。// 因此,不需要立即复制页面,也不需要初始化 anon_vma。tmp->anon_vma = NULL;} else {// 这是 COW 的关键!// anon_vma_fork() 为子进程的 VMA 设置 anon_vma 结构。// 这使得父进程和子进程的 VMA 都链接到同一个或相关的 anon_vma。// 当后续发生写操作时,内核可以通过 anon_vma 找到所有共享该页的 VMA,并执行 COW。anon_vma_fork(tmp, mpnt);}}
}
1.2 anon_vma_fork
anon_vma_fork 函数用于在进程 fork 时处理匿名内存区域(Anonymous Memory Area)的反向映射关系。
该函数主要完成以下工作:
将子进程的 VMA 附加到父进程的 anon_vma 结构
创建新的 anon_vma 结构供子进程使用
建立父子进程间的反向映射关系链
/*** anon_vma_fork - 为子进程的 VMA 设置匿名内存映射* @vma: 子进程的新 VMA* @pvma: 父进程的对应 VMA** 返回 0 成功,非 0 失败。*/
int anon_vma_fork(struct vm_area_struct *vma, struct vm_area_struct *pvma)
{struct anon_vma_chain *avc;struct anon_vma *anon_vma;int error;/* 1. 如果父进程的 VMA 没有 anon_vma,说明它可能不是匿名可写映射,无需处理。 */if (!pvma->anon_vma)return 0;/* 2. 清除子进程 VMA 原有的 anon_vma 指针。我们准备复用或重新分配。 */vma->anon_vma = NULL;/** 3. 【核心:继承】将子进程的 VMA 链接到父进程 VMA 所关联的 *所有* anon_vma 上。* 这样,当内核需要查找映射了某个物理页的所有 VMA 时,它可以通过父进程 VMA 的 anon_vma* 找到父进程的 VMA *和* 子进程的 VMA,即使这些页当前是共享的(COW 前)。*/error = anon_vma_clone(vma, pvma);if (error)return error;/* 4. 如果 anon_vma_clone 成功复用了现有的 anon_vma,则无需创建新的,直接返回。 */if (vma->anon_vma)return 0;/* 5. 【核心:创建】为子进程创建一个 *新的* anon_vma。* 这个新的 anon_vma 将专门用于存放子进程 *新写入* 的、被 COW 分离出来的页面。*/anon_vma = anon_vma_alloc(); // 分配 anon_vma 结构if (!anon_vma)goto out_error;avc = anon_vma_chain_alloc(GFP_KERNEL); // 分配 anon_vma_chain 结构if (!avc)goto out_error_free_anon_vma;/** 6. 【构建树结构】设置新 anon_vma 的层级关系。* - root: 指向父进程 anon_vma 的根。整个树共享同一个根锁。* - parent: 直接指向父进程的 anon_vma。形成父子关系。*/anon_vma->root = pvma->anon_vma->root;anon_vma->parent = pvma->anon_vma;/** 7. 【引用计数】增加根 anon_vma 的引用计数。* 因为这个新创建的 anon_vma 的锁依赖于根节点的锁,所以必须确保根节点不会被提前释放。*/get_anon_vma(anon_vma->root);/* 8. 【标记】将这个新创建的 anon_vma 关联到子进程的 VMA 上。* 这意味着,当子进程写入并触发 COW 时,新分配的页面将被归入这个 anon_vma 的管理之下。*/vma->anon_vma = anon_vma;/* 9. 【链接】执行实际的链接操作。这会:* - 获取 anon_vma 的写锁(实际是 root->rwsem)。* - 将 avc 链接到 vma->anon_vma_node 链表。* - 将 avc 链接到 anon_vma->rb_root(红黑树)或链表。*/anon_vma_lock_write(anon_vma);anon_vma_chain_link(vma, avc, anon_vma);/* 10. 【维护度数】增加父节点的“度数”(degree),表示它有一个新的子 anon_vma。 */anon_vma->parent->degree++;/* 11. 释放锁 */anon_vma_unlock_write(anon_vma);return 0; // 成功out_error_free_anon_vma:put_anon_vma(anon_vma); // 释放已分配的 anon_vmaout_error:unlink_anon_vmas(vma); // 回滚 anon_vma_clone 的操作,清理已链接的结构return -ENOMEM;
}
注意:anon_vma_clone这里会分配一个struct anon_vma_chain,把这个anon_vma_chain枢纽挂入子进程的VMA的anon_vma_chain链表中,同时把anon_vma_chain枢纽添加到属于父进程的anon_vma->rb_root的红黑树中,使子进程和父进程的VMA之间有一个联系的纽带。
AVC的作为主要就是为了建立父进程和子进程之间的联系。通过AVC父子进程枢纽,父进程可以找到子进程的VMA(因为AVC_父子进程枢纽 插入AV_p的红黑树中),而子进程也可以找到父进程的AV(因为AVC_父子进程枢纽 插入VMA_c的链表中),这对父子进程共享一个物理页的情况反向映射效率有很大帮助。
然后为子进程又分配一个 anon_vma 和 anon_vma_chain。
1.3 anon_vma_clone
anon_vma_clone 是 Linux 内核中用于维护匿名内存反向映射(reverse mapping, rmap)结构的关键函数。它负责将一个 VMA(源 VMA)所关联的所有 anon_vma “链接”到另一个 VMA(目标 VMA)上。这个机制对于实现写时复制(Copy-on-Write, COW)和高效地查找映射了某个物理页的所有虚拟地址至关重要。
anon_vma_clone 的主要目标是:
复制匿名映射关系:让目标 VMA(dst)能够“看到”源 VMA(src)所关联的所有 anon_vma。
优化反向映射树结构:在特定场景下(主要是 fork),尝试复用现有的 anon_vma,以防止反向映射树退化成无限增长的线性链,从而优化内存使用和查找性能。
int anon_vma_clone(struct vm_area_struct *dst, struct vm_area_struct *src)
{struct anon_vma_chain *avc, *pavc;struct anon_vma *root = NULL; // 用于管理锁的根节点// 1. 逆向遍历源 VMA (src) 的所有 anon_vma_chain。// 逆向遍历是为了优先处理树中更底层(更具体)的 anon_vma,// 这对于复用策略至关重要(优先考虑“空闲”的底层节点)。list_for_each_entry_reverse(pavc, &src->anon_vma_chain, same_vma) {struct anon_vma *anon_vma;// 2. 为新的链接分配 anon_vma_chain 结构。avc = anon_vma_chain_alloc(GFP_NOWAIT | __GFP_NOWARN);if (unlikely(!avc)) {unlock_anon_vma_root(root);root = NULL;avc = anon_vma_chain_alloc(GFP_KERNEL);if (!avc)goto enomem_failure;}anon_vma = pavc->anon_vma; // 获取当前要链接的 anon_vma// 3. 获取该 anon_vma 所属树的根节点锁。// lock_anon_vma_root 会处理锁的嵌套和升级。root = lock_anon_vma_root(root, anon_vma);// 4. 执行实际的链接操作。/* * 设置新的avc->vma指向子进程的vma* 设置新的avc->anon_vma指向父进程anon_vma_chain结点指向的anon_vma(这个anon_vma不一定属于父进程)* 将新的avc->same_vma加入到子进程的anon_vma_chain链表头部* 将新的avc->rb加入到父进程anon_vma_chain结点指向的anon_vma*/anon_vma_chain_link(dst, avc, anon_vma);// 5. 【核心优化】尝试复用符合条件的 anon_vma。if (!dst->anon_vma && src->anon_vma &&anon_vma != src->anon_vma && anon_vma->degree < 2) {dst->anon_vma = anon_vma; // 将复用的 anon_vma 作为 dst 的主 anon_vma}// 注意:这里没有 break,即使复用了,也会继续链接其他 anon_vma(但 dst->anon_vma 不会再改变)}// 6. 如果通过复用设置了一个主 anon_vma,增加其度数(因为它现在是某个 dst 的主节点)。if (dst->anon_vma)dst->anon_vma->degree++;// 7. 释放之前获取的所有锁。unlock_anon_vma_root(root);return 0; // 成功enomem_failure:// 8. 内存分配失败的错误处理。dst->anon_vma = NULL; // 清除可能被设置的 anon_vma,防止 unlink 时错误递减 degreeunlink_anon_vmas(dst); // 回滚所有已建立的链接,清理资源return -ENOMEM;
}
anon_vma_clone:
(1)遍历父进程VMA中的anon_vma_chain链表寻找anon_vma_chain实例,这里称这个实例为pavc;
(2)分配一个新的anon_vma_chain数据结构,这里称为anon_vma_chain枢纽;
(3)通过pavc找到父进程VMA中的anon_vma;
(4)把这个anon_vma_chain枢纽挂入子进程的VMA的anon_vma_chain链表中,同时把anon_vma_chain枢纽添加到属于父进程的anon_vma->rb_root的红黑树中,使子进程和父进程的VMA之间有一个联系的纽带。
/* * 设置新的avc->vma指向子进程的vma* 设置新的avc->anon_vma指向父进程anon_vma_chain结点指向的anon_vma(这个anon_vma不一定属于父进程)* 将新的avc->same_vma加入到子进程的anon_vma_chain链表头部* 将新的avc->rb加入到父进程anon_vma_chain结点指向的anon_vma*/anon_vma_chain_link(dst, avc, anon_vma);
如下图所示:
anon_vma 复用
这是 anon_vma_clone 最精妙的部分。其目的是防止在频繁 fork 的场景下,反向映射树变得过深和低效。
关键的部分是检查是否可以复用现有的anon_vma。注释中提到,当dst->anon_vma为NULL且src->anon_vma非空时,函数会尝试找到一个已有的anon_vma来复用,条件是该anon_vma的degree小于2,即没有VMA且只有一个子anon_vma。这可以防止anon_vma层次结构退化为无限链表。但不会复用父anon_vma,因为根anon_vma有自引用且至少有一个子节点。
(1)尝试复用 anon_vma:
if (!dst->anon_vma && src->anon_vma &&anon_vma != src->anon_vma && anon_vma->degree < 2)dst->anon_vma = anon_vma;
条件说明:
dst->anon_vma 为 NULL:dst 是新 VMA,尚未分配自己的 anon_vma。(说明这是fork的情况)
src->anon_vma 非空:src 有有效的 anon_vma。(即源VMA有anon_vma)
anon_vma != src->anon_vma:,并且当前遍历到的anon_vma不是src->anon_vma(避免选择父进程的anon_vma)
anon_vma->degree < 2:且这个anon_vma的degree小于2(意味着它没有关联的VMA并且只有一个子anon_vma)
如果满足条件,则将dst->anon_vma设置为这个anon_vma。
复用目的:
避免为每个 fork 创建新的 anon_vma,减少内存开销。
防止 anon_vma 层级退化为无限链表(如 A -> B -> C -> …)。
复用 anon_vma 的条件
degree < 2:只有当 anon_vma 的 degree 小于 2 时才会被复用。这意味着:
(1)该 anon_vma 当前没有关联的 VMA(degree == 0 )。
(2)或者它只有一个子anon_vma (degree == 1)
避免复用根 anon_vma:根 anon_vma 有自引用(root->root = root)且至少有一个子节点,因此不会被复用。
关于重用anon_vma的条件解释:
- 在fork时(dst->anon_vma为NULL且src->anon_vma存在),我们尝试找到一个可以重用的anon_vma。这个anon_vma必须不是src->anon_vma(因为src->anon_vma是父进程的,我们不能直接重用父进程的anon_vma,否则第一个子进程会一直重用,导致后续无法再重用),并且它的degree小于2(即没有VMA关联,只有一个子anon_vma)。这样的anon_vma可以被视为空闲的,可以重用,从而避免创建新的anon_vma,减少层次深度。
- 根anon_vma永远不会被重用,因为它有自引用(parent指向自己)并且至少有一个子节点(degree>=1,实际上通常大于1,因为至少有一个子进程)。
这个重用的目的是防止在持续fork的情况下,anon_vma层次结构退化为长链。通过重用符合条件的anon_vma,可以保持层次结构的宽度,减少反向映射(rmap)遍历的深度,提高性能。
注意:在遍历过程中,可能会为dst添加多个anon_vma_chain(因为源VMA可能关联多个anon_vma),但dst->anon_vma只设置一次(重用的那个)。
最后,如果重用了anon_vma,则增加其degree(因为现在这个anon_vma被dst->anon_vma引用了,所以它的子节点数增加)。
(2)degree
if (dst->anon_vma)dst->anon_vma->degree++;
遍历结束后,如果已经为dst->anon_vma设置了一个anon_vma(即重用了某个anon_vma),则增加该anon_vma的degree(引用计数)。
degree 含义:统计子 anon_vma 数量(非 VMA 数量),直接影响反向映射查找的效率:
(1)degree 越小,说明该 anon_vma 的关联链表越短,查找速度越快。
(2)复用低 degree 的 anon_vma 可以减少链表遍历时间。
1.4 示例
(1)进程被创建时,如下图所示:
(2)父进程fork子进程,如下图所示:
父进程fork子进程时,会给子进程分配自己的struct anon_vma av1、 struct anon_vma_chain avc1和struct vm_area_struct vma1。
为了建立和父进程的联系,然后还会分配一个struct anon_vma_chain avc0-1,它是一个桥梁,连接了父子进程。通过这个桥梁,父进程可以找到子进程的vma1(因为avc0-1插入av0的红黑树中),而子进程也可以找到父进程的av0(因为avc0-1插入vma1的链表中)。
当然,自己的anon_vma也需要创建。在上图中,av1就是子进程的anon_vma,同时分配一个avc1来连接该子进程的vma1和av1,并调用anon_vma_chain_link函数将avc1插入vma1的链表和av1的红黑树中。
父进程也会创建其他新的子进程,新创建的子进程的层次和vma1、av1的类似,这里就不描述了。不过需要注意的是:父进程每创建一个子进程,av0的红黑树中会增加每一个起“桥梁”作用的avc,以此连接到子进程的vma。
(3)子进程fork孙进程,如下图所示:
在fork的时候,我们进行VMA的拷贝:即分配vma2并以vma1为原型copy到vma2中。Copy是沿着vma1的avc链表进行的,该链表有两个元素:avc1和 avc_0-1,分别和父进程A和子进程B的AV关联。因此,在孙进程C中,我们会分配avc0-2和avc1-2两个avc,并建立level 2层和level 0层以及level 1层之间的关系。
同样的,自己level的anon_vma也需要创建。在上图中,av2就是孙进程C的anon_vma,同时分配一个avc2来连接该孙进程的vma2和av2,并调用anon_vma_chain_link函数将avc2插入vma2的链表和av2的红黑树中。
av2中的root指向root av,也就是进程A的av。Parent成员指向其B进程(C的父进程)的av。通过Parent这样的指针,不同level的av建立了父子关系,而通过root指针,每一个level的av都可以寻找找到root av。
二、子进程发生写时复制COW
Linux fork写时复制请参考:Linux fork 写时复制
handle_mm_fault()-->__handle_mm_fault()-->handle_pte_fault(){if (vmf->flags & FAULT_FLAG_WRITE) {if (!pte_write(entry))return do_wp_page(vmf);entry = pte_mkdirty(entry);}}
2.1 wp_page_copy
do_wp_page()-->wp_page_copy()-->page_add_new_anon_rmap()
wp_page_copy函数是 Linux 内核中处理写时复制(Copy-On-Write, COW)的核心逻辑,当进程尝试写入一个共享的匿名页时触发。主要完成以下任务:
分配新物理页:复制旧页内容到新页。
更新页表项:将新页映射到触发缺页的虚拟地址。
管理反向映射(rmap):维护新/旧页的映射关系。
2.2 page_add_new_anon_rmap
page_add_new_anon_rmap 函数的作用是:为一个全新的匿名页面(通常是通过 wp_page_copy 分配的 COW 页面)建立反向映射(reverse mapping, rmap)。
它将一个物理页面(struct page)与一个虚拟内存区域(VMA)和一个虚拟地址(address)关联起来,使得内核在后续需要查找“哪些进程的哪些虚拟地址映射了这个物理页面”时,能够快速找到。
函数名中的 new 和注释都强调了这一点:此函数必须且只能用于刚刚分配、尚未建立任何映射的“新”页面。
/*** 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)
{int nr = compound ? thp_nr_pages(page) : 1; // 1. 计算页数VM_BUG_ON_VMA(address < vma->vm_start || address >= vma->vm_end, vma); // 2. 地址范围检查__SetPageSwapBacked(page); // 3. 设置交换支持标志if (compound) {// 4a. 处理复合页 (Compound Page / THP)VM_BUG_ON_PAGE(!PageTransHuge(page), page); // 确保确实是透明大页/* increment count (starts at -1) */atomic_set(compound_mapcount_ptr(page), 0); // 直接设置复合页的 _mapcount 为 0if (hpage_pincount_available(page))atomic_set(compound_pincount_ptr(page), 0); // 初始化 pincount (如果可用)__mod_lruvec_page_state(page, NR_ANON_THPS, nr); // 5a. 更新 LRU 统计:匿名大页数} else {// 4b. 处理小页 (Small Page)/* Anon THP always mapped first with PMD */VM_BUG_ON_PAGE(PageTransCompound(page), page); // 确保不是复合页/* increment count (starts at -1) */atomic_set(&page->_mapcount, 0); // 直接设置小页的 _mapcount 为 0}__mod_lruvec_page_state(page, NR_ANON_MAPPED, nr); // 5b. 更新 LRU 统计:匿名映射页数__page_set_anon_rmap(page, vma, address, 1); // 6. 核心:建立反向映射链
}
__page_set_anon_rmap这是函数的核心。
__page_set_anon_rmap(page, vma, address, 1) 调用底层函数,将 page 与 vma 和 address 关联起来。
它会利用 anon_vma 和 anon_vma_chain 结构,将这个映射关系添加到反向映射链中。这样,当未来需要查找映射了 page 的所有 VMA 时,就可以通过遍历这些链表来实现。
2.3 __page_set_anon_rmap
__page_set_anon_rmap 函数是建立匿名页面反向映射(rmap)的最底层核心操作。它的作用是将一个物理页面(struct page)与一个 anon_vma 结构关联起来,并设置页面的索引(index),从而完成反向映射链的最终建立。
/*** __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)
{struct anon_vma *anon_vma = vma->anon_vma; // 1. 获取 VMA 的 anon_vmaBUG_ON(!anon_vma); // 2. 断言检查:确保 anon_vma 存在if (PageAnon(page))return; // 3. 安全检查:如果页面已是匿名映射,直接返回/** 如果页面不是独占映射,则必须使用该 anon_vma 链中最“古老”的 anon_vma(root)。*/if (!exclusive)anon_vma = anon_vma->root; // 4. 选择正确的 anon_vma/** page_idle 操作会在 page->mapping 上进行无锁/乐观的 rmap 扫描。* 必须确保编译器不会将 anon_vma 和 PAGE_MAPPING_ANON 标识符的存储操作分开,* 否则 rmap 代码可能会将 mapping 误认为是 struct address_space 而导致崩溃。*/anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON; // 5. 编码 anon_vma 指针WRITE_ONCE(page->mapping, (struct address_space *) anon_vma); // 6. 原子写入 mappingpage->index = linear_page_index(vma, address); // 7. 设置页面在线性映射中的索引
}
关键设计思想:
(1)匿名页标识的高效存储
mapping 字段复用:
普通文件页的 page->mapping 指向 address_space。
匿名页通过 PAGE_MAPPING_ANON 标志位和指针编码,节省一个专用字段。
page->mapping 字段在文件页中指向 struct address_space(用于文件反向映射)。
在匿名页中,它被复用来存储指向 anon_vma 的指针。
(2)独占 vs 非独占映射优化
场景 | 使用的 anon_vma | 目的 |
---|---|---|
独占映射 | 当前 VMA 的 anon_vma | 减少反向映射遍历深度(子进程独立管理 COW 页)。 |
非独占映射 | anon_vma->root | 确保共享进程(如共享内存)能通过最老 anon_vma 找到所有映射的页。 |
选择正确的 anon_vma (关键逻辑):
如果 exclusive 为 true:使用 vma->anon_vma 本身。这通常用于新分配的页面或 COW 页面,其 anon_vma 是专门为这个 VMA 创建的,可以精确地指向它。
如果 exclusive 为 false:使用 anon_vma->root。root 是 anon_vma 链中最“古老”的那个 anon_vma(通常是父进程创建的)。为什么:
(1)当多个进程通过 fork 共享页面时,它们的 anon_vma 链最终都指向同一个 root。
(2)为了确保反向映射查找的一致性和完整性,所有共享该页面的进程都必须链接到同一个 anon_vma(即 root)。这样,当需要查找所有映射时,遍历 root 的 same_anon_vma 链表就能找到所有相关的 VMA。
(3)如果不这样做,每个进程都链接到自己的 anon_vma,那么查找就会遗漏其他进程的映射。
参考资料
https://www.slideshare.net/AdrianHuang/reverse-mapping-rmap-in-linux-kernel
http://www.wowotech.net/memory_management/reverse_mapping.html
https://blog.csdn.net/u010923083/article/details/116456497
https://www.cnblogs.com/tolimit/p/5398552.html
https://blog.csdn.net/qq_58538265/article/details/135287909
https://www.cnblogs.com/LoyenWang/p/12164683.html