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


150

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です。メモリを1 MBずつ割り当て、それ以上割り当てられなかったポイントを正確に出力するので、どの最大値を取得できたかがわかります。

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

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

システム仕様:Linuxカーネル3.16(Debian)は、ほとんどの場合、オーバーコミットが有効になっていて(チェックアウトの方法がわからない)、スワップなしで仮想化されています。


16
多分あなたは実際にこのメモリを使用する必要があります(つまり、それに書き込む)?
ms

4
コンパイラがそれを最適化するとは思わない。もしそれが本当なら、50GBの仮想メモリを割り当てられないだろう。
Petr

18
@Magischコンパイラではないと思いますが、コピーオンライトのようなOSです。
cadaniluk、2015年

4
あなたは正しい、私はそれに書き込もうとした、そして私は自分の仮想ボックスを裸にした...
Petr

4
sysctl -w vm.overcommit_memory=2ルートとして実行すると、元のプログラムは期待どおりに動作します。mjmwired.net/kernel/Documentation/vm/overcommit-accountingを参照してください。これが他の結果をもたらすかもしれないことに注意してください。特に、非常に大きなプログラム(Webブラウザなど)は、ヘルパープログラム(PDFリーダーなど)の起動に失敗する可能性があります。
zwol 2015年

回答:


221

あなたの場合は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バイトを含む)に書き込むだけで十分です。これは、カーネルからプロセスへのすべてのメモリ割り当てがメモリページの粒度で行われるためです。これは、ハードウェアが小さい粒度でのページングを許可しないためです。


6
mmapand を使用してメモリをコミットすることもできますMAP_POPULATE(ただし、manページには「MAP_POPULATEはLinux 2.6.23以降のプライベートマッピングでのみサポートされています」と記載されています)。
Toby Speight、

2
それは基本的には正しいですが、ページはすべて、ページテーブルにまったく存在しないのではなく、ゼロに設定されたページにコピーオンライトでマップされていると思います。これが、すべてのページを読み取るだけでなく、書き込む必要がある理由です。また、物理メモリを使い果たす別の方法は、ページをロックすることです。たとえばを呼び出しますmlockall(MCL_FUTURE)。(ulimit -lDebian / Ubuntuのデフォルトインストールのユーザーアカウントでは64kiBなので、これにはrootが必要です。)Linux 3.19でデフォルトのsysctlをvm/overcommit_memory = 0使用して試したところ、ロックされたページがスワップ/物理RAMを使い果たしました。
Peter Cordes、2015年

2
@cad X86-64は2つの大きなページサイズ(2 MiBと1 GiB)をサポートしますが、それらはLinuxカーネルによって特別に扱われます。たとえば、これらは明示的な要求でのみ、システムがそれらを許可するように構成されている場合にのみ使用されます。また、4 kiBページは、メモリがマップされる細かさのままです。だから、巨大なページに言及しても答えに何も追加されないと私は思いません。
cmaster-2015年

1
@AlecTealはい、そうです。そのため、少なくともLinuxでは、メモリを大量に消費しているプロセスが、out-of-memory-killerによって、そのmalloc()コールが返すプロセスよりも撃たれる可能性が高くなりますnull。これは明らかに、メモリ管理へのこのアプローチの欠点です。ただし、fork()カーネルが実際にどれだけのメモリが必要になるかを知ることを不可能にするのは、コピーオンライトマッピング(動的ライブラリーやなど)の存在です。したがって、メモリをオーバーコミットしないと、実際にすべての物理メモリを使用するよりもずっと前に、マップ可能なメモリが不足します。
cmaster-2015年

2
@BillBarthハードウェアにとって、ページフォールトと呼ぶものとsegfaultとの間に違いはありません。ハードウェアは、ページテーブルに規定されているアクセス制限に違反するアクセスのみを認識し、セグメンテーションフォールトによってその状態をカーネルに通知します。ページを提供する(ページテーブルを更新する)ことでセグメンテーションフォールトを処理するかどうか、またはSIGSEGVシグナルをプロセスに配信するかどうかを決定するのはソフトウェア側だけです。
cmaster-2015年

28

すべての仮想ページは、同じゼロ化された物理ページにマップされたコピーオンライトから始まります。物理ページを使い果たすには、各仮想ページに何かを書き込むことでダーティにすることができます。

rootとして実行している場合は、使用することができますmlock(2)またはmlockall(2)それらが割り当てられているとき汚れにそれらをせずに、ページアップカーネルワイヤーを持っています。(通常の非rootユーザーの場合、ulimit -l64kiBしかありません。)

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

OPが望んでいたことを実行するコードの改良版:

これは、整数を出力%ziするために使用する、memory_to_eatおよびeaten_memoryのタイプとのprintfフォーマット文字列の不一致も修正しsize_tます。食べるメモリサイズ(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;
}

はい、そうです。それは理由でしたが、技術的な背景はわかりませんが、理にかなっています。しかし、奇妙なことに、実際に使用できるよりも多くのメモリを割り当てることができます。
Petr

OSレベルでは、メモリは実際にメモリに書き込まれるときにのみ使用されると思います。これは、OSが理論的に持っているすべてのメモリではなく、実際に使用しているメモリにのみタブを保持することを考えると理にかなっています。
Magisch 2015年

@ピーター・マインド私の答えをコミュニティーWikiとしてマークし、ユーザーが読みやすいようにコードを編集した場合、
Magisch

@Petr変ではありません。これが、今日のOSでのメモリ管理の仕組みです。プロセスの主な特徴は、プロセスに個別のアドレススペースがあることです。これは、各プロセスに仮想アドレススペースを提供することによって実現されます。x86-64は、1つの仮想アドレスに対して48ビットをサポートし、1GBのページでもサポートするため、理論的には、プロセスあたり数テラバイトのメモリが可能です。Andrew Tanenbaumは、OSに関する素晴らしい本を何冊か書いています。興味があれば読んでください!
cadaniluk、2015年

1
「明らかなメモリリーク」という言葉は使用しません。メモリリークに対処するために、オーバーコミットまたはこの「メモリコピーオンライト」という技術が発明されたとは思いません。
Petr

13

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

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


2
本気ですか?彼の割り当て量が使用可能な仮想メモリの最大値に達すると、mallocは何があっても失敗するでしょう。malloc()は、誰もメモリを使用しないことをどのようにして知るのでしょうか?それはできないので、sbrk()または彼のOSの同等のものを呼び出す必要があります。
ピーター-モニカを

1
私はかなり確信しています。(mallocは知りませんが、ランタイムは確かにそうします)。テストするのは簡単です(現在のところ、簡単ではありませんが、電車に乗っています)。
バトシェバ

@Bathsheba各ページに1バイトを書き込むだけでも十分でしょうか?mallocページ境界に割り当てると仮定すると、私にはかなり可能性が高いと思われます。
cadaniluk 2015年

2
@doronここにはコンパイラは含まれていません。これはLinuxカーネルの動作です。
el.pescado 2015年

1
glibc callocはmmap(MAP_ANONYMOUS)を利用してページをゼロにすることで、カーネルのページのゼロ化作業を複製しないと思います。
Peter Cordes、2015年

6

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

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


forkこれとは何の関係もありません。このプログラムを使用してLinuxを起動した場合と同じ動作が見られ/sbin/initます。(つまり、PID 1、最初のユーザーモードプロセス)。コピーオンライトに関しては、正しい一般的なアイデアがありました。ダーティになるまで、新しく割り当てられたページはすべて、コピーオンライトが同じゼロ化されたページにマップされます。
Peter Cordes、2015年

フォークについて知っていたので、推測することができました。
doron、2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.