Por qué Linux usa un archivo de intercambio, parte 2

La primera parte de una pequeña "estafa" sobre el subsistema de memoria virtual, la conexión de los mecanismos mmap, las bibliotecas compartidas y los cachés provocó una discusión tan acalorada que no pude resistirme a seguir investigando en la práctica.



Por eso, hoy lo haremos ... .Un minúsculo trabajo de laboratorio. En forma de un pequeño programa en C que escribimos, compilamos y probamos en acción, con y sin intercambio.



El programa hace algo muy simple: solicita una gran cantidad de memoria, accede a ella y trabaja activamente con ella. Para no sufrir con la carga de bibliotecas, simplemente crearemos un archivo grande que se mapeará en la memoria como lo hace el sistema al cargar bibliotecas compartidas.



Y simplemente emulamos la llamada del código de esta "biblioteca" leyendo de dicho archivo mmap.



El programa realizará varias iteraciones, en cada iteración accederá simultáneamente al "código" y una de las secciones de un gran segmento de datos.



Y, para no escribir código innecesario, definiremos dos constantes que determinarán el tamaño del "segmento de código" y el tamaño total de la RAM:



  • MEM_GBYTES: el tamaño de la RAM para la prueba
  • LIB_GBYTES - tamaño del "código"


La cantidad de "datos" que tenemos es menor que la cantidad de memoria física:



  • DATA_GBYTES = MEM_GBYTES - 2


La cantidad total de "código" y "datos" es ligeramente mayor que la cantidad de memoria física:



  • DATA_GBYTES + LIB_GBYTES = MEM_GBYTES + 1


Para una prueba en una computadora portátil, tomé MEM_GBYTES = 16 y obtuve las siguientes características:



  • MEM_GBYTES = 16
  • DATA_GBYTES = 14 - significa que "datos" serán 14 GB, es decir, "memoria suficiente"
  • Tamaño de intercambio = 16 GB


Texto del programa



#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
 
#define GB              1073741824l
 
#define MEM_SIZE        16
#define LIB_GBYTES      3
#define DATA_GBYTES     (MEM_SIZE - 2)
 
long random_read(char * code_ptr, char * data_ptr, size_t size) {
   long rbt = 0;
   for (unsigned long i=0 ; i<size ; i+=4096) {
       rbt += code_ptr[(8l * random() % size)] + data_ptr[i];
   }
   return rbt;
}
 
int main() {
   size_t libsize = LIB_GBYTES * GB;
   size_t datasize = DATA_GBYTES * GB;
   int fd;
   char * dataptr;
   char * libptr;
 
   srandom(256);
   if ((fd = open("library.bin", O_RDONLY)) < 0) {
       printf("Required library.bin of size %ld\n", libsize);
       return 1;
   }
 
   if ((libptr = mmap(NULL, libsize,
                     PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
       printf("Failed build libptr due %d\n", errno);
       return 1;
   }
 
   if ((dataptr = mmap(NULL, datasize,
                       PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS,
                       -1, 0)) == MAP_FAILED) {
       printf("Failed build dataptr due %d\n", errno);
       return 1;
   }
 
   printf("Preparing test ...\n");
   memset(dataptr, 0, datasize);
   printf("Doing test ...\n");
 
   unsigned long chunk_size = GB;
   unsigned long chunk_count = (DATA_GBYTES - 3) * GB / chunk_size;
   for (unsigned long chunk=0 ; chunk < chunk_count; chunk++) {
       printf("Iteration %d of %d\n", 1 + chunk, chunk_count);
       random_read(libptr, dataptr + (chunk * chunk_size), libsize);
   }
   return 0;
}

      
      





Prueba sin usar swap



Deshabilite el intercambio especificando vm.swappines = 0 y ejecute la prueba

$ time ./swapdemo 
Preparing test ...
Killed

real 0m6,279s
user 0m0,459s
sys 0m5,791s
      
      







¿Que pasó? El valor de intercambio = 0 deshabilitó el intercambio: las páginas anónimas ya no se insertan en él, es decir, los datos siempre están en la memoria. El problema es que los 2 GB restantes no eran suficientes para que Chrome y VSCode se ejecutaran en segundo plano, y el asesino de OOM eliminó el programa de prueba. Y al mismo tiempo, la falta de memoria enterró la pestaña de Chrome en la que escribí este artículo. Y no me gustó, incluso si el autoguardado funcionó. No me gusta cuando mis datos están enterrados.



Swap incluido



Establezca vm_swappines = 60 (predeterminado)

Ejecute la prueba:



$ time ./swapdemo 
Preparing test ...
Doing test ...
Iteration 1 of 11
Iteration 2 of 11
Iteration 3 of 11
Iteration 4 of 11
Iteration 5 of 11
Iteration 6 of 11
Iteration 7 of 11
Iteration 8 of 11
Iteration 9 of 11
Iteration 10 of 11
Iteration 11 of 11

real 1m55,291s
user 0m2,692s
sys 0m20,626s

      
      





Parte superior del fragmento:



Tasks: 298 total,   2 running, 296 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0,6 us,  3,1 sy,  0,0 ni, 85,7 id, 10,1 wa,  0,5 hi,  0,0 si,  0,0 st
MiB Mem :  15670,0 total,    156,0 free,    577,5 used,  14936,5 buff/cache
MiB Swap:  16384,0 total,  12292,5 free,   4091,5 used.   3079,1 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  10393 viking    20   0   17,0g  14,2g  14,2g D  17,3  93,0   0:18.78 swapdemo
    136 root      20   0       0      0      0 S   9,6   0,0   4:35.68 kswapd0

      
      





Linux malo, malo !!! Utiliza casi 4 gigabytes de intercambio, ¡aunque tiene 14 gigabytes de caché y 3 gigabytes disponibles! ¡Linux tiene una configuración incorrecta! Malo outlingo, malos viejos administradores, no entienden nada, dijeron que habilitaran el intercambio y ahora hacen que el sistema intercambie y funcione mal para mí. Es necesario desactivar el intercambio como aconsejan los expertos en Internet mucho más jóvenes y prometedores, ¡porque saben exactamente qué hacer!



Bueno ... que así sea. ¿Apaguemos el intercambio tanto como sea posible siguiendo el consejo de los expertos?



Prueba casi sin intercambio



Establecemos vm_swappines = 1



Este valor conducirá al hecho de que el intercambio de páginas anónimas se realizará solo si no hay otra salida.



Confío en Chris Down porque creo que es un gran ingeniero y sabe lo que dice cuando explica que el archivo de intercambio hace que el sistema funcione mejor. Por lo tanto, esperando que “algo” saliera mal y el sistema pudiera funcionar terriblemente ineficientemente, me aseguré de antemano y ejecuté el programa de prueba, limitándolo con un temporizador para ver al menos su terminación anormal.



Veamos primero la salida superior:



Tasks: 302 total,   1 running, 301 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0,2 us,  4,7 sy,  0,0 ni, 84,6 id, 10,0 wa,  0,4 hi,  0,0 si,  0,0 st
MiB Mem :  15670,0 total,    162,8 free,   1077,0 used,  14430,2 buff/cache
MiB Swap:  20480,0 total,  18164,6 free,   2315,4 used.    690,5 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   6127 viking    20   0   17,0g  13,5g  13,5g D  20,2  87,9   0:10.24 swapdemo
    136 root      20   0       0      0      0 S  17,2   0,0   2:15.50 kswapd0

      
      





¡¿Hurra ?! El intercambio se usa solo para 2.5 gigabytes, que es casi 2 veces menos que en la prueba con el intercambio habilitado (e intercambio = 60). Swap se usa menos. También hay menos memoria libre. Y probablemente podamos dar la victoria con seguridad a los jóvenes expertos. Pero aquí está lo extraño: nuestro programa nunca pudo completar ni siquiera 1 (¡UNA!) Iteración en 2 (¡DOS!) Minutos:



$ { sleep 120 ; killall swapdemo ; } &
[1] 6121
$ time ./swapdemo
Preparing test …
Doing test …
Iteration 1 of 11
[1]+  Done                    { sleep 120; killall swapdemo; }
Terminated

real	1m58,791s
user	0m0,871s
sys	0m23,998s
      
      





Repetimos: el programa no pudo completar 1 iteración en 2 minutos, aunque en la prueba anterior hizo 11 iteraciones en 2 minutos, es decir, con el intercambio casi deshabilitado, el programa se ejecuta más de 10 (!) Veces más lento.



Pero hay una ventaja: no se dañó ni una sola pestaña de Chrome. Y esto es bueno.



Prueba con swap completamente inhabilitante



¿Pero tal vez simplemente "aplastar" el intercambio a través del intercambio no es suficiente, y debería desactivarse por completo? Naturalmente, esta teoría también debería probarse. Vinimos aquí para realizar pruebas, ¿o qué?



Este es el caso ideal:



  • no tenemos intercambio y todos nuestros datos estarán garantizados en memoria
  • el intercambio no se usará ni siquiera accidentalmente, porque no está allí


Y ahora nuestra prueba terminará a la velocidad del rayo, los ancianos irán al lugar que se merecen y cambiarán los cartuchos, el camino para los jóvenes.



Desafortunadamente, el resultado de ejecutar el programa de prueba es similar: ni siquiera se ha completado una iteración.



Salida superior:



Tasks: 217 total,   1 running, 216 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0,0 us,  2,2 sy,  0,0 ni, 85,2 id, 12,6 wa,  0,0 hi,  0,0 si,  0,0 st
MiB Mem :  15670,0 total,    175,2 free,    331,6 used,  15163,2 buff/cache
MiB Swap:      0,0 total,      0,0 free,      0,0 used.    711,2 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    136 root      20   0       0      0      0 S  12,5   0,0   3:22.56 kswapd0
   7430 viking    20   0   17,0g  14,5g  14,5g D   6,2  94,8   0:14.94 swapdemo

      
      





Por qué está pasando esto



La explicación es muy simple: el "segmento de código" que conectamos a través de mmap (libptr) está en la caché. Por lo tanto, cuando prohibimos (o casi prohibimos) el intercambio de una manera u otra, no importa cómo, al deshabilitar físicamente el intercambio, o mediante vm.swappines = 0 | 1, siempre termina con el mismo escenario: vaciar el archivo mmap del caché y luego cargarlo desde el disco. Y las bibliotecas se cargan exactamente a través de mmap, y para verificar esto, solo necesita hacer ls -l / proc // map_files:



$ ls -l /proc/8253/map_files/ | head -n 10
total 0
lr-------- 1 viking viking 64   7 12:58 556799983000-55679998e000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64   7 12:58 55679998e000-5567999af000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64   7 12:58 5567999af000-5567999bf000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64   7 12:58 5567999c0000-5567999c4000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64   7 12:58 5567999c4000-5567999c5000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64   7 12:58 7fb22a033000-7fb22a062000 -> /usr/share/glib-2.0/schemas/gschemas.compiled
lr-------- 1 viking viking 64   7 12:58 7fb22b064000-7fb238594000 -> /usr/lib/locale/locale-archive
lr-------- 1 viking viking 64   7 12:58 7fb238594000-7fb2385a7000 -> /usr/lib64/gvfs/libgvfscommon.so
lr-------- 1 viking viking 64   7 12:58 7fb2385a7000-7fb2385c3000 -> /usr/lib64/gvfs/libgvfscommon.so

      
      





Y, como consideramos en la primera parte del artículo, el sistema, en condiciones de falta real de memoria, cuando el intercambio de páginas anónimas está deshabilitado, elegirá la única opción que dejó el propietario que deshabilitó el intercambio. Y esta opción está recuperando (liberando) páginas en blanco ocupadas por los datos de las bibliotecas cargadas en mmap.



Conclusión



El uso activo del método de distribución de software “Me llevo todo conmigo” (flatpak, snap, docker image) conduce al hecho de que la cantidad de código que se conecta a través de mmap aumenta significativamente.



Esto puede llevar al hecho de que el uso de "optimizaciones extremas" asociadas con la configuración / desactivación del intercambio puede producir efectos completamente inesperados, porque un archivo de intercambio es un mecanismo para optimizar el subsistema de memoria virtual en condiciones de presión de memoria, y la memoria disponible es no completamente "memoria no utilizada" sino la suma de la caché y la memoria libre.



Al deshabilitar el archivo de intercambio, no "elimina la opción incorrecta", sino que "no deja opciones".



Debe tener mucho cuidado al interpretar los datos de consumo de memoria del proceso: VSS y RSS. Representan "estado actual" y no "estado óptimo".



Si no desea que el sistema utilice el intercambio, agregue memoria, pero no desactive el intercambio . Deshabilitar el intercambio en los niveles de umbral hará que la situación sea mucho peor de lo que hubiera sido si el sistema se hubiera intercambiado un poco.



PD: En las discusiones, las preguntas se hacen regularmente "pero si habilita la compresión de memoria a través de zram ...". Sentí curiosidad y ejecuté las pruebas apropiadas: si habilita zram y swap, como se hace de forma predeterminada en Fedora, entonces el tiempo de ejecución se acelera a aproximadamente 1 minuto.



Pero la razón de esto es que las páginas con ceros se comprimen muy bien, por lo que en realidad los datos no se intercambian, sino que se almacenan comprimidos en la RAM. Si llena un segmento de datos con datos aleatorios, poco comprimibles, la imagen se volverá menos espectacular y el tiempo de ejecución de la prueba aumentará nuevamente a 2 minutos, lo que es comparable (e incluso ligeramente peor) que el de un archivo de intercambio "honesto".



All Articles