为什么RegisterClass失败并出现ERROR_NOT_ENOUGH_MEMORY?

时间:2013-09-19 11:26:49

标签: c++ winapi memory-management out-of-memory vcl

很快,我的问题是,当有大量内存空闲时,为什么WinAPI RegisterClass会失败ERROR_NOT_ENOUGH_MEMORY,我该怎么做才能阻止它呢?

背景 我正在开发一个应用程序(WinSCP FTP / SFTP客户端),许多人使用它来自动执行文件传输。有些人每天都会从Windows Scheduler每分钟运行一次。

我收到很多报告,经过一定数量的运行后,应用程序停止工作。触发问题的运行次数似乎并不准确,但它在数万甚至数十万的范围内。此外,似乎只有在Windows Scheduler下运行时才会出现问题,而不是在常规Windows会话中运行时。虽然我无法100%确认这一点。

此外,所有报告似乎都适用于Windows 2008 R2 +一些适用于Windows 7.此外,这可能只是巧合。

我自己能够在Windows 7上重现该问题。一旦系统进入此状态,我的应用程序就不再在Scheduler的会话中启动。但它在正常的常规会话中开始很好。而且一些其他应用程序(不一定全部)甚至在Scheduler的会话中启动。同样在这种状态下我无法调试应用程序,因为它甚至在调试器(或Process Monitor等工具)运行时都没有加载。

该应用程序正在使用Embarcadero(前Borland)C ++ Builder VCL库。它在VCL初始化代码中崩溃(我的WinMain甚至没有启动)并退出代码3.检查初始化代码正在做什么,我可能能够识别触发崩溃的代码(虽然它可以只是是许多可能的原因之一)。

罪魁祸首似乎是RegisterClass WinAPI函数返回8ERROR_NOT_ENOUGH_MEMORY)。发生这种情况时,VCL代码会抛出异常;并且因为还没有异常处理程序,它会崩溃应用程序。

我使用在VS 2012中开发的非常简单的C ++控制台应用程序验证了这一点(将问题与C ++ Builder和VCL隔离开来)。核心代码是:

SetLastError(ERROR_SUCCESS);
fout << L"Registering class" << std::endl;
WNDCLASS WndClass;
memset(&WndClass, 0, sizeof(WndClass));
WndClass.lpfnWndProc = &DefWindowProc;
WndClass.lpszClassName = L"TestClass";
WndClass.hInstance = GetModuleHandle(NULL);
ATOM Atom = RegisterClass(&WndClass);
DWORD Error = GetLastError();
// The Atom is NULL and Error is ERROR_NOT_ENOUGH_MEMORY here

(测试应用程序的完整代码在最后)

尽管有错误,但它似乎不是内存问题。我通过在RegisterClass调用之前和之后分配10 MB内存来验证的内容(可以在最后的完整测试代码中看到)。

绝望,我甚至偷看了RegisterClass的Wine实施。确实可能会失败ERROR_NOT_ENOUGH_MEMORY,但只有当它无法为类注册分配内存时才会失败。什么是字节。它也使用HeapAlloc分配内存。由于任何其他原因,任何其他错误代码都不会使RegisterClass的葡萄酒失败。

对我来说,它首先看起来像Windows中的一个错误。我相信Windows应该在它退出时释放进程分配的所有资源。因此,无论应用程序实施得多么糟糕,以前的运行都不会对资源(例如内存)的后续运行产生任何影响。无论如何,我很乐意找到一个解决方法。

更多事实:除标准系统流程(总共约50个)外,测试系统不会运行任何特殊操作。在我的情况下,它是VMware虚拟机,虽然我的用户显然在真实的物理机器上看到了问题。该过程的先前实例已经消失,因此它们不会被正确终止,这将阻止系统释放资源。大约有500 MB的内存空闲(占总数的一半)。分配的句柄只有大约16000个。


测试VS应用程序的完整代码:

#include "stdafx.h"
#include "windows.h"
#include <fstream>

int _tmain(int argc, _TCHAR* argv[])
{
    std::wofstream fout;
    fout.open(L"log.txt",std::ios::app);

    SetLastError(ERROR_SUCCESS);
    fout << L"Allocating heap" << std::endl;
    LPVOID Mem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 10 * 1024 * 1024);
    DWORD Error = GetLastError();
    fout << L"HeapAlloc [" << std::hex << intptr_t(Mem) << std::dec 
         << L"] Error [" << Error << "]" << std::endl;

    // ===== Main testing code begins =====
    SetLastError(ERROR_SUCCESS);
    fout << L"Registering class" << std::endl;
    WNDCLASS WndClass;
    memset(&WndClass, 0, sizeof(WndClass));
    WndClass.lpfnWndProc = &DefWindowProc;
    WndClass.lpszClassName = L"TestClass";
    WndClass.hInstance = GetModuleHandle(NULL);
    ATOM Atom = RegisterClass(&WndClass);
    Error = GetLastError();
    fout << L"RegisterClass [" << std::hex << intptr_t(Atom) << std::dec 
         << L"] Error [" << Error << "]" << std::endl;
    // ===== Main testing code ends =====

    SetLastError(ERROR_SUCCESS);
    fout << L"Allocating heap" << std::endl;
    Mem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 10 * 1024 * 1024);
    Error = GetLastError();
    fout << L"HeapAlloc [" << std::hex << intptr_t(Mem) << std::dec 
         << L"] Error [" << Error << "]" << std::endl;

    fout << L"Done" << std::endl;

    return 0;
}

输出是(当从Windows 7系统上的Windows Scheduler运行到我的应用程序的数万次运行时进入上述状态时):

Allocating heap
HeapAlloc [ec0020] Error [0]
Registering class
RegisterClass [0] Error [8]
Allocating heap
HeapAlloc [18d0020] Error [0]
Done

1 个答案:

答案 0 :(得分:6)

  1. 在RAM用完之前,可能会耗尽可用的虚拟地址空间(特别是对于32位进程)。但是,这似乎不是这种情况。
  2. 错误可能指的是用尽实际RAM之外的其他资源,例如原子。由于ATOM是16位类型,因此只有65536个可能的原子值。然而,像窗口类的全局原子具有更有限的范围 - 0xC000到0xFFFF,理论上只给出0x4000(16384)个最大注册类(在实践中可能更少)。
  3. 检查从RegisterClass()获得的原子值。如果他们在出错之前接近FFFF,那可能是你的问题。

    编辑:似乎其他人已经运行into the same issue并确定了罪魁祸首:

      

    VCL中有一个严重的错误会消耗掉原子中的原子   私人原子表。 Windows中包含有限数量的私有原子   私有原子表(32767),这是由Windows类共享的,   Windows消息,剪贴板格式等每次控件   模块已初始化,它会创建一个新的Windows消息:

     ControlAtomString := Format('ControlOfs%.8X%.8X', 
                                  [HInstance, GetCurrentThreadID]); 
     ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
     RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));
    
         

    问题乘以应用程序的DLL数量   包含控件模块。如果你有10个dll,和   一个应用程序,每次运行时,此代码将消耗11个原子。

         

    当系统用完私有原子表中的原子时,否   窗口类可以注册。这意味着,没有窗口程序会   私有原子表已满后能够打开。

    您也可以dump the atom table using WinDbg并自行检查此模式。