Linux mremap()扩容匿名内存返回EFAULT问题及替代方案咨询
测试场景中使用非POSIX标准的mremap()系统调用,尝试将mmap()分配的多段匿名内存拼接为一段连续内存区域。参考man7官方文档的接口说明,预期该操作可正常执行,但实际运行中mremap()的grow(扩容)操作意外失败,初步推测故障与内核的虚拟内存区域(VMA)表示机制有关:用户视角下的连续内存区域,在内核中仍可能对应两个独立的VM/VMA结构。
测试执行流程
- 初始存在内存块A、B,将A扩容至A+B的总大小(该步骤允许A被内核移动到新的地址)
- 将B重映射到A扩容后新增的尾部地址空间
- 新增内存块C,尝试将A扩容至A+B+C的总大小,该步骤执行失败,返回错误码
EFAULT 14 Bad address - 原计划将C重映射到A再次扩容后新增的尾部地址空间
完整测试代码可参考对应开源仓库中提交的mremap_test.c文件
故障原因初步判断
第一次扩容操作可正常执行,第二次扩容失败,初步判断根因为第二次扩容时传入的旧地址范围在内核中由两个独立的VMA组成,mremap无法处理这类跨VMA的入参。
该判断可从内核源码注释与逻辑中找到直接依据,mm/mremap.c中mremap_to()函数存在如下逻辑:
/* We can't remap across vm area boundaries */ if (old_len > vma->vm_end - addr) return ERR_PTR(-EFAULT);
但官方文档中并未提及该限制,用户态系统调用的行为依赖内核内部实现结构,不符合常规接口设计预期。
官方文档中对EFAULT错误码的说明如下:
EFAULT Some address in the range old_address to old_address+old_size is an invalid virtual memory address for this process. You can also get EFAULT even if there exist mappings that cover the whole address space requested, but those mappings are of different types.
这段说明使用了复数形式的“mappings”,容易误导用户认为只要映射类型一致,接口就支持传入由多个映射组成的内存区域。
问题解答
1. 当前mremap()的异常表现是内核bug,还是使用方式存在错误?
这不是内核bug,属于对接口能力的认知偏差。mremap()从实现逻辑上就明确要求传入的旧地址范围必须完全落在单个VMA内,源码中的边界检查就是专门用于拦截跨VMA的调用请求。文档中提到的复数形式“mappings”,仅用于说明“地址范围被映射覆盖但类型不匹配时也会返回EFAULT”的场景,并非承诺支持跨多个同类型VMA的操作。
2. 是否mremap()仅实现了开发者当时所需的特定功能,使用预期本身不符合该接口的设计定位?
是的。mremap()最初的核心设计目标是高效调整单块内存区域(比如用户态堆、单个文件映射)的大小或位置,从来没有将“拼接多个独立VMA为连续区域”纳入支持场景。跨VMA扩容、拼接多段独立映射的预期,本身不在该接口的设计范围内。
3. 用户态是否存在其他方案可以实现任意内存区域的重映射?
不存在零拷贝的通用重映射方案,可选的替代实现路径有两类:
- 预占虚拟地址空间:第一次调用
mmap()时就传入PROT_NONE标志预留足够大的连续虚拟地址范围,后续需要扩容时直接通过mprotect()调整对应区间的权限、提交物理页,全程不会产生多个独立VMA,从根源上避开跨VMA调用mremap()的问题 - 拷贝式拼接:如果必须对已分配的多段独立映射做连续化处理,只能新分配一块足够大的目标内存区域,将原有多段内存的数据拷贝到新区域后,再解除原有区域的映射。该方案存在明确的内存拷贝开销,无法实现零拷贝重映射。
内容的提问来源于stack exchange,提问作者Maciej Polański

