而今天要深入剖析的Linux内核反向映射 RMAP(Reverse Mapping),正是一把能大幅加速数据访问的利刃,它在内存管理的舞台上扮演着举足轻重的角色。从基础概念到复杂的实现机制,RMAP 背后藏着诸多奥秘,接下来,就让我们一同踏上探索之旅,揭开它的神秘面纱 。
一、Linux 内存管理的 “前世今生”
在计算机系统中,内存管理一直是操作系统的核心功能之一,对于 Linux 系统而言,其内存管理机制随着时间的推移不断演进,从早期的简单形式逐渐发展为如今复杂而高效的体系。
早期的计算机系统中,内存管理非常直接和简单。程序直接运行在物理内存上,采用连续分配的方式,将物理内存划分为不同的区域,每个区域分配给一个程序使用。这种方式虽然简单易懂,但存在诸多严重问题,比如进程地址空间无法隔离,一个进程可以随意访问其他进程的内存空间,这严重威胁系统安全;内存使用效率低下,由于需要连续的内存空间,容易产生内存碎片,导致内存浪费;而且程序运行地址不确定,每次运行都要在内存中寻找足够大的空闲区域,增加了程序重定位的复杂性。
为了解决这些问题,虚拟内存的概念应运而生。虚拟内存作为程序和物理内存之间的中间层,为每个进程提供独立的地址空间,实现进程地址空间的隔离,增强系统安全性。在 Linux 系统中,虚拟内存通过分页和分段技术实现虚拟地址到物理地址的映射。分段技术将程序地址空间划分为不同逻辑段,如代码段、数据段、栈段等,每个段在物理内存中可以不连续存储,解决程序运行地址不确定问题,但仍存在外部碎片问题。分页技术则把虚拟内存和物理内存划分为固定大小的页,以页为单位进行映射和管理,大大提高内存利用率,减少内存碎片。
随着 Linux 系统应用场景不断扩展和硬件技术发展,内存管理面临新挑战。特别是在多进程、多线程环境下,如何快速准确地确定物理页面与虚拟页面之间的映射关系,成为提高内存管理效率的关键。在早期 Linux 内核版本中(如 2.4 内核),当需要确定某物理页面是否被某个进程映射时,必须遍历每个进程的页表,这一过程工作量巨大,效率极低。比如在一个拥有大量进程的服务器系统中,若要查找某个物理页面的映射关系,遍历所有进程页表可能需要耗费大量 CPU 时间,严重影响系统性能。
为应对这一挑战,在 Linux 2.5 内核开发期间,反向映射(Reverse Mapping,RMAP)的概念被提出并逐步完善。RMAP 的出现,彻底改变 Linux 内存管理中查找页面映射关系的方式,极大提高内存管理效率,为 Linux 系统在各种复杂环境下稳定高效运行奠定坚实基础。
二、RMAP是什么?
RMAP,即反向映射(Reverse Mapping),是 Linux 内核中用于解决从物理页面快速查找其对应的虚拟地址映射关系的关键机制 ,与传统的从虚拟地址到物理地址的正向映射方向相反,RMAP 建立了从物理页面到虚拟地址空间的反向映射关系。在实际运行中,当系统需要对某个物理页面进行操作时,如回收、迁移或共享,RMAP 能帮助内核迅速定位到所有映射到该物理页面的虚拟地址,极大提高操作效率。
在页面回收场景中,当系统内存不足时,需要将一些长时间未使用的页面从物理内存中回收,释放出空间供其他更急需内存的进程使用。在没有 RMAP 机制之前,要回收一个物理页面,内核必须遍历系统中每个进程的页表,检查该物理页面是否被某个进程映射,这一过程效率极低,因为现代计算机系统中往往运行着大量进程,每个进程又有庞大的页表,遍历所有进程页表的时间开销巨大。而有了 RMAP 后,内核可以直接通过物理页面的相关数据结构,快速找到所有映射该页面的虚拟地址,然后断开这些映射关系,将页面回收,大大提高内存回收效率。
在页面迁移场景中,为了实现内存的高效利用和负载均衡,有时需要将一个物理页面从一个内存区域迁移到另一个内存区域,例如在 NUMA(Non - Uniform Memory Access)架构的系统中,为了让进程更高效地访问内存,可能需要将进程使用的页面迁移到距离其 CPU 更近的内存节点上。在迁移之前,同样需要找到所有映射到该页面的虚拟地址,以便在迁移完成后更新映射关系。如果没有 RMAP,查找这些映射关系的过程会非常耗时,影响系统性能;而RMAP 的存在使得这一查找过程变得快速而准确,确保页面迁移能够顺利进行。
RMAP 机制对于 Linux 系统的内存管理效率提升至关重要,它是解决复杂内存管理问题的核心方案,为系统在各种内存操作场景下的高效运行提供了有力支持。
2.1正向映射
当进程分配内存并发生写操作时,会分配虚拟地址并产生缺页,进而分配物理内存并建立虚拟地址到物理地址的映射关系, 这个叫正向映射。
图片
2.2反向映射
反过来, 通过物理页面找到映射它的所有虚拟页面叫反向映射(reverse-mapping, RMAP)。
图片
2.3RMAP的背景
用户进程在使用虚拟内存的过程中,从虚拟内存页面映射到物理内存页面时,PTE保留这个记录,page数据结构中的_mapcount记录有多少个用户PTE映射到物理页面。用户PTE是指用户进程地址空间和物理页面建立映射的PTE,不包括内核地址空间映射物理页面时产生的PTE。有的页面需要迁移,有的页面长时间不使用,需要交换到磁盘。在交换之前,必须找出哪些进程使用这个页面,然后解除这些映射的用户PTE。一个物理页面可以同时被多个进程的虚拟内存映射,但是一个虚拟页面同时只能映射到一个物理页面。
在Linux 2.4内核中,为了确定某一个页面是否被某个进程映射,必须遍历每个进程的页表,因此工作量相当大,效率很低。在Linux2.5内核开发期间,提出了反向映射(Reverse Mapping,RMAP)的概念。
三、RMAP 关键数据结构剖析
RMAP的主要目的是从物理页面的page数据结构中找到有哪些映射的用户PTE,这样页面回收模块就可以很快速和高效地把这个物理页面映射的所有用户PTE都解除并回收这个页面。在深入了解 RMAP 的工作原理之前,我们先来剖析一下支撑 RMAP 机制运行的关键数据结构,它们是 RMAP 实现高效反向映射的基石,理解这些数据结构的构成和相互关系,对于掌握 RMAP 的核心思想至关重要。
为了达到这个目的,内核在页面创建时需要建立RMAP的“钩子”,即建立相关的数据结构,RMAP系统中有两个重要的数据结构:一个是anon_vma,简称AV;另一个是anon_vma_chain,简称AVC。
3.1struct anon_vma(AV)
struct anon_vma 是 RMAP 机制中用于管理匿名内存映射区域的关键数据结构,在连接物理页面的 page 数据结构和虚拟内存区域的 vm_area_struct(VMA) 中扮演着核心角色 。当进程创建匿名内存映射(比如通过 malloc 分配内存,在缺页异常时创建的匿名页面)时,anon_vma 就开始发挥作用。
从定义来看,struct anon_vma 包含多个重要字段:
其中,refcount 字段用于引用计数,它记录着当前有多少个 VMA 引用了该 anon_vma。当一个新的 VMA 与该 anon_vma 建立关联时,refcount 会增加;反之,当 VMA 与 anon_vma 解除关联时,refcount 减少。当 refcount 变为 0 时,说明没有 VMA 再引用这个 anon_vma,此时就可以对 anon_vma 进行回收。
rb_root 字段是一个红黑树的根节点,通过这个红黑树,anon_vma 可以高效地管理和查找与它相关的 VMA。在实际场景中,当系统需要查找所有引用某个匿名页面的 VMA 时,就可以通过 page 结构中的 mapping 指针找到对应的 anon_vma,然后遍历 anon_vma 的红黑树,快速获取所有相关的 VMA,大大提高查找效率。
3.2struct anon_vma_chain(AVC)
struct anon_vma_chain 是连接 vm_area_struct(VMA) 和 anon_vma 的桥梁,在 RMAP 机制中起着不可或缺的枢纽作用。它主要用于维护 VMA 和 anon_vma 之间的关联关系,使得内核能够方便地从 VMA 找到对应的 anon_vma,反之亦然。
在进程创建子进程时,子进程会复制父进程的地址空间和页表。此时,anon_vma_chain 就会发挥作用,它会在子进程的 VMA 和父进程的 anon_vma 之间建立连接。具体来说,子进程的每个 VMA 都会创建一个 anon_vma_chain 节点,该节点的 vma 指针指向子进程的 VMA,anon_vma 指针指向父进程的 anon_vma,然后通过 same_vma 链表和 rb 红黑树节点,将这个 anon_vma_chain 节点插入到相应的链表和红黑树中,从而实现子进程 VMA 与父进程 anon_vma 的关联 。这样,当系统需要对某个匿名页面进行操作时,就可以通过 anon_vma_chain 快速找到所有与该页面相关的 VMA,无论是在父进程还是子进程中。
3.3struct vm_area_struct(VMA)
struct vm_area_struct 用于描述进程地址空间中的一段虚拟内存区域,是进程虚拟内存管理的重要数据结构,在 RMAP 机制中也有着关键作用,它记录了虚拟内存区域的起始地址、结束地址、访问权限、所属的内存描述符等重要信息,与 RMAP 相关的字段主要有 anon_vma_chain 和 anon_vma。
在进程运行过程中,当发生缺页异常需要创建新的匿名页面时,内核会为该页面所在的 VMA 分配一个 anon_vma,并通过 anon_vma_chain 将 VMA 和 anon_vma 连接起来。例如,当一个进程调用 malloc 分配内存时,会在进程的地址空间中创建一个新的 VMA 来管理这块内存,同时为这个 VMA 关联一个 anon_vma,并通过 anon_vma_chain 建立两者之间的联系。这样,在后续的内存操作中,如页面回收、迁移等,就可以通过 VMA 的这些字段快速找到相关的 anon_vma 和其他关联信息,从而高效地完成内存管理任务。
四、RMAP的工作流程与原理
4.1匿名页面的创建与 RMAP 初始化
在进程运行过程中,当访问的虚拟地址尚未映射到物理页面时,会触发缺页异常。此时,内核会调用 do_anonymous_page 函数来处理匿名页面的创建。以 do_anonymous_page 函数为例,当进程调用 malloc 分配内存时,如果相应的虚拟地址尚未映射物理页面,就会触发缺页异常,进而调用 do_anonymous_page 函数。在这个函数中,会先后调用 anon_vma_prepare 和 page_add_new_anon_rmap 函数来完成 RMAP 相关的初始化工作。
anon_vma_prepare 函数主要负责为 VMA 分配和初始化 anon_vma 结构,并建立 anon_vma_chain 连接。在这个函数中,首先尝试查找是否存在可合并的 anon_vma,如果不存在则分配一个新的 anon_vma。然后,通过 anon_vma_chain_alloc 分配一个 anon_vma_chain 结构,并将其与 VMA 和 anon_vma 进行关联。具体来说,它会将 anon_vma_chain 的 vma 指针指向 VMA,anon_vma 指针指向分配的 anon_vma,然后将 anon_vma_chain 添加到 VMA 的 anon_vma_chain 链表和 anon_vma 的红黑树中,这样就建立了 VMA 和 anon_vma 之间的双向关联。
page_add_new_anon_rmap 函数则将新分配的物理页面与 VMA 建立反向映射关系。它会设置页面的 mapping 字段指向 anon_vma,并计算页面在 VMA 中的索引值存储在 index 字段中。具体实现中,先设置页面的 SwapBacked 标志,表示该页面可交换到磁盘。然后根据页面是否为复合页(如大页),设置 mapcount 字段。接着,通过 __page_set_anon_rmap 函数将 anon_vma 加上 PAGE_MAPPING_ANON 后赋值给页面的 mapping 字段,并计算页面在 VMA 中的线性索引值赋值给 index 字段,从而完成物理页面与 VMA 的反向映射关联 。
4.2子进程创建时的 RMAP 处理
当父进程通过 fork 创建子进程时,子进程需要复制父进程的地址空间及页表。在这个过程中,dup_mmap 函数负责复制父进程的进程地址空间,遍历父进程的 VMA 链表,为每个 VMA 创建一个新的 VMA 数据结构,并将父进程 VMA 中的相关信息复制到子进程的 VMA 中。
anon_vma_fork 函数在子进程创建过程中起着关键作用,负责为子进程的 VMA 创建相应的 anon_vma 数据结构,并建立子进程 VMA 与父进程 anon_vma 之间的关联。如果父进程的 VMA 没有关联的 anon_vma,则直接返回。否则,尝试克隆父进程的 anon_vma 到子进程。如果克隆失败,则分配一个新的 anon_vma 给子进程,并将其与子进程的 VMA 通过 anon_vma_chain 连接起来,同时设置相关的父子关系和引用计数 。
在实际场景中,假设父进程有一个 VMA 用于管理堆内存,当通过 fork 创建子进程时,子进程会复制父进程的这个 VMA 结构,并通过 anon_vma_fork 函数建立与父进程 anon_vma 的关联。这样,在后续内存操作中,系统可以通过 RMAP 机制统一管理父子进程中与该 VMA 相关的匿名页面,实现内存的高效利用和共享。
4.3页面回收与迁移时的 RMAP 应用
当系统内存不足时,kswapd 内核线程会启动页面回收机制,查找可以回收的页面。对于匿名页面,kswapd 会利用 RMAP 机制来找到所有映射该页面的 PTE 并断开映射。具体来说,kswapd 会调用 try_to_unmap 函数,该函数是反向映射的核心函数之一,它会遍历页面的反向映射关系,找到所有映射该页面的 VMA,并调用 try_to_unmap_one 函数来断开每个 VMA 中对该页面的映射。
在页面迁移场景中,当需要将一个物理页面从一个内存区域迁移到另一个内存区域时,同样需要利用 RMAP 机制。例如在 NUMA 架构系统中,为了优化内存访问性能,可能需要将某个进程使用的页面迁移到距离其 CPU 更近的内存节点上。在迁移过程中,首先通过 RMAP 找到所有映射该页面的 PTE,然后将这些 PTE 标记为无效,并在目标内存区域分配新的物理页面。迁移完成后,再更新 PTE 的映射关系,指向新的物理页面 。整个过程中,RMAP 机制确保了在页面迁移前后,所有相关进程的页表能够正确更新,保证进程对内存的正常访问。
五、反向映射RMAP应用
内核中通过struct page找到所有映射到这个页面的VMA典型场景有:
- kswapd内核线程回收页面需要断开所有映射了该匿名页面的用户PTE页表项。
- 页面迁移时,需要断开所有映射到匿名页面的用户PTE页表项。
try_to_unmap()是反向映射的核心函数,内核中其他模块会调用此函数来断开一个页面的所有映射:
内核中有三种页面需要unmap操作,即KSM页面、匿名页面、文件映射页面:
面以匿名页面的unmap为例:
struct rmap_walk_control中的rmap_one实现是try_to_unmap_one,最终调用page_remove_rmap()和page_cache_release()来断开PTE映射关系。
六、补充:其他反向映射
6.1KSM反向映射
KSM(kernel shared memory)是一种内存共享机制,启用 ksm后,ksm守护进程会定期扫描用户内存区域,并合并具有相同内存的匿名物理页面以减少页面的冗余。
和“多个子进程”不一样的是,映射到同一个物理页面, ksm 在不同进程的虚拟地址空间的虚拟地址肯定是不一样的,因此就不能使用 page->index 来表示虚拟页号。
对于ksm 页面,内核维护了一个结构体 rmap_item,用它来保存同一物理页面在不同的虚拟地址空间信息;物理页的 mapping 指向了一个 struct stable_node 结构体,通过 stable_node->hlist 将 rmap_item 连接起来。
rmap_item中维护了page对应的虚拟地址address及anon_vma结构。(函数实现:mm\rmap.c:page_referenced -> rmap_walk ->rmap_walk_ksm);
图片
6.2文件页的反向映射
文件页的反向映射是指在操作系统中,将虚拟内存地址与物理内存地址之间进行映射的过程。它允许操作系统跟踪每个虚拟页到物理页的对应关系。
通常,操作系统使用页表来管理虚拟内存和物理内存之间的映射关系。每个进程都有自己独立的页表,通过查阅页表,操作系统可以将进程的虚拟地址转换为对应的物理地址。
反向映射则是根据给定的物理内存地址,找到对应的虚拟内存地址。这样可以在需要时快速找到一个物理页面所属的进程和虚拟地址。
实现反向映射通常需要一些数据结构支持,比如逆向页表或者其他类似结构。这样,在需要时就可以从给定的物理地址追溯到相应的虚拟地址。
图片
物理页的 mapping 会指向文件对应的 address_space,address_space 的i_mmap 保存了所有的vma。物理页的所有 vma,很容易找到,那不同vma的虚拟地址呢?
物理页的index 表示物理页在文件内的offset(以page size为单位),vma的vm_pgoff 表示 vma 区域在文件中的偏移量。那么,在不同的vma的虚拟地址= page->index - vma->vm_pgoff + vma->vm_start 。
图片
知道了 vma和 虚拟地址,就可以解除pte映射了,(函数实现:mm\rmap.c:page_referenced -> rmap_walk ->rmap_walk_file)。