web-dev-qa-db-ja.com

なぜこの記憶を食べる人は本当に記憶を食べないのですか?

Unixサーバー上のメモリ不足(OOM)状況をシミュレートするプログラムを作成したい。この超シンプルなメモリイーターを作成しました。

#include <stdio.h>
#include <stdlib.h>

unsigned long long memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
void *memory = NULL;

int eat_kilobyte()
{
    memory = realloc(memory, (eaten_memory * 1024) + 1024);
    if (memory == NULL)
    {
        // realloc failed here - we probably can't allocate more memory for whatever reason
        return 1;
    }
    else
    {
        eaten_memory++;
        return 0;
    }
}

int main(int argc, char **argv)
{
    printf("I will try to eat %i kb of ram\n", memory_to_eat);
    int megabyte = 0;
    while (memory_to_eat > 0)
    {
        memory_to_eat--;
        if (eat_kilobyte())
        {
            printf("Failed to allocate more memory! Stucked at %i kb :(\n", eaten_memory);
            return 200;
        }
        if (megabyte++ >= 1024)
        {
            printf("Eaten 1 MB of ram\n");
            megabyte = 0;
        }
    }
    printf("Successfully eaten requested memory!\n");
    free(memory);
    return 0;
}

memory_to_eatで定義されているのと同じ量のメモリを消費しますが、現在はちょうど50 GBのRAMです。メモリを1MBずつ割り当て、それ以上割り当てられないポイントを正確に出力するため、どの最大値を消費したかがわかります。

問題は、それが機能することです。物理メモリが1GBのシステムでも。

Topを確認すると、プロセスが50GBの仮想メモリを消費し、1MB未満の常駐メモリしか消費していないことがわかります。本当にそれを消費するメモリイーターを作成する方法はありますか?

システム仕様:Linuxカーネル3.16( Debian )スワップなしで仮想化されたオーバーコミットが有効(チェックアウト方法がわからない)である可能性が高い。

148
Petr

malloc()実装が(sbrk()またはmmap()システムコールを介して)システムカーネルにメモリを要求すると、カーネルはメモリを要求したことのみを記録します。アドレス空間内のどこに配置するか。 これらのページはまだ実際にはマップされていません

その後、プロセスが新しい領域内のメモリにアクセスすると、ハードウェアはセグメンテーション障害を認識し、その状態をカーネルに警告します。その後、カーネルは独自のデータ構造でページを検索し、そこにゼロページがあるはずだと判断するため、ゼロページにマップし(最初にページキャッシュからページを削除する可能性があります)、割り込みから戻ります。あなたのプロセスはこれが起こったことを認識せず、カーネル操作は完全に透過的です(カーネルが作業を行う間の短い遅延を除く)。

この最適化により、システムコールは非常に迅速に戻ることができ、最も重要なことは、マッピングの作成時にリソースがプロセスにコミットされることを回避することです。これにより、プロセスは、通常の状況では必要のないかなり大きなバッファを予約できます。メモリを大量に消費する心配はありません。


したがって、メモリイーターをプログラムする場合は、割り当てたメモリを実際に使用する必要があります。このためには、コードに1行追加するだけです。

int eat_kilobyte()
{
    if (memory == NULL)
        memory = malloc(1024);
    else
        memory = realloc(memory, (eaten_memory * 1024) + 1024);
    if (memory == NULL)
    {
        return 1;
    }
    else
    {
        //Force the kernel to map the containing memory page.
        ((char*)memory)[1024*eaten_memory] = 42;

        eaten_memory++;
        return 0;
    }
}

各ページ内の1バイト(X86では4096バイトを含む)に書き込むだけで十分であることに注意してください。これは、カーネルからプロセスへのすべてのメモリ割り当てがメモリページの粒度で行われるためです。これは、ハードウェアがより小さい粒度でのページングを許可しないためです。

219
cmaster

すべての仮想ページは、同じゼロ化された物理ページにマップされたコピーオンライトを開始します。物理ページを使い果たすために、各仮想ページに何かを書くことでそれらを汚すことができます。

ルートとして実行している場合は、mlock(2)またはmlockall(2)を使用して、ページを割り当てたときに、それらをダーティにせずにカーネルに接続させることができます。 (通常の非rootユーザーのulimit -lは64kiBのみです。)

他の多くの人が示唆したように、Linuxカーネルは、書き込みをしない限り実際にメモリを割り当てないようです

OPが望んでいたことを行うコードの改良バージョン:

これにより、%ziを使用してsize_t整数を出力し、memory_to_eatおよびeaten_memoryのタイプとのprintf形式文字列の不一致も修正されます。オプションで、食べるメモリサイズをkiBでコマンドライン引数として指定できます。

グローバル変数を使用し、4kページではなく1kページずつ増加する乱雑なデザインは変更されていません。

#include <stdio.h>
#include <stdlib.h>

size_t memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
char *memory = NULL;

void write_kilobyte(char *pointer, size_t offset)
{
    int size = 0;
    while (size < 1024)
    {   // writing one byte per page is enough, this is overkill
        pointer[offset + (size_t) size++] = 1;
    }
}

int eat_kilobyte()
{
    if (memory == NULL)
    {
        memory = malloc(1024);
    } else
    {
        memory = realloc(memory, (eaten_memory * 1024) + 1024);
    }
    if (memory == NULL)
    {
        return 1;
    }
    else
    {
        write_kilobyte(memory, eaten_memory * 1024);
        eaten_memory++;
        return 0;
    }
}

int main(int argc, char **argv)
{
    if (argc >= 2)
        memory_to_eat = atoll(argv[1]);

    printf("I will try to eat %zi kb of ram\n", memory_to_eat);
    int megabyte = 0;
    int megabytes = 0;
    while (memory_to_eat-- > 0)
    {
        if (eat_kilobyte())
        {
            printf("Failed to allocate more memory at %zi kb :(\n", eaten_memory);
            return 200;
        }
        if (megabyte++ >= 1024)
        {
            megabytes++;
            printf("Eaten %i  MB of ram\n", megabytes);
            megabyte = 0;
        }
    }
    printf("Successfully eaten requested memory!\n");
    free(memory);
    return 0;
}
26
Magisch

賢明な最適化がここで行われています。ランタイムは、実際に使用するまでメモリを取得しません。

この最適化を回避するには、単純なmemcpyで十分です。 (callocは、使用時点までメモリ割り当てを最適化することに気付くかもしれません。)

13
Bathsheba

これについては定かではありませんが、私ができることの唯一の説明は、Linuxはコピーオンライトオペレーティングシステムであるということです。 forkを呼び出すと、両方のプロセスが同じ物理メモリを指します。メモリは、1つのプロセスが実際にメモリに書き込みを行った後にのみコピーされます。

ここで、実際の物理メモリは、何かを書き込もうとしたときにのみ割り当てられると思います。 sbrkまたはmmapを呼び出すと、カーネルのメモリブックキープのみが更新されます。実際のRAMは、実際にメモリにアクセスしようとしたときにのみ割り当てられます。

6
doron