¿Por qué este devorador de memoria realmente no come memoria?
linux memory (4)
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
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 confirmación habilitado (no estoy seguro de cómo verificarlo) sin intercambio y virtualizado.
Aquí se está haciendo una optimización sensata. El tiempo de ejecución en realidad no adquiere la memoria hasta que lo usa.
Una simple
memcpy
será suficiente para eludir esta optimización.
(Puede encontrar que
calloc
aún optimiza la asignación de memoria hasta el punto de uso).
Cuando su implementación de
malloc()
solicita memoria del kernel del sistema (a través de una
sbrk()
al sistema
sbrk()
o
mmap()
), el kernel solo toma nota de que ha solicitado la memoria y dónde se 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 del núcleo 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 almacenamientos intermedios 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 desde el kernel a un proceso se realiza con granularidad de página de memoria, que a su vez se debe al hardware que no permite la paginación a granularidades más pequeñas.
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)
para que el núcleo conecte las páginas cuando están asignadas, sin tener que ensuciarlas.
(los usuarios normales no root tienen un
ulimit -l
de solo 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, usando
%zi
para imprimir enteros
size_t
.
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;
}
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 se llama a
fork
ambos procesos apuntan 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.
Llamar a
sbrk
o
mmap
solo puede actualizar la reserva de memoria del kernel.
La RAM real solo se puede asignar cuando realmente intentamos acceder a la memoria.