[c] 왜이 메모리 먹는 사람이 실제로 메모리를 먹지 않습니까?

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정확히 50GB의 RAM 인 정의 된만큼 많은 메모리를 사용 합니다. 메모리를 1MB 씩 할당하고 더 많은 할당에 실패한 지점을 정확하게 인쇄하여 어느 최대 값을 먹을 수 있는지 알 수 있습니다.

문제는 작동한다는 것입니다. 실제 메모리가 1GB 인 시스템에서도 가능합니다.

맨 위로 확인하면 프로세스가 50GB의 가상 메모리와 1MB 미만의 상주 메모리 만 사용한다는 것을 알 수 있습니다. 실제로 그것을 소비하는 메모리 이터를 만드는 방법이 있습니까?

시스템 사양 : Linux 커밋 3.16 ( Debian )은 스왑 및 가상화없이 오버 커밋이 활성화 된 (확실한 체크 아웃 방법) 가능성이 높습니다.



답변

귀하의 경우 malloc()구현 (AN 통해 시스템 커널에서 메모리를 요청 sbrk()또는 mmap()시스템 호출), 커널은 당신이 메모리를 요청하고 그것이 당신의 주소 공간 내에 배치 될 위치하는 메모를합니다. 실제로 해당 페이지를 매핑하지는 않습니다 .

프로세스가 이후에 새로운 영역 내에서 메모리에 액세스 할 때, 하드웨어는 세그먼테이션 결함을 인식하고 커널에게 조건을 알려줍니다. 그런 다음 커널은 자체 데이터 구조에서 페이지를 찾고 페이지에 0 페이지가 있어야 함을 발견하여 0 페이지에 맵핑하고 (아마 먼저 페이지 캐시에서 페이지를 제거 할 수 있음) 인터럽트에서 리턴합니다. 프로세스는 이런 일이 발생했다는 것을 인식하지 못합니다. 커널 작업은 완전히 투명합니다 (커널이 작동하는 동안 짧은 지연 시간 제외).

이 최적화는 시스템 호출이 매우 빠르게 리턴되도록하며, 가장 중요한 것은 맵핑 할 때 프로세스에 자원이 커미트되지 않도록합니다. 따라서 프로세스는 메모리를 너무 많이 차지할 염려없이 정상적인 환경에서는 필요하지 않은 오히려 큰 버퍼를 예약 할 수 있습니다.


따라서 메모리 사용자를 프로그래밍하려면 할당 한 메모리로 실제로 무언가를 수행해야합니다. 이를 위해 코드에 한 줄만 추가하면됩니다.

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 바이트 포함)에 쓰면 충분합니다. 커널에서 프로세스로의 모든 메모리 할당은 메모리 페이지 단위로 이루어지기 때문입니다. 이는 더 작은 단위로 페이징을 허용하지 않는 하드웨어 때문입니다.


답변

모든 가상 페이지는 동일한 0으로 된 실제 페이지에 맵핑시 복사시 시작됩니다. 실제 페이지를 사용하려면 각 가상 페이지에 무언가를 작성하여 더티 페이지를 더럽힐 수 있습니다.

루트로 실행하는 경우 mlock(2)또는 mlockall(2)커널을 사용 하여 할당 된 페이지를 더럽 히지 않고 페이지를 연결하도록 할 수 있습니다. 루트가 아닌 일반 사용자는 ulimit -l64kiB 만 있습니다.

다른 많은 사람들이 제안했듯이 Linux 커널은 쓰지 않는 한 실제로 메모리를 할당하지 않는 것 같습니다.

OP가 원하는 것을 수행하는 코드의 개선 된 버전 :

또한 정수 %zi를 인쇄 하는 데 사용하여 printf 형식 문자열이 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;
}


답변

합리적인 최적화가 이루어지고 있습니다. 런타임은 실제로 사용할 때까지 메모리를 확보 하지 않습니다 .

간단한은 memcpy이 최적화를 회피하기에 충분합니다. ( calloc사용 시점까지 여전히 메모리 할당을 최적화하는 것을 알 수 있습니다 .)


답변

이것에 대해 확실하지 않지만 내가 할 수있는 유일한 설명은 Linux가 COW (Copy-On-Write) 운영 체제라는 것입니다. 하나를 호출 fork하면 두 프로세스 모두 동일한 물리적 메모리를 가리 킵니다. 메모리는 한 프로세스가 실제로 메모리에 기록 된 후에 만 ​​복사됩니다.

여기서 실제 물리 메모리는 무언가를 쓰려고 할 때만 할당됩니다. 커널의 메모리 예약을 호출 sbrk하거나 mmap업데이트 만 할 수 있습니다. 실제 RAM은 실제로 메모리에 액세스하려고 할 때만 할당 될 수 있습니다.


답변