Linux 内存映射机制:正向映射与反向映射深度解析
一、反向映射
在 Linux 系统里,针对匿名映射的管理,需要一种机制来明确一个物理页 page
在不同进程的 vm_area_struct
中的具体虚拟地址位置。这主要依靠反向映射机制,结合 anon_vma
和 vm_area_struct
的信息达成。
(一)反向映射的核心数据结构
page
结构中的反向映射信息
每个物理页page
会依据映射类型包含不同的指针。对于文件映射,包含一个struct address_space
指针;对于匿名映射,则包含一个struct anon_vma
指针。在匿名映射场景下,page->mapping
指向其所属的anon_vma
,而anon_vma
维护着一个红黑树,该红黑树记录了所有映射该匿名区域的vm_area_struct
,这些vm_area_struct
可能来自不同进程,代表着跨进程共享该匿名页的虚拟内存区域(VMA)。vm_area_struct
中的关键字段vm_mm
:指向所属进程的mm_struct
(地址空间),用于区分不同进程的 VMA。vm_start
:该 VMA 在所属进程虚拟地址空间中的起始地址。vm_pgoff
:表示文件或匿名映射在该vm_area_struct
里的起始页偏移量(以页为单位)。对于文件映射,代表从文件起始处开始的页偏移;对于匿名映射,代表在匿名映射区域内的页偏移。
(二)从 page
定位到具体虚拟地址的步骤
若有一个匿名页 page
,要找出它在所有共享进程中的虚拟地址位置,可按以下步骤操作:
- 通过
page->mapping
获取anon_vma
对于匿名页,page->mapping
指向其所属的anon_vma
。anon_vma
的红黑树(vma_list
)存储了所有映射该匿名区域的vm_area_struct
,这些vm_area_struct
可能来自不同进程。 - 遍历红黑树中的所有
vm_area_struct
对红黑树中的每个vma
执行以下操作:- 获取所属进程的地址空间:通过
vma->vm_mm
得到该 VMA 所属进程的mm_struct
。 - 计算虚拟地址:
- 首先,借助
page->index
和vma->vm_pgoff
确定该页在vma
里的相对位置,计算公式为:
页在vm_area_struct
内的相对页偏移 =page->index - vm_area_struct->vm_pgoff
- 然后,根据相对页偏移计算该页对应的虚拟地址,计算公式为:
虚拟地址 =vm_area_struct->vm_start + (页在 vm_area_struct 内的相对页偏移 * PAGE_SIZE)
- 首先,借助
- 获取所属进程的地址空间:通过
(三)关键区别:匿名映射的 vm_pgoff
是 “局部偏移”
- 文件映射的
pgoff
:基于文件的逻辑偏移,所有进程映射同一文件偏移时,pgoff
相同,可直接通过address_space->i_pages
进行全局索引。 - 匿名映射的
vm_pgoff
:仅表示页在当前 VMA 映射区域内的相对位置(例如,第n
个页)。不同进程的 VMA 可以有相同的vm_pgoff
,但虚拟地址会因vm_start
不同而不同。
(四)反向映射的典型场景:写时复制(COW)
当匿名页被多个进程共享时,若其中一个进程修改该页,就会触发写时复制机制。此时,内核会通过 page->mapping->anon_vma
找到所有共享该页的 VMA。对于每个 VMA(除当前进程外),内核会检查是否需要分离(例如,子进程的 VMA 可能继续共享,父进程的 VMA 需创建新页)。在分离时,内核会通过 vma->vm_start + vm_pgoff×PAGE_SIZE
确定原虚拟地址,并更新页表使其指向新分配的物理页。
(五)示例说明
【示例一】
假设存在一个 vm_area_struct
结构体 vma
,其 vm_start = 0x10000
,vm_pgoff = 2
,页大小 PAGE_SIZE = 4KB
(即 0x1000
)。现在有两个物理页 page1
和 page2
都映射到这个 vma
上,page1->index = 2
,page2->index = 3
。
- 对于
page1
- 页在
vma
内的相对页偏移:page1->index - vma->vm_pgoff = 2 - 2 = 0
- 对应的虚拟地址:
vma->vm_start + (0 * PAGE_SIZE) = 0x10000
- 页在
- 对于
page2
- 页在
vma
内的相对页偏移:page2->index - vma->vm_pgoff = 3 - 2 = 1
- 对应的虚拟地址:
vma->vm_start + (1 * PAGE_SIZE) = 0x10000 + 0x1000 = 0x11000
- 页在
综上所述,尽管 vm_area_struct
里只有一个 vm_pgoff
字段,但通过结合物理页的 index
字段,就能计算出每个物理页在 vm_area_struct
里的相对位置,进而得到对应的虚拟地址,从而可以索引到同一个 vm_area_struct
的不同位置。同时,同一物理页在不同进程中的虚拟地址由各自 VMA 的 vm_start
和 vm_pgoff
共同决定,而 anon_vma
的红黑树仅用于跟踪哪些 VMA 共享了该页。
【示例二】
假设条件:
- 页大小:假设页大小
PAGE_SIZE = 4KB
(即 4096 字节,在计算中用0x1000
表示十六进制)。 - 匿名页信息:有一个匿名页,其
page->index = 3
。 - 进程 A 的
vm_area_struct
(VMA1):vm_start = 0x10000
vm_pgoff = 2
- 进程 B 的
vm_area_struct
(VMA2):vm_start = 0x20000
vm_pgoff = 1
计算过程:
- 计算该匿名页在进程 A 的
vm_area_struct
中的虚拟地址- 计算相对页偏移:
根据公式 页在vm_area_struct
内的相对页偏移 =page->index - vm_area_struct->vm_pgoff
,可得该匿名页在 VMA1 内的相对页偏移为:3 - 2 = 1
- 计算虚拟地址:
根据公式 虚拟地址 =vm_area_struct->vm_start + (页在 vm_area_struct 内的相对页偏移 * PAGE_SIZE)
,可得该匿名页在进程 A 中的虚拟地址为:0x10000 + (1 * 0x1000) = 0x11000
- 计算相对页偏移:
- 计算该匿名页在进程 B 的
vm_area_struct
中的虚拟地址- 计算相对页偏移:
同样根据相对页偏移公式,该匿名页在 VMA2 内的相对页偏移为:3 - 1 = 2
- 计算虚拟地址:
再根据虚拟地址计算公式,该匿名页在进程 B 中的虚拟地址为:0x20000 + (2 * 0x1000) = 0x22000
- 计算相对页偏移:
二、正向映射
(一)正向映射的核心流程
当进程访问一个文件映射的虚拟地址时,内核通过以下步骤找到对应的物理页(页缓存):
- 根据虚拟地址找到
vm_area_struct
(VMA)
通过进程的虚拟地址空间(mm_struct
)的红黑树或链表,快速定位包含该地址的vm_area_struct
。 - 计算虚拟地址相对于 VMA 的偏移
虚拟地址偏移:offset_in_vma = 虚拟地址 - vm_start
(单位为字节,例如0x10004 - 0x10000 = 4 字节
)。 - 转换为文件逻辑页偏移(
pgoff
)
页偏移计算:
pgoff = vm_pgoff + (offset_in_vma / PAGE_SIZE)
vm_pgoff
:VMA 映射的文件起始页偏移(以页为单位,已记录在vm_area_struct
中)。offset_in_vma / PAGE_SIZE
:虚拟地址在 VMA 内的页偏移(例如,4 字节偏移在 4KB 页中对应页偏移 0)。
- 通过
address_space
的xarray
查找页缓存
使用文件的address_space
结构,以pgoff
为键,从xarray
(file->f_mapping->i_pages
)中直接获取页缓存:
page = xa_load(&file->f_mapping->i_pages, pgoff)
。
(二)示例验证
假设场景:
- 文件:大小 12KB,分为 3 页(
pgoff=0,1,2
)。 - 进程映射:
- 创建两个
vm_area_struct
(VMA1 和 VMA2):- VMA1:
vm_start = 0x10000
,vm_end = 0x11000
(映射 4KB)。vm_pgoff = 1
(映射文件的第二页,即pgoff=1
)。
- VMA2:
vm_start = 0x20000
,vm_end = 0x22000
(映射 8KB,两页)。vm_pgoff = 0
(映射文件的前两页,即pgoff=0
和pgoff=1
)。
- VMA1:
- 创建两个
案例 1:访问 VMA1 的虚拟地址 0x10004
- 查找 VMA:定位到 VMA1(
0x10000-0x11000
)。 - 计算偏移:
offset_in_vma = 0x10004 - 0x10000 = 4 字节
。 - 转换为
pgoff
:
pgoff = vm_pgoff(1) + (4 / 4096) = 1 + 0 = 1
。 - 查找页缓存:通过
pgoff=1
找到文件的第二页。
案例 2:访问 VMA2 的虚拟地址 0x21000
- 查找 VMA:定位到 VMA2(
0x20000-0x22000
)。 - 计算偏移:
offset_in_vma = 0x21000 - 0x20000 = 0x1000(4KB)
。 - 转换为
pgoff
:
pgoff = vm_pgoff(0) + (0x1000 / 0x1000) = 0 + 1 = 1
。 - 查找页缓存:通过
pgoff=1
找到文件的第二页。
(三)关键点验证
vm_pgoff
的作用vm_pgoff
表示该 VMA 映射的文件起始页偏移。例如,若 VMA 映射文件的 4KB-8KB(即第二页到第三页),则vm_pgoff = 1
。
- 虚拟地址到
pgoff
的转换- 内核通过
offset_in_vma / PAGE_SIZE
将字节偏移转换为页偏移。例如,offset_in_vma=4096
对应页偏移 1。 - 最终
pgoff
是全局唯一的:pgoff = vm_pgoff + 页偏移
,唯一标识文件中的逻辑页,与进程的虚拟地址无关。
- 内核通过
- xarray 的全局管理
- 无论进程如何映射文件,同一
pgoff
始终对应同一物理页。例如:进程 A 的 VMA1(pgoff=1
)和进程 B 的 VMA(pgoff=1
)共享同一物理页。 - 物理页的分配、释放、回写均由
address_space
统一管理。
- 无论进程如何映射文件,同一
(四)对比匿名映射
- 匿名映射无全局
pgoff
:匿名页的物理地址由进程的虚拟地址空间独立管理,无法通过类似pgoff
的键全局索引。例如:进程 A 的0x10000
和进程 B 的0x10000
映射匿名页,物理页不同。 - 匿名页的共享管理:匿名页的共享需通过
MAP_SHARED
或fork()
等方式声明,依赖anon_vma
的红黑树管理。