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”.
01 | #include <stdio.h> |
02 | #include <sys/mman.h> |
03 | #include <string.h> |
04 | #include <sys/stat.h> |
05 | #include <fcntl.h> |
06 | #include <unistd.h> |
10 | int main ( int argc, char * argv[]){ |
19 | fd = open(argv[1],O_RDWR); |
21 | printf ( "Falha ao abrir o arquivo\n" ); |
24 | stat(argv[1],&st); |
26 | ptr=mmap(NULL,st.st_size,PROT_WRITE,MAP_SHARED,fd,0); |
27 | if ( ptr == MAP_FAILED ){ |
28 | printf ( "Falha ao realizar o mapeamento\n" ); |
35 | printf ( "Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n" ); |
36 | printf ( "Digite a opçao:" ); |
37 | option= ( char ) getchar (); |
38 | while (( getchar ()) != '\n' ); |
41 | printf ( "Configuração atual:\n" ); |
43 | for (count=0;count < st.st_size;count++){ |
48 | printf ( "Digite a nova configuração:\n" ); |
49 | fgets (ptr,st.st_size,stdin); |
53 | printf ( "\nEncerrando\n" ); |
56 | printf ( "\nInvalid Option" ); |
65 | munmap(ptr,st.st_size); |
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.
01 | #include <stdio.h> |
02 | #include <stdlib.h> |
03 | #include <sys/mman.h> |
04 | #include <string.h> |
05 | #include <sys/stat.h> |
06 | #include <fcntl.h> |
07 | #include <unistd.h> |
08 | #include <signal.h> |
12 | int main ( int argc, char * argv[]){ |
16 | size_t size = 1024*1024*100; |
20 | ptr=mmap(NULL,size,PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0); |
21 | if ( ptr == MAP_FAILED ){ |
22 | printf ( "Falha ao realizar o mapeamento\n" ); |
30 | printf ( "Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n" ); |
31 | printf ( "Digite a opçao:" ); |
32 | option= ( char ) getchar (); |
33 | while (( getchar ()) != '\n' ); |
36 | printf ( "Configuração atual:\n" ); |
40 | printf ( "Digite a nova configuração:\n" ); |
41 | fgets (ptr,size,stdin); |
45 | printf ( "\nEncerrando\n" ); |
49 | printf ( "\nInvalid Option" ); |
57 | log = fopen ( "/tmp/memory.log" , "w+" ); |
60 | printf ( "Falha ao abrir arquivo de log\n" ); |
64 | fprintf ( log , "Iniciando Logger\n" ); |
69 | fprintf ( log , "%s\n" ,ptr); |
74 | printf ( "Falha ao criar processo\n" ); |
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.
01 | #include <stdio.h> |
02 | #include <sys/mman.h> |
03 | #include <string.h> |
04 | #include <sys/stat.h> |
05 | #include <fcntl.h> |
06 | #include <unistd.h> |
07 | #include <sys/ipc.h> |
08 | #include <sys/shm.h> |
12 | int main ( int argc, char * argv[]){ |
17 | size_t size = 1024*1024*100; |
21 | shm_id=shmget(334,size,IPC_CREAT|0666); |
24 | printf ( "Falha alocacao de memoria \n" ); |
28 | ptr = shmat(shm_id,NULL,0); |
29 | if ( ( void *) ptr < 0 ){ |
30 | printf ( "Falha em atachar memoria compartilhada \n" ); |
38 | printf ( "Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n" ); |
39 | printf ( "Digite a opçao:" ); |
40 | option= ( char ) getchar (); |
41 | while (( getchar ()) != '\n' ); |
44 | printf ( "Configuração atual:\n" ); |
46 | for (count=0;count<size;count++){ |
52 | printf ( "Digite a nova configuração:\n" ); |
53 | fgets (ptr,size-1024,stdin); |
57 | printf ( "\nEncerrando\n" ); |
60 | printf ( "\nInvalid Option" ); |
70 | 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.
01 | #include <stdio.h> |
02 | #include <sys/mman.h> |
03 | #include <string.h> |
04 | #include <sys/stat.h> |
05 | #include <fcntl.h> |
06 | #include <unistd.h> |
10 | int main ( int argc, char * argv[]){ |
14 | size_t size = 1024*1024*100; |
20 | fd = shm_open(argv[1],O_RDWR|O_CREAT,S_IRWXU); |
22 | printf ( "Falha ao abrir o arquivo\n" ); |
27 | ptr=mmap(NULL,size,PROT_WRITE,MAP_SHARED,fd,0); |
28 | if ( ptr == MAP_FAILED ){ |
29 | printf ( "Falha ao realizar o mapeamento\n" ); |
36 | printf ( "Acoes:\n (1)Exibir configuração atual\n (2)Alterar a configuração\n (3)Encerrar\n" ); |
37 | printf ( "Digite a opçao:" ); |
38 | option= ( char ) getchar (); |
39 | while (( getchar ()) != '\n' ); |
42 | printf ( "Configuração atual:\n" ); |
44 | for (count=0;count<size;count++){ |
49 | printf ( "Digite a nova configuração:\n" ); |
50 | fgets (ptr,size,stdin); |
54 | printf ( "\nEncerrando\n" ); |
57 | printf ( "\nInvalid Option" ); |
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:
01 | #include <stdio.h> |
02 | #include <stdlib.h> |
03 | #include <unistd.h> |
04 | #include <fcntl.h> |
06 | int main( int argc, char *argv[]){ |
09 | fd=open(argv[1],O_RDWR); |
10 | 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:
CURSOSCONSULTORIA