Neden bu hafıza yiyen gerçekten hafıza yemiyor?


150

Unix sunucusunda bellek yetersiz (OOM) durumunu simüle edecek bir program oluşturmak istiyorum. Bu süper basit bellek yiyiciyi yarattım:

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

Şu memory_to_eatanda tam olarak 50 GB RAM olan tanımlandığı kadar yer kaplıyor. 1 MB bellek ayırır ve tam olarak daha fazla ayırmak başarısız olduğu noktaya yazdırır, böylece hangi maksimum değeri yemek başardı biliyorum.

Sorun şu ki, işe yarıyor. 1 GB fiziksel belleğe sahip bir sistemde bile.

En üste baktığımda, işlemin 50 GB sanal bellek ve sadece 1 MB'den az yerleşik bellek yediğini görüyorum. Gerçekten tüketen bir bellek yiyici oluşturmanın bir yolu var mı?

Sistem özellikleri: Linux çekirdeği 3.16 ( Debian ), büyük olasılıkla aşırı taahhüt etkinken (nasıl kontrol edileceğinden emin değilim) takas ve sanallaştırılmış.


16
belki de bu hafızayı gerçekten kullanmalısınız (yani ona yazmalısınız)?
ms

4
Derleyicinin onu optimize ettiğini düşünmüyorum, eğer doğruysa, 50GB sanal bellek ayırmayacaktı.
Petr

18
@Magisch Bunun derleyici olduğunu düşünmüyorum ama işletim sistemi üzerine yazma-kopyalama gibi.
cadaniluk

4
Haklısın, yazmaya çalıştım ve sanal kutumu çektim ...
Petr

4
sysctl -w vm.overcommit_memory=2Kök olarak yaparsanız orijinal program beklediğiniz gibi davranacaktır ; bkz. mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Bunun başka sonuçları olabileceğini unutmayın; özellikle çok büyük programlar (örneğin web tarayıcınız) yardımcı programları (örneğin PDF okuyucu) ortaya çıkaramayabilir.
zwol

Yanıtlar:


221

Senin ne zaman malloc()uygulanması (bir yoluyla sistem çekirdeğinden bellek istekleri sbrk()veya mmap()sistem çağrısı), çekirdek yalnızca bellek talep etmiş ve adres alanı içinde yerleştirilmek üzere olduğu bir not yapar. Henüz bu sayfaları eşlemiyor .

İşlem daha sonra yeni bölge içindeki belleğe eriştiğinde, donanım bir segmentasyon hatasını tanır ve çekirdeği duruma uyarır. Çekirdek daha sonra sayfayı kendi veri yapılarında arar ve orada sıfır sayfanız olması gerektiğini bulur, böylece sıfır sayfada eşlenir (muhtemelen ilk olarak sayfa önbelleğinden bir sayfa çıkarır) ve kesmeden geri döner. İşleminiz, bunların hiçbirinin gerçekleşmediğini, çekirdek işleminin mükemmel derecede şeffaf olduğunu fark etmez (çekirdek işini yaparken kısa gecikme hariç).

Bu optimizasyon, sistem çağrısının çok hızlı bir şekilde geri dönmesini sağlar ve en önemlisi, eşleme yapıldığında tüm kaynakların işleminize adanmasını önler. Bu, işlemlerin normal şartlar altında asla ihtiyaç duymadıkları oldukça büyük tamponları ayırmasına izin verir, çok fazla bellek artırma korkusu olmadan.


Yani, bir bellek yiyiciyi programlamak istiyorsanız, kesinlikle tahsis ettiğiniz bellekle bir şeyler yapmanız gerekir. Bunun için kodunuza yalnızca tek bir satır eklemeniz gerekir:

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

Her sayfada tek bir bayta (X86'da 4096 bayt içeren) yazmanın mükemmel olduğunu unutmayın. Bunun nedeni, çekirdekten bir sürece tüm bellek ayırma işlemlerinin bellek sayfası ayrıntı düzeyinde yapılmasıdır; bu da, daha küçük ayrıntı düzeylerinde sayfalamaya izin vermeyen donanım nedeniyle.


6
Ayrıca mmapve ile bellek işlemek de mümkündür MAP_POPULATE(ancak kılavuz sayfasında " MAP_POPULATE özel eşlemeler için yalnızca Linux 2.6.23'ten beri desteklenmektedir " yazıyor ).
Toby Speight

2
Bu temelde doğru, ancak sayfaların tümünün sayfa tablolarında hiç mevcut değil, sıfırlanmış bir sayfaya eşlendiğini düşünüyorum. Bu yüzden her sayfayı sadece okumakla kalmayıp, yazmak zorundasınız. Ayrıca, fiziksel belleği kullanmanın başka bir yolu da sayfaları kilitlemektir. örneğin arayın mlockall(MCL_FUTURE). (Bu, root gerektirir, çünkü ulimit -lDebian / Ubuntu'nun varsayılan yüklemesindeki kullanıcı hesapları için yalnızca 64kiB'dir.) Sadece varsayılan sysctl ile Linux 3.19'da denedim vm/overcommit_memory = 0ve kilitli sayfalar takas / fiziksel RAM kullanıyor.
Peter Cordes

2
@cad X86-64 iki büyük sayfa boyutunu (2 MiB ve 1 GiB) desteklese de, linux çekirdeği tarafından oldukça özel bir şekilde ele alınır. Örneğin, yalnızca açık istek üzerine ve yalnızca sistem onlara izin verecek şekilde yapılandırılmışsa kullanılır. Ayrıca, 4 kiB sayfası hala belleğin haritalanabileceği ayrıntı düzeyini korumaktadır. Bu yüzden büyük sayfalardan bahsetmenin cevaba bir şey kattığını düşünmüyorum.
cmaster - eski haline monica

1
@AlecTeal Evet, var. Bu yüzden, en azından linux'da, çok fazla bellek tüketen bir işlemin, bellek dışı katil tarafından malloc()çağrıldığından daha fazla vurulması daha olasıdır null. Bu, bellek yönetimine bu yaklaşımın dezavantajı. Bununla birlikte, fork()çekirdeğin gerçekten ne kadar belleğe ihtiyaç duyulacağını bilmesini imkansız kılan, üzerine yazma-kopyalama-eşlemelerinin (dinamik kütüphaneleri ve ) düşünmesi zaten var . Bu nedenle, bellek fazla işlemeseydi, tüm fiziksel belleği kullanmadan çok önce eşlenebilir bellek kalmazdı.
cmaster - eski haline monica

2
@BillBarth Donanıma göre, sayfa hatası ile segfault dediğiniz şey arasında bir fark yoktur. Donanım yalnızca sayfa tablolarında belirtilen erişim kısıtlamalarını ihlal eden bir erişim görür ve bu durumu bir segmentasyon hatası yoluyla çekirdeğe koşullandırır. Sadece yazılım tarafı, bölümleme hatasının bir sayfa sağlayarak (sayfa tablolarını güncelleyerek) veya işleme bir SIGSEGVsinyal iletilip iletilmeyeceğine karar verir.
cmaster - eski haline monica

28

Tüm sanal sayfalar, aynı sıfırlanmış fiziksel sayfayla eşlenen yazma üzerine kopyalamaya başlar. Fiziksel sayfaları kullanmak için, her sanal sayfaya bir şeyler yazarak onları kirletebilirsiniz.

Root olarak çalışan varsa, kullanabilir mlock(2)veya mlockall(2)kirli onları gerek kalmadan, onlar ayrılan sayfalarla kadar çekirdek teli olması. (normal root olmayan kullanıcıların ulimit -l64KB'si vardır.)

Diğerlerinin de belirttiği gibi, Linux çekirdeği, siz yazmadığınız sürece belleği gerçekten ayırmıyor gibi görünüyor

OP'nin istediği şeyi yapan kodun geliştirilmiş bir sürümü:

Bu aynı zamanda printf biçimindeki dize uyumsuzluklarını, tamsayıları %ziyazdırmak için memory_to_eat ve eaten_memory türleriyle düzeltir size_t. KiB cinsinden yenilecek bellek boyutu isteğe bağlı olarak bir komut satırı argümanı olarak belirtilebilir.

Global değişkenleri kullanan ve 4k sayfalar yerine 1k büyüyen dağınık tasarım değişmez.

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

Evet haklısın, nedeni, teknik arka plandan emin değilim, ama mantıklı. Yine de tuhaf, aslında kullanabileceğimden daha fazla bellek ayırmama izin veriyor.
Petr

İşletim sistemi düzeyinde sadece belleğe yazdığınızda bellek kullanıldığını düşünüyorum, bu da işletim sisteminin teorik olarak sahip olduğunuz tüm bellek üzerinde sekmeler bulundurmadığını düşünmek mantıklı, ama sadece gerçekten kullandığınız bellekte.
Magisch

@Petr mind Cevabımı topluluk wiki'si olarak işaretlersem ve gelecekte kullanıcı tarafından okunabilirlik için kodunuzda düzenleme yaparsanız?
Magisch

@Petr Hiç garip değil. Günümüz işletim sistemlerinde bellek yönetimi bu şekilde çalışır. Süreçlerin en önemli özelliği, her birine sanal bir adres alanı sağlayarak gerçekleştirilen farklı adres alanlarına sahip olmalarıdır. x86-64, 1 GB'lik sayfalarla bile bir sanal adres için 48 biti destekler, bu nedenle teorik olarak işlem başına bazı Terabayt bellek mümkündür. Andrew Tanenbaum, OS'ler hakkında harika kitaplar yazdı. Eğer ilgileniyorsanız, onları okuyun!
cadaniluk

1
"Açık bellek sızıntısı" ifadesini kullanmazdım Aşırı alışkanlığın veya "yazmada bellek kopyası" teknolojisinin bellek sızıntılarıyla başa çıkmak için icat edildiğine inanmıyorum.
Petr

13

Burada mantıklı bir optimizasyon yapılıyor. Çalışma zamanı aslında değil kazanmak kullanana kadar bellek.

memcpyBu optimizasyonu atlatmak için bir basit yeterli olacaktır. ( callocKullanım noktasına kadar bellek ayırmayı yine de optimize ettiğini görebilirsiniz .)


2
Emin misiniz? Bence onun tahsis miktarı mevcut sanal bellek maksimum ulaşırsa ne olursa olsun, malloc başarısız olur. Malloc () kimsenin hafızayı kullanmayacağını nasıl bilebilir? Olamaz, bu yüzden sbrk () veya işletim sistemindeki eşdeğeri ne olursa olsun çağırmalıdır.
Peter - Monica'yı

1
Ben oldukça emin. (malloc bilmiyor ama çalışma zamanı kesinlikle olurdu). Test etmek önemsiz (şu an benim için kolay olmasa da: Bir trenindeyim).
Bathsheba

@Bathsheba Her sayfaya bir bayt yazmak da yeterli olur mu? mallocBenim için oldukça muhtemel görünen sayfa sınırlarını ayırdığını varsayarsak .
cadaniluk

2
@doron burada yer alan bir derleyici yok. Linux çekirdek davranışı.
el.pescado

1
Ben glibc callocsıfırlanmış sayfalar veren mmap (MAP_ANONYMOUS) yararlanır düşünüyorum , bu yüzden çekirdek sayfa sıfırlama çalışma çoğaltmaz.
Peter Cordes

6

Bu konuda emin değilim ama yapabileceğim tek açıklama linux'un bir kopyala-yaz işletim sistemi olmasıdır. Biri forkher iki işlemi çağırdığında aynı fiziksel belleği gösterir. Bellek yalnızca bir işlem belleğe YAZIYOR yazıldığında kopyalanır.

Bence burada, gerçek fiziksel bellek sadece biri ona bir şeyler yazmaya çalıştığında tahsis edilir. Arayarak sbrkveya mmapyalnızca çekirdeğin bellek defteri tutmasını güncelleyebilir. Gerçek RAM yalnızca belleğe gerçekten erişmeye çalıştığımızda tahsis edilebilir.


forkbununla hiçbir ilgisi yok. Linux'u bu programla önyüklediyseniz aynı davranışı görürsünüz /sbin/init. (yani ilk kullanıcı modu işlemi olan PID 1). Yine de yazma üzerine kopyalama konusunda doğru genel fikre sahipsiniz: Onları kirletene kadar, yeni tahsis edilen sayfaların tümü aynı sıfırlanmış sayfaya eşlenmiş yazma üzerine kopyalanır.
Peter Cordes

çatal hakkında bilmek tahmin yapmama izin verdi.
doron
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.