यह मेमोरी ईटर वास्तव में मेमोरी क्यों नहीं खाती है?


150

मैं एक प्रोग्राम बनाना चाहता हूं जो एक यूनिक्स सर्वर पर आउट-ऑफ-मेमोरी (ओओएम) स्थिति का अनुकरण करेगा। मैंने यह सुपर-सिंपल मेमोरी ईटर बनाया:

#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 जीबी रैम है। यह 1 एमबी द्वारा मेमोरी आवंटित करता है और ठीक उसी बिंदु को प्रिंट करता है जहां यह अधिक आवंटित करने में विफल रहता है, ताकि मुझे पता चले कि यह अधिकतम मूल्य खाने में कामयाब रहा।

समस्या यह है कि यह काम करता है। यहां तक ​​कि 1 जीबी भौतिक मेमोरी वाले सिस्टम पर भी।

जब मैं टॉप चेक करता हूं तो देखता हूं कि यह प्रक्रिया 50 जीबी की वर्चुअल मेमोरी खाती है और केवल 1 एमबी से कम निवासी मेमोरी है। क्या मेमोरी भक्षक बनाने का एक तरीका है जो वास्तव में इसका उपभोग करता है?

सिस्टम स्पेसिफिकेशन्स: लिनक्स कर्नेल 3.16 ( डेबियन ) सबसे अधिक संभावना है जो बिना किसी स्वैप और वर्चुअलाइज्ड के साथ सक्षम होना चाहिए।


16
हो सकता है कि आपको वास्तव में इस मेमोरी (यानी इसे लिखना) का उपयोग करना पड़े?
एमएस

4
मुझे नहीं लगता कि कंपाइलर इसका अनुकूलन करता है, अगर यह सच था, तो यह 50GB वर्चुअल मेमोरी आवंटित नहीं करेगा।
पेट्र

18
@Magisch मुझे नहीं लगता कि यह संकलक है, लेकिन OS जैसा कॉपी-ऑन-राइट है।
कैडनीलुक

4
आप सही कह रहे हैं, मैंने इसे लिखने की कोशिश की और मैंने अपने वर्चुअल बॉक्स को ठीक किया ...
पेट्र

4
मूल कार्यक्रम व्यवहार करेगा जैसा कि आप अपेक्षा करते हैं यदि आप sysctl -w vm.overcommit_memory=2रूट के रूप में करते हैं ; mjmwired.net/kernel/Documentation/vm/overcommit-accounting देखें । ध्यान दें कि इसके अन्य परिणाम हो सकते हैं; विशेष रूप से, बहुत बड़े कार्यक्रम (जैसे आपका वेब ब्राउज़र) सहायक कार्यक्रमों (जैसे पीडीएफ रीडर) को स्पॉन करने में विफल हो सकते हैं।
१२:४५ बजे zwol

जवाबों:


221

जब आपका malloc()कार्यान्वयन सिस्टम कर्नेल ( sbrk()या किसी mmap()सिस्टम कॉल के माध्यम से ) से मेमोरी का अनुरोध करता है , तो कर्नेल केवल एक नोट करता है कि आपने मेमोरी का अनुरोध किया है और इसे आपके एड्रेस स्पेस में रखा जाना है। यह वास्तव में उन पृष्ठों को अभी तक मैप नहीं करता है

जब प्रक्रिया बाद में नए क्षेत्र में मेमोरी एक्सेस करती है, तो हार्डवेयर एक विभाजन दोष को पहचानता है और कर्नेल को स्थिति से सचेत करता है। कर्नेल तब अपने स्वयं के डेटा संरचनाओं में पृष्ठ को देखता है, और पाता है कि आपके पास वहां एक शून्य पृष्ठ होना चाहिए, इसलिए यह एक शून्य पृष्ठ में मैप करता है (संभवत: पहले पृष्ठ-कैश से पृष्ठ निकाल रहा है) और बीच में से लौटता है। आपकी प्रक्रिया को यह महसूस नहीं होता है कि इनमें से कोई भी हुआ, गुठली का संचालन पूरी तरह से पारदर्शी है (केवल थोड़ी देर के लिए छोड़कर जबकि कर्नेल अपना काम करता है)।

यह अनुकूलन सिस्टम कॉल को बहुत जल्दी वापस करने की अनुमति देता है, और, सबसे महत्वपूर्ण बात यह है कि मैपिंग किए जाने पर यह आपकी प्रक्रिया के लिए प्रतिबद्ध किसी भी संसाधन से बचता है। यह प्रक्रियाओं को बड़े बफ़र्स को आरक्षित करने की अनुमति देता है, जिन्हें सामान्य परिस्थितियों में कभी भी ज़रूरत नहीं होती है, बहुत अधिक स्मृति के डर के बिना।


इसलिए, यदि आप एक मेमोरी खाने वाले को प्रोग्राम करना चाहते हैं, तो आपको वास्तव में आपके द्वारा आवंटित मेमोरी के साथ कुछ करना होगा। इसके लिए, आपको केवल अपने कोड में एक पंक्ति जोड़ने की आवश्यकता है:

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

ध्यान दें कि यह प्रत्येक पृष्ठ के भीतर एक एकल बाइट को लिखने के लिए पूरी तरह से पर्याप्त है (जिसमें X86 पर 4096 बाइट्स हैं)। ऐसा इसलिए है क्योंकि कर्नेल से एक प्रक्रिया में सभी मेमोरी आवंटन मेमोरी पेज ग्रैन्युलैरिटी पर किया जाता है, जो कि बदले में, हार्डवेयर के कारण होता है जो छोटे ग्रैन्युलैरिटी में पेजिंग की अनुमति नहीं देता है।


6
इसके साथ मेमोरी को कम करना भी संभव है mmapऔर MAP_POPULATE(हालांकि ध्यान दें कि मैन पेज " MAP_POPULATE केवल लिनक्स 2.6.23 के बाद से मैपिंग के लिए समर्थित है ")।
टोबी स्पाइट

2
यह मूल रूप से सही है, लेकिन मुझे लगता है कि पृष्ठ-तालिकाओं में सभी मौजूद नहीं होने के बजाय, पृष्ठ सभी कॉपी-ऑन-राइट मैप किए गए हैं। यही कारण है कि आपको लिखना है, न कि केवल पढ़ना, हर पृष्ठ। इसके अलावा, भौतिक मेमोरी का उपयोग करने का एक और तरीका पृष्ठों को लॉक करना है। जैसे कॉल करना mlockall(MCL_FUTURE)। (इसके लिए रूट की आवश्यकता होती है, क्योंकि ulimit -lडेबियन / उबंटू के डिफॉल्ट इंस्टॉलेशन पर उपयोगकर्ता खातों के लिए केवल 64kiB है।) मैंने अभी इसे डिफ़ॉल्ट 3. ssctl के साथ लिनक्स 3.19 पर आज़माया था vm/overcommit_memory = 0, और लॉक किए गए पृष्ठ स्वैप / फिजिकल रैम का उपयोग करते हैं।
पीटर कॉर्ड्स

2
@cad जबकि X86-64 दो बड़े पृष्ठ आकारों (2 MiB और 1 GiB) का समर्थन करता है, फिर भी उन्हें लिनक्स कर्नेल द्वारा काफी विशेष माना जाता है। उदाहरण के लिए, उनका उपयोग केवल स्पष्ट अनुरोध पर किया जाता है, और केवल अगर सिस्टम उन्हें अनुमति देने के लिए कॉन्फ़िगर किया गया है। इसके अलावा, 4 kiB पेज अभी भी ग्रैन्युलैरिटी बना हुआ है जिस पर मेमोरी मैप की जा सकती है। इसलिए मुझे नहीं लगता कि विशाल पृष्ठों का उल्लेख करना उत्तर में कुछ भी जोड़ता है।
सेंटास्टर -

1
@AlecTeal हाँ, यह करता है। इसीलिए, कम से कम लिनेक्स पर, यह अधिक संभावना है कि एक ऐसी प्रक्रिया जो बहुत अधिक मेमोरी का उपभोग करती है, उसे आउट-ऑफ-मेमोरी-किलर द्वारा गोली मार दी जाती है, क्योंकि यह malloc()रिटर्न कहलाता है null। यह स्पष्ट रूप से स्मृति प्रबंधन के लिए इस दृष्टिकोण के नकारात्मक पक्ष है। हालांकि, यह पहले से ही कॉपी-ऑन-राइट-मैपिंग (डायनेमिक लाइब्रेरीज़ लगता है fork()) का अस्तित्व है जो कर्नेल के लिए यह जानना असंभव बनाता है कि वास्तव में मेमोरी की कितनी आवश्यकता होगी। इसलिए, यदि यह मेमोरी को ओवरकम नहीं करता है, तो आप वास्तव में सभी भौतिक मेमोरी का उपयोग करने से बहुत पहले मैप करने योग्य मेमोरी से बाहर निकल जाएंगे।
सेंटास्टर -

2
@BillBarth हार्डवेयर के लिए आप पेज फॉल्ट और सेगफॉल्ट को क्या कहते हैं, इसके बीच कोई अंतर नहीं है। हार्डवेयर केवल एक एक्सेस देखता है जो पेज टेबल में नीचे दिए गए एक्सेस प्रतिबंधों का उल्लंघन करता है, और उस स्थिति को कर्नेल में एक विभाजन दोष के माध्यम से संकेत देता है। यह केवल सॉफ्टवेयर पक्ष है जो तब तय करता है कि पेज को सप्लाई करके (पेज टेबल को अपडेट करते हुए) सेगमेंटेशन फॉल्ट को हैंडल किया जाना चाहिए या SIGSEGVसिग्नल को प्रोसेस में डिलीवर किया जाना चाहिए।
सेंटास्टर - मोनिका

28

सभी वर्चुअल पेज एक ही शून्य भौतिक पृष्ठ पर कॉपी-ऑन-राइट मैप किए गए हैं। भौतिक पृष्ठों का उपयोग करने के लिए, आप प्रत्येक आभासी पृष्ठ पर कुछ लिखकर उन्हें गंदा कर सकते हैं।

यदि रूट के रूप में चल रहा है, तो आप उन्हें गंदे होने के बिना पेज को कर्नेल वायर का उपयोग mlock(2)या mlockall(2)आबंटित कर सकते हैं। (सामान्य गैर-रूट उपयोगकर्ताओं ulimit -lमें केवल 64kiB है।)

जैसा कि कई अन्य ने सुझाव दिया है, ऐसा लगता है कि लिनक्स कर्नेल वास्तव में मेमोरी को आवंटित नहीं करता है जब तक कि आप इसे नहीं लिखते

कोड का एक बेहतर संस्करण, जो वही करता है जो ओपी चाहता था:

यह भी पूर्णांक %ziप्रिंट करने के लिए का उपयोग करते हुए memory_to_eat और eaten_memory के प्रकारों के साथ प्रिंटफ़ प्रारूप स्ट्रिंग बेमेल को ठीक करता है 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 मन अगर मैं अपने जवाब को सामुदायिक विकि के रूप में चिह्नित करता हूं और आप भावी उपयोगकर्ता की पठनीयता के लिए अपने कोड में संपादित करते हैं?
मैगिस

@Petr यह बिल्कुल भी अजीब नहीं है। आज के ओएस पर स्मृति प्रबंधन कैसे काम करता है। प्रक्रियाओं का एक प्रमुख गुण यह है कि उनके पास अलग-अलग पता स्थान होते हैं, जो उनमें से प्रत्येक को एक आभासी पता स्थान प्रदान करके पूरा किया जाता है। x86-64 एक आभासी पते के लिए 48 बिट्स का समर्थन करता है, यहां तक ​​कि 1 जीबी पृष्ठों के साथ, इसलिए, सिद्धांत रूप में, प्रति प्रक्रिया कुछ टेराबाइट्स मेमोरी संभव हैं। एंड्रयू टेनबाम ने OSes के बारे में कुछ बेहतरीन किताबें लिखी हैं। यदि आप रुचि रखते हैं, तो उन्हें पढ़ें!
कैडेनिलुक

1
मैं "स्पष्ट स्मृति रिसाव" शब्द का उपयोग नहीं करूंगा। मुझे विश्वास नहीं है कि "मेमोरी कॉपी ऑन राइट" की इस तकनीक का आविष्कार मेमोरी लीक से निपटने के लिए किया गया था।
पेट्र

13

एक समझदार अनुकूलन यहाँ बनाया जा रहा है। रनटाइम वास्तव में मेमोरी का अधिग्रहण नहीं करता है जब तक आप इसका उपयोग नहीं करते हैं।

memcpyइस अनुकूलन को दरकिनार करने के लिए एक सरल पर्याप्त होगा। (आप पा सकते हैं कि callocअभी भी उपयोग के बिंदु तक मेमोरी आवंटन को अनुकूलित करता है।)


2
क्या आपको यकीन है? मुझे लगता है कि अगर उनकी आवंटन राशि वर्चुअल मेमोरी के अधिकतम तक पहुंच जाती है, तो मॉलॉक विफल हो जाएगा, चाहे जो भी हो। कैसे होगा मालोक () को पता है कि कोई भी मेमोरी का उपयोग करने वाला नहीं है ?? यह नहीं हो सकता है, इसलिए इसे sbrk () या जो कुछ भी उसके OS में है उसे कॉल करना होगा।
पीटर - मोनिका

1
मुझे पूरा यकीन है। (मालॉक पता नहीं है, लेकिन रनटाइम निश्चित रूप से होगा)। यह परीक्षण करने के लिए तुच्छ है (हालांकि अभी मेरे लिए आसान नहीं है: मैं एक ट्रेन पर हूं)।
बतशेबा

@ बाथशीबा प्रत्येक पृष्ठ पर एक बाइट लिखना भी पर्याप्त होगा? मान लें कि mallocपृष्ठ सीमाओं पर आबंटित होता है जो मुझे बहुत संभावना है।
Cadaniluk

2
@doron यहाँ कोई संकलक शामिल नहीं है। यह लिनक्स कर्नेल व्यवहार है।
el.pescado 10

1
मुझे लगता है कि glibc callocmmap (MAP_ANONYMOUS) को शून्य पृष्ठ देने का लाभ उठाता है, इसलिए यह कर्नेल के पृष्ठ-शून्यिंग कार्य की नकल नहीं करता है।
पीटर कॉर्ड्स

6

इस बारे में निश्चित नहीं है, लेकिन केवल यही स्पष्टीकरण कि मैं यह कर सकता हूं कि लिनक्स एक कॉपी-ऑन-राइट ऑपरेटिंग सिस्टम है। जब कोई forkदोनों प्रक्रियाओं को एक ही भौतिक रूप से याद करने के लिए कहता है। स्मृति को केवल एक बार कॉपी किया जाता है क्योंकि एक प्रक्रिया वास्तव में स्मृति को गलत बनाती है।

मुझे लगता है कि यहां वास्तविक भौतिक स्मृति केवल तभी आवंटित की जाती है, जब कोई इसे कुछ लिखने की कोशिश करता है। कॉलिंग sbrkया mmapअच्छी तरह से केवल कर्नेल की मेमोरी बुक-अपडेट को अपडेट कर सकता है। वास्तविक रैम को केवल तब आवंटित किया जा सकता है जब हम वास्तव में मेमोरी तक पहुंचने की कोशिश करते हैं।


forkइससे कोई लेना-देना नहीं है। यदि आप लिनक्स को इस प्रोग्राम के साथ बूट करते हैं तो आपको वही व्यवहार दिखाई देगा /sbin/init। (यानी पीआईडी ​​1, पहली उपयोगकर्ता-मोड प्रक्रिया)। आपको कॉपी-ऑन-राइट के साथ सही सामान्य विचार था, हालांकि: जब तक आप उन्हें गंदा नहीं करते हैं, तब तक नए-आवंटित पृष्ठ सभी कॉपी-ऑन-राइट एक ही शून्य पृष्ठ पर मैप किए जाते हैं।
पीटर कॉर्ड्स

कांटे के बारे में जानने से मुझे अनुमान लगाने की अनुमति मिली।
डोरॉन
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.