Conhecendo o kernel Linux pelo /proc (parte 5) – Recursos da memória virtual
No post anterior vimos comportamentos relacionados ao uso de memória de virtual, como a “sobre-alocação” de memória, uso de SWAP, estouro de memória RAM e o que pode ocorrer em máquinas com uma grande quantidade de memória RAM. Neste post vamos abordar recursos disponíveis através da memória virtual, como a memória compartilhada, o Page Cache e a abstração da arquitetura NUMA.
Memória compartilhada
Apesar da memória virtual isolar a área de memória de processos distintos, existem situações onde diferentes processos precisam se comunicar e compartilhar recursos entre si. Nestes cenários o kernel oferece o recurso de memória compartilhada, que permite que uma determinada região da memória seja compartilhada entre diversos processos.
Existem diversas formas de realizar o compartilhamento de memória no Linux, no entanto, para simplificar a implementação interna do Kernel, todos os métodos acabam realizando o mapeamento de um arquivo em memória, seja de forma explícita, ou implícita através de arquivos nos filesystems do tipo “tmpfs”.
MMAP com arquivo mapeado
A forma mais simples de compartilhar memória entre processos é abrir o mesmo arquivo em dois processos e realizar o mapeamento do arquivo em memória através da chamada “mmap” com a flag MAP_SHARED.
Para mostrar como isso funciona criamos o “shared_mem_mmap.c”, que abre um arquivo cujo o nome é definido através do seu primeiro parâmetro de execução, e em seguida é mapeado em memória com a flag “MAP_SHARED”. A ideia deste exemplo é simular uma situação onde diversos processos precisam compartilhar uma mesma configuração através da memória e por isso, assim que iniciado ele entra em looping onde o usuário pode alterar a “configuração”, visualizá-la ou encerrar sua execução.
Outro ponto importante, é que logo após a visualização da “configuração” forçamos a leitura de toda área de memória somente com propósito de forçar a alocação completa e assim poder observar como ela se comporta no “/proc”.
#include <stdio.h> #include <sys/mman.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main (int argc, char* argv[]){ char *ptr, tmp; char option; struct stat st; int fd; size_t count=0; fd = open(argv[1],O_RDWR); if( fd < 0 ){ printf("Falha ao abrir o arquivo\n"); return 1; } stat(argv[1],&st); ptr=mmap(NULL,st.st_size,PROT_WRITE,MAP_SHARED,fd,0); if( ptr == MAP_FAILED ){ printf("Falha ao realizar o mapeamento\n"); return 1; } while(1){ printf("Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n"); printf("Digite a opçao:"); option= (char) getchar(); while ((getchar()) != '\n'); switch(option){ case '1': printf("Configuração atual:\n"); printf("%s\n",ptr); for(count=0;count < st.st_size;count++){ tmp = *(ptr+count); } break; case '2': printf("Digite a nova configuração:\n"); fgets(ptr,st.st_size,stdin); printf("\n"); break; case '3': printf("\nEncerrando\n"); break; default : printf("\nInvalid Option"); } if(option == '3'){ break; } } munmap(ptr,st.st_size); close(fd); }
Para realizar os testes com o nosso programa, inicialmente vamos gerar o nosso arquivo de “configuração”, denominado “file.cfg”, com 100MB através da ferramenta “dd” e em seguida vamos executá-lo em um terminal conforme os passos abaixo:
$ gcc shared_mem_mmap.c -o shared_mem_mmap.o $ dd if=/dev/zero of=file.cfg bs=1048576 count=100 100+0 records in 100+0 records out 104857600 bytes (105 MB, 100 MiB) copied, 0,286279 s, 366 MB/s $ free -m total used free shared buff/cache available Mem: 19947 2970 10354 667 6622 15949 Swap: 2047 0 2047 $ ./shared_mem_mmap.o file.cfg Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:2 Digite a nova configuração: iniciando config Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: iniciando config Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Agora, em um segundo terminal, vamos consultar o processo “free -m” e o “/proc/PID/status” para verificar como está seu consumo de memória:
# free -m total used free shared buff/cache available Mem: 19947 2973 10237 667 6736 15945 Swap: 2047 0 2047 # cat /proc/$(pgrep shared_mem_mmap)/status | egrep 'Vm|Rss' VmPeak: 106908 kB VmSize: 106908 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 101716 kB VmRSS: 101716 kB RssAnon: 80 kB RssFile: 101636 kB RssShmem: 0 kB VmData: 176 kB VmStk: 132 kB VmExe: 4 kB VmLib: 2112 kB VmPTE: 256 kB VmSwap: 0 kB
Na saída acima, vemos um comportamento curioso, mesmo após utilizarmos os 100MB de memória, o “free” ainda mostra aproximadamente a mesma quantidade de memória disponível (available) e utilizada (used). Isso ocorre, pois o arquivo é carregado em memória através do “Page Cache”, onde o uso de memória é computado como cache e é exibido através da coluna “buff/cache” no “free”.
Através do campo VmRSS podemos ver que nosso processo está consumindo por volta 100MB de memória RAM e através do campo RssFile podemos ver que praticamente todo este consumo se trata do arquivo mapeado em memória. Neste caso sabemos qual é o arquivo responsável por este consumo, porém, na vida real, poderíamos descobrir qual é o arquivo analisando conteúdo do “/proc/PID/smaps”, conforme exemplo abaixo:
# cat /proc/$(pgrep shared_mem_mmap)/smaps | egrep -B4 -i 'Rss' 56015416f000-560154170000 r-xp 00000000 08:02 114426097 /home/william/Documents/memory-tests/shared_mem_mmap.o Size: 4 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 4 kB -- .... -- 56015507c000-56015509d000 rw-p 00000000 00:00 0 [heap] Size: 132 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 4 kB -- 7f5bed860000-7f5bf3c60000 -w-s 00000000 08:02 114426317 /home/william/Documents/memory-tests/file.cfg Size: 102400 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 102400 kB -- 7f5bf3c60000-7f5bf3e47000 r-xp 00000000 08:02 10223863 /lib/x86_64-linux-gnu/libc-2.27.so Size: 1948 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 1248 kB -- ... -- 7ffed7d27000-7ffed7d48000 rw-p 00000000 00:00 0 [stack] Size: 132 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 12 kB -- 7ffed7da9000-7ffed7dac000 r--p 00000000 00:00 0 [vvar] Size: 12 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 0 kB -- 7ffed7dac000-7ffed7dae000 r-xp 00000000 00:00 0 [vdso] Size: 8 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 4 kB -- ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] Size: 4 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 0 kB # cat /proc/$(pgrep shared_mem_mmap)/smaps | egrep -A20 -i 'file.cfg' 7f5bed860000-7f5bf3c60000 -w-s 00000000 08:02 114426317 /home/william/Documents/memory-tests/file.cfg Size: 102400 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 102400 kB Pss: 102400 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 102400 kB Private_Dirty: 0 kB Referenced: 102396 kB Anonymous: 0 kB LazyFree: 0 kB AnonHugePages: 0 kB ShmemPmdMapped: 0 kB Shared_Hugetlb: 0 kB Private_Hugetlb: 0 kB Swap: 0 kB SwapPss: 0 kB Locked: 0 kB VmFlags: wr sh mr mw me ms sd
Na execução acima, inicialmente procuramos pelo arquivo com maior consumo de memória e em seguida realizamos um novo filtro exibindo somente o estado do mapeamento deste arquivo.
De posse dos dados presentes no “smaps” podemos concluir que:
- O “file.cfg” é o maior consumidor de memória, pois tem o maior valor de “Rss”
- O “file.cfg” é o maior arquivo mapeado, pois tem o maior valor no campo “Size”
- O “file.cfg” foi acessado em quase sua totalidade, já que o campo “Referenced” é muito próximo ao campo “Size”
- O “file.cfg” deve estar sendo acessado somente por este programa, pois o campo PSS é igual ao tamanho do arquivo
- O mapeamento do “file.cfg” é do tipo compartilhado com permissão escrita, pois possui as flags “sh” e “wr” no campo “VmFlags”
Vale explicar que o campo PSS tenta dar uma ideia de proporcionalidade do uso de memória RAM por processo quando temos uso de memória compartilhada. Ou seja, ele basicamente irá dividir o tamanho de memória mapeada pelo número de processos que a compartilham. No nosso exemplo temos apenas um processo acessando a área de memória compartilhada, por isso o PSS é igual ao tamanho da área de memória.
Agora, em um terceiro terminal, vamos executar mais um processo do “shared_mem_mmap” apontando para o mesmo arquivo de “configuração”:
$ ./shared_mem_mmap.o file.cfg Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: iniciando config Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Neste ponto, podemos ver que, de fato, a memória esta sendo compartilhada, pois, ao selecionar a opção “Exibir a configuração” temos o mesmo conteúdo inserido no processo do primeiro terminal. Agora, só para validar o funcionamento do processo inverso, vamos alterar a “configuração” através do processo do terceiro terminal:
Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:2 Digite a nova configuração: NOVA CONFIGURACAO Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Agora, se visualizarmos o conteúdo no primeiro terminal, teremos a “configuração” atualizada:
Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: NOVA CONFIGURACAO Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Outro ponto que podemos observar, é o valor do campo PSS no /proc/PID/smaps, que agora deve ser a metade do tamanho da memória, já que temos 2 processos compartilhando este mesmo segmento:
cat /proc/$(pgrep --oldest shared_mem_mmap)/smaps | egrep -A20 -i 'file.cfg' 7f5bed860000-7f5bf3c60000 -w-s 00000000 08:02 114426317 /home/william/Documents/memory-tests/file.cfg Size: 102400 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 102400 kB Pss: 51202 kB Shared_Clean: 102396 kB Shared_Dirty: 0 kB Private_Clean: 4 kB Private_Dirty: 0 kB Referenced: 102400 kB Anonymous: 0 kB LazyFree: 0 kB AnonHugePages: 0 kB ShmemPmdMapped: 0 kB Shared_Hugetlb: 0 kB Private_Hugetlb: 0 kB Swap: 0 kB SwapPss: 0 kB Locked: 0 kB VmFlags: wr sh mr mw me ms sd
MMAP com mapeamento anônimo
O MMAP também nos permite realizar o compartilhamento de memória através de um mapeamento anônimo, no entanto, neste caso o compartilhamento só será possível com processos filhos. Para realizar este tipo de mapeamento basta incluir a flag MAP_SHARED assim como foi feito no método anterior.
Para mostrar como isso funciona criamos o “shared_mem_mmap_anonymous.c”, onde realizamos um mapeamento de memória anônimo de 100MB com a flag “MAP_SHARED”. A ideia deste exemplo é simular uma situação onde temos um processo pai que efetua a gerência de “configurações” e um processo filho responsável por gerar logs. Neste caso o nosso processo de log será responsável por registrar em log as “configurações” atuais a cada 5 segundos. Assim como no exemplo anterior, no momento em que é iniciado, ele entra em looping onde o usuário pode alterar a “configuração”, visualizá-la ou encerrar sua execução.
#include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> int main (int argc, char* argv[]){ char *ptr; char option; size_t size = 1024*1024*100; int pid; ptr=mmap(NULL,size,PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0); if( ptr == MAP_FAILED ){ printf("Falha ao realizar o mapeamento\n"); return 1; } pid=fork(); if(pid>0){ while(1){ printf("Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n"); printf("Digite a opçao:"); option= (char) getchar(); while ((getchar()) != '\n'); switch(option){ case '1': printf("Configuração atual:\n"); printf("%s\n",ptr); break; case '2': printf("Digite a nova configuração:\n"); fgets(ptr,size,stdin); printf("\n"); break; case '3': printf("\nEncerrando\n"); kill(pid,SIGTERM); break; default : printf("\nInvalid Option"); } if(option == '3'){ break; } } }else if(pid==0){ FILE* log; log = fopen("/tmp/memory.log", "w+"); if(log==NULL){ printf("Falha ao abrir arquivo de log\n"); return 1; } fprintf(log,"Iniciando Logger\n"); fflush(log); while(1){ sleep(5); fprintf(log,"%s\n",ptr); fflush(log); } fclose(log); }else{ printf("Falha ao criar processo\n"); return 1; } munmap(ptr,size); }
Para realizar os testes com o nosso programa, inicialmente vamos executá-lo em um terminal e observar seus logs em um segundo terminal, conforme os passos abaixo:
$ free -m total used free shared buff/cache available Mem: 19947 3087 8567 731 8293 15768 Swap: 2047 0 2047 $ ./shared_mem_mmap_anonymous.o Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:2 Digite a nova configuração: nova config Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar
Agora, em um segundo terminal vamos validar a presença do processo filho, verificar se o log está funcionando corretamente e como está o uso de memória:
# free -m total used free shared buff/cache available Mem: 19947 3086 8465 831 8394 15668 Swap: 2047 0 2047 # ps auxf | grep mmap william 4944 0.0 0.5 106908 103748 pts/1 S+ 22:56 0:00 | | \_ ./shared_mem_mmap_anonymous.o william 4945 0.0 0.0 106908 72 pts/1 S+ 22:56 0:00 | | \_ ./shared_mem_mmap_anonymous.o # cat /tmp/memory.log Iniciando Logger nova config nova config nova config # cat /proc/$(pgrep -o shared_mem_mmap)/status | egrep 'Vm|Rss' VmPeak: 106908 kB VmSize: 106908 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 103748 kB VmRSS: 103748 kB RssAnon: 80 kB RssFile: 1324 kB RssShmem: 102344 kB VmData: 176 kB VmStk: 132 kB VmExe: 4 kB VmLib: 2112 kB VmPTE: 252 kB VmSwap: 0 kB
Através da saída acima, podemos ver que temos um processo filho sendo executado e que o log está sendo gerado corretamente no arquivo “/tmp/memory.log”. Outro ponto que podemos observar aqui, é que neste caso é possível diferenciar essa área de memória compartilhada da área de “Page Cache” utilizada pelos arquivos mapeados em memória, isso é visto no campo RssShmem no “/proc/PID/status” e no free através do campo “shared”.
Essa diferenciação de uso é possível, pois, para realizar este tipo de compartilhamento o Kernel cria um arquivo a partir do “/dev/zero” e o armazena no “tmpfs”, em um ponto de montagem restrito ao Kernel (não visível ao VFS), e que pode pode ter seu consumo monitorado através do campo “Shmem” do “/proc/meminfo” conforme os comandos abaixo:
# grep 'Shmem:' /proc/meminfo Shmem: 850944 kB # cat /proc/$(pgrep --oldest shared_mem_mmap)/smaps | egrep -A20 -i 'zero' 7f90484eb000-7f904e8eb000 -w-s 00000000 00:05 161584 /dev/zero (deleted) Size: 102400 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 102400 kB Pss: 102398 kB Shared_Clean: 0 kB Shared_Dirty: 4 kB Private_Clean: 102396 kB Private_Dirty: 0 kB Referenced: 102400 kB Anonymous: 0 kB LazyFree: 0 kB AnonHugePages: 0 kB ShmemPmdMapped: 0 kB Shared_Hugetlb: 0 kB Private_Hugetlb: 0 kB Swap: 0 kB SwapPss: 0 kB Locked: 0 kB VmFlags: wr sh mr mw me ms sd
SysVIPC
Outra maneira de realizar compartilhamento de memória é através do System V IPC, que é um mecanismo de comunicação interprocessos disponível na maior parte do sistemas Unix. Além de memória compartilhada ele também permite o uso de filas de mensagens e semáforos.
O SysVIPC nos permite compartilhar um determinado segmento de memória entre diversos processos. Para isso, ele fornece as chamadas “shmget” e “shmat”, que nos permitem criar e anexá-las a um determinado segmento de memória.
Para ilustrar como isso funciona criamos o “shared_mem_sysvipc.c”, onde realizamos criação de um segmento de memória identificado pelo número 334, de 100MB através do “shmget” e em seguida anexamos esse segmento através da “shmat”. Neste caso podemos iniciar diversos processos, pois como todos utilizam a mesma chave “334”, todos utilizarão o mesmo segmento de memória. Assim como no exemplo anterior, no momento que é iniciado, ele entra em looping e o usuário pode alterar a “configuração”, visualizá-la ou encerrar sua execução.
#include <stdio.h> #include <sys/mman.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> int main (int argc, char* argv[]){ char *ptr, tmp; char option; int shm_id; size_t size = 1024*1024*100; size_t count; shm_id=shmget(334,size,IPC_CREAT|0666); if( shm_id < 0 ){ printf("Falha alocacao de memoria \n"); return 1; } ptr = shmat(shm_id,NULL,0); if( (void *) ptr < 0 ){ printf("Falha em atachar memoria compartilhada \n"); return 1; } while(1){ printf("Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n"); printf("Digite a opçao:"); option= (char) getchar(); while ((getchar()) != '\n'); switch(option){ case '1': printf("Configuração atual:\n"); printf("%s\n",ptr); for(count=0;count<size;count++){ tmp = *(ptr+count); } break; case '2': printf("Digite a nova configuração:\n"); fgets(ptr,size-1024,stdin); printf("\n"); break; case '3': printf("\nEncerrando\n"); break; default : printf("\nInvalid Option"); } if(option == '3'){ break; } } shmdt(ptr); shmctl(shm_id,IPC_RMID,NULL); }
Para realizar os testes com o nosso programa, inicialmente vamos executá-lo em um terminal e observar seus logs em um segundo terminal, conforme os passos abaixo:
$ gcc shared_mem_sysvipc.c -o shared_mem_sysvipc.o $ free -m total used free shared buff/cache available Mem: 19947 3747 2610 1161 13589 14679 Swap: 2047 0 2047 $ ./shared_mem_sysvipc.o Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:2 Digite a nova configuração: Teste Config Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Agora, em um segundo terminal, vamos executar mais um processo do nosso programa e exibir o conteúdo da “configuração atual” para validar se a memória realmente está compartilhada:
$ ./shared_mem_sysvipc.o Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: Teste Config Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Com as duas instâncias do nosso programa em execução, em um terceiro terminal, vamos verificar como ficou o consumo de memória:
# free -m total used free shared buff/cache available Mem: 19947 3752 2470 1261 13724 14573 Swap: 2047 0 2047 # grep 'Shmem:' /proc/meminfo Shmem: 1292200 kB # cat /proc/$(pgrep --oldest shared_mem_ )/smaps | egrep -A20 -i 'deleted' 7f3c11582000-7f3c17982000 rw-s 00000000 00:05 3440674 /SYSV0000014e (deleted) Size: 102400 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 102400 kB Pss: 51200 kB Shared_Clean: 102396 kB Shared_Dirty: 4 kB Private_Clean: 0 kB Private_Dirty: 0 kB Referenced: 102400 kB Anonymous: 0 kB LazyFree: 0 kB AnonHugePages: 0 kB ShmemPmdMapped: 0 kB Shared_Hugetlb: 0 kB Private_Hugetlb: 0 kB Swap: 0 kB SwapPss: 0 kB Locked: 0 kB VmFlags: rd wr sh mr mw me ms sd
Conforme podemos ver acima, neste caso também é possível identificar o uso da memória compartilhada através do campo “shared” do free e através do campo “Shmem” do “/proc/meminfo”. A principal diferença neste caso é que o arquivo não é gerado a partir do “/dev/zero”, e sim, gerado a partir de um arquivo com cuja nomeclatura é “SYSV” seguido do identificador do segmento, que no nosso caso é 334, 14e em hexadecimal.
Outro ponto importante, é que no caso do SysVIPC, existem alguns limites configuráveis através do “/proc”, que são:
- /proc/sys/kernel/shmmax: define o tamanho máximo de um segmento de memória compartilhado
- /proc/sys/kernel/shmall: define a quantidade máxima de memória a ser utilizada em todo o sistema por segmentos de memória compartilhados
- /proc/sys/kernel/shmmni: define a quantidade máxima de segmentos de memória compartilhados no sistema
Na execução abaixo, vemos que, devido ao programa utilizar um segmento de 100MB de memória, se reduzirmos o limite para 90MB teremos falha na alocação de memória:
# cat /proc/sys/kernel/shmmax 18446744073692774399 # echo 94371840 > /proc/sys/kernel/shmmax # ./shared_mem_sysvipc.o Falha alocacao de memoria # echo 18446744073692774399 > /proc/sys/kernel/shmmax # ./shared_mem_sysvipc.o Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:3 Encerrando
POSIX
Por fim, ainda temos o compartilhamento de memória através do padrão POSIX, que consiste em abrir/criar um “pseudo” arquivo através da chamada “shm_open” e em seguida mapeá-lo em memória através da chamada “mmap” com a flag “MAP_SHARED”.
Para demonstrar seu funcionamento criamos o “shared_mem_posix.c”, que abre/cria um “pseudo” arquivo através do “shm_open” cujo o nome é definido através do seu primeiro parâmetro de execução, em seguida definimos seu tamanho através da chamada “ftruncate” e por fim realizamos o mapeamento em memória através da chamada “mmap” com a flag “MAP_SHARED”. Assim como nos exemplos anteriores, a ideia deste exemplo é simular uma situação onde diversos processos precisam compartilhar uma configuração através da memória e por isso, assim que iniciado ele entra em looping onde o usuário poderá alterar a “configuração”, visualizá-la ou encerrar sua execução.
#include <stdio.h> #include <sys/mman.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main (int argc, char* argv[]){ char *ptr, tmp; char option; size_t size = 1024*1024*100; size_t count; int fd; fd = shm_open(argv[1],O_RDWR|O_CREAT,S_IRWXU); if( fd <0 ){ printf("Falha ao abrir o arquivo\n"); return 1; } ftruncate(fd,size); ptr=mmap(NULL,size,PROT_WRITE,MAP_SHARED,fd,0); if( ptr == MAP_FAILED ){ printf("Falha ao realizar o mapeamento\n"); return 1; } while(1){ printf("Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n"); printf("Digite a opçao:"); option= (char) getchar(); while ((getchar()) != '\n'); switch(option){ case '1': printf("Configuração atual:\n"); printf("%s\n",ptr); for(count=0;count<size;count++){ tmp = *(ptr+count); } break; case '2': printf("Digite a nova configuração:\n"); fgets(ptr,size,stdin); printf("\n"); break; case '3': printf("\nEncerrando\n"); break; default : printf("\nInvalid Option"); } if(option == '3'){ break; } } munmap(ptr,size); shm_unlink(argv[1]); }
Para realizar os testes com o nosso programa, vamos executá-lo inicialmente em um primeiro terminal, conforme os passos abaixo:
$ gcc shared_mem_posix.c -o shared_mem_posix.o -lrt $ free -m total used free shared buff/cache available Mem: 19947 8971 1333 1410 9641 9203 Swap: 2047 65 1982 $ ./shared_mem_posix.o blog4linux Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:2 Digite a nova configuração: ConfigV2 Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: ConfigV2 Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Agora, em um segundo terminal, vamos executar mais um processo do nosso programa e exibir o conteúdo da “configuração atual” para validar se a memória realmente está compartilhada:
$ ./shared_mem_posix.o blog4linux Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:1 Configuração atual: ConfigV2 Acoes: (1)Exibir configuração atual (2)Alterar a configuração (3)Encerrar Digite a opçao:
Em um terceiro terminal, vamos analisar o uso de memória deste meétodo de compartilhamento de memória:
$ free -m total used free shared buff/cache available Mem: 19947 8975 1229 1510 9742 9100 Swap: 2047 65 1982 $ grep 'Shmem:' /proc/meminfo Shmem: 1547044 kB $ cat /proc/$(pgrep -o shared_mem_)/status | egrep 'Vm|Rss' VmPeak: 111172 kB VmSize: 111172 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 101768 kB VmRSS: 101768 kB RssAnon: 112 kB RssFile: 1504 kB RssShmem: 100152 kB VmData: 212 kB VmStk: 132 kB VmExe: 4 kB VmLib: 2244 kB VmPTE: 260 kB VmSwap: 0 kB $ cat /proc/$(pgrep --oldest shared_mem_ )/smaps | egrep -A20 -i 'blog4linux' 7fd420ed2000-7fd4272d2000 -w-s 00000000 00:18 679 /dev/shm/blog4linux Size: 102400 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Rss: 102400 kB Pss: 51200 kB Shared_Clean: 102396 kB Shared_Dirty: 4 kB Private_Clean: 0 kB Private_Dirty: 0 kB Referenced: 102400 kB Anonymous: 0 kB LazyFree: 0 kB AnonHugePages: 0 kB ShmemPmdMapped: 0 kB Shared_Hugetlb: 0 kB Private_Hugetlb: 0 kB Swap: 0 kB SwapPss: 0 kB Locked: 0 kB VmFlags: wr sh mr mw me ms sd $ ls -lh /proc/$(pgrep --oldest shared_mem_ )/fd total 0 lrwx------ 1 william william 64 dez 4 01:24 0 -> /dev/pts/15 lrwx------ 1 william william 64 dez 4 01:24 1 -> /dev/pts/15 lrwx------ 1 william william 64 dez 4 01:24 2 -> /dev/pts/15 lrwx------ 1 william william 64 dez 4 01:24 3 -> /dev/shm/blog4linux $ ls -lh /dev/shm/blog4linux -rwx------ 1 william william 100M dez 4 01:17 /dev/shm/blog4linux $ mount | grep shm tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) $ cat /dev/shm/blog4linux ConfigV3
Como podemos ver na saída acima, seu comportamento é bem similar ao dos métodos anteriores, onde é possível identificar o uso de memória compartilhada através de “shared” do free e do campo “Shmem” no “/proc/meminfo”. A grande diferença neste caso, é que o nosso pseudo arquivo “blog4linux”, é criado em um tmpfs que se encontra montado em “/dev/shm”, e que é visível para os usuários.
Observações:
Um ponto importante a se observar é que em todos os casos é possível ver que o “free” também mostra um aumento do consumo de memória na coluna “buffer/cache”. Isso ocorre devido a todos os métodos trabalharem utilizando arquivos carregados em memória, seja de forma implícita ou explícita, de forma que essas páginas de memória acabam sendo gerenciadas pelo “Page Cache”.
Page cache
O “Page Cache” é um recurso do Kernel destinado a “cachear” arquivos em memória de forma transparente e dinâmica, ou seja, toda vez que realizamos um mapeamento de arquivo em memória, ou que realizamos a leitura/escrita de um arquivo em disco, o Kernel irá armazenar o arquivo em memória – sempre que existir memória RAM disponível no sistema.
Esse recurso, além de evitar leituras desnecessárias em disco, também permite a escrita atrasada no disco, pois a escrita é realizada primeiramente nas páginas do “Page Cache”, as quais posteriormente serão efetivamente escritas em disco.
As páginas contendo dados ainda não escritos em disco disco são chamadas de “página sujas”, e podem ser “limpas”(escritas efetivamente em disco) de diversas formas: por tempo máximo de execução, de forma explícita por um chamada “fsync” na aplicação , pelo comando sync, ou por estar próxima dos seus limites de armazenamento de páginas sujas.
Vale lembrar que mesmo que a área utilizada alocada pelo “Page Cache” seja grande, apenas as páginas sujas realmente importam para efeitos de uso de memória, pois as páginas limpas podem ser liberadas instantaneamente caso o sistema necessite de memória.
Por ser dinâmico e ter a tendência de utilizar toda a memória disponível no sistema operacional, este recurso causava muita confusão a respeito do uso real de memória em distribuições mais antigas, onde o comando “free” não exibia a coluna “available”, de forma que muitos acreditavam que a memória disponível no sistema era exibida na coluna “free”, e que seu sistema estava constantemente no limite de memória.
Para acompanhar o consumo de memória do “Page Cache” em mais detalhes podemos inspecionar o “/proc/meminfo” conforme os comandos abaixo:
# grep 'Cached' /proc/meminfo Cached: 7573200 kB # grep 'Dirty' /proc/meminfo Dirty: 3884 kB
No comando acima podemos ver que, apesar de termos aproximadamente 7,2GB de memória sendo utilizada para o cache, apenas 3,7MB deles são páginas sujas, o que significa que temos praticamente 7GB de memória disponível para o sistema caso seja necessário.
Para efeito de execução de testes de desempenho, o Kernel permite realizar um descarte das página limpas do cache de maneira forçada escrevendo o número “1” no “/proc/sys/vm/drop_caches”.
# grep 'Cached' /proc/meminfo Cached: 5586108 kB # echo 1 > /proc/sys/vm/drop_caches # grep 'Cached' /proc/meminfo Cached: 2637628 kB
No exemplo acima vimos que após realizar o “drop_caches”, o “Page Cache” foi reduzido de aproximadamente 5,3GB para 2,6GB. A redução normalmente não é total pois temos as páginas sujas e também devido aos mecanismos de memória compartilhada utilizarem esta área de memória para armazenar seus segmentos de memória.
Somente para se ter uma ideia da diferença de desempenho entre a leitura de um arquivo em “cache” e outro sem “cache” criamos o “file_reader.c”, que apenas realiza a leitura completa de um arquivo definido por parâmetro:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> int main(int argc, char *argv[]){ int fd; char buff[100]; fd=open(argv[1],O_RDWR); while(read(fd,buff,100)){ } }
Para realizar nosso teste de desempenho, inicialmente iremos criar um arquivo de 100MB através do “dd” e seguida iremos executar o “file_reader.c” duas vezes com o comando time, sendo a segunda execução após uma limpeza do “Page Cache”, conforme mostrado nos comandos abaixo:
$ gcc file_reader.c -o file_reader.o $ sudo su # dd bs=1048576 count=100 if=/dev/zero of=file 100+0 records in 100+0 records out 104857600 bytes (105 MB, 100 MiB) copied, 0,358191 s, 293 MB/s # time ./file_reader.o file real 0m0,707s user 0m0,341s sys 0m0,360s # echo 1 > /proc/sys/vm/drop_caches # time ./file_reader.o file real 0m5,608s user 0m0,766s sys 0m0,973s
Na execução acima é bem claro o ganho desempenho, quando temos o cache populado: a leitura levou apenas aproximadamente 700ms, enquanto que, quando temos o cache limpo a leitura levou mais de 5 segundos, ou seja, sem o cache a leitura do arquivo levou praticamente 5 vezes mais tempo para ser realizada.
Para realizar um teste de desempenho de gravação vamos utilizar o próprio comando “dd”, criando duas vezes um arquivo de 100MB, uma vez utilizando a flag “sync” que força um fsync a cada escrita e uma segunda execução sem a flag “sync”, ou seja deixando para que o mecanismo de limpeza do “Page Cache” realize a escrita em disco posteriormente, conforme podemos ver nos comandos abaixo:
$dd bs=1048576 count=100 if=/dev/zero of=file oflag=sync 100+0 records in 100+0 records out 104857600 bytes (105 MB, 100 MiB) copied, 16,5911 s, 6,3 MB/s $ dd bs=1048576 count=100 if=/dev/zero of=file 100+0 records in 100+0 records out 104857600 bytes (105 MB, 100 MiB) copied, 0,286432 s, 366 MB/s
Nos resultados acima, também vimos um claro ganho de desempenho quando deixamos o “Page Cache” trabalhar, uma diferença de 366MB por segundo para 6MB por segundo quando forçamos a sincronia das operações de escrita e praticamente deixamos de usar o “Page Cache” para isso.
No entanto, o que muitos devem estar se perguntando é: quando não forçamos a sincronia, em quanto tempo teremos esse arquivo escrito em disco? Pois se ocorrer um crash neste período podem ocorrer perdas de dados….
A resposta é: normalmente é muito rápido, porém depende de vários fatores que vamos ver mais a frente, mas, somente a título de curiosidade podemos acompanhar (através de um segundo terminal) o campo Dirty do “/proc/meminfo” durante a criação do arquivo.
# while [ 1=1 ] ; do grep 'Dirty' /proc/meminfo; sleep 2 ; done Dirty: 400 kB Dirty: 480 kB Dirty: 548 kB Dirty: 724 kB Dirty: 84 kB Dirty: 839856 kB Dirty: 754104 kB Dirty: 559932 kB Dirty: 385148 kB Dirty: 262432 kB Dirty: 145812 kB Dirty: 284 kB Dirty: 272 kB Dirty: 1376 kB Dirty: 1544 kB Dirty: 1944 kB Dirty: 2084 kB Dirty: 568 kB
$ dd bs=1048576 count=1000 if=/dev/zero of=file 1000+0 records in 1000+0 records out 1048576000 bytes (1,0 GB, 1000 MiB) copied, 13,1907 s, 79,5 MB/s
Na execução acima, vemos que tivemos um pico de aproximadamente 820MB de páginas sujas, as quais levaram aproximadamente 10 segundos para serem escritas em disco.
Atualmente o processo de gravação das páginas sujas é realizada pelo Kernel através das “worker” threads (“kworker”), no entanto em versões mais antigas existiam threads dedicadas a realizar a escrita para cada disco do sistema, denominadas “flush threads” e em versões anteriores ao kernel 2.6.32 a thread responsável pela gravação era a “pdflush”.
Apesar das mudanças de implementação ao longo do tempo, o mecanismo de limpeza se manteve similar, essas threads trabalham em turnos cujo o intervalo é definido em centésimos de segundos através do “/proc/sys/vm/dirty_writeback_centisecs”. As páginas elegíveis para limpeza em cada turno são definidas a partir do “/proc/sys/vm/dirty_expire_centisecs”, onde é definido a partir de quanto tempo de vida uma página esta elegível a ser limpa.
Outro ponto importante, é que mesmo fora do seu “turno”, as threads de limpeza podem ser iniciadas, caso a quantidade de páginas a sujas atinja os valores dos parâmetros “/proc/vm/sys/dirty_background_ratio” ou “/proc/vm/sys/dirty_background_bytes”, onde podemos definir uma porcentagem máxima de páginas sujas em relação a memória disponível ou uma quantidade (absoluta) máxima de páginas sujas em bytes respectivamente.
Para ilustrar o funcionamento do processo de limpeza vamos manter um looping monitorando o estado das páginas sujas, conforme o comando abaixo:
# cat /proc/sys/vm/dirty_writeback_centisecs 500 # cat /proc/sys/vm/dirty_expire_centisecs 300 # cat /proc/sys/vm/dirty_background_ratio 10 # while [ 1=1 ] ; do grep 'Dirty' /proc/meminfo; sleep 2 ; done Dirty: 1820 kB Dirty: 2380 kB Dirty: 136 kB Dirty: 236 kB Dirty: 480 kB Dirty: 1824 kB Dirty: 2096 kB Dirty: 352 kB Dirty: 860 kB Dirty: 1028 kB Dirty: 880 kB Dirty: 2252 kB Dirty: 180 kB Dirty: 364 kB Dirty: 700 kB Dirty: 384 kB Dirty: 520 kB Dirty: 720 kB Dirty: 1368 kB Dirty: 2388 kB Dirty: 144 kB
Conforme vimos acima, com a configuração atual, a quantidade de páginas sujas é constantemente alterada, mantendo-se sempre abaixo de 3MB.
Agora se alterarmos o “dirty_writeback_centisecs” para zero, desabilitando o mecanismo de writeback, veja o que ocorre:
Dirty: 1880 kB Dirty: 2872 kB Dirty: 4344 kB Dirty: 4620 kB Dirty: 4812 kB Dirty: 5720 kB Dirty: 5772 kB Dirty: 5984 kB Dirty: 6136 kB Dirty: 6176 kB Dirty: 6400 kB Dirty: 6756 kB Dirty: 6820 kB Dirty: 7324 kB Dirty: 8560 kB Dirty: 9528 kB Dirty: 10628 kB Dirty: 11328 kB Dirty: 13320 kB Dirty: 13540 kB Dirty: 14480 kB Dirty: 14648 kB Dirty: 14920 kB Dirty: 15744 kB Dirty: 15784 kB Dirty: 16292 kB Dirty: 16532 kB Dirty: 16584 kB Dirty: 17000 kB Dirty: 17132 kB Dirty: 17900 kB Dirty: 18696 kB Dirty: 18824 kB Dirty: 19728 kB Dirty: 19768 kB Dirty: 20016 kB Dirty: 20848 kB Dirty: 21100 kB Dirty: 21600 kB Dirty: 35400 kB Dirty: 35492 kB Dirty: 36292 kB Dirty: 36612 kB Dirty: 37160 kB Dirty: 37940 kB Dirty: 38092 kB Dirty: 38628 kB Dirty: 39556 kB Dirty: 39804 kB Dirty: 40028 kB ...
Neste cenário temos a quantidade de páginas sujas aumentando constantemente, porém caso executarmos o comando sync em um terminal, vejamos o que ocorre:
Dirty: 42452 kB Dirty: 42480 kB Dirty: 42600 kB Dirty: 60 kB // Momento onde foi executado o sync Dirty: 304 kB Dirty: 8 kB Dirty: 112 kB Dirty: 668 kB Dirty: 88 kB Dirty: 472 kB Dirty: 532 kB Dirty: 748 kB Dirty: 2272 kB Dirty: 2388 kB Dirty: 2508 kB Dirty: 2536 kB Dirty: 2604 kB Dirty: 4044 kB
Na saída acima, fica evidente que mesmo com o processo de writeback desativado, ainda é possível realizar a limpeza das páginas sujas através de chamadas de sync no sistema.
Outro ponto importante, é que mesmo com o writeback desabilitado (definido para zero), caso o número de páginas sujas atinja o percentual do “dirty_background_ratio” ou o valor absoluto em “dirty_background_bytes”, o kernel irá iniciar o processo de limpeza, conforme podemos ver na demonstração abaixo:
# cat /proc/sys/vm/dirty_writeback_centisecs 0 # echo 10362880 > /proc/sys/vm/dirty_background_bytes # while [ 1=1 ] ; do grep 'Dirty' /proc/meminfo; sleep 2 ; done Dirty: 20336 kB Dirty: 21368 kB Dirty: 22088 kB Dirty: 6916 kB Dirty: 7980 kB Dirty: 8228 kB Dirty: 8396 kB Dirty: 8452 kB Dirty: 8568 kB Dirty: 9012 kB Dirty: 10044 kB Dirty: 5232 kB Dirty: 5488 kB Dirty: 6008 kB Dirty: 6676 kB Dirty: 7532 kB Dirty: 8348 kB Dirty: 9140 kB Dirty: 9892 kB Dirty: 10704 kB Dirty: 10412 kB Dirty: 11240 kB Dirty: 11976 kB Dirty: 1976 kB Dirty: 4200 kB Dirty: 4408 kB Dirty: 4480 kB Dirty: 4624 kB Dirty: 5568 kB Dirty: 6580 kB Dirty: 7124 kB Dirty: 7968 kB Dirty: 9592 kB Dirty: 8568 kB Dirty: 11016 kB Dirty: 11224 kB Dirty: 3196 kB Dirty: 3460 kB
A execução acima, mostra que, ao definir o “dirty_background_bytes” para aproximandamente 10MB, toda vez que o número de paginas sujas ultrapassar esse limite, o processo de limpeza será ativado e essa quantidade de páginas será reduzida imediatamente.
Quando a quantidade de páginas sujas for muito grande, e as threads de sincronia não conseguirem dar conta da gravação, as páginas sujas poderão se acumular até um teto definido através dos parâmetros “/proc/sys/vm/dirty_ratio” ou “/proc/sys/vm/dirty_bytes”, onde definimos uma porcentagem máxima de páginas sujas em relação a memória disponível ou uma quantidade absoluta máxima de páginas sujas em bytes respectivamente e ao atingir este teto, todas as novas operações de IO serão bloqueadas até que as páginas sejam sejam escritas em disco causando uma percepção de “congelamento” momentâneo da máquina.
Devido a isso, principalmente em máquinas com grande quantidade de memória RAM, é recomendável que se mantenha o “dirty_background_ratio” com valores baixos, para evitar acúmulo de páginas sujas, e que o “diry_ratio” também seja baixo, pois caso o valor seja muito alto e aconteça de se alcançar esse limite, a máquina pode passar algum tempo congelada efetuando as operações dedicadas a escrita de páginas sujas. Imagine uma máquina de 2TB de RAM com 1TB de RAM disponível, caso o dirty_ratio seja 20 e esse limite seja atingido, teremos por volta de 200GB de dados a serem escritos em disco.
Uma boa prática em máquinas com muita memória RAM, é definir estes parâmetros através de tamanhos absolutos (“dirty_background_bytes” e “dirty_bytes”), já que em alguns casos 1% já pode ser considerado um valor alto.
NUMA
A arquitura NUMA (Non-Uniform Memory Access) consiste de um hardware onde temos diversos agrupamentos de CPUs e memórias, denominadas células, interconectados por barramentos. A principal questão nesta arquitetura é que o tempo de acesso a memória varia de acordo com a “distância” entre a o processador e a memória.
O Linux utiliza a nomenclatura “node” para se referir a uma célula de processador e memória, e trata cada “node” de forma independente quando está realizando a alocação física de memória, ou seja, cada node terá seu endereçamento, controle de páginas livres, páginas em uso e tudo que é necessário para a alocação e liberação de memória.
Para otimizar o desempenho, o Kernel sempre dá preferência pela alocação de memória física no mesmo “node” do processador que se encontra executando o programa e durante as trocas de contexto o escalonador de tarefas tende a manter o processo no mesmo processador, sempre evitando o acesso a memória de outros nodes.
Graças ao uso do conceito de memória virtual o Kernel consegue abstrair todo esse mecanismo, de forma que, para a aplicação, isso se torna completamente transparente.
Para checar se sua máquina utiliza arquitetura NUMA basta consultar a quantidade nodes no sistema, o que pode ser feito de diversas formas:
# lscpu | grep -i numa NUMA node(s): 1 NUMA node0 CPU(s): 0-3 # cat /sys/devices/system/node/online 0 # cat /proc/zoneinfo | grep Node Node 0, zone DMA Node 0, zone DMA32 Node 0, zone Normal Node 0, zone Movable Node 0, zone Device
Na saída acima fica claro que a máquina em questão não utiliza NUMA, pois temos apenas um “node” disponível.
Ainda assim é possível visualizar as estatísticas referente a NUMA desse “node” através do “/proc/zoneinfo” conforme o comando abaixo:
# cat /proc/zoneinfo | grep -A 30 Normal | grep numa numa_hit 319270577 numa_miss 0 numa_foreign 0 numa_interleave 45129 numa_local 319270577 numa_other 0
No comando acima estamos exibindo a estatística de NUMA da zona “normal” (veremos mais sobre as zonas no próximo post), e conforme esperado, temos 100% (numa_local=numa_hit e numa_foreign=0 ) das alocações acontecendo no próprio node e nenhuma alocação vinda de outro “node” (numa_miss=0).
Próximos passos
No próximo post iremos encerrar o tema de memória explicando como o Kernel realiza o gerenciamento da memória física e o que podemos observar através do “/proc”.
Líder em Treinamento e serviços de Consultoria, Suporte e Implantação para o mundo open source. Conheça nossas soluções:
About author
Você pode gostar também
Kompose: Ferramenta para conversão do Docker Compose para Kubernetes
Hoje, no blog da 4Linux, vamos falar sobre o Kompose: uma ferramenta que faz a conversão do Docker Compose para orquestradores de containers como o Kubernetes. O Kompose tem como
Engenheiro de Dados: a profissão essencial na era da informação
Profissões tendem a desaparecer e surgir com outras roupagens em um mundo onde a quantidade de conhecimento cresce exponencialmente. Embora esse fenômeno cause crises em algumas áreas, ele pode ser
Linux 5.17: Novo lançamento traz melhorias e ajustes de desempenho
Linux 5.17 estável chega ao mercado com uma série de ajustes e melhorias. Confira! Esta semana foi lançada a versão estável do Linux 5.17, que apresenta uma série de melhorias