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


150

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 )은 스왑 및 가상화없이 오버 커밋이 활성화 된 (확실한 체크 아웃 방법) 가능성이 높습니다.


16
실제로이 메모리를 사용해야합니까 (예 : 쓰기)?
ms

4
나는 컴파일러가 그것을 최적화한다고 생각하지 않는다. 그것이 사실이라면, 50GB의 가상 메모리를 할당하지 않을 것이다.
Petr

18
@ Magagi : 나는 그것이 컴파일러가 아니라 복사시 쓰기와 같은 OS라고 생각합니다.
cadaniluk

4
당신 말이 맞아요, 제가 글을 쓰려고했는데 방금 가상 상자를 k어요 ...
Petr

4
원래 프로그램은 sysctl -w vm.overcommit_memory=2루트 권한 으로 수행하면 예상대로 작동 합니다. mjmwired.net/kernel/Documentation/vm/overcommit-accounting을 참조하십시오 . 이것은 다른 결과를 초래할 수 있습니다. 특히 매우 큰 프로그램 (예 : 웹 브라우저)이 도우미 프로그램 (예 : PDF 리더)을 생성하지 못할 수 있습니다.
zwol

답변:


221

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


6
그것은과 메모리를 커밋 수도 mmapMAP_POPULATE(man 페이지는 말한다 참고 "하지만 MAP_POPULATE은 리눅스 2.6.23 이후 개인 매핑을 지원한다 ").
Toby Speight

2
기본적으로는 맞지만 페이지 테이블에 전혀 표시되지 않고 페이지가 모두 0으로 복사 된 페이지에 매핑되어 있다고 생각합니다. 그렇기 때문에 모든 페이지를 읽지 말고 작성해야합니다. 또한 실제 메모리를 사용하는 또 다른 방법은 페이지를 잠그는 것입니다. 예를 들어 전화하십시오 mlockall(MCL_FUTURE). ( ulimit -l데비안 / 우분투의 기본 설치에서는 사용자 계정에 64kiB 만 있기 때문에 루트가 필요합니다 .) 방금 기본 sysctl로 Linux 3.19에서 시도했지만 vm/overcommit_memory = 0잠긴 페이지는 스왑 / 물리적 RAM을 사용합니다.
Peter Cordes

2
@cad X86-64는 두 개의 더 큰 페이지 크기 (2MiB 및 1GiB)를 지원하지만 여전히 Linux 커널에서 매우 특별하게 취급됩니다. 예를 들어, 시스템은 명시적인 요청과 시스템이 허용하도록 구성된 경우에만 사용됩니다. 또한 4KB 페이지는 여전히 메모리가 매핑 될 수있는 단위로 유지됩니다. 그렇기 때문에 큰 페이지를 언급하면 ​​답변에 아무것도 추가되지 않는다고 생각합니다.
cmaster-monica reinstate

1
@AlecTeal 그렇습니다. 그렇기 때문에 적어도 리눅스에서는 메모리를 너무 많이 소비하는 프로세스가 malloc()호출 중 하나보다 메모리 부족 킬러에 의해 발생했을 가능성이 높습니다 null. 이는 메모리 관리에 대한이 접근 방식의 단점입니다. 그러나 fork()커널에 실제로 필요한 메모리 양을 알 수없는 것은 이미 동적 라이브러리 작성 및 복사 (write-on-mapping) 맵핑이 이미 존재 합니다. 따라서 메모리를 오버 커밋하지 않으면 실제로 모든 실제 메모리를 사용하기 오래 전에 매핑 가능한 메모리가 부족합니다.
cmaster-monica reinstate

2
@BillBarth 하드웨어에는 페이지 폴트와 segfault의 차이점이 없습니다. 하드웨어는 페이지 테이블에 제시된 액세스 제한을 위반하는 액세스 만보고 분할 오류를 통해 커널에 해당 신호를 보냅니다. 그런 다음 페이지를 제공하여 (페이지 테이블 업데이트) 세그먼트 오류를 ​​처리해야하는지 또는 SIGSEGV신호를 프로세스에 전달 해야하는지 여부를 결정하는 것은 소프트웨어 측뿐입니다 .
cmaster-복원 monica

28

모든 가상 페이지는 동일한 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;
}

네, 그렇습니다. 기술적 인 배경에 대해서는 확실하지 않지만 그 이유는 맞습니다. 그러나 실제로 사용할 수있는 것보다 더 많은 메모리를 할당 할 수 있다는 것이 이상합니다.
Petr

OS 수준에서는 메모리가 실제로 쓸 때 메모리가 실제로 사용된다고 생각합니다 .OS가 이론적으로 가지고있는 모든 메모리에 탭을 유지하지는 않지만 실제로 사용하는 메모리에만 탭을 유지한다는 점을 고려하는 것이 좋습니다.
Magisch

@Petr mind 내 답변을 커뮤니티 위키로 표시하고 향후 사용자 가독성을 위해 코드를 편집하면?
Magisch

@Petr 전혀 이상하지 않습니다. 이것이 오늘날의 OS에서 메모리 관리가 작동하는 방식입니다. 프로세스의 주요 특징은 프로세스에 고유 한 주소 공간이 있으며, 각 주소 공간에 가상 주소 공간을 제공함으로써 달성됩니다. x86-64는 1GB 페이지까지 하나의 가상 주소에 대해 48 비트를 지원하므로 이론적으로 프로세스 당 일부 테라 바이트 메모리 가 가능합니다. Andrew Tanenbaum은 OS에 관한 훌륭한 책을 썼습니다. 관심이 있으시면 읽어보십시오!
cadaniluk

1
나는 "명백한 메모리 누수"라는 문구를 사용하지 않을 것이다. 나는 오버 커밋이나이 "메모리 복사시 쓰기"기술이 전혀 메모리 누수를 다루기 위해 발명되었다고 생각하지 않는다.
Petr

13

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

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


2
확실합니까? 그의 할당량이 사용 가능한 가상 메모리 의 최대 값에 도달 하면 malloc이 무엇이든간에 실패 할 것이라고 생각합니다. malloc ()은 아무도 메모리를 사용하지 않는다는 것을 어떻게 알 수 있을까요? 그것은 할 수 없으므로 sbrk () 또는 그의 OS에서 동등한 것을 호출해야합니다.
피터-모니카 복원 복원

1
확인합니다. (malloc은 모르지만 런타임은 확실합니다.) 테스트하기는 쉽지 않습니다 (지금 당장은 쉽지는 않지만 기차를 타고 있습니다).
Bathsheba

@Bathsheba 각 페이지에 1 바이트를 쓰면 충분할까요? malloc페이지 경계에 나에게 가능성이 높은 것을 할당 한다고 가정 합니다.
cadaniluk

2
@ doron 여기에는 컴파일러가 없습니다. Linux 커널 동작입니다.
el.pescado

1
glibc calloc는 mmap (MAP_ANONYMOUS)을 사용하여 0으로 된 페이지를 제공하므로 커널의 페이지 제로 작업을 복제하지 않습니다.
Peter Cordes

6

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

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


fork이것과 아무 상관이 없습니다. 이 프로그램으로 Linux를 부팅하면 동일한 동작을 보게됩니다 /sbin/init. (즉, 첫 번째 사용자 모드 프로세스 인 PID 1). 그러나 기록 중 복사에 대한 올바른 아이디어는 다음과 같습니다. 더러워 질 때까지 새로 할당 된 페이지는 모두 기록 중 복사가 동일한 0으로 지정된 페이지에 맵핑됩니다.
Peter Cordes

포크에 대해 아는 것만으로도 추측 할 수있었습니다.
doron
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.