Por que o Linux usa um arquivo de troca, parte 2

A primeira parte de um pequeno "roubo" sobre o subsistema de memória virtual, a conexão de mecanismos mmap, bibliotecas compartilhadas e caches causou uma discussão tão acalorada que não resisti em continuar pesquisando na prática.



Portanto, hoje faremos .. Um minúsculo trabalho de laboratório. Na forma de um pequeno programa C que escrevemos, compilamos e testamos em ação - com e sem swap.



O programa faz uma coisa muito simples - solicita uma grande quantidade de memória, acessa-a e trabalha ativamente com ela. Para não sofrer com o carregamento de nenhuma biblioteca, vamos simplesmente criar um grande arquivo que será mapeado na memória da mesma forma que o sistema faz ao carregar bibliotecas compartilhadas.



E simplesmente emulamos a chamada do código desta "biblioteca" lendo esse arquivo mmap.



O programa fará várias iterações, a cada iteração acessará simultaneamente o “código” e uma das seções de um grande segmento de dados.



E, para não escrever código desnecessário, definiremos duas constantes que determinarão o tamanho do "segmento de código" e o tamanho total da RAM:



  • MEM_GBYTES - o tamanho da RAM para o teste
  • LIB_GBYTES - tamanho do "código"


A quantidade de "dados" que temos é menor que a quantidade de memória física:



  • DATA_GBYTES = MEM_GBYTES - 2


A quantidade total de "código" e "dados" é ligeiramente maior do que a quantidade de memória física:



  • DATA_GBYTES + LIB_GBYTES = MEM_GBYTES + 1


Para um teste em um laptop, peguei MEM_GBYTES = 16 e obtive as seguintes características:



  • MEM_GBYTES = 16
  • DATA_GBYTES = 14 - significa que "dados" serão 14 GB, ou seja, "memória suficiente"
  • Tamanho de troca = 16 GB


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

      
      





Teste sem usar troca



Desative a troca especificando vm.swappines = 0 e execute o teste

$ time ./swapdemo 
Preparing test ...
Killed

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







O que aconteceu? O valor de troca = 0 desabilitou a troca - páginas anônimas não são mais inseridas nela, ou seja, os dados estão sempre na memória. O problema é que os 2 GB restantes não foram suficientes para o Chrome e o VSCode rodarem em segundo plano, e o OOM-killer matou o programa de teste. E, ao mesmo tempo, a falta de memória enterrou a guia do Chrome em que escrevi este artigo. E eu não gostei - mesmo se o salvamento automático funcionou. Não gosto quando meus dados são enterrados.



Troca incluída



Defina vm_swappines = 60 (padrão)

Execute o teste:



$ 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 do 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 ruim, ruim !!! Ele usa quase 4 gigabytes de swap, embora tenha 14 gigabytes de cache e 3 gigabytes disponíveis! Linux tem configurações erradas! Outlingo ruim, administradores antigos ruins, eles não entendem nada, disseram para habilitar a troca e agora fazem a troca do sistema e funcionam mal para mim. É necessário desabilitar a troca conforme aconselhado por especialistas em Internet muito mais jovens e promissores, porque eles sabem exatamente o que fazer!



Bem ... assim seja. Vamos desligar a troca o máximo possível seguindo os conselhos dos especialistas?



Teste quase sem troca



Definimos vm_swappines = 1



Este valor levará ao fato de que a troca de páginas anônimas será realizada somente se não houver outra saída.



Confio em Chris Down porque acho ele um grande engenheiro e sabe o que diz quando explica que o arquivo de troca melhora o desempenho do sistema. Portanto, esperando que “algo” “desse errado” e o sistema funcionasse de maneira terrivelmente ineficiente, me certifiquei com antecedência e executei o programa de teste, limitando-o com um temporizador para ver pelo menos seu encerramento anormal.



Vejamos primeiro o resultado principal:



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

      
      





Hooray ?! A troca é usada apenas para 2,5 gigabytes, o que é quase 2 vezes menos do que no teste com a troca habilitada (e troca = 60). A troca é menos usada. Também há menos memória livre. E provavelmente podemos dar a vitória com segurança aos jovens especialistas. Mas aqui está a coisa estranha - nosso programa nunca foi capaz de completar 1 (UMA!) Iteração em 2 (DOIS!) 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 - o programa não conseguiu completar 1 iteração em 2 minutos, embora no teste anterior tenha feito 11 iterações em 2 minutos - ou seja, com a troca quase desabilitada, o programa roda mais de 10 (!) Vezes mais lento.



Mas há uma vantagem - nem uma única guia do Chrome foi danificada. E isso é bom.



Teste com a desativação completa da troca



Mas talvez apenas "esmagar" a troca por meio de troca não seja suficiente, e ela deve ser completamente desativada? Naturalmente, essa teoria também deve ser testada. Viemos aqui para fazer testes ou o quê?



Este é o caso ideal:



  • não temos swap e todos os nossos dados serão garantidos na memória
  • a troca não será usada nem mesmo acidentalmente, porque não está lá


E agora nosso teste vai terminar na velocidade da luz, os velhos vão para o lugar que merecem e vão trocar os cartuchos - o caminho dos jovens.



Infelizmente, o resultado da execução do programa de teste é semelhante - nem mesmo uma iteração foi concluída.



Resultado principal:



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 que isso está acontecendo



A explicação é muito simples - o “segmento de código” que conectamos via mmap (libptr) está no cache. Portanto, quando proibimos (ou quase proibimos) a troca de uma forma ou de outra, não importa como - desativando fisicamente a troca ou por meio de vm.swappines = 0 | 1 - sempre termina com o mesmo cenário - liberando o arquivo mmap do cache e depois carregá-lo do disco. E as bibliotecas são carregadas exatamente por meio do mmap e, para verificar isso, você só precisa fazer 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

      
      





E, como consideramos na primeira parte do artigo, o sistema, em condições de real falta de memória, quando a troca de páginas anônimas estiver desabilitada, irá escolher a única opção que foi deixada pelo proprietário que desabilitou a troca. E esta opção está recuperando (liberando) páginas em branco ocupadas pelos dados de bibliotecas carregadas por mmap.



Conclusão



O uso ativo do método de distribuição de software “Eu levo tudo comigo” (flatpak, snap, imagem docker) leva ao fato de que a quantidade de código conectado via mmap aumenta significativamente.



Isso pode levar ao fato de que o uso de "otimizações extremas" associadas à configuração / desativação da troca pode levar a efeitos completamente inesperados, porque um arquivo de troca é um mecanismo para otimizar o subsistema de memória virtual sob condições de pressão de memória, e a memória disponível é completamente não "memória não utilizada", mas a soma do cache e da memória livre.



Ao desativar o arquivo de troca, você não "remove a opção errada", mas "não deixa opções"



Você deve ter muito cuidado ao interpretar os dados de consumo de memória do processo - VSS e RSS. Eles representam o "estado atual" e não o "estado ideal".



Se você não quiser que o sistema use a troca, adicione memória a ele, mas não desative a troca . Desativar a troca em níveis de limite tornará a situação muito pior do que teria sido se o sistema tivesse sido trocado um pouco.



PS: Nas discussões, perguntas são feitas regularmente "mas se você ativar a compactação de memória via zram ...". Fiquei curioso e executei os testes apropriados: se você habilitar zram e trocar, como é feito por padrão no Fedora, o tempo de execução acelera para cerca de 1 minuto.



Mas a razão para isso é que as páginas com zeros são muito bem compactadas, então, na verdade, os dados não vão para a troca, mas são armazenados em uma forma compactada na RAM. Se você preencher um segmento de dados com dados aleatórios e pouco compactáveis, a imagem se tornará menos espetacular e o tempo de execução do teste aumentará novamente para 2 minutos, o que é comparável (e até um pouco pior) do que um arquivo de troca "honesto".



All Articles