9

簡単に言うと、私の質問は、大量の空きメモリがある場合に WinAPIRegisterClassが失敗するERROR_NOT_ENOUGH_MEMORYのはなぜですか? また、それを防ぐにはどうすればよいですか?

背景: 多くの人がファイル転送の自動化に使用するアプリケーション (WinSCP FTP/SFTP クライアント) を開発しています。Windows スケジューラから毎日、毎分実行している人もいます。

一定回数実行するとアプリケーションが動作しなくなるという報告が多数寄せられています。問題を引き起こす実行回数は正確ではないようですが、数万から数十万の範囲です。また、問題は通常の Windows セッションで実行した場合ではなく、Windows スケジューラで実行した場合にのみ発生するようです。これを100%確認することはできませんが。

また、すべての報告は Windows 2008 R2 に関するもので、一部は Windows 7 に関するものであるようです。

私自身、Windows 7 で問題を再現することができました。システムがこの状態になると、アプリケーションはスケジューラのセッションで起動しなくなります。しかし、通常の通常のセッションでは問題なく起動します。また、他の一部のアプリケーション (すべてである必要はありません) は、スケジューラのセッションでも開始されます。また、この状態ではアプリケーションをデバッグできません。デバッガー (または Process Monitor などのツール) が実行されているときにアプリケーションが読み込まれないためです。

アプリケーションは Embarcadero (旧 Borland) C++ Builder VCL ライブラリを使用しています。VCL 初期化コードのどこかでクラッシュし (myWinMainは開始されていません)、コード 3 で終了します。初期化コードが何を行っているかを調べたところ、おそらくクラッシュを引き起こしたコードを特定できました (ただし、考えられる多くのコードの 1 つにすぎない可能性があります)。原因)。

原因は、( )RegisterClassを返す WinAPI 関数のようです。これが発生すると、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

(テストアプリケーションの完全なコードは最後にあります)

エラーにもかかわらず、メモリの問題ではないようです。呼び出しの前後に 10 MB のメモリを割り当てて確認したことRegisterClass(最後の完全なテスト コードで確認できます)。

必死になって、私は の 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 と 1 つのアプリケーションがある場合、このコードは実行されるたびに 11 個のアトムを消費します。

プライベート アトム テーブルのアトムがなくなると、ウィンドウ クラスを登録できなくなります。つまり、プライベート アトム テーブルがいっぱいになると、ウィンドウ化されたプログラムを開くことができなくなります。

WinDbg を使用してアトム テーブルをダンプし、このパターンを自分で確認することもできます。

于 2013-09-19T13:51:59.667 に答える