同虚拟地址替换完整内存映射时,指针访问是否安全?
进程共享动态字节数组的并发访问安全性问题
需求背景
我想要实现一个进程共享的动态字节数组:
- 每个进程为该数组保留一段固定的虚拟地址范围
- 数组由共享内存/内存映射文件支撑,基地址在进程生命周期内保持不变
概念性API定义
public unsafe interface ISharedByteArray : IDisposable { byte* BasePointer { get; } long Capacity { get; } long Length { get; } long MappedLength { get; } void Resize(long newLength); byte* GetPointer(long offset); }
每次Resize时,会在同一虚拟地址替换完整的活动映射,底层逻辑数据保持一致,仅调整映射大小,重叠前缀的现有数据需保留。
核心问题
若某线程持有指向重叠前缀的指针(比如char* p = (char*)base + 0;),且该指针指向的地址在新旧映射中均有效,当此线程通过p持续写入时,另一线程执行Resize替换同一虚拟地址的完整映射,这种操作是否安全?
换句话说:Linux或Windows是否保证在同一虚拟地址替换映射时,对重叠前缀的并发指针访问是原子/安全的?
特指以下场景:
- 线程A:写入在新旧映射中均有效的地址
- 线程B:替换同一基地址的整个映射为更大/更小的映射
注:不涉及线程A写入被移除的尾部的情况
平台API的安全性分析
Linux平台(涉及mmap/mremap/munmap/MAP_FIXED)
Linux的内存映射操作并没有提供这种并发访问的安全性保证。当使用MAP_FIXED替换虚拟地址映射时,内核会直接修改页表项,这个过程对于用户态线程来说是非原子的。如果线程A正在对旧映射的地址执行写入,此时线程B替换映射,可能会出现以下问题:
- 写入操作可能被截断或部分完成,导致数据损坏
- 页表切换过程中,可能出现短暂的地址无效状态,触发段错误
- 即使是对齐的原子写入(如单字节操作),也无法保证不会被映射替换操作打断
Windows平台(涉及内存映射文件/MapViewOfFile/MapViewOfFile3/占位符)
Windows的内存映射视图替换同样没有文档化的并发安全保证。调用MapViewOfFile系列函数替换现有视图时,系统会重新映射虚拟地址空间,这个过程会修改进程的页表。并发的读写操作可能导致:
- 读写操作的结果不确定,出现脏数据
- 触发访问违规(Access Violation),因为映射切换过程中地址可能短暂无效
- 即使是针对有效重叠区域的操作,也无法保证原子性
结论与解决方案
必须在替换完整映射前停止所有指针使用者,具体可以通过以下方式实现:
- 全局同步机制:使用跨进程的互斥锁(如Linux的
pthread_mutex_t、Windows的CreateMutex),在Resize操作前后加锁,确保没有线程在访问映射地址 - 引用计数:维护一个跨进程的引用计数器,记录当前持有指针的线程数,
Resize前等待计数器归零 - 版本化指针:不直接返回原始指针,而是返回包含版本号的包装对象,每次
Resize更新版本号,线程访问前检查版本号是否有效,无效则重新获取指针
C#实现建议
在C#中实现时,需要结合平台调用(P/Invoke)来操作底层的内存映射API,同时处理好unsafe代码的安全性:
- 使用
MemoryMappedFile类作为基础,但它不支持固定虚拟地址的动态重映射,因此需要直接调用Windows的MapViewOfFile/UnmapViewOfFile或Linux的mmap等原生API - 实现跨进程同步:使用
Mutex类(Windows)或通过P/Invoke调用Linux的互斥锁API - 封装指针访问逻辑,避免直接暴露原始
byte*给上层代码,而是提供安全的读写方法,内部处理同步和版本检查
内容的提问来源于stack exchange,提问作者Tobias Blümlhuber
相关产品推荐
相关产品推荐

