9

很快,我的问题是,当有大量可用内存时,为什么 WinAPIRegisterClass会失败,我能做些什么来防止它?ERROR_NOT_ENOUGH_MEMORY

背景: 我正在开发许多人用于自动化文件传输的应用程序(WinSCP FTP/SFTP 客户端)。有些人每分钟、每天都从 Windows 调度程序运行它。

我收到很多报告,在运行一定次数后,应用程序停止工作。触发问题的运行次数似乎并不准确,但在数万到数十万之间。此外,似乎问题仅在 Windows 调度程序下运行时出现,而不是在常规 Windows 会话中运行时出现。虽然我不能 100% 确认这一点。

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

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

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

罪魁祸首似乎是返回( )的RegisterClassWinAPI 函数。发生这种情况时,VCL 代码会抛出异常;由于还没有异常处理程序,它会使应用程序崩溃。8ERROR_NOT_ENOUGH_MEMORY

我已经使用在 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 内存验证了什么(可以在最后的完整测试代码中看到)。

绝望,我什至偷看了 Wine 的RegisterClass. 它确实可以失败ERROR_NOT_ENOUGH_MEMORY,但只有当它无法为类注册分配内存时。什么是几个字节。它也使用分配内存HeapAlloc。Wine 不会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 调度程序运行时,通过我的应用程序的数万次运行进入上述状态):

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

1 回答 1

8
  1. 在用完 RAM(尤其是 32 位进程)之前,您可能会用完可用的虚拟地址空间。然而,这里似乎并非如此。
  2. 该错误可能是指用完其他资源而不是实际 RAM,例如原子。由于ATOM是 16 位类型,因此只有 65536 个可能的原子值。然而,像窗口类这样的全局原子的范围更有限——从 0xC000 到 0xFFFF,理论上只给你 0x4000 (16384) 个最大注册类(实际上可能更少)。

检查您从中获得的原子值RegisterClass()。如果他们FFFF在出错之前已经接近,那可能是你的问题。

编辑:似乎其他人遇到了同样的问题并确定了罪魁祸首:

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 个原子。

当系统用完私有原子表中的原子时,将无法注册任何窗口类。这意味着,私有原子表已满后,将无法打开任何窗口程序。

您还可以使用 WinDbg 转储原子表并自己检查此模式。

于 2013-09-19T13:51:59.667 回答