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
Automatizando Tarefas com Shell Script
Já se aventurou pela linha de comando de sistemas Unix ou Linux? Se sua resposta for sim, então provavelmente já ouviu falar de Shell Script. Mas o que seria exatamente
Descubra o poder do comando sed para manipulação de texto no Linux
No vasto universo de linha de comando, existem ferramentas extremamente versáteis e poderosas que podem ser utilizadas em diversas situações, e dentre dezenas de ferramentas, podemos facilmente destacar o sed,
Transforme sua empresa com soluções open source para alta demanda
Conheça nossa solução baseada 100% em software open source. Estamos vivendo um momento ímpar. De repente, as empresas foram obrigadas a se reinventar. O negócio foi afetado e como a