Mengapa pemakan memori ini tidak benar-benar memakan memori?

150

Saya ingin membuat program yang akan mensimulasikan situasi out-of-memory (OOM) pada server Unix. Saya membuat pemakan memori yang sangat sederhana ini:

#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;
}

Ini memakan banyak memori seperti yang didefinisikan di memory_to_eatmana sekarang adalah tepat 50 GB RAM. Ini mengalokasikan memori sebesar 1 MB dan mencetak titik di mana ia gagal mengalokasikan lebih banyak, sehingga saya tahu nilai maksimum mana yang berhasil dimakan.

Masalahnya adalah itu berfungsi. Bahkan pada sistem dengan memori fisik 1 GB.

Ketika saya memeriksa bagian atas saya melihat bahwa proses memakan 50 GB memori virtual dan hanya kurang dari 1 MB memori penduduk. Apakah ada cara untuk membuat pemakan memori yang benar-benar mengkonsumsinya?

Spesifikasi sistem: Linux kernel 3.16 ( Debian ) kemungkinan besar dengan overcommit diaktifkan (tidak yakin bagaimana cara memeriksanya) tanpa swap dan tervirtualisasi.

Petr
sumber
16
mungkin Anda harus benar-benar menggunakan memori ini (yaitu menulis padanya)?
ms
4
Saya tidak berpikir kompiler mengoptimalkannya, jika itu benar, ia tidak akan mengalokasikan 50GB memori virtual.
Petr
18
@ Magag Saya tidak berpikir itu adalah kompiler tetapi OS suka copy-on-write.
cadaniluk
4
Anda benar, saya mencoba menulis ke sana dan saya hanya nuked kotak virtual saya ...
Petr
4
Program asli akan berperilaku seperti yang Anda harapkan jika Anda melakukan sysctl -w vm.overcommit_memory=2sebagai root; lihat mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Perhatikan bahwa ini mungkin memiliki konsekuensi lain; khususnya, program yang sangat besar (mis. browser web Anda) mungkin gagal menelurkan program pembantu (mis. pembaca PDF).
zwol

Jawaban:

221

Ketika malloc()implementasi Anda meminta memori dari kernel sistem (melalui panggilan sistem sbrk()atau mmap()), kernel hanya membuat catatan bahwa Anda telah meminta memori dan di mana memori akan ditempatkan di dalam ruang alamat Anda. Itu sebenarnya belum memetakan halaman-halaman itu .

Ketika proses selanjutnya mengakses memori dalam wilayah baru, perangkat keras mengenali kesalahan segmentasi dan memperingatkan kernel untuk kondisi tersebut. Kernel kemudian mencari halaman di struktur datanya sendiri, dan menemukan bahwa Anda harus memiliki halaman nol di sana, sehingga peta di halaman nol (mungkin pertama kali mengusir halaman dari cache halaman) dan kembali dari interupsi. Proses Anda tidak menyadari bahwa semua ini terjadi, operasi kernel sangat transparan (kecuali untuk penundaan singkat saat kernel melakukan tugasnya).

Pengoptimalan ini memungkinkan panggilan sistem untuk kembali dengan sangat cepat, dan, yang paling penting, itu menghindari sumber daya apa pun untuk berkomitmen pada proses Anda ketika pemetaan dilakukan. Ini memungkinkan proses untuk menyimpan buffer yang agak besar yang tidak pernah mereka butuhkan dalam keadaan normal, tanpa takut menelan terlalu banyak memori.


Jadi, jika Anda ingin memprogram pemakan memori, Anda benar-benar harus melakukan sesuatu dengan memori yang Anda alokasikan. Untuk ini, Anda hanya perlu menambahkan satu baris ke kode Anda:

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;
    }
}

Perhatikan bahwa cukup memadai untuk menulis satu byte dalam setiap halaman (yang berisi 4096 byte pada X86). Itu karena semua alokasi memori dari kernel untuk suatu proses dilakukan pada granularity halaman memori, yang pada gilirannya, karena perangkat keras yang tidak memungkinkan paging pada granularities yang lebih kecil.

cmaster - mengembalikan monica
sumber
6
Dimungkinkan juga untuk melakukan memori dengan mmapdan MAP_POPULATE(meskipun perhatikan bahwa halaman manual mengatakan " MAP_POPULATE didukung untuk pemetaan pribadi hanya sejak Linux 2.6.23 ").
Toby Speight
2
Itu pada dasarnya benar, tapi saya pikir halaman-halaman itu semuanya copy-on-write dipetakan ke halaman zeroed, daripada tidak hadir sama sekali di halaman-tabel. Inilah sebabnya mengapa Anda harus menulis, tidak hanya membaca, setiap halaman. Juga, cara lain untuk menggunakan memori fisik adalah dengan mengunci halaman. mis mlockall(MCL_FUTURE). menelepon . (Ini membutuhkan root, karena ulimit -lhanya 64kiB untuk akun pengguna pada instalasi default Debian / Ubuntu.) Saya baru mencobanya di Linux 3.19 dengan sysctl default vm/overcommit_memory = 0, dan halaman yang dikunci menggunakan swap / fisik RAM.
Peter Cordes
2
@cad Sementara X86-64 mendukung dua ukuran halaman yang lebih besar (2 MiB dan 1 GiB), mereka masih diperlakukan cukup istimewa oleh kernel linux. Misalnya, mereka hanya digunakan atas permintaan eksplisit, dan hanya jika sistem telah dikonfigurasi untuk memperbolehkannya. Juga, halaman 4 kiB masih tetap granularity di mana memori dapat dipetakan. Itu sebabnya saya tidak berpikir bahwa menyebutkan halaman besar akan menambah apa pun pada jawabannya.
cmaster - mengembalikan monica
1
@AlecTeal Ya, benar. Itu sebabnya, setidaknya di linux, itu lebih mungkin bahwa proses yang mengkonsumsi terlalu banyak memori ditembak oleh-out-of-memory-killer daripada salah satu malloc()panggilan itu kembali null. Itu jelas kelemahan dari pendekatan manajemen memori ini. Namun, sudah ada salinan-on-tulis-pemetaan (pikirkan perpustakaan dinamis dan fork()) yang membuatnya tidak mungkin bagi kernel untuk mengetahui berapa banyak memori yang sebenarnya akan dibutuhkan. Jadi, jika itu tidak terlalu banyak memori, Anda akan kehabisan memori peta jauh sebelum Anda benar-benar menggunakan semua memori fisik.
cmaster - mengembalikan monica
2
@ BillBarth Untuk perangkat keras tidak ada perbedaan antara apa yang Anda sebut kesalahan halaman dan segfault. Perangkat keras hanya melihat akses yang melanggar pembatasan akses yang ditetapkan dalam tabel halaman, dan menandakan kondisi itu ke kernel melalui kesalahan segmentasi. Hanya sisi perangkat lunak yang kemudian memutuskan apakah kesalahan segmentasi harus ditangani dengan memasok halaman (memperbarui tabel halaman), atau apakah SIGSEGVsinyal harus dikirim ke proses.
cmaster - mengembalikan monica
28

Semua halaman virtual memulai copy-on-write dipetakan ke halaman fisik zeroed yang sama. Untuk menggunakan halaman fisik, Anda dapat mengotori dengan menulis sesuatu untuk setiap halaman virtual.

Jika berjalan sebagai root, Anda dapat menggunakan mlock(2)atau mlockall(2)meminta kernel memasang halaman ketika dialokasikan, tanpa harus mengotori mereka. (pengguna non-root normal ulimit -lhanya memiliki 64kiB.)

Seperti yang banyak orang lain sarankan, tampaknya kernel Linux tidak benar-benar mengalokasikan memori kecuali Anda menulisnya

Versi kode yang ditingkatkan, yang melakukan apa yang diinginkan OP:

Ini juga memperbaiki ketidakcocokan string format printf dengan jenis memory_to_eat dan dimakan_mory, gunakan %ziuntuk mencetak size_tbilangan bulat. Ukuran memori untuk dimakan, dalam kiB, secara opsional dapat ditentukan sebagai baris perintah arg.

Desain berantakan menggunakan variabel global, dan tumbuh dengan 1k bukannya halaman 4k, tidak berubah.

#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;
}
Magisch
sumber
Ya Anda benar, itu alasannya, tidak yakin tentang latar belakang teknis, tetapi masuk akal. Anehnya, ini memungkinkan saya mengalokasikan lebih banyak memori daripada yang sebenarnya bisa saya gunakan.
Petr
Saya pikir pada level OS memori hanya benar-benar digunakan ketika Anda menulis ke dalamnya, yang masuk akal mengingat OS tidak mengawasi semua memori yang secara teoritis Anda miliki, tetapi hanya pada apa yang sebenarnya Anda gunakan.
Magisch
@Petr mind Jika saya menandai jawaban saya sebagai komunitas wiki dan Anda mengedit kode Anda untuk keterbacaan pengguna di masa depan?
Magisch
@Petr Ini tidak aneh sama sekali. Begitulah cara manajemen memori pada OS saat ini bekerja. Ciri utama dari proses adalah bahwa mereka memiliki ruang alamat yang berbeda, yang dicapai dengan menyediakan masing-masing ruang alamat virtual. x86-64 mendukung 48-bit untuk satu alamat virtual, bahkan dengan halaman 1GB, jadi, secara teori, beberapa Terabyte memori per proses dimungkinkan. Andrew Tanenbaum telah menulis beberapa buku hebat tentang OS. Jika Anda tertarik, bacalah!
cadaniluk
1
Saya tidak akan menggunakan kata-kata "kebocoran memori yang jelas" Saya tidak percaya bahwa overcommit atau teknologi "memory copy on write" ini diciptakan untuk menangani kebocoran memori sama sekali.
Petr
13

Optimalisasi yang masuk akal sedang dilakukan di sini. Runtime tidak benar-benar mendapatkan memori sampai Anda menggunakannya.

Sederhana memcpyakan cukup untuk menghindari optimasi ini. (Anda mungkin menemukan bahwa callocmasih mengoptimalkan alokasi memori hingga titik penggunaan.)

Batsyeba
sumber
2
Apakah kamu yakin Saya pikir jika jumlah alokasinya mencapai maksimum memori virtual yang tersedia, malloc akan gagal, tidak peduli apa. Bagaimana malloc () tahu bahwa tidak ada yang akan menggunakan memori ?? Tidak bisa, jadi harus memanggil sbrk () atau apa pun yang setara dalam OS-nya.
Peter - Reinstate Monica
1
Saya cukup yakin. (malloc tidak tahu tetapi runtime pasti akan). Mudah untuk menguji (walaupun tidak mudah bagi saya sekarang: saya naik kereta).
Batsyeba
@ Bathsheba Apakah menulis satu byte untuk setiap halaman juga sudah cukup? Dengan asumsi mallocmengalokasikan pada batas halaman apa yang tampaknya cukup mungkin bagi saya.
cadaniluk
2
@doron tidak ada kompiler yang terlibat di sini. Ini perilaku kernel Linux.
el.pescado
1
Saya pikir glibc callocmengambil keuntungan dari mmap (MAP_ANONYMOUS) memberikan halaman-halaman yang di-zeroed, jadi itu tidak menduplikasi pekerjaan kernel-page-zeroing.
Peter Cordes
6

Tidak yakin tentang yang satu ini tetapi satu-satunya penjelasan yang dapat saya lakukan adalah bahwa linux adalah sistem operasi copy-on-write. Ketika seseorang memanggil forkkedua proses menunjuk ke memori fisik yang sama. Memori hanya disalin setelah satu proses benar-benar MENULIS ke memori.

Saya pikir di sini, memori fisik yang sebenarnya hanya dialokasikan ketika seseorang mencoba menulis sesuatu untuk itu. Memanggil sbrkatau mmapmungkin hanya memperbarui pembukuan memori kernel. RAM yang sebenarnya hanya dapat dialokasikan ketika kita benar-benar mencoba mengakses memori.

doron
sumber
forktidak ada hubungannya dengan ini. Anda akan melihat perilaku yang sama jika Anda mem-boot Linux dengan program ini sebagai /sbin/init. (yaitu PID 1, proses mode pengguna pertama). Anda memiliki ide umum yang tepat dengan copy-on-write, meskipun: Sampai Anda mengotori mereka, halaman yang baru dialokasikan semuanya copy-on-write dipetakan ke halaman zeroed yang sama.
Peter Cordes
mengetahui tentang garpu memungkinkan saya untuk menebak.
doron