C++/Windows:如何报告内存不足异常(bad_alloc)?

C++/Windows: How to report an out-of-memory exception (bad_alloc)?

提问人:dyp 提问时间:8/21/2010 最后编辑:dyp 更新时间:8/24/2010 访问量:3540

问:

我目前正在为Windows MSVC++(9.0)应用程序开发一个基于异常的错误报告系统(即异常结构和类型/继承,调用堆栈,错误报告和日志记录等)。

我现在的问题是:如何正确报告和记录内存不足错误?

当此错误发生时,例如,作为操作抛出的错误,可能有许多“功能”不可用,主要是关于进一步的内存分配。通常,如果异常被抛出到库中,我会将异常传递给应用程序,然后使用消息框和错误日志文件来报告和记录它。另一种方法(主要用于服务)是使用 Windows 事件日志。
我遇到的主要问题是组装错误消息。为了提供一些错误信息,我想定义一个静态错误消息(可能是字符串文字,最好是消息文件中的条目,然后使用 FormatMessage)并包含一些运行时信息,例如调用堆栈。
此用途所需的函数/方法
bad_allocnew

  • STL (std::string, std::stringstream, std::ofstream)
  • 显像管 (swprintf_s, fwrite)
  • 或者 Win32 API (StackWalk64, MessageBox, FormatMessage, ReportEvent, WriteFile)

除了在 MSDN 上记录之外,它们都在 Windows 中更多 (Win32) 或更少 (STL) 闭源,所以我真的不知道它们在内存不足问题下的行为。

为了证明可能存在问题,我写了一个微不足道的小应用程序来引发bad_alloc:

int main()
{
    InitErrorReporter();  

    try
    {
        for(int i = 0; i < 0xFFFFFFFF; i++)
        {
            for(int j = 0; j < 0xFFFFFFFF; j++)
            {
                char* p = new char;
            }
        }
    }catch(bad_alloc& e_b)
    {
        ReportError(e_b);
    }

    DeinitErrorReporter();

    return 0;
}

运行了两个没有附加调试器的实例(在 VS 2008 的发布配置中),但“什么也没发生”,即我在错误报告中内部使用的 ReportEvent 或 WriteFile 中没有错误代码。然后,启动一个带有和一个不带调试器的实例,并让它们尝试使用 ReportError 行上的断点一个接一个地报告其错误。对于附加了调试器的实例来说,这工作正常(正确报告并记录了错误,即使使用 LocalAlloc 没有问题)!但是 taskman 表现出一种奇怪的行为,在应用程序退出之前释放了大量内存,我想当抛出异常时。


请考虑可能有多个进程 [edit] 和多个线程 [/edit] 消耗大量内存,因此释放预分配的堆空间不是避免想要报告错误的进程内存环境不足的安全解决方案。

先谢谢你!

C++ Windows 错误报告 内存不足

评论

2赞 Sniggerfardimungus 8/21/2010
您是否考虑过提前预留一个内存块,每当系统出现内存不足情况时,它将成为放置分配的来源?我使用这种方法只是为了优雅地退出应用程序,但操作系统(OpenSolaris、Linux)会做类似的事情,让应用程序有足够的时间释放或交换低优先级分配并优雅地恢复。
0赞 dyp 8/21/2010
目前,我使用一些堆栈空间(调用 InitErrorReporter 时声明的成员变量)为 CRT/WinSDK 函数提供缓冲区。但我不知道他们在内部做什么——参见 Alex Farber 的 anser。
0赞 Chubsdad 8/23/2010
stackoverflow.com/questions/1308052/......谈论类似的事情
0赞 Billy ONeal 9/5/2010
可能的重复 在 C/C++ 中处理内存不足情况的优雅方式是什么?
0赞 Billy ONeal 9/5/2010
对不起,欺骗性信息 - 本来是想把它放在另一个问题上。

答:

3赞 Alex F 8/21/2010 #1

“释放预先分配的堆空间...”。这正是我读到你的问题时所想的。但我认为你可以尝试一下。每个进程都有自己的虚拟内存空间。对于另一个进程消耗大量内存,如果整个计算机都在工作,这仍然可能起作用。

评论

0赞 dyp 8/21/2010
k,但假设一个应用程序中的两个线程都运行不同库的功能。一个进程 ->一个虚拟内存空间。因此,当一个人抛出一个bad_alloc然后释放预先分配的空间时,另一个人可以分配它吗?问题是当我使用OS / SDK功能时,我不知道它们内部是否依赖于头部空间,因此使用预先分配的空间进行操作是我认为没有解决方案的。
0赞 dyp 8/21/2010
在使用此技术之前,我必须暂停所有其他线程吗?
0赞 Alex F 8/21/2010
另一个线程可能会立即使用刚刚释放的预分配内存...应停止每个活动,以确保错误报告成功。
1赞 TheUndeadFish 8/21/2010 #2

请注意,可能有多个进程占用大量内存,因此释放预分配的堆空间并不是一个安全的解决方案,以避免想要报告错误的进程的内存环境不足。

在 Windows(和其他现代操作系统)下,每个进程都有自己的地址空间(也称为内存),与其他正在运行的进程分开。所有这些都与机器中的字面 RAM 是分开的。操作系统已将进程地址空间虚拟化,使其远离物理 RAM。

这就是 Windows 能够将进程使用的内存推送到硬盘上的页面文件中的方式,而这些进程不知道发生了什么。

这也是单个进程可以分配比计算机具有的物理 RAM 更多的内存,但仍能运行的方式。例如,在具有 512MB RAM 的机器上运行的程序仍然可以分配 1GB 的内存。Windows 无法同时将所有内容保存在 RAM 中,其中一些将位于页面文件中。但程序不会知道。

因此,如果一个进程分配内存,它不会导致另一个进程的内存减少。每个过程都是独立的。

每个过程只需要担心自己。因此,释放预先分配的内存块的想法实际上非常可行。

评论

0赞 Ana Betts 8/21/2010
哼?这是不正确的。虽然每个进程都有自己的 VA 空间,但物理帧和交换空间的数量有限,无法满足页面传入请求。如果总提交费用超过此数字,则每个进程的分配请求都会失败。如果遇到这种情况,释放内存块意味着系统中的任何进程都可以接受它(即任何进程都可以再次提高提交费用)。
0赞 TheUndeadFish 8/21/2010
@Paul:你说得有道理。无论操作系统做什么,最终总会有一个限制。但是,除了操作系统的内存机制已达到最大值的情况之外,我想知道程序的行为是否不合理,就好像它们总是有完整的虚拟地址空间可以使用一样。但我想这可能是特定于情况的,这取决于最终用户机器将其资源最大化的可能性以及登录以响应这种情况的实际有用程度。
0赞 Puppy 8/23/2010
是的,这是有限制的。这是硬盘的大小。那是很多记忆。在绝大多数机器上,RAM的分配率不到50%,而且有如此多的HDD空间可用,现实情况是,您的进程只需要担心自己。
0赞 dyp 8/23/2010
我认为主要限制是 32 位应用程序中的虚拟地址空间。参见 msdn.microsoft.com/en-us/library/aa366778(VS.85).aspx但这不是主要问题。当你唯一的问题是你的地址空间已满时(如何确定?),你可以释放一块内存并继续工作。
2赞 Ana Betts 8/24/2010
@DeadMG blogs.technet.com/b/markrussinovich/archive/2008/11/17/... - “最大值是 RAM 大小的三倍或 4GB,以较大者为准”
0赞 Ana Betts 8/21/2010 #3

如上所述,不能使用 CRT 或 MessageBox 函数来处理 OOM,因为它们可能需要内存。唯一真正安全的事情是在启动时分配一个内存块,您可以将信息写入并打开文件或管道的句柄,然后在 OOM 输出时将 WriteFile 写入它。

评论

0赞 TheUndeadFish 8/21/2010
因此,您的意思是,与其预先分配然后释放块,不如预先分配一个区域,专门用于处理 OOM 场景中记录的数据(一种用于组装字符串或任何需要的暂存缓冲区)。假设 WriteFile 在 OOM 场景中没有理由失败,那么我不得不同意这听起来像是记录信息的最佳保证方式。
0赞 Ana Betts 8/23/2010
是的,如果操作系统必须保证某些东西在内存不足的情况下工作(例如能够引发 SEH 异常,ntdll 始终在其后袋中预先分配一个),这就是操作系统所做的
0赞 dyp 8/23/2010
您知道创建不同类型的 Win32 对象在 OOM 条件下的行为吗?即内核对象(如文件(->文件句柄)和 GUI 对象(如 MessageBoxes)?据我所知,至少内核对象不是您的虚拟内存/地址空间的一部分,因此操作系统实际上为它们分配了内存。
1赞 Puppy 8/23/2010
@DyP:你错了。操作系统确实会在您的地址空间内进行分配。但是,两者分开的方式,可能会没事的。这就是为什么在 32 位机器上,您获得 2GB 的限制,而不是 4GB——因为操作系统会保留另外 2GB 以备不时之需。但是,如果您未能分配,那是因为您的一半已满 - 不一定是整个空间。不能保证 OS 2GB 是否处于任何状态,包括是否已满。
0赞 Ana Betts 8/24/2010
@DyP 内核对象也需要内存,并且它们也受系统范围 OOM 的影响 - 如果分配失败,它最终将使用 GetLastError 传播回去
2赞 peterchen 8/24/2010 #4
  • 预先分配您需要的缓冲区
  • 静态链接并使用 _beginthreadex 而不是 CreateThread(否则,CRT 函数可能会失败) -- 或者 -- 自己实现字符串 concat / i2a
  • 使用 MessageBox (MB_SYSTEMMODAL |MB_OK) MSDN 在报告 OOM 条件时提到了这一点(一些 MS 博主将这种行为描述为预期:消息框不会分配内存。

日志记录更难,至少,日志文件需要已经打开。

可能最好使用 和 ,以避免任何缓冲尝试。第一个要求写入和内存缓冲区对齐(即,您需要查询 GetDiskFreeSpace,按此对齐缓冲区,并且仅写入“扇区大小的倍数”文件偏移量,并且块是扇区大小的倍数。我不确定这是否必要或有帮助,但每次分配都失败的系统范围的 OOM 很难模拟。FILE_FLAG_NO_BUFFERINGFILE_FLAG_WRITE_THROUGH

评论

1赞 dyp 8/24/2010
谢谢。我做了一些测试(但我不知道它们有多重要)和 MessageBox |MB_SYSTEMMODAL总是会出现,即使抓了几只bad_allocs。为此,我使用了一些基于我自己答案中的代码,但决定为 x64 / Release 构建它。我在 x86 上收到的bad_allocs主要是虚拟地址空间外的错误,因为我有 4 GB 的 RAM 加上(现在)2 GB 的最大页面文件大小。使用 x64 版本,系统确实会严重挂起,有时甚至会挂起光标。但是,MB仍然有效,我提到的页面也是如此!