¿Por qué este devorador de memoria realmente no come memoria?

150

Quiero crear un programa que simule una situación de falta de memoria (OOM) en un servidor Unix. Creé este comedor de memoria súper simple:

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

Come tanta memoria como se define en la memory_to_eatque ahora es exactamente 50 GB de RAM. Asigna memoria en 1 MB e imprime exactamente el punto donde no puede asignar más, de modo que sé qué valor máximo logró comer.

El problema es que funciona. Incluso en un sistema con 1 GB de memoria física.

Cuando reviso la parte superior, veo que el proceso consume 50 GB de memoria virtual y solo menos de 1 MB de memoria residente. ¿Hay alguna manera de crear un comedor de memoria que realmente lo consuma?

Especificaciones del sistema: el kernel 3.16 de Linux ( Debian ) probablemente con un exceso de compromiso habilitado (no estoy seguro de cómo verificarlo) sin intercambio y virtualizado.

Petr
fuente
16
¿tal vez tengas que usar esta memoria (es decir, escribirle)?
ms
44
No creo que el compilador lo optimice, si eso fuera cierto, no asignaría 50 GB de memoria virtual.
Petr
18
@ Magisch No creo que sea el compilador sino el sistema operativo como copiar y escribir.
cadaniluk
44
Tienes razón, traté de escribirle y acabo de bombardear mi caja virtual ...
Petr
44
El programa original se comportará como esperaba si lo hace sysctl -w vm.overcommit_memory=2como root; ver mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Tenga en cuenta que esto puede tener otras consecuencias; en particular, los programas muy grandes (por ejemplo, su navegador web) pueden no generar programas auxiliares (por ejemplo, el lector de PDF).
zwol

Respuestas:

221

Cuando su malloc()aplicación solicita la memoria del núcleo del sistema (a través de una sbrk()o mmap()llamada al sistema), el núcleo sólo hace una nota que ha solicitado la memoria y donde se va a colocar dentro de su espacio de direcciones. En realidad, todavía no asigna esas páginas .

Cuando el proceso accede posteriormente a la memoria dentro de la nueva región, el hardware reconoce una falla de segmentación y alerta al núcleo sobre la condición. Luego, el núcleo busca la página en sus propias estructuras de datos y descubre que debe tener una página cero allí, por lo que se asigna en una página cero (posiblemente primero desalojando una página del caché de la página) y regresa de la interrupción. Su proceso no se da cuenta de que nada de esto sucedió, la operación de los núcleos es perfectamente transparente (excepto por el breve retraso mientras el núcleo hace su trabajo).

Esta optimización permite que la llamada del sistema regrese muy rápidamente y, lo más importante, evita que se asignen recursos a su proceso cuando se realiza el mapeo. Esto permite que los procesos reserven buffers bastante grandes que nunca necesitarán en circunstancias normales, sin temor a engullir demasiada memoria.


Entonces, si desea programar un comedor de memoria, absolutamente tiene que hacer algo con la memoria que asigna. Para esto, solo necesita agregar una sola línea a su código:

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

Tenga en cuenta que es perfectamente suficiente escribir en un solo byte dentro de cada página (que contiene 4096 bytes en X86). Esto se debe a que toda la asignación de memoria del kernel a un proceso se realiza con granularidad de página de memoria, lo que a su vez se debe al hardware que no permite la paginación a granularidades más pequeñas.

cmaster - restablecer monica
fuente
66
También es posible comprometer memoria con mmapy MAP_POPULATE(aunque tenga en cuenta que la página del manual dice " MAP_POPULATE es compatible con asignaciones privadas solo desde Linux 2.6.23 ").
Toby Speight
2
Eso es básicamente correcto, pero creo que las páginas son todas copiadas en escritura asignadas a una página puesta a cero, en lugar de no estar presentes en absoluto en las tablas de páginas. Es por eso que debe escribir, no solo leer, cada página. Además, otra forma de utilizar la memoria física es bloquear las páginas. por ejemplo, llamada mlockall(MCL_FUTURE). (Esto requiere root, porque ulimit -lsolo es de 64 KB para cuentas de usuario en una instalación predeterminada de Debian / Ubuntu.) Acabo de probarlo en Linux 3.19 con el sysctl predeterminado vm/overcommit_memory = 0, y las páginas bloqueadas usan RAM de intercambio / física.
Peter Cordes
2
@cad Si bien el X86-64 admite dos tamaños de página más grandes (2 MiB y 1 GiB), el núcleo de Linux todavía los trata de manera bastante especial. Por ejemplo, solo se usan por solicitud explícita, y solo si el sistema se ha configurado para permitirlos. Además, la página de 4 kiB sigue siendo la granularidad en la que se puede asignar la memoria. Es por eso que no creo que mencionar grandes páginas agregue algo a la respuesta.
cmaster - reinstalar a monica el
1
@AlecTeal Sí, lo hace. Es por eso que, al menos en Linux, es más probable que un asesino sin memoria dispare un proceso que consume demasiada memoria que el que malloc()devuelve una de sus llamadas null. Esa es claramente la desventaja de este enfoque para la gestión de la memoria. Sin embargo, ya es la existencia de asignaciones de copia en escritura (piense en bibliotecas dinámicas y fork()) lo que hace imposible que el núcleo sepa cuánta memoria se necesitará realmente. Por lo tanto, si no comprometiera demasiado la memoria, se quedaría sin memoria de mapa mucho antes de que realmente estuviera usando toda la memoria física.
cmaster - reinstalar a monica el
2
@BillBarth Para el hardware no hay diferencia entre lo que llamarías un fallo de página y un defecto de seguridad. El hardware solo ve un acceso que viola las restricciones de acceso establecidas en las tablas de páginas y señala esa condición al núcleo a través de un fallo de segmentación. Es solo el lado del software el que decide si la falla de segmentación debe manejarse proporcionando una página (actualizando las tablas de páginas) o si SIGSEGVse debe entregar una señal al proceso.
cmaster - reinstalar a monica el
28

Todas las páginas virtuales comienzan copia-en-escritura asignada a la misma página física puesta a cero. Para usar páginas físicas, puede ensuciarlas escribiendo algo en cada página virtual.

Si se ejecuta como root, puede usar mlock(2)o mlockall(2)hacer que el núcleo conecte las páginas cuando están asignadas, sin tener que ensuciarlas. (los usuarios normales no root tienen ulimit -lsolo 64 KB).

Como muchos otros sugirieron, parece que el kernel de Linux realmente no asigna la memoria a menos que usted le escriba

Una versión mejorada del código, que hace lo que el OP quería:

Esto también corrige los desajustes de la cadena de formato printf con los tipos de memory_to_eat y eaten_memory, que se utilizan %zipara imprimir size_tenteros. El tamaño de la memoria para comer, en kiB, se puede especificar opcionalmente como una línea de comando arg.

El diseño desordenado que usa variables globales y crece en 1k en lugar de 4k páginas, no ha cambiado.

#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;
}
Magisch
fuente
Sí, tienes razón, esa fue la razón, aunque no estoy seguro sobre los antecedentes técnicos, pero tiene sentido. Sin embargo, es extraño que me permita asignar más memoria de la que realmente puedo usar.
Petr
Creo que, en el nivel del sistema operativo, la memoria solo se usa realmente cuando se escribe en él, lo que tiene sentido teniendo en cuenta que el sistema operativo no controla toda la memoria que tiene teóricamente, sino solo la que realmente usa.
Magisch
@Petr mind ¿Si marco mi respuesta como wiki de la comunidad y usted edita su código para la futura legibilidad del usuario?
Magisch
@Petr No es extraño en absoluto. Así es como funciona la administración de memoria en los sistemas operativos actuales. Una característica importante de los procesos es que tienen espacios de direcciones distintos, lo que se logra al proporcionar a cada uno de ellos un espacio de direcciones virtual. x86-64 admite 48 bits para una dirección virtual, incluso con páginas de 1 GB, por lo que, en teoría, son posibles algunos terabytes de memoria por proceso . Andrew Tanenbaum ha escrito algunos libros excelentes sobre sistemas operativos. Si estás interesado, léelos!
cadaniluk
1
No usaría la frase "pérdida de memoria obvia". No creo que se haya inventado demasiado o esta tecnología de "copia de memoria en escritura" para tratar las pérdidas de memoria.
Petr
13

Aquí se está haciendo una optimización sensata. El tiempo de ejecución en realidad no adquiere la memoria hasta que lo usa.

Un simple memcpy será suficiente para evitar esta optimización. (Puede encontrar que calloctodavía optimiza la asignación de memoria hasta el punto de uso).

Betsabé
fuente
2
¿Estás seguro? Creo que si su cantidad de asignación alcanza el máximo de memoria virtual disponible, el malloc fallará, pase lo que pase. ¿Cómo malloc () sabe que nadie va a usar la memoria? No puede, por lo que debe llamar a sbrk () o lo que sea equivalente en su sistema operativo.
Peter - Restablece a Mónica el
1
Estoy bastante seguro (Malloc no sabe, pero el tiempo de ejecución ciertamente lo haría). Es trivial probar (aunque no es fácil para mí en este momento: estoy en un tren).
Betsabé
@Bathsheba ¿Sería suficiente escribir un byte en cada página? Asumir mallocasigna en los límites de la página lo que me parece bastante probable.
cadaniluk
2
@doron no hay compilador involucrado aquí. Es el comportamiento del kernel de Linux.
el.pescado
1
Creo que glibc callocaprovecha que mmap (MAP_ANONYMOUS) proporciona páginas puestas a cero, por lo que no duplica el trabajo de puesta a cero de páginas del núcleo.
Peter Cordes
6

No estoy seguro de esto, pero la única explicación de la que puedo hablar es que Linux es un sistema operativo de copia en escritura. Cuando uno llamafork ambos procesos, apunta a la misma memoria física. La memoria solo se copia una vez que un proceso realmente ESCRIBE en la memoria.

Creo que aquí, la memoria física real solo se asigna cuando uno intenta escribirle algo. Llamando sbrko mmapbien solo puede actualizar la memoria del kernel. La RAM real solo puede asignarse cuando realmente intentamos acceder a la memoria.

doron
fuente
forkNo tiene nada que ver con esto. Vería el mismo comportamiento si iniciara Linux con este programa como /sbin/init. (es decir, PID 1, el primer proceso en modo de usuario). Sin embargo, tuvo la idea general correcta de copiar en escritura: hasta que las ensucie, las páginas recién asignadas se asignarán todas a copia en escritura en la misma página puesta a cero.
Peter Cordes
saber sobre el tenedor me permitió adivinar.
doron