アプリケーションのメモリ使用量を制限する方法(Windows)

Windows には、指定したプロセスに対してメモリ使用量の上限を設定する API があります。これを利用して、既存のアプリケーションに制限をかけたり、開発中のプログラムのストレステストを行ったりできます。実際に試してみましょう。

動作確認環境

  • Windows 11 Home 23H2
  • Visual Studio Community 2022 (Visual C++)

メモリの使用量を制限する API

メモリの使用量を制限する Windows API の使い方について簡単に説明します。

最初に CreateJobObject 関数で「ジョブオブジェクト」というものを作ります。第 1 引数にセキュリティ記述子を、第 2 引数にジョブの名前を指定します。以下ではどちらも NULL にしています。

HANDLE hJob = CreateJobObject(NULL, NULL);

次に AssignProcessToJobObject 関数を使って、任意のプロセスをジョブオブジェクトに割り当てます。以下の例では自プロセスのハンドルを取得し、ジョブオブジェクトに割り当てています。

HANDLE hProcess = GetCurrentProcess();
AssignProcessToJobObject(hJob, hProcess);

そして、SetInformationJobObject 関数を呼び出し、メモリ使用量の上限を設定します。このとき、引数の構造体に必要な情報を詰めておきます。

JOBOBJECT_EXTENDED_LIMIT_INFORMATION exLimit;
ZeroMemory(&exLimit, sizeof(exLimit));
exLimit.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_MEMORY;
exLimit.ProcessMemoryLimit = 10 * 1024 * 1024; // メモリ使用量の上限

SetInformationJobObject(hJob,
    JobObjectExtendedLimitInformation ,
    &exLimit,
    sizeof(exLimit));

最初に作ったジョブオブジェクトのハンドルは、不要になったらクローズします。

CloseHandle(hJob);

以上です。難しくはないと思います。

メモリを制限するプログラム

これらの API を利用して、メモリの使用量を制限するテストツールを作ってみます。
書式は次の通り。名前は MemLimit、第 1 引数は PID(プロセス ID)、第 2 引数はメモリ使用量の上限(メガバイト単位)とします。

MemLimit <PID> <メモリサイズ(MB)>

プログラム全体です。基本的に、さきほど説明した API を呼び出しているだけです。

#include <windows.h>

int main(int argc, char *argv[])
{
    int result = -1;

    HANDLE hProcess = NULL;
    HANDLE hJob = NULL;
    
    BOOL bSuccess;
    DWORD dwPid;
    DWORD dwMemLimitMB;

    // 引数のチェック
    if (argc != 3)
    {
        printf("【機能】 指定のプロセスに対してメモリ使用量の上限を設定。\n");
        printf("【書式】 MemLimit <PID> <メモリサイズ(MB)>\n");
        result = 10;
        goto L_Cleanup;
    }

    // 引数からプロセス ID を取得
    dwPid = atoi(argv[1]);
    if (dwPid == 0)
    {
        printf("PID に 0 は指定できません。\n");
        result = 20;
        goto L_Cleanup;
    }

    // 引数からメモリサイズを取得
    dwMemLimitMB = atoi(argv[2]);
    if (dwMemLimitMB == 0)
    {
        printf("メモリサイズに 0 は指定できません。\n");
        result = 30;
        goto L_Cleanup;
    }

    // ジョブオブジェクトの生成
    hJob = CreateJobObject(NULL, NULL);
    if (hJob == NULL)
    {
        printf("ジョブオブジェクト生成失敗(err=%d)。\n", GetLastError());
        result = 40;
        goto L_Cleanup;
    }
    
    // プロセスハンドルの取得
    hProcess = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, FALSE, dwPid);
    if (hProcess == NULL)
    {
        printf("プロセスハンドル取得失敗(PID=%d, err=%d)。\n", dwPid, GetLastError());
        result = 50;
        goto L_Cleanup;
    }

    // ジョブオブジェクトにプロセスを割り当て
    bSuccess = AssignProcessToJobObject(hJob, hProcess);
    if (! bSuccess)
    {
        printf("プロセス割り当て失敗(PID=%d, err=%d)。\n", dwPid, GetLastError());
        result = 60;
        goto L_Cleanup;
    }

    // メモリ制限の設定
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION exLimit;
    ZeroMemory(&exLimit, sizeof(exLimit));
    exLimit.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_MEMORY;
    exLimit.ProcessMemoryLimit = dwMemLimitMB * 1024 * 1024;
    bSuccess = SetInformationJobObject(hJob,
        JobObjectExtendedLimitInformation ,
        &exLimit,
        sizeof(exLimit));
    if (! bSuccess)
    {
        printf("メモリ制限設定失敗(err=%d)。\n", GetLastError());
        result = 70;
        goto L_Cleanup;
    }    

    // 正常終了
    printf("PID=%d に %d MBのメモリ制限を設定。\n", dwPid, dwMemLimitMB);
    result = 0;
    goto L_Cleanup;
    
L_Cleanup:
    // 後始末
    if (hJob)
    {
        CloseHandle(hJob);
    }
    
    if (hProcess)
    {
        CloseHandle(hProcess);
    }
    
    // 終了
    return result;
}

Visual C++ のコマンドプロンプトを起動し、

「c:\tmp」に置いた「MemLimit.c」をビルドします。「/Od」は最適化抑止(デフォルト)、「/Zi」はデバッグ情報を生成、の意味です。

C:\tmp> cl /Od /Zi MemLimit.c

MemLimit.exe が生成されました。

アプリケーションのメモリ使用量を制限する

いま作った MemLimit ツールを使って、いろいろなアプリケーションのメモリ使用量を制限してみましょう。

メモ帳を起動します。

タスクマネージャーでメモ帳の「コミットサイズ」を見ると、起動しただけで約 24MB 消費していることがわかります。タスクマネージャーはさまざまな観点のメモリ使用量を表示できますが、本件で参照するのはコミットサイズです。

ツールでメモリ使用量を 100MB に制限します。第 1 引数にはメモ帳の PID を、第 2 引数には 100MB を意味する 100 を指定しています。

C:\tmp> MemLimit.exe 16592 100
PID=16592 に 100 MBのメモリ制限を設定。

メモ帳のメニューから [ファイル]-[開く] を選択し、あらかじめ用意しておいた約 100MB のテキストファイルを開きます。

「このコマンドを処理するにはメモリリソースが足りません。」というメッセージが表示され、処理が中断されました。制限が効いているようです。

ファイルの読み込みに失敗したので、コピー & ペーストを繰り返して長い長いテキストを入力していきます。するとそのうち、「メモリ不足のためこの操作を実行できません。1 つ以上のアプリケーションを終了して空きメモリ領域を増やしてから、もう一度お試しください。」というメッセージが表示されました。

[OK] ボタンを押してもこのメッセージが繰り返し表示され、強制終了するしかありませんでした。

ペイントでも試してみましょう。
ペイントを起動します。

タスクマネージャーを見ると、起動直後のコミットサイズは約 43MB です。

ペイントのメモリ使用量を 100MB に制限します。

C:\tmp> MemLimit.exe 8568 100
PID=8568 に 100 MBのメモリ制限を設定。

あらかじめ用意しておいた約 100MB のビットマップファイルを開きます。すると、実際には読める形式であるにも関わらず「このファイルは読み取れません。このビットマップファイルは無効であるか、または現在サポートされていない形式です。」というメッセージが表示され、

続いて、「メモリまたはリソースが足りないため、作業を完了できません。いくつかのプログラムを終了して、もう一度やり直してください。」と表示されました。

Word ではどうなるでしょうか。
起動します。

起動直後のコミットサイズは約 62MB です。

Word のメモリ使用量を 100MB に制限します。

C:\tmp> MemLimit.exe 11108 100
PID=11108 に 100 MBのメモリ制限を設定。

コピー & ペーストを利用して長い長いテキストを入力していきます。

突然 Word が強制終了し、文書が失われ、まっさらな WORD が別の PID で起動しました。

Word は世界を代表するアプリケーションのひとつだと思うのですが、こんなもんなんでしょうか。

メモリ確保関数の失敗を見る

メモリ不足でメモリ確保関数が失敗する様子を見てみます。

自身の PID を表示した後、malloc 関数で 1MB のメモリを繰り返し確保するプログラム「MemAlloc1」を作ります。

#include <windows.h>
#include <stdio.h>

int main()
{
    printf("PID=%u\n", GetCurrentProcessId());
    printf("Press ENTER key\n");
    while (getchar() != '\n');

    char *p;
    int i = 1;
    do {
        p = malloc(1024 * 1024); // 1MB
        printf("#%d, p=%p\n", i++, p);
    } while (p);

    printf("err=%d\n", errno); // 12=ENOMEM

    return 0;
}

ビルドします。

C:\tmp> cl /Od /Zi MemAlloc1.c

起動します。

C:\tmp> MemAlloc1.exe
PID=19208
Press ENTER key

別のコマンドプロンプトから、メモリ使用量の上限を 10MB に制限します。

C:\tmp> MemLimit.exe 19208 10
PID=19208 に 10 MBのメモリ制限を設定。

実行を継続します。

C:\tmp> MemAlloc1.exe
PID=19208
Press ENTER key

#1, p=000001E6016A8040
#2, p=000001E6017B8040
#3, p=000001E6018CB040
#4, p=000001E6019D7040
#5, p=000001E601AE0040
#6, p=000001E601BF5040
#7, p=000001E601D0F040
#8, p=000001E601E2F040
#9, p=000001E601F49040
#10, p=0000000000000000
err=12

10 回目に malloc 関数が失敗し、NULL が返りました。エラー番号は 12 (ENOMEM) でした。

今度は VirtualAlloc 関数で 1MB のメモリを繰り返し確保するプログラム「MemAlloc2」を作ります。

#include <windows.h>
#include <stdio.h>

int main()
{
    printf("PID=%u\n", GetCurrentProcessId());
    printf("Press ENTER key\n");
    while (getchar() != '\n');

    char *p;
    int i = 1;
    do {
        p = (char *)VirtualAlloc(NULL, 
            1024 * 1024, // 1MB
            MEM_COMMIT, 
            PAGE_READWRITE);
        printf("#%d, p=%p\n", i++, p);
    } while (p);
    
    printf("err=%d\n", GetLastError()); // 1455=ERROR_COMMITMENT_LIMIT

    return 0;
}

ビルドします。

C:\tmp> cl /Od /Zi MemAlloc2.c

起動します。

C:\tmp> MemAlloc2.exe
PID=13536
Press ENTER key

別のコマンドプロンプトから、メモリ使用量の上限を 10MB に制限します。

C:\tmp> MemLimit.exe 13536 10
PID=13536 に 10 MBのメモリ制限を設定。

実行を継続します。

C:\tmp> MemAlloc2.exe
PID=13536
Press ENTER key

#1, p=000001D1149C0000
#2, p=000001D114AC0000
#3, p=000001D114BC0000
#4, p=000001D114CC0000
#5, p=000001D114DC0000
#6, p=000001D114EC0000
#7, p=000001D114FC0000
#8, p=000001D1150C0000
#9, p=000001D1151C0000
#10, p=0000000000000000
err=1455

10 回目に VirtualAlloc 関数が失敗し、NULL が返りました。詳細エラーコードは 1455 (ERROR_COMMITMENT_LIMIT) でした。

このように、メモリ使用量を小さく設定するとすぐにメモリ確保関数がエラーを返すようになります。
開発者はエラーに対して適切なエラー処理を作り込む必要があります。

おわりに

プロセスのメモリ使用量の上限を設定する方法について説明しました。また、上限を設定した場合のプログラムの動作について見てきました。

参考文献

[1] マイクロソフト「ジョブオブジェクト」
https://learn.microsoft.com/ja-jp/windows/win32/procthread/job-objects