程序崩溃闪退——MFC共享内存多次OpenFileMapping和MapViewOfFile而没有相应的UnmapViewOfFile和CloseHandle

文章目录

写在前面,崩溃点如图:
注:如果想看到系统函数的崩溃点,需要下载对应的pdb文件,下载方法参考:
如何下载dump(C++程序生成)文件所需要的pdb文件,包含自动下载和手动拼接下载

崩溃原因之一如下文:

在MFC中使用共享内存时,多次调用OpenFileMappingMapViewOfFile而不相应释放会导致一系列问题,包括:

重复调用 OpenFileMappingMapViewOfFile 而不释放,会导致多层次的资源泄漏和系统不稳定

1. 资源泄漏的逐层累积效应

第一层:句柄泄漏(进程级)

复制代码
第一次: OpenFileMapping -> 句柄A
第二次: OpenFileMapping -> 句柄B (相同内核对象,但新句柄)
第三次: OpenFileMapping -> 句柄C
...
进程句柄表逐渐填满 -> 后续任何API调用失败

第二层:虚拟地址空间泄漏(进程级)

复制代码
第一次: MapViewOfFile -> 视图1 (占用4KB-2MB虚拟地址)
第二次: MapViewOfFile -> 视图2 (又占一块虚拟地址)
...
32位进程: 2GB用户空间很快耗尽
64位进程: 128TB空间也会碎片化

第三层:系统资源耗尽(系统级)

复制代码
单个进程泄漏 -> 多个进程泄漏
-> 系统句柄表饱和
-> 内存映射文件资源耗尽
-> 系统整体性能下降

2. 具体问题场景分析

场景1:重复打开同一共享内存

cpp 复制代码
// 错误示例
for(int i = 0; i < 100; i++) {
    HANDLE hMap = OpenFileMapping(FILE_MAP_READ, FALSE, "Global\\MySharedMem");
    LPVOID pData = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 4096);
    // 没有UnmapViewOfFile和CloseHandle!
    // 循环100次后:100个句柄 + 100个视图泄漏
}

场景2:异常路径未清理

cpp 复制代码
BOOL LoadSharedData()
{
    HANDLE hMap = OpenFileMapping(...);
    if(!hMap) return FALSE;
    
    LPVOID pData = MapViewOfFile(...);
    if(!pData) {
        // 这里缺少 CloseHandle(hMap) !!!
        return FALSE;
    }
    
    // 使用数据...
    
    // 忘记释放
    // UnmapViewOfFile(pData);
    // CloseHandle(hMap);
    return TRUE;
}

3. 问题症状表现

进程内症状

复制代码
1. 句柄数持续增长
   Process Explorer显示: Handles计数不断增加
   任务管理器: 句柄数持续上升

2. 虚拟内存使用异常
   私有字节数正常,但虚拟大小异常增长
   内存碎片化,无法分配大块连续内存

3. 程序行为异常
   - 新窗口无法创建
   - 文件无法打开
   - 内存分配失败 (ERROR_NOT_ENOUGH_MEMORY)
   - 随机访问违规

调试器观察

cpp 复制代码
// 在Visual Studio调试器中查看
// 命令窗口输入:
!handle 0 0  // 查看句柄统计
!address    // 查看虚拟地址空间使用
!heap -s    // 查看堆状态

4. 系统级影响

内核对象泄漏

复制代码
OpenFileMapping创建的文件映射对象是内核对象
内核对象表是全局共享资源
过多的泄漏会影响系统所有进程

分页文件压力

复制代码
每个映射视图都需要分页文件支持
大量未释放映射 -> 分页文件碎片化
-> 系统换页效率下降
-> 整体系统响应变慢

5. 特殊危险情况

跨进程影响

cpp 复制代码
// 进程A: 创建共享内存
HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, ...);

// 进程B: 多次打开但不关闭
for(;;) {
    HANDLE h = OpenFileMapping(...);
    LPVOID p = MapViewOfFile(...);
    // 不释放
}

// 进程C: 再也无法打开同一共享内存
// 因为系统句柄资源紧张

DLL卸载问题

cpp 复制代码
// DLL中分配共享内存
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved)
{
    switch(reason) {
    case DLL_PROCESS_ATTACH:
        g_hMap = OpenFileMapping(...);
        g_pData = MapViewOfFile(...);
        break;
        
    case DLL_PROCESS_DETACH:
        // 忘记释放!!!
        // 如果忘记UnmapViewOfFile,DLL卸载时映射视图不会自动释放
        // 导致进程地址空间残留映射
        break;
    }
    return TRUE;
}

6. 检测和诊断方法

编程检测

cpp 复制代码
// 在调试版本中添加跟踪
#ifdef _DEBUG
class HandleTracker {
    static std::map<HANDLE, std::string> openHandles;
    static std::map<LPVOID, std::string> mappedViews;
    
public:
    static void TrackOpen(HANDLE h, const char* location) {
        openHandles[h] = location;
        TRACE("OpenHandle: 0x%p at %s\n", h, location);
    }
    
    static void TrackMap(LPVOID p, const char* location) {
        mappedViews[p] = location;
        TRACE("MapView: 0x%p at %s\n", p, location);
    }
    
    static void ReportLeaks() {
        if(!openHandles.empty()) {
            TRACE("=== HANDLE LEAKS (%d) ===\n", openHandles.size());
            for(auto& pair : openHandles) {
                TRACE("Leaked handle 0x%p from %s\n", pair.first, pair.second.c_str());
            }
        }
        
        if(!mappedViews.empty()) {
            TRACE("=== VIEW LEAKS (%d) ===\n", mappedViews.size());
            for(auto& pair : mappedViews) {
                TRACE("Leaked view 0x%p from %s\n", pair.first, pair.second.c_str());
            }
        }
    }
};
#endif

运行时监控

bat 复制代码
# 使用Process Explorer监控
# 查看进程的句柄类型统计
# Handles -> Show Unnamed Handles and Mappings

# 使用RAMMap查看内存映射
# Sysinternals RAMMap工具

7. 预防和修复策略

使用智能管理类

cpp 复制代码
class SafeFileMapping {
private:
    HANDLE m_hMap = NULL;
    LPVOID m_pView = NULL;
    size_t m_size = 0;
    
public:
    SafeFileMapping(LPCTSTR name, DWORD access = FILE_MAP_ALL_ACCESS, 
                    size_t size = 0, bool create = false) 
    {
        if(create) {
            m_hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, 
                                     PAGE_READWRITE, 0, (DWORD)size, name);
        } else {
            m_hMap = OpenFileMapping(access, FALSE, name);
        }
        
        if(m_hMap) {
            m_pView = MapViewOfFile(m_hMap, access, 0, 0, size);
            m_size = size;
        }
    }
    
    ~SafeFileMapping() { Release(); }
    
    // 禁止拷贝
    SafeFileMapping(const SafeFileMapping&) = delete;
    SafeFileMapping& operator=(const SafeFileMapping&) = delete;
    
    // 允许移动
    SafeFileMapping(SafeFileMapping&& other) noexcept 
        : m_hMap(other.m_hMap), m_pView(other.m_pView), m_size(other.m_size) 
    {
        other.m_hMap = NULL;
        other.m_pView = NULL;
        other.m_size = 0;
    }
    
    void Release() {
        if(m_pView) {
            UnmapViewOfFile(m_pView);
            m_pView = NULL;
        }
        if(m_hMap) {
            CloseHandle(m_hMap);
            m_hMap = NULL;
        }
        m_size = 0;
    }
    
    operator bool() const { return m_pView != NULL; }
    LPVOID data() const { return m_pView; }
    size_t size() const { return m_size; }
};

使用作用域保护

cpp 复制代码
#define SCOPED_MAPPING(name, access, size) \
    SafeFileMapping mapping(name, access, size); \
    if(!mapping) { /* 错误处理 */ } \
    auto scoped_cleanup = std::shared_ptr<void>( \
        (void*)1, void* { mapping.Release(); })

// 使用示例
void ProcessData() {
    SCOPED_MAPPING(L"Global\\MyData", FILE_MAP_READ, 4096);
    // 离开作用域自动释放
}

8. 最佳实践总结

  1. 始终配对使用 :每个OpenFileMapping必须对应一个CloseHandle,每个MapViewOfFile必须对应一个UnmapViewOfFile

  2. 逆序释放 :先UnmapViewOfFile,后CloseHandle

  3. 异常安全:使用RAII确保异常情况下资源仍能释放

  4. 尽早释放:不再需要时立即释放,不要等到程序结束

  5. 单一职责:每个模块负责自己打开的资源

  6. 定期检查:在调试版本中添加泄漏检测代码

关键结论:虽然Windows在进程退出时会自动清理这些资源,但运行期间的泄漏会导致程序不稳定、性能下降,并可能影响整个系统。养成良好的资源管理习惯是高质量Windows编程的基础。

上一篇:程序崩溃闪退------C++中,如果没有使用CoInitializeEx初始化,但却调用了CoUninitialize释放


不积跬步,无以至千里。


代码铸就星河,探索永无止境

在这片由逻辑与算法编织的星辰大海中,每一次报错都是宇宙抛来的谜题,每一次调试都是与未知的深度对话。不要因短暂的"运行失败"而止步,因为真正的光芒,往往诞生于反复试错的暗夜。

请铭记

  • 你写下的每一行代码,都在为思维锻造韧性;
  • 你破解的每一个Bug,都在为认知推开新的门扉;
  • 你坚持的每一分钟,都在为未来的飞跃积蓄势能。

技术的疆域没有终点,只有不断刷新的起点。无论是递归般的层层挑战,还是如异步并发的复杂困局,你终将以耐心为栈、以好奇心为指针,遍历所有可能。

向前吧,开发者

让代码成为你攀登的绳索,让逻辑化作照亮迷雾的灯塔。当你在终端看到"Success"的瞬间,便是宇宙对你坚定信念的回响------
此刻的成就,永远只是下一个奇迹的序章! 🚀


(将技术挑战比作宇宙探索,用代码、算法等意象强化身份认同,传递"持续突破"的信念,结尾以动态符号激发行动力。)

cpp 复制代码
//c++ hello world示例
#include <iostream>  // 引入输入输出流库

int main() {
    std::cout << "Hello World!" << std::endl;  // 输出字符串并换行
    return 0;  // 程序正常退出
}

print("Hello World!")  # 调用内置函数输出字符串

package main  // 声明主包
py 复制代码
#python hello world示例
import "fmt"  // 导入格式化I/O库
go 复制代码
//go hello world示例
func main() {
    fmt.Println("Hello World!")  // 输出并换行
}
C# 复制代码
//c# hello world示例
using System;  // 引入System命名空间

class Program {
    static void Main() {
        Console.WriteLine("Hello World!");  // 输出并换行
        Console.ReadKey();  // 等待按键(防止控制台闪退)
    }
}
相关推荐
问君能有几多愁~2 小时前
C++ 日志实现
java·前端·c++
JANGHIGH2 小时前
c++ 多线程(二)
开发语言·c++
珑墨2 小时前
【浏览器】页面加载原理详解
前端·javascript·c++·node.js·edge浏览器
hd51cc3 小时前
MFC控件 学习笔记二
笔记·学习·mfc
a伊雪4 小时前
c++ 引用参数
c++·算法
应茶茶4 小时前
从 C 到 C++:详解不定参数的两种实现方式(va_args 与参数包)
c语言·开发语言·c++
code bean5 小时前
【C++】Scoop 包管理器与 MinGW 工具链详解
开发语言·c++
hetao17338375 小时前
2025-12-11 hetao1733837的刷题笔记
c++·笔记·算法
saltymilk7 小时前
C++ 语言特性的变更可能让你的防御成为马奇诺防线
c++