Reserva de memoria en Linux
Ya sabemos que en C las reservas de memoria se hacen mediante la llamada al sistema malloc() y similares, pero cómo opera realmente el kernel es algo que puede no estar del todo claro.
Mediante una prueba de concepto voy a ir mostrando cómo se comporta un kernel Linux estándar, sin configuraciones especiales. Me voy a basar en el concepto de Tamaño del Conjunto Residente (Resident Set Size, RSS).
El Conjunto Residente en el conjunto de páginas de nuestro proceso que se encuentran en memoria física, que será igual o inferior al conjunto de páginas de nuestro proceso (cada página o bien está en memoria física, o bien en el archivo de intercambio).
Hay que recordar que para que puede accederse a una información o ejecutarse código, debe estar en memoria física, y que los algoritmos de gestión de memoria del S.O. tienden, cuando necesitan localizar un nuevo hueco de memoria libre, a enviar las páginas menos usadas al espacio de intercambio.
Inicialmente tenemos un programa que arranca:
#include
#include
#include
void mi_pause(const char *str);
int main(int arg, char **argv)
{
int pagesize=sysconf(_SC_PAGE_SIZE);
int num_pag=10000,i;
char *buffer;
time_t t0;
short d_t=30;
float d_p=0.1;
/*Sin reservar memoria*/
mi_pause("El RSS deberia ser minimo");
Hasta aquí, si vemos su RSS:
352 mem
Hasta este punto, este programa ha sido cargado en memoria por el cargador del sistema operativo, junto con las librerías requeridas si es que no estaban ya cargadas.
Así que el proceso tiene asignadas al menos una serie de páginas donde está el código del programa y otras páginas donde está la pila (el kernel Linux por defecto asigna una pila de 8K a los procesos, y en la arquitectura i386 las páginas son de 4K, luego dos páginas para la pila). Ahora mismo desconozco si tendrá más secciones, aparte de código y pila, pero todas estas páginas suman 352k, tal y como nos indica ps.
Se sigue ejecutando el código:
buffer=(char *)malloc(num_pag*pagesize);
if(!buffer) {
perror("Error en malloc()");
exit(EXIT_FAILURE);
}
mi_pause("El RSS deberia seguir siendo minimo");
Ahora reservamos memoria, 10000 páginas en el heap (malloc() reserva en el heap o montículo, que es una tercera sección de memoria). En i386 eso son 40000k. Aparentemente, cabe esperar que el RSS haya subido de 352k que teníamos antes a 40352k, aproximadamente:
388 mem
Pues no, tenemos tan sólo 36k más, 9 páginas. Y las otras 9991, ¿dónde andan?
El comportamiento por defecto del kernel Linux es proporcionar las páginas bajo demanda, es decir, que mientras no vayas a usar la página, ¿para qué molestarse en hacer que esté disponible? (esto se llama deferred page allocation, y pareceser que AIX también disponía de un sistema como éste)
Si tú solicitas –malloc()– la memoria y no has superado el límite que tiene establecido tu usuario, etc., el sistema te dice que sí, que vale, que te da la memoria, pero en realidad aún no ha ido a buscarte una página libre.
Cuando por fin accedes a esa página que es legítimamente tuya, es cuando el S.O. se molesta en recorrer toda la lista de páginas que tiene, encontrar una libre –si la hay, y si no, ya se sabe: elegir una candidata y mandarla al espacio de intercambio para dejar un hueco libre–, marcarla como tuya, y devolver el control a tu proceso.
O resumidamente: que hasta que no accedes a las páginas no son tuyas del todo, y por tanto no pertenecen a tu conjunto residente.
Ventajas:
- Ahorro de tiempo ante procesos que reservan mucho y usan poco.
- Ahorro de tiempo en el arranque de un proceso (que es donde se suele reservar memoria), al contrario que durante su ejecución (donde se usa esa memoria y hay que localizarla, si es que llega a usarse).
- Permite aprovechar al máximo el sistema, a expensas de tener una menor estabilidad. En sistemas de misión crítica esto puede no ser deseable, y de hecho creo que hay una opcion del kernel para desactivar este comportamiento.
Voy a tratar de explicar este último punto con el siguiente escenario:
Sea un sistema con 64M de RAM (y nada más de memoria), si el proceso A reserva 50M (aunque sólo usa 20M de esas 50M), y a continuación el proceso B reserva 30M y usa enteros esos 30M.
En total ambos procesos tratan de reservar 80M, de las cuales la máquina no dispone, así que en principio cabe pensar que no se pueden ejecutar a la vez ambos procesos.
En cambio, con el enfoque que hemos visto, el kernel le localiza 20M al proceso A (aunque le informe de que le ha reservado 50M) y 30M al proceso B, dando un total de 50M localizadas (abarcables por la máquina).
Si ahora el proceso A que sólo usaba 20M y que está conviviendo con B, poco a poco empieza a aumentar su consumo de memoria hasta usar por entero esos 50M, ¿qué ocurre? Pues sencillamente que no hay sitio para los dos procesos (situación Out-of-Memory) y al menos uno debe morir.
¿Quién se encarga de hacer justicia? El OOM Killer ¿Bajo qué política? Eso es más complicado, y en el enlace anterior se muestra un fragmento de código que explica un poco los fundamentos, aunque hay quien discrepa con este sistema.
¿Qué ocurre si mientras A engorda, a B le da tiempo a acabar? Pues todos felices y contentos y hemos podido ejecutar dos procesos que de otra manera no podríamos.
Sigamos ejecutando:
for(i=0;i
buffer[i*pagesize]=0;
}
mi_pause("Ahora deberia ser maximo");
El código ahora se dedica a poner el primer byte de cada página reservada a 0. Ahora sí que accedemos a las páginas que habíamos solicitado con malloc().
El resultado es que ahora nuestro RSS engorda estrepitosamente:
40384 mem
Ahora sí que hemos hecho la reserva efectiva.
Sigamos ejecutando:
printf("Ahora se esta accediendo al %f%% de las paginas reservadas\n",d_p);
printf("– Comprueba el RSS mientras se accede (%hds.) –\n",d_t);
printf("– Correr paralelamente un devorador de memoria ayudara –\n");
t0=time(NULL);
while(time(NULL)
for(i=0;i
buffer[i*pagesize]=0;
}
}
Nos dedicamos, durante 30 segundos, a acceder al primer 10% de las páginas que reservamos, es decir, las primeras 1000 páginas.
¿Cuál es el objetivo de esto? Que si ahora otros procesos requieren grandes cantidades de memoria, podamos ver cómo el S.O. nos quita páginas del conjunto residente y las vaya enviando al espacio de intercambio.
El S.O. va a quitar primero las menos utilizadas, que son ese 90% que no estamos accediendo. Si los otros procesos no requieren demasiada memoria, lo que nos quite probablemente no llegue al 90%.
Si requieren muchísima, puede superarlo, dando lugar a que constantemente haya que estar metiendo y sacando páginas del espacio de intercambio, ya que parte de las que se han metido sí se usan con frecuencia (hiperpaginación o thrashing).
Para comer memoria he hecho devorador.c, que se reserva (y accede durante 20 segundos) 200M de memoria (estoy ejecutando todo esto en una máquina virtual con 256M de RAM):
#include
#include
#include
int main(int argc, char **argv)
{
size_t size=200*1024*1024, offs;
char *buffer;
unsigned short pagesize=sysconf(_SC_PAGE_SIZE);
time_t t0, d_t=20;
buffer=(char *)malloc(size);
if(!buffer){
perror("Error en malloc()");
exit(EXIT_FAILURE);
}
offs=0;
t0=time(NULL);
while(time(NULL)
buffer[offs]=0;
offs=(offs+pagesize)%size;
}
free(buffer);
exit(EXIT_SUCCESS);
}
Si paralelamente a la ejecución del código anterior, ejecutamos el devorador en otra terminal, podemos ver como el RSS decae:
23268 mem
pablo@linux-rt:# ps ax -o rss,ucmd | grep mem
17484 mem
No llega hasta el 10% de los 40000k que (más o menos) teníamos, pero se ve cómo la mitad de las páginas se han ido al espacio de intercambio.
Y ya para acabar liberamos la memoria reservada, visualizamos cómo disminuye el RSS:
188 mem
y finalizamos el programa.
Espero que con esto se hayan refrescado o dado a conocer algunos conceptos sobre gestión de memoria de los sistemas operativos, y más particularmente sobre el deferred page allocation de Linux, y sus ventajas e inconvenientes.
Ficheros: mem.c y devorador.c