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_eat
que 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.
fuente
sysctl -w vm.overcommit_memory=2
como 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).Respuestas:
Cuando su
malloc()
aplicación solicita la memoria del núcleo del sistema (a través de unasbrk()
ommap()
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:
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.
fuente
mmap
yMAP_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 ").mlockall(MCL_FUTURE)
. (Esto requiere root, porqueulimit -l
solo 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 predeterminadovm/overcommit_memory = 0
, y las páginas bloqueadas usan RAM de intercambio / física.malloc()
devuelve una de sus llamadasnull
. 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 yfork()
) 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.SIGSEGV
se debe entregar una señal al proceso.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)
omlockall(2)
hacer que el núcleo conecte las páginas cuando están asignadas, sin tener que ensuciarlas. (los usuarios normales no root tienenulimit -l
solo 64 KB).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
%zi
para imprimirsize_t
enteros. 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.
fuente
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 quecalloc
todavía optimiza la asignación de memoria hasta el punto de uso).fuente
malloc
asigna en los límites de la página lo que me parece bastante probable.calloc
aprovecha 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.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
sbrk
ommap
bien solo puede actualizar la memoria del kernel. La RAM real solo puede asignarse cuando realmente intentamos acceder a la memoria.fuente
fork
No 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.