Linux 地址空间与 mremap:从页表、VMA 到 MREMAP_DONTUNMAP
导读mremap是 Linux 系统提供的一个内存管理系统调用用于调整扩大、缩小或移动已有的内存映射区域它在mmap建立映射之后使用能避免重新映射带来的数据拷贝开销。mremap 在动态数组/分配器扩容场景中很常见。例如一段以mmap映射的连续内存需要追加空间时调用mremap(..., MREMAP_MAYMOVE)可让内核尝试原地扩展或高效迁移避免了显式的数据拷贝。它也是内存管理相关实现如某些自定义分配器、部分运行时库用来优化大块内存重分配的底层手段之一。mremap看起来只是把一段映射搬到另一个地址但它搬完之后新旧两个地址看到的数据是否一致、会产生几个 VMA、对性能有什么影响全都取决于更底层的东西页表和 VMA 的分工、缺页时物理页从哪里来、VMA 什么时候能合并。为了内容的自成一个体系因此本文分两部分第一部分§1–§8建立地址空间的基础模型得出三个关键结论第二部分§9–§16用这三个结论逐步拆解mremap特别是MREMAP_FIXED和MREMAP_DONTUNMAP。如果有些细节不清楚可以参考本专栏的其他文章。本文回答的问题#问题位置1CPU 访问一个虚拟地址时硬件和内核分别做了什么VMA 扮演什么角色§12MAP_SHARED/MAP_PRIVATE× 匿名 / 文件四种映射在缺页时物理页从哪里来§2–§43什么条件下两个不同的虚拟地址会映射到同一个物理页§54两个相邻 VMA 什么时候能合并THP 对 VMA 有什么要求§6–§75mremap到底搬了什么会不会复制数据§10、§136MREMAP_FIXED对目标地址做了什么新映射的属性从哪里来§11–§127MREMAP_DONTUNMAP之后新旧两个 VA 看到的是不是同一份数据§148循环多次mremap把离散段拼到一段连续 VA 上最后 VMA 是什么形状代价是什么§15–§169哪些情况内核会直接拒绝§8、§9第一部分地址空间基础1. 三层模型VA → 页表 → 物理页要点页表PTE/PMD/PUD是硬件可见的数据结构决定 VA → PA 的映射。TLB是页表的缓存。VMAstruct vm_area_struct是纯软件结构描述这一段虚拟地址应该映射什么。硬件从不读 VMA。VMA 只在以下场景被使用缺页处理、mmap/munmap/mremap/mprotect等系统调用、/proc/pid/maps、GUP/HMM 等内核遍历用户页的路径。推论已经建立好页表之后VMA 的数量和形状不影响正常访存性能。它只影响慢路径缺页、系统调用以及间接影响能否使用大页见 §7。2. VMA 的关键字段字段含义vm_start/vm_end这段 VA 的范围[start, end)vm_flags权限与属性VM_READ/WRITE/EXEC、VM_SHARED、VM_MAYSHARE、VM_LOCKED、VM_IO、VM_PFNMAP、VM_DONTEXPAND、VM_ACCOUNT等vm_file后备文件文件映射、memfd、共享匿名都有私有匿名为 NULLvm_pgoffvm_start对应的文件页偏移单位页。文件映射里是真实偏移私有匿名里是创建时的vm_start PAGE_SHIFT用于反向映射anon_vma私有匿名页以及私有文件映射里 COW 出来的页的反向映射根vm_ops缺页等回调-fault、-mremap等一个重要的不变式VMA 内地址与后备对象偏移是线性关系pgoff ( a d d r ) vm_pgoff a d d r − vm_start PAGE_SIZE \text{pgoff}(addr) \text{vm\_pgoff} \frac{addr - \text{vm\_start}}{\text{PAGE\_SIZE}}pgoff(addr)vm_pgoffPAGE_SIZEaddr−vm_start缺页时内核正是用这个公式在vm_file的 page cache 里找到对应的页。这个公式在后面会反复出现VMA 合并§6要求合并后仍满足它mremap创建新 VMA§12也要按它计算新的vm_pgoff。mmap时 VMA 的类型如何确定mm/vma.cif(map-file)error__mmap_new_file_vma(map,vma);/* 文件 / memfd */elseif(map-vm_flagsVM_SHARED)errorshmem_zero_setup(vma);/* MAP_SHARED|MAP_ANONYMOUS */elsevma_set_anonymous(vma);/* MAP_PRIVATE|MAP_ANONYMOUS */注意第二行MAP_SHARED | MAP_ANONYMOUS在内核里并不是匿名的它通过 shmem_zero_setup 创建一个 shmem 内部文件此后行为和 memfd 一样属于文件映射。3. 缺页物理页从哪里来入口 do_pte_missingif(vma_is_anonymous(vmf-vma))returndo_anonymous_page(vmf);/* 只有 MAP_PRIVATE|MAP_ANONYMOUS 走这里 */elsereturndo_fault(vmf);/* 文件、memfd、共享匿名(shmem) */do_fault 再按读写与VM_SHARED分三路elseif(!(vmf-flagsFAULT_FLAG_WRITE))retdo_read_fault(vmf);/* 读映射 page cache 页 */elseif(!(vma-vm_flagsVM_SHARED))retdo_cow_fault(vmf);/* 私有写复制一份匿名页 */elseretdo_shared_fault(vmf);/* 共享写直接写 page cache 页 */4. 四种映射的对比根据是否共享和匿名组合成了四种典型的映射总结如下映射内核类型缺页时物理页来源写入的去向物理页的身份A.MAP_PRIVATE | MAP_ANONYMOUS匿名新分配的零页该私有页属于本 VMA 的anon_vma没有文件身份B.MAP_SHARED | MAP_ANONYMOUSshmem 文件shmem page cache直接写 page cache(shmem inode, pgoff)C.MAP_SHARED 文件/memfd文件文件 page cache直接写 page cache(inode, pgoff)D.MAP_PRIVATE 文件/memfd文件读page cache写COW 出私有页写入私有副本文件不变未写(inode, pgoff)写过私有匿名页后文用 A、B、C、D 指代这四种映射。5. 多个 VA 映射同一物理页的条件核心结论只有当物理页拥有可被重新查到的身份inode pgoff时多个 VA 才能稳定地共享它。B、C共享映射任何 VMA只要vm_file相同、计算出的pgoff相同缺页时都会在 page cache 中找到同一个页。因此对同一 memfd 多次mmap(MAP_SHARED)或在不同进程间共享 shm都天然是同一批物理页。A私有匿名页没有文件身份缺页只会分配新页。唯一的共享是fork()后的 COW 共享且第一次写就会分裂。D私有文件未写过的页暂时共享 page cache一旦任何一方写入就 COW 成私有页从此分离。小实验examples/share_demo.c对同一个 memfd 建立两个MAP_SHARED映射a、b和一个MAP_PRIVATE映射c。X X ← a 写入后b 和未写过的 c 都读到 page cache 中的同一页 Z Y ← c 写入触发 COW 后a 再写只有 b 能看到c 已与 page cache 分离6. VMA 合并内核总是尽量把相邻、属性一致的 VMA 合并以减少 VMA 数量。判断逻辑见 mm/vma.cis_mergeable_vmavm_flags相同忽略少数无关位vm_file相同NUMA mempolicy、userfaultfd 上下文、anon_vma_name相同is_mergeable_anon_vma两边的anon_vma兼容相同或一方为空且不是 fork 子进程的情形can_vma_merge_before / aftervm_pgoff必须线性连续if(vmg-prev-vm_pgoffvma_pages(vmg-prev)vmg-pgoff)/* 与前一个合并 */if(vmg-next-vm_pgoffvmg-pgoffpglen)/* 与后一个合并 */第 3 条的含义合并后整个 VMA 仍要满足 §2 的线性公式。也就是说只有VA 连续且后备对象偏移也连续的两段才能合并。场景能否合并同一 memfdVA 相邻文件偏移也相邻能同一 memfdVA 相邻但文件偏移不连续离散页拼在一起不能一个 memfd 一个私有匿名不能vm_file不同权限不同一个 RW一个 RO不能7. 透明大页THP与 VMAPMD 级大页x86-64 上 2 MiB能否用于某个地址由 thp_vma_suitable_order 判断/* 非匿名 VMAVA 与文件偏移必须按大页对齐 */if(!vma_is_anonymous(vma)){if(!IS_ALIGNED((vma-vm_startPAGE_SHIFT)-vma-vm_pgoff,hpage_sizePAGE_SHIFT))returnfalse;}/* 对齐后的 2M 区间必须完整落在同一个 VMA 内 */haddrALIGN_DOWN(addr,hpage_size);if(haddrvma-vm_start||haddrhpage_sizevma-vm_end)returnfalse;所以VMA 被切成小于 2 MiB 的碎片 → 该区间只能用 4 KiB PTE 映射。对 memfd/shmem 来说还要求VA - pgoff按 2 MiB 对齐。大页映射可以显著减少 TLB miss。这是 VMA 形状间接影响访存性能的主要途径。8. 特殊映射VM_PFNMAP/VM_IO设备 mmap如/dev/mem、GPU 显存 BAR通常带有VM_PFNMAP | VM_IO | VM_DONTEXPANDPTE 直接指向 PFN可能没有对应的struct page或不应被内核当作普通页管理GUP / HMM 等需要struct page的路径会拒绝或受限mremap的部分功能被禁止见 §9。排查时可在/proc/pid/smaps中查看对应映射的VmFlags行pf表示VM_PFNMAPio表示VM_IOde表示VM_DONTEXPANDsh表示VM_SHARED。承上启下带入第二部分的三个结论第二部分分析mremap时会反复用到第一部分的三个结论硬件只看页表§1mremap只要搬动页表项就能让新地址指向原来的物理页不需要复制数据。共享靠 page cache不靠映射手段§4–§5一个 VA 的 PTE 被搬走之后它再次缺页能否找回原来的物理页只取决于映射类型是 A、B、C 还是 D。VMA 合并要求vm_pgoff连续§6–§7把离散段拼到连续 VA 上VA 连续了但vm_pgoff通常不连续因此 VMA 合并不了也影响 THP。第二部分mremap9. 接口与参数约束void*mremap(void*old_address,size_told_size,size_tnew_size,intflags,.../* void *new_address */);flag含义MREMAP_MAYMOVE允许把映射搬到新地址MREMAP_FIXED必须搬到new_address需同时带MAYMOVEMREMAP_DONTUNMAP搬移后保留源 VMA需带MAYMOVE且old_size new_size参数检查见 check_mremap_params 与 check_prep_vma几个容易踩的点一次mremap不能跨越源 VMA 的末尾if(old_lenvma-vm_end-addr)return-EFAULT;如果要搬的源区间由多个 VMA 组成必须分段调用。MREMAP_DONTUNMAP不支持VM_PFNMAP/VM_DONTEXPAND即 §8 的特殊映射if((vrm-flagsMREMAP_DONTUNMAP)(vma-vm_flags(VM_DONTEXPAND|VM_PFNMAP)))return-EINVAL;old_size 0复制映射只允许共享映射这与 §5 的结论一致——只有共享映射才能被另一个 VA 稳定地共享if(!old_len!(vma-vm_flags(VM_SHARED|VM_MAYSHARE)))return-EINVAL;MREMAP_DONTUNMAP相当于地址空间增加了old_len需要通过may_expand_vm检查mremap.c对带VM_ACCOUNT的私有可写映射还会额外计入 commit 计数。历史MREMAP_DONTUNMAP在 5.7 引入时只支持私有匿名映射5.13 起扩展到共享映射和文件映射。10. 整体流程对应代码move_vma → copy_vma_and_data → dontunmap_complete。接下来按流程依次展开目标地址§11→ 新 VMA§12→ 搬页表§13→ 源 VMA 的去留§14。11. 目标地址MREMAP_FIXED先清空mremap_to 中会先对目标区间做do_munmap。因此目标地址上原来是什么映射、带什么 flag都不影响结果——它只起到占住一段地址不让别人用的作用。常见的占位写法/* 只占地址不占 commit 额度误访问立即 SIGSEGV */void*placeholdermmap(NULL,total,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE,-1,0);用MAP_SHARED | MAP_ANONYMOUS做占位并不会让结果变成共享反而会额外创建一个 shmem 对象§2并计入 commit。12. 新 VMA完全继承自源 VMAcopy_vma_and_data 按 §2 的线性公式计算新 VMA 的pgoffunsignedlonginternal_offsetvrm-addr-vrm-vma-vm_start;unsignedlonginternal_pgoffinternal_offsetPAGE_SHIFT;unsignedlongnew_pgoffvrm-vma-vm_pgoffinternal_pgoff;...new_vmacopy_vma(vma,vrm-new_addr,vrm-new_len,new_pgoff,...);copy_vma 先尝试与目标位置的邻居合并按 §6 的规则失败则vm_area_dup(vma)复制一份vm_flags包括VM_SHARED、读写权限、vm_file、vm_ops、mempolicy 全部来自源 VMAvm_pgoff保持与源相同的后备对象偏移这样缺页时仍能找到同一个 page cache 页私有匿名映射会anon_vma_clone让搬过去的匿名页仍能被反向映射找到。一个特例源是尚未缺页过的私有匿名 VMAanon_vma NULL时pgoff会改成新地址对应的值以提高与邻居合并的机会。结论mremap无法改变映射类型。新映射属于 A、B、C、D 中的哪一种完全由源决定。13. 搬移页表不复制数据move_page_tablesmmu_notifier_range_init(range,MMU_NOTIFY_UNMAP,0,mm,pmc-old_addr,pmc-old_end);mmu_notifier_invalidate_range_start(range);for(...){/* 对齐时尽量整级搬移 PUD / PMD */if(pmd_trans_huge(*old_pmd)){if(extentHPAGE_PMD_SIZEmove_pgt_entry(pmc,HPAGE_PMD,old_pmd,new_pmd))continue;split_huge_pmd(pmc-old,old_pmd,pmc-old_addr);/* 不对齐则拆分大页 */}elseif(extentPMD_SIZE){if(move_pgt_entry(pmc,NORMAL_PMD,old_pmd,new_pmd))continue;}move_ptes(pmc,extent,old_pmd,new_pmd);/* 逐个搬 PTE */}mmu_notifier_invalidate_range_end(range);要点只搬页表项物理页不动数据不复制。由于硬件只看页表§1搬完后新 VA 就指向原来那些物理页。源区间会触发MMU_NOTIFY_UNMAP通知——任何通过 MMU notifier 跟踪源区间的设备例如 GPU userptr、HMM 镜像都会收到失效通知。源区间中是 THP 的部分只有在 2 MiB 对齐且整段搬移时才会保持大页否则会被拆成 4 KiB对照 §7。源中从未被访问过的页没有 PTE自然什么也不搬新 VA 将来访问时按新 VMA 的类型缺页§3。14.MREMAP_DONTUNMAP源 VMA 保留但变空不带该 flag 时源区间被unmap带该 flag 时走 dontunmap_complete/* 源 VMA 已没有页表项无法再保持 mlock 状态 */vm_flags_clear(vrm-vma,VM_LOCKED_MASK);/* 整个源 VMA 都被搬走时解除其 anon_vma 关联 */if(new_vma!vrm-vmastartold_startendold_end)unlink_anon_vmas(vrm-vma);源 VMA 本身不会被拆分除非它带VM_LOCKED需要清标志或带VM_ACCOUNT需要调整计费属性不变只是对应范围里已经没有 PTE。之后访问源地址会重新缺页缺页结果取决于源 VMA 的类型——这正是第一部分 §3–§5 的内容。14.1 四种映射下的结果把 §4 的四种映射逐一代入源映射已搬走的页再访问源 VA源中未访问过的页新旧 VA 是否一直同步A.MAP_PRIVATE | MAP_ANONYMOUSdo_anonymous_page分配新零页新旧两边各自缺页、各分配一页否搬移瞬间即分离B.MAP_SHARED | MAP_ANONYMOUS在 shmem page cache 中找到同一页两边缺页落到同一 shmem 页是C.MAP_SHARED 文件/memfd在 page cache 中找到同一页同上是D.MAP_PRIVATE 文件/memfd映射 page cache 中的文件原始内容写过的私有副本已在新 VA暂时共享 page cache任一方写入后 COW 分离否本质MREMAP_DONTUNMAP只是搬走 PTE、留下空 VMA。两个 VA 能否共享同一份数据取决于物理页是否有 page cache 身份§5而不是mremap本身。14.2 实验验证examples/mremap_demo.c 的run()在 v6.18.46 上的输出private anon dst[0]W src[0]0 | dst写p1→src见0 | src写p2→dst见0 shared anon dst[0]W src[0]W | dst写p1→src见D | src写p2→dst见S shared memfd dst[0]W src[0]W | dst写p1→src见D | src写p2→dst见S private memfd dst[0]W src[0]F | dst写p1→src见F | src写p2→dst见F解读memfd 预先填充为F搬移前对源第 0 页写了Wdst[0]所有情况下新 VA 都拿到了原物理页W证明搬的是 PTE。src[0]私有匿名得到零页共享映射重新找到同一页W私有文件回落到文件原始内容F。之后的双向写入只有共享映射B、C两边能互相看到。15. 循环mremap拼接离散段典型用法先用一段PROT_NONE占位映射§11占住目标区间再对每段离散区间依次调用mremap(..., MREMAP_MAYMOVE | MREMAP_FIXED | MREMAP_DONTUNMAP, dst off)把它们顺序搬到这段连续 VA 上。实现见 examples/mremap_demo.c 的loop_demo()。每次调用都会MREMAP_FIXED清掉目标子区间里的占位映射§11copy_vma为这一段建一个新 VMA并尝试与左边上一轮放进来的段合并§12合并条件就是 §6vm_flags、vm_file、anon_vma一致且vm_pgoff连续。因此源段的分布目标区间的 VMA 数量来自同一后备对象偏移离散常见N 个无法合并来自同一后备对象偏移恰好首尾相接且按顺序搬移合并为更少的 VMA来自不同文件/不同类型N 个实验结果loop_demo()同一 memfd取每隔一页的离散页 vs. 连续页scattered: src VMAs 1 - 1, dst VMAs 8 contiguous: dst VMAs 1可以看到偏移离散时目标区间有 8 个 VMA偏移连续时合并为 1 个。源 VMA 没有被拆分仍是 1 个与 §14 的分析一致。注意目标区间最终 VA 是连续的但 VMA 不一定是一个。许多内核子系统例如 HMM 的hmm_range_fault、GUP只要求 VA 区间连续、无空洞可以跨越多个 VMA 工作。16. 开销与限制项目影响系统调用次数每段一次N 段就是 N 次mmap_lock每次都持写锁会与同进程其他线程的缺页、mmap 竞争TLB 刷新每次搬移后需要刷新源区间的 TLB多线程进程会触发 IPIMMU notifier每次都会对源区间发出失效通知§13VMA 数量目标侧最多增加 N 个进程总数受vm.max_map_count默认 65530限制THP段不按 2 MiB 对齐时源中的大页被拆分目标侧 VMA 碎片化后也无法再使用 PMD 映射§7稳态访存搬移完成后与普通映射无差别硬件只看页表§117. 总结地址空间基础硬件只看页表/TLBVMA 是软件描述只在慢路径使用。四种映射中只有共享映射含MAP_SHARED|MAP_ANONYMOUS、memfd的物理页有可重新查找的身份因此能被多个 VA 稳定共享。VMA 合并要求属性一致且vm_pgoff线性连续VMA 被切碎或不对齐会使 THP 失效。VM_PFNMAP/VM_IO映射是一类特殊对象许多通用内存操作对其受限。mremapmremap搬的是页表项物理页不动、数据不复制。MREMAP_FIXED会先munmap目标区间占位映射的类型和 flag 无关紧要。新 VMA 的所有属性继承自源 VMAmremap无法改变映射类型。MREMAP_DONTUNMAP留下一个空的源 VMA共享映射B、C的新旧 VA 通过 page cache 共享同一批物理页私有映射A、D的新旧 VA 立即或最终分离。循环拼接离散段时目标区间 VA 连续但通常是 N 个 VMA只有后备偏移连续时才会合并。VM_PFNMAP/VM_DONTEXPAND映射不能用MREMAP_DONTUNMAP单次mremap不能跨 VMA。附录实验程序文件对应章节内容examples/share_demo.c§5同一 memfd 的 shared / private 映射是否共享物理页examples/mremap_demo.c§14.2、§15run()四种映射下MREMAP_DONTUNMAP的结果loop_demo()循环拼接后的 VMA 数量编译运行cddocs/mm-basics/examples gcc-oshare_demo share_demo.c./share_demo gcc-omremap_demo mremap_demo.c./mremap_demo注上述代码程序无法附加到文件有需要的私我。