เหตุใดหน่วยความจำนี้จึงไม่กินหน่วยความจำจริงๆ


150

ฉันต้องการสร้างโปรแกรมที่จะจำลองสถานการณ์ out-of-memory (OOM) บนเซิร์ฟเวอร์ Unix ฉันสร้างหน่วยความจำที่กินง่ายสุด ๆ นี้:

#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ซึ่งตอนนี้เป็น RAM 50 GB มันจัดสรรหน่วยความจำ 1 MB และพิมพ์จุดที่มันไม่สามารถจัดสรรได้มากขึ้นดังนั้นฉันจึงรู้ว่ามันคุ้มค่าที่จะกินมากที่สุด

ปัญหาคือมันใช้งานได้ แม้แต่ในระบบที่มีหน่วยความจำกายภาพ 1 GB

เมื่อฉันตรวจสอบด้านบนฉันเห็นว่ากระบวนการกินหน่วยความจำเสมือน 50 GB และมีหน่วยความจำภายในน้อยกว่า 1 MB มีวิธีการสร้างหน่วยความจำที่กินมันจริง ๆ หรือไม่?

รายละเอียดของระบบ: Linux kernel 3.16 ( Debian ) เป็นไปได้มากที่สุดเมื่อเปิดใช้งาน overcommit (ไม่แน่ใจว่าจะตรวจสอบได้อย่างไร) โดยไม่มีการสลับและการจำลองเสมือน


16
บางทีคุณต้องใช้หน่วยความจำนี้จริง ๆ (เช่นเขียนถึงมัน)?
ms

4
ฉันไม่คิดว่าคอมไพเลอร์ปรับให้เหมาะสมถ้าเป็นจริงมันจะไม่จัดสรรหน่วยความจำเสมือน 50GB
Petr

18
@Magisch ฉันไม่คิดว่ามันเป็นคอมไพเลอร์ แต่เป็นระบบปฏิบัติการเช่นการคัดลอกเมื่อเขียน
cadaniluk

4
คุณมีสิทธิที่ผมพยายามที่จะเขียนถึงมันและฉันก็อบกล่องเสมือนของฉัน ...
Petr

4
โปรแกรมต้นฉบับจะทำงานได้ตามที่คุณคาดหวังหากคุณsysctl -w vm.overcommit_memory=2เป็นเหมือนราก ดูmjmwired.net/kernel/Documentation/vm/overcommit-accounting โปรดทราบว่านี่อาจมีผลกระทบอื่น ๆ โดยเฉพาะอย่างยิ่งโปรแกรมที่มีขนาดใหญ่มาก (เช่นเว็บเบราว์เซอร์ของคุณ) อาจล้มเหลวในการวางไข่โปรแกรมช่วยเหลือ (เช่นโปรแกรมอ่าน PDF)
zwol

คำตอบ:


221

เมื่อmalloc()การใช้งานของคุณร้องขอหน่วยความจำจากเคอร์เนลระบบ (ผ่านการโทรsbrk()หรือการmmap()เรียกใช้ระบบ) เคอร์เนลจะทำหมายเหตุว่าคุณได้ร้องขอหน่วยความจำแล้วและวางไว้ที่ไหนภายในพื้นที่ที่อยู่ของคุณ มันยังไม่ได้แมปหน้าเหล่านั้นจริงๆจริงๆ

เมื่อกระบวนการเข้าถึงหน่วยความจำภายในภูมิภาคใหม่ในภายหลังฮาร์ดแวร์จะรับรู้ข้อผิดพลาดการแบ่งส่วนและแจ้งเตือนเคอร์เนลให้เข้ากับสภาพ เคอร์เนลค้นหาหน้าในโครงสร้างข้อมูลของตัวเองและพบว่าคุณควรมีหน้าศูนย์ดังนั้นจึงแมปในหน้าศูนย์ (อาจเป็นหน้าแรก evicting หน้าจากหน้าแคช) และผลตอบแทนจากการขัดจังหวะ กระบวนการของคุณไม่ทราบว่ามีสิ่งใดเกิดขึ้นการดำเนินการของเมล็ดนั้นโปร่งใสอย่างสมบูรณ์ (ยกเว้นการหน่วงเวลาสั้น ๆ ในขณะที่เคอร์เนลทำงาน)

การปรับให้เหมาะสมนี้ช่วยให้การเรียกใช้ระบบกลับมาอย่างรวดเร็วและที่สำคัญที่สุดคือมันหลีกเลี่ยงทรัพยากรใด ๆ ที่จะมุ่งมั่นในกระบวนการของคุณเมื่อทำการแมป วิธีนี้ช่วยให้กระบวนการจองบัฟเฟอร์ที่ค่อนข้างใหญ่ซึ่งพวกเขาไม่ต้องการภายใต้สถานการณ์ปกติโดยไม่ต้องกลัวการกลืนหน่วยความจำมากเกินไป


ดังนั้นหากคุณต้องการตั้งโปรแกรมการกินหน่วยความจำคุณจะต้องทำอะไรบางอย่างกับหน่วยความจำที่คุณจัดสรร สำหรับสิ่งนี้คุณจะต้องเพิ่มบรรทัดเดียวในรหัสของคุณ:

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

โปรดทราบว่ามันสมบูรณ์เพียงพอที่จะเขียนไปยังหนึ่งไบต์ภายในแต่ละหน้า (ซึ่งมี 4096 ไบต์บน X86) นั่นเป็นเพราะการจัดสรรหน่วยความจำทั้งหมดจากเคอร์เนลไปยังโปรเซสนั้นทำที่หน้าหน่วยความจำอย่างละเอียดซึ่งก็คือในทางกลับกันเนื่องจากฮาร์ดแวร์ที่ไม่อนุญาตให้ใช้การเพจที่มีขนาดเล็กกว่า


6
อาจเป็นไปได้ที่จะใช้หน่วยความจำด้วยmmapและMAP_POPULATE(แม้ว่าโปรดทราบว่าหน้าคนบอกว่า " MAP_POPULATE ได้รับการสนับสนุนสำหรับการแมปส่วนตัวเฉพาะตั้งแต่ Linux 2.6.23 ")
Toby Speight

2
ถูกต้องโดยทั่วไป แต่ฉันคิดว่าหน้าทั้งหมดคัดลอกเมื่อเขียนแมปไปยังหน้าเว็บที่เป็นศูนย์แทนที่จะแสดงไม่ได้เลยในตารางหน้า นี่คือเหตุผลที่คุณต้องเขียนไม่ใช่แค่อ่านทุกหน้า อีกวิธีหนึ่งในการใช้หน่วยความจำกายภาพก็คือล็อคหน้า mlockall(MCL_FUTURE)เช่นการโทร (สิ่งนี้ต้องการรูทเพราะulimit -lมีเพียง 64kiB สำหรับบัญชีผู้ใช้ในการติดตั้ง Debian / Ubuntu เริ่มต้น) ฉันเพิ่งลองบน Linux 3.19 ด้วย sysctl ที่เป็นค่าเริ่มต้นvm/overcommit_memory = 0และหน้าที่ถูกล็อคใช้ swap / ฟิสิคัลแรม
Peter Cordes

2
@cad ในขณะที่ X86-64 รองรับขนาดหน้าใหญ่ขึ้นสองขนาด (2 MiB และ 1 GiB) แต่ก็ยังคงได้รับการดูแลเป็นพิเศษจากเคอร์เนล linux ตัวอย่างเช่นพวกเขาจะใช้เฉพาะกับคำขอที่ชัดเจนและเฉพาะเมื่อระบบได้รับการกำหนดค่าเพื่อให้พวกเขา นอกจากนี้หน้า 4 kiB ยังคงเป็นความละเอียดที่หน่วยความจำอาจถูกแมป นั่นเป็นเหตุผลที่ฉันไม่คิดว่าการกล่าวถึงหน้าเว็บขนาดใหญ่จะเพิ่มอะไรให้กับคำตอบ
cmaster - คืนสถานะโมนิกา

1
@AlecTeal ใช่มันเป็นเช่นนั้น นั่นเป็นเหตุผลว่าทำไมอย่างน้อยบน linux จึงเป็นไปได้มากขึ้นที่กระบวนการที่ใช้หน่วยความจำมากเกินไปถูกยิงโดยตัวกำจัดหน่วยความจำไม่เพียงพอกว่าmalloc()สายที่โทรกลับnullมา เห็นได้ชัดว่าข้อเสียของวิธีนี้ในการจัดการหน่วยความจำ อย่างไรก็ตามมันมีอยู่แล้วของการทำแผนที่การคัดลอกเมื่อเขียน (คิดว่าห้องสมุดแบบไดนามิกและfork()) ที่ทำให้มันเป็นไปไม่ได้สำหรับเคอร์เนลที่จะรู้ว่าหน่วยความจำจะต้องใช้จริง ดังนั้นถ้ามันไม่ได้เกินหน่วยความจำคุณจะหมดหน่วยความจำ mapable นานก่อนที่คุณจะใช้หน่วยความจำกายภาพจริง ๆ
cmaster - คืนสถานะโมนิกา

2
@BillBarth สำหรับฮาร์ดแวร์ไม่มีความแตกต่างระหว่างสิ่งที่คุณจะเรียกว่า page fault และ segfault ฮาร์ดแวร์จะเห็นการเข้าถึงที่ละเมิดข้อ จำกัด การเข้าถึงที่วางไว้ในตารางหน้าและส่งสัญญาณว่าเงื่อนไขไปยังเคอร์เนลผ่านความผิดพลาดในการแบ่งส่วน เป็นเพียงด้านซอฟต์แวร์ที่ตัดสินว่าควรจัดการความผิดพลาดในการแบ่งเซกเมนต์โดยการจัดหาหน้า (อัปเดตตารางหน้า) หรือให้SIGSEGVส่งสัญญาณไปยังกระบวนการหรือไม่
cmaster - คืนสถานะโมนิกา

28

หน้าเสมือนทั้งหมดเริ่มต้นการทำสำเนาการเขียนเพื่อแมปไปยังหน้าฟิสิคัลที่เป็นศูนย์เดียวกัน หากต้องการใช้ฟิสิคัลเพจคุณสามารถสกปรกได้โดยการเขียนบางสิ่งลงในเพจเสมือนแต่ละเพจ

หากทำงานในฐานะรูทคุณสามารถใช้mlock(2)หรือmlockall(2)ให้เคอร์เนลวางสายเมื่อเพจถูกจัดสรรโดยไม่ต้องทำให้สกปรก (ผู้ใช้ที่ไม่ใช่รูทปกติมีulimit -lเพียง 64kiB เท่านั้น)

ตามที่คนอื่น ๆ แนะนำไว้ดูเหมือนว่าเคอร์เนล Linux จะไม่จัดสรรหน่วยความจำจริงๆเว้นแต่คุณจะเขียนลงไป

รหัสปรับปรุงรุ่นซึ่งทำในสิ่งที่ OP ต้องการ:

สิ่งนี้ยังแก้ไขสตริงรูปแบบ printf ที่ไม่ตรงกันกับประเภท memory_to_eat และ eaten_memory โดยใช้%ziเพื่อพิมพ์size_tจำนวนเต็ม ขนาดหน่วยความจำที่จะกินเป็น kiB สามารถระบุหรือไม่ก็ได้ว่าเป็นบรรทัดคำสั่ง arg

การออกแบบที่ยุ่งเหยิงโดยใช้ตัวแปรทั่วโลกและเพิ่มขึ้น 1k แทนที่จะเป็น 4k หน้าไม่มีการเปลี่ยนแปลง

#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

ฉันคิดว่าในระดับระบบปฏิบัติการหน่วยความจำจะถูกใช้จริง ๆ เมื่อคุณเขียนลงไปเท่านั้นซึ่งทำให้รู้สึกได้ว่าระบบปฏิบัติการไม่ได้เก็บแท็บไว้ในหน่วยความจำทั้งหมดที่คุณมีอยู่ในทางทฤษฎี แต่ใช้กับสิ่งที่คุณใช้จริงเท่านั้น
Magisch

@Petr mind หากฉันทำเครื่องหมายคำตอบเป็นวิกิชุมชนและคุณแก้ไขในรหัสของคุณเพื่อให้ผู้ใช้งานสามารถอ่านได้ในอนาคต
Magisch

@ Petr มันไม่แปลกเลย นั่นคือวิธีการจัดการหน่วยความจำในระบบปฏิบัติการวันนี้ ลักษณะสำคัญของกระบวนการคือพวกเขามีช่องว่างที่อยู่ที่แตกต่างกันซึ่งทำได้โดยการให้แต่ละพื้นที่เสมือนที่อยู่ x86-64 สนับสนุน 48 บิตสำหรับที่อยู่เสมือนหนึ่งกับหน้า 1GB แม้ดังนั้นในทางทฤษฎีของเทราไบต์หน่วยความจำบางต่อกระบวนการที่เป็นไปได้ Andrew Tanenbaum ได้เขียนหนังสือยอดเยี่ยมเกี่ยวกับระบบปฏิบัติการ หากคุณสนใจอ่านพวกเขา!
cadaniluk

1
ฉันจะไม่ใช้ถ้อยคำ "การรั่วไหลของหน่วยความจำที่เห็นได้ชัด" ฉันไม่เชื่อว่า overcommit หรือเทคโนโลยีนี้ของ "การคัดลอกหน่วยความจำเมื่อเขียน" ถูกคิดค้นเพื่อจัดการกับการรั่วไหลของหน่วยความจำเลย
Petr

13

มีการเพิ่มประสิทธิภาพที่เหมาะสมที่นี่ รันไทม์ไม่ได้รับจริงหน่วยความจำจนกว่าคุณจะใช้มัน

วิmemcpyจะง่ายพอที่จะหลีกเลี่ยงการเพิ่มประสิทธิภาพนี้ (คุณอาจพบว่าcallocยังคงปรับการจัดสรรหน่วยความจำให้เหมาะสมจนกว่าจะถึงจุดใช้งาน)


2
คุณแน่ใจไหม? ฉันคิดว่าถ้าจำนวนการจัดสรรของเขาถึงหน่วยความจำเสมือนสูงสุดที่ malloc ใช้จะล้มเหลวไม่ว่าจะเกิดอะไรขึ้น malloc () จะรู้ได้อย่างไรว่าไม่มีใครจะใช้หน่วยความจำได้? ไม่สามารถทำได้ดังนั้นจึงต้องเรียก sbrk () หรือสิ่งที่เทียบเท่าในระบบปฏิบัติการของเขา
ปีเตอร์ - Reinstate Monica

1
ฉันค่อนข้างแน่ใจ (malloc ไม่ทราบ แต่รันไทม์จะแน่นอน) ทดสอบเล็กน้อย (แม้ว่าตอนนี้ฉันจะไม่สะดวกเลย: ฉันอยู่บนรถไฟ)
Bathsheba

@Bathsheba จะเขียนหนึ่งไบต์ไปยังแต่ละหน้าด้วยหรือไม่ สมมติว่าmallocจัดสรรในขอบเขตของหน้าสิ่งที่ดูเหมือนว่าฉันน่าจะเป็น
cadaniluk

2
@doron ไม่มีคอมไพเลอร์ที่เกี่ยวข้องที่นี่ มันเป็นพฤติกรรมของเคอร์เนล Linux
el.pescado

1
ฉันคิดว่า glibc callocใช้ประโยชน์จาก mmap (MAP_ANONYMOUS) ให้หน้าเว็บที่มีค่าศูนย์ดังนั้นจึงไม่ได้ทำหน้าที่เป็นศูนย์หน้าของเคอร์เนล
Peter Cordes

6

ไม่แน่ใจเกี่ยวกับอันนี้ แต่คำอธิบายเดียวที่ฉันทำได้คือลินุกซ์เป็นระบบปฏิบัติการแบบคัดลอก - เขียน - เขียน เมื่อหนึ่งเรียกforkกระบวนการทั้งสองชี้ไปที่หน่วยความจำทางกายภาพเดียวกัน หน่วยความจำจะถูกคัดลอกเมื่อกระบวนการเดียวจริงเขียนไปยังหน่วยความจำ

ฉันคิดว่าที่นี่หน่วยความจำกายภาพจริงถูกจัดสรรเมื่อพยายามเขียนบางสิ่งเท่านั้น การโทรsbrkหรือmmapอัปเดตหน่วยความจำของเคอร์เนลเก็บไว้อย่างดีเท่านั้น RAM จริงอาจได้รับการจัดสรรเมื่อเราพยายามเข้าถึงหน่วยความจำเท่านั้น


forkไม่มีอะไรเกี่ยวข้องกับเรื่องนี้ คุณจะเห็นลักษณะการทำงานเดียวกันหากคุณบูท Linux ด้วยโปรแกรม/sbin/initนี้ (เช่น PID 1 ซึ่งเป็นกระบวนการผู้ใช้โหมดแรก) คุณมีความคิดทั่วไปที่ถูกต้องด้วยการคัดลอกเมื่อเขียนแม้ว่า: จนกว่าคุณจะสกปรกพวกเขาหน้าใหม่ที่ได้รับการจัดสรรจะถูกแมปการคัดลอกเมื่อเขียนไปที่หน้าศูนย์ที่เหมือนกัน
Peter Cordes

การรู้เรื่องส้อมทำให้ฉันเดาได้
doron
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.