如何释放已删除内存映射文件的磁盘簇以触发访问异常
问题场景与现象
测试目标
模拟如下Windows系统行为:文件被映射到内存后,在通过内存映射访问文件的过程中执行删除操作,触发映射内存访问时的EXCEPTION_IN_PAGE_ERROR访问违规异常。
示例代码同时演示了支持栈展开的C++环境下,Windows结构化异常处理(SEH)的正确封装方式:
#include <Windows.h> #include <iostream> #include <atomic> using namespace std; using XHANDLE = unique_ptr<void, decltype([]( void *h ) { h && h != INVALID_HANDLE_VALUE && CloseHandle( (HANDLE)h ); })>; using XMAP_VIEW = unique_ptr<void, decltype([]( void *p ) { p && UnmapViewOfFile( p ); })>; template<typename Fn, typename Filter, typename Handler> requires requires( Fn fn, Filter filter, EXCEPTION_POINTERS *pEp, Handler handler ) { { fn() }; { filter( pEp ) } -> same_as<LONG>; { handler() }; } void seh_encapsulate( Fn fn, Filter filter, Handler handler ); int wmain( int argc, wchar_t **argv ) { if( argc < 2 ) return EXIT_FAILURE; XHANDLE xhFile( CreateFileW( argv[1], GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ) ); if( xhFile.get() == INVALID_HANDLE_VALUE ) return EXIT_FAILURE; LARGE_INTEGER liFileSize; if( !GetFileSizeEx( xhFile.get(), &liFileSize ) || liFileSize.QuadPart > (size_t)-1 ) return EXIT_FAILURE; XHANDLE xhMappging( CreateFileMapping( xhFile.get(), nullptr, PAGE_READONLY, 0, 0, nullptr ) ); if( !xhMappging.get() ) return EXIT_FAILURE; XMAP_VIEW mapView( MapViewOfFile( xhMappging.get(), FILE_MAP_READ, 0, 0, 0 ) ); if( !mapView.get() ) return EXIT_FAILURE; atomic_char *pa = (atomic_char *)mapView.get(), *paEnd = pa + (size_t)liFileSize.QuadPart; seh_encapsulate( [&]() { for( ; ; ) for( atomic_char *paScn = pa; paScn != paEnd; ++paScn ) (void)paScn->load( memory_order_relaxed ); }, [&]( EXCEPTION_POINTERS *pEp ) -> LONG { if( pEp->ExceptionRecord->ExceptionCode != EXCEPTION_IN_PAGE_ERROR ) return EXCEPTION_CONTINUE_SEARCH; if( pEp->ExceptionRecord->NumberParameters < 2 ) return EXCEPTION_CONTINUE_SEARCH; void *where = (void *)pEp->ExceptionRecord->ExceptionInformation[1]; if( where < pa || where >= paEnd ) return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_EXECUTE_HANDLER; }, []() { cout << "I/O error" << endl; } ); } template<typename Fn, typename Filter, typename Handler> requires requires( Fn fn, Filter filter, EXCEPTION_POINTERS *pEp, Handler handler ) { { fn() }; { filter( pEp ) } -> same_as<LONG>; { handler() }; } void seh_encapsulate( Fn fn, Filter filter, Handler handler ) { __try { fn(); } __except( filter( GetExceptionInformation() ) ) { handler(); } }
遇到的问题
实际测试时,DeleteFile调用可以成功返回、文件在资源管理器中不可见,但文件占用的磁盘簇会一直保留到所有内存映射句柄、映射视图全部关闭后才会释放,循环访问映射内存始终无法触发预期的EXCEPTION_IN_PAGE_ERROR异常。
原因说明与复现方案
根本原因
Windows内存管理器对已创建映射的文件采用引用计数生命周期管理:只要存在活跃的文件映射段(Section)对象、映射视图,NTFS/FAT32等本地文件系统就会保留文件的磁盘簇分配,DeleteFile成功仅移除文件的目录项,底层存储资源不会释放,内存管理器访问映射页时仍能从磁盘正常读取数据,自然不会触发换页错误。这是系统的设计行为,没有公开接口可以绕过该机制提前释放已映射文件的本地磁盘簇。
可成功复现异常的方案
要触发EXCEPTION_IN_PAGE_ERROR,核心是让映射页对应的底层存储在换入时不可访问,普通本地固定磁盘上的删除操作无法实现该效果,可选择以下方案:
- 方案1:使用SMB网络共享路径承载测试文件
将测试文件放在SMB共享目录中,本地完成内存映射后,直接在共享服务端删除该文件。此时本地访问未被缓存到内存的映射页时,SMB重定向器无法从服务端获取对应数据,会直接抛出目标异常。 - 方案2:使用可移动存储/临时虚拟磁盘承载测试文件
将测试文件放在可弹出的U盘、或手动挂载的VHD/VHDX虚拟磁盘中,完成内存映射后直接弹出/脱机该存储卷,此时映射页对应的底层存储不可用,访问未驻留内存的页时即可触发异常。测试时建议使用远大于系统可用内存的测试文件,避免所有页被系统提前读入缓存导致异常无法触发。 - 方案3:人为构造文件系统元数据错误
完成内存映射后,通过低层级磁盘IO直接覆写测试文件对应的NTFS MFT项、FAT表簇链记录,让文件系统无法定位文件对应的数据簇,访问未缓存页时也会触发换页错误。该方法操作复杂度高,容易损坏磁盘上的其他有效数据,不推荐使用。
测试注意:如果访问的映射页已经被系统缓存到物理内存中,即使底层存储不可用也不会触发异常,测试前可以通过清空系统工作集、使用大文件顺序遍历的方式保证访问的页需要从存储层换入。
内容的提问来源于stack exchange,提问作者Bonita Montero
相关产品推荐
相关产品推荐

