导读
mremap 是 Linux 系统提供的一个内存管理系统调用,用于调整(扩大、缩小或移动)已有的内存映射区域,它在 mmap 建立映射之后使用,能避免重新映射带来的数据拷贝开销。mremap 在动态数组/分配器扩容场景中很常见。例如一段以 mmap 映射的连续内存需要追加空间时,调用 mremap(..., MREMAP_MAYMOVE) 可让内核尝试原地扩展或高效迁移,避免了显式的数据拷贝。它也是内存管理相关实现(如某些自定义分配器、部分运行时库)用来优化大块内存重分配的底层手段之一。
mremap 看起来只是"把一段映射搬到另一个地址",但它搬完之后新旧两个地址看到的数据是否一致、会产生几个 VMA、对性能有什么影响,全都取决于更底层的东西:页表和 VMA 的分工、缺页时物理页从哪里来、VMA 什么时候能合并。
为了内容的自成一个体系,因此本文分两部分:
- 第一部分(§1--§8) 建立地址空间的基础模型,得出三个关键结论;
- 第二部分(§9--§16) 用这三个结论逐步拆解
mremap,特别是MREMAP_FIXED和MREMAP_DONTUNMAP。
如果有些细节不清楚,可以参考本专栏的其他文章。
本文回答的问题
| # | 问题 | 位置 |
|---|---|---|
| 1 | CPU 访问一个虚拟地址时,硬件和内核分别做了什么?VMA 扮演什么角色? | §1 |
| 2 | MAP_SHARED / MAP_PRIVATE × 匿名 / 文件,四种映射在缺页时物理页从哪里来? |
§2--§4 |
| 3 | 什么条件下,两个不同的虚拟地址会映射到同一个物理页? | §5 |
| 4 | 两个相邻 VMA 什么时候能合并?THP 对 VMA 有什么要求? | §6--§7 |
| 5 | mremap 到底搬了什么?会不会复制数据? |
§10、§13 |
| 6 | MREMAP_FIXED 对目标地址做了什么?新映射的属性从哪里来? |
§11--§12 |
| 7 | MREMAP_DONTUNMAP 之后,新旧两个 VA 看到的是不是同一份数据? |
§14 |
| 8 | 循环多次 mremap 把离散段拼到一段连续 VA 上,最后 VMA 是什么形状?代价是什么? |
§15--§16 |
| 9 | 哪些情况内核会直接拒绝? | §8、§9 |
第一部分:地址空间基础
1. 三层模型:VA → 页表 → 物理页

要点:
- 页表(PTE/PMD/PUD) 是硬件可见的数据结构,决定 VA → PA 的映射。
- TLB 是页表的缓存。
- VMA(
struct 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、共享匿名都有;私有匿名为 NULL) |
vm_pgoff |
vm_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_pgoff+PAGE_SIZEaddr−vm_start
缺页时内核正是用这个公式,在 vm_file 的 page cache 里找到对应的页。这个公式在后面会反复出现:VMA 合并(§6)要求合并后仍满足它,mremap 创建新 VMA(§12)也要按它计算新的 vm_pgoff。
mmap 时 VMA 的类型如何确定
c
if (map->file)
error = __mmap_new_file_vma(map, vma); /* 文件 / memfd */
else if (map->vm_flags & VM_SHARED)
error = shmem_zero_setup(vma); /* MAP_SHARED|MAP_ANONYMOUS */
else
vma_set_anonymous(vma); /* MAP_PRIVATE|MAP_ANONYMOUS */
注意第二行:MAP_SHARED | MAP_ANONYMOUS 在内核里并不是"匿名"的 ,它通过 shmem_zero_setup 创建一个 shmem 内部文件,此后行为和 memfd 一样,属于"文件映射"。
3. 缺页:物理页从哪里来
入口 do_pte_missing:
c
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); /* 只有 MAP_PRIVATE|MAP_ANONYMOUS 走这里 */
else
return do_fault(vmf); /* 文件、memfd、共享匿名(shmem) */
do_fault 再按读写与 VM_SHARED 分三路:
c
else if (!(vmf->flags & FAULT_FLAG_WRITE))
ret = do_read_fault(vmf); /* 读:映射 page cache 页 */
else if (!(vma->vm_flags & VM_SHARED))
ret = do_cow_fault(vmf); /* 私有写:复制一份匿名页 */
else
ret = do_shared_fault(vmf); /* 共享写:直接写 page cache 页 */

4. 四种映射的对比
根据是否共享和匿名,组合成了四种典型的映射,总结如下:
| 映射 | 内核类型 | 缺页时物理页来源 | 写入的去向 | 物理页的"身份" |
|---|---|---|---|---|
| **A. `MAP_PRIVATE | MAP_ANONYMOUS`** | 匿名 | 新分配的零页 | 该私有页 |
| **B. `MAP_SHARED | MAP_ANONYMOUS`** | shmem 文件 | shmem page cache | 直接写 page cache |
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.c:
-
vm_flags相同(忽略少数无关位)vm_file相同- NUMA mempolicy、userfaultfd 上下文、
anon_vma_name相同
-
is_mergeable_anon_vma:两边的anon_vma兼容(相同,或一方为空且不是 fork 子进程的情形) -
can_vma_merge_before / after:
vm_pgoff必须线性连续cif (vmg->prev->vm_pgoff + vma_pages(vmg->prev) == vmg->pgoff) /* 与前一个合并 */ if (vmg->next->vm_pgoff == vmg->pgoff + pglen) /* 与后一个合并 */
第 3 条的含义:合并后整个 VMA 仍要满足 §2 的线性公式。也就是说,只有"VA 连续且后备对象偏移也连续"的两段才能合并。
| 场景 | 能否合并 |
|---|---|
| 同一 memfd,VA 相邻,文件偏移也相邻 | 能 |
| 同一 memfd,VA 相邻,但文件偏移不连续(离散页拼在一起) | 不能 |
| 一个 memfd + 一个私有匿名 | 不能(vm_file 不同) |
| 权限不同(一个 RW,一个 RO) | 不能 |
7. 透明大页(THP)与 VMA
PMD 级大页(x86-64 上 2 MiB)能否用于某个地址,由 thp_vma_suitable_order 判断:
c
/* 非匿名 VMA:VA 与文件偏移必须按大页对齐 */
if (!vma_is_anonymous(vma)) {
if (!IS_ALIGNED((vma->vm_start >> PAGE_SHIFT) - vma->vm_pgoff,
hpage_size >> PAGE_SHIFT))
return false;
}
/* 对齐后的 2M 区间必须完整落在同一个 VMA 内 */
haddr = ALIGN_DOWN(addr, hpage_size);
if (haddr < vma->vm_start || haddr + hpage_size > vma->vm_end)
return false;
所以:
- 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_DONTEXPAND:
- PTE 直接指向 PFN,可能没有对应的
struct page,或不应被内核当作普通页管理; - GUP / HMM 等需要
struct page的路径会拒绝或受限; mremap的部分功能被禁止(见 §9)。
排查时可在 /proc/<pid>/smaps 中查看对应映射的 VmFlags 行(pf 表示 VM_PFNMAP,io 表示 VM_IO,de 表示 VM_DONTEXPAND,sh 表示 VM_SHARED)。
承上启下:带入第二部分的三个结论
第二部分分析 mremap 时,会反复用到第一部分的三个结论:
- 硬件只看页表 (§1):
mremap只要搬动页表项,就能让新地址指向原来的物理页,不需要复制数据。 - 共享靠 page cache,不靠映射手段(§4--§5):一个 VA 的 PTE 被搬走之后,它再次缺页能否找回原来的物理页,只取决于映射类型是 A、B、C 还是 D。
- VMA 合并要求
vm_pgoff连续 (§6--§7):把离散段拼到连续 VA 上,VA 连续了,但vm_pgoff通常不连续,因此 VMA 合并不了,也影响 THP。
第二部分:mremap
9. 接口与参数约束
c
void *mremap(void *old_address, size_t old_size, size_t new_size,
int flags, ... /* void *new_address */);
| flag | 含义 |
|---|---|
MREMAP_MAYMOVE |
允许把映射搬到新地址 |
MREMAP_FIXED |
必须搬到 new_address(需同时带 MAYMOVE) |
MREMAP_DONTUNMAP |
搬移后保留 源 VMA(需带 MAYMOVE,且 old_size == new_size) |
参数检查见 check_mremap_params 与 check_prep_vma,几个容易踩的点:
-
一次
mremap不能跨越源 VMA 的末尾:cif (old_len > vma->vm_end - addr) return -EFAULT;如果要搬的源区间由多个 VMA 组成,必须分段调用。
-
MREMAP_DONTUNMAP不支持VM_PFNMAP/VM_DONTEXPAND(即 §8 的特殊映射):cif ((vrm->flags & MREMAP_DONTUNMAP) && (vma->vm_flags & (VM_DONTEXPAND | VM_PFNMAP))) return -EINVAL; -
old_size == 0(复制映射)只允许共享映射,这与 §5 的结论一致------只有共享映射才能被另一个 VA 稳定地共享:cif (!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,都不影响结果------它只起到"占住一段地址,不让别人用"的作用。
常见的占位写法:
c
/* 只占地址:不占 commit 额度,误访问立即 SIGSEGV */
void *placeholder = mmap(NULL, total, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
用 MAP_SHARED | MAP_ANONYMOUS 做占位并不会让结果变成"共享",反而会额外创建一个 shmem 对象(§2)并计入 commit。
12. 新 VMA:完全继承自源 VMA
copy_vma_and_data 按 §2 的线性公式计算新 VMA 的 pgoff:
c
unsigned long internal_offset = vrm->addr - vrm->vma->vm_start;
unsigned long internal_pgoff = internal_offset >> PAGE_SHIFT;
unsigned long new_pgoff = vrm->vma->vm_pgoff + internal_pgoff;
...
new_vma = copy_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 全部来自源 VMA;vm_pgoff保持与源相同的"后备对象偏移",这样缺页时仍能找到同一个 page cache 页;- 私有匿名映射会
anon_vma_clone,让搬过去的匿名页仍能被反向映射找到。
一个特例:源是尚未缺页过 的私有匿名 VMA(anon_vma == NULL)时,pgoff 会改成新地址对应的值,以提高与邻居合并的机会。
结论:
mremap无法改变映射类型。新映射属于 A、B、C、D 中的哪一种,完全由源决定。
13. 搬移页表:不复制数据
c
mmu_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 (extent == HPAGE_PMD_SIZE &&
move_pgt_entry(pmc, HPAGE_PMD, old_pmd, new_pmd))
continue;
split_huge_pmd(pmc->old, old_pmd, pmc->old_addr); /* 不对齐则拆分大页 */
} else if (extent == PMD_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:
c
/* 源 VMA 已没有页表项,无法再保持 mlock 状态 */
vm_flags_clear(vrm->vma, VM_LOCKED_MASK);
/* 整个源 VMA 都被搬走时,解除其 anon_vma 关联 */
if (new_vma != vrm->vma && start == old_start && end == old_end)
unlink_anon_vmas(vrm->vma);
源 VMA 本身不会被拆分 (除非它带 VM_LOCKED 需要清标志,或带 VM_ACCOUNT 需要调整计费),属性不变,只是对应范围里已经没有 PTE。之后访问源地址会重新缺页,缺页结果取决于源 VMA 的类型------这正是第一部分 §3--§5 的内容。
14.1 四种映射下的结果
把 §4 的四种映射逐一代入:
| 源映射 | 已搬走的页,再访问源 VA | 源中未访问过的页 | 新旧 VA 是否一直同步 |
|---|---|---|---|
| **A. `MAP_PRIVATE | MAP_ANONYMOUS`** | do_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 页写了 W):
dst[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清掉目标子区间里的占位映射(§11);copy_vma为这一段建一个新 VMA,并尝试与左边(上一轮放进来的段)合并(§12);- 合并条件就是 §6:
vm_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,多线程进程会触发 IPI |
| MMU notifier | 每次都会对源区间发出失效通知(§13) |
| VMA 数量 | 目标侧最多增加 N 个;进程总数受 vm.max_map_count(默认 65530)限制 |
| THP | 段不按 2 MiB 对齐时,源中的大页被拆分;目标侧 VMA 碎片化后也无法再使用 PMD 映射(§7) |
| 稳态访存 | 搬移完成后与普通映射无差别(硬件只看页表,§1) |
17. 总结
地址空间基础
- 硬件只看页表/TLB;VMA 是软件描述,只在慢路径使用。
- 四种映射中,只有共享映射(含
MAP_SHARED|MAP_ANONYMOUS、memfd)的物理页有可重新查找的身份,因此能被多个 VA 稳定共享。 - VMA 合并要求属性一致且
vm_pgoff线性连续;VMA 被切碎或不对齐会使 THP 失效。 VM_PFNMAP/VM_IO映射是一类特殊对象,许多通用内存操作对其受限。
mremap
mremap搬的是页表项,物理页不动、数据不复制。MREMAP_FIXED会先munmap目标区间,占位映射的类型和 flag 无关紧要。- 新 VMA 的所有属性继承自源 VMA,
mremap无法改变映射类型。 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、§15 | run():四种映射下 MREMAP_DONTUNMAP 的结果;loop_demo():循环拼接后的 VMA 数量 |
编译运行:
bash
cd docs/mm-basics/examples
gcc -o share_demo share_demo.c && ./share_demo
gcc -o mremap_demo mremap_demo.c && ./mremap_demo
注:上述代码程序无法附加到文件,有需要的私我。