Conhecendo o Kernel Linux pelo /proc (Parte 2) – Memória
Para você que está acompanhando nossa série sobre o Kernel Linux, daremos continuidade ao que iniciamos no último post, falando agora um pouco sobre memória e se você ainda não viu o post anterior, confere lá e depois volta aqui.
Memória Virtual
Os processos no Linux trabalham com o conceito de memória virtual, onde cada processo tem sua área de memória isolada e com seu próprio endereçamento de memória virtual, ou seja, o mesmo endereço de memória virtual em dois processos apontam para endereços físicos completamente distintos (nos próximos posts iremos explorar mais sobre o mecanismo de tradução de páginas de memória ). Esse mecanismo de memória virtual promove segurança, pois isola a área de memória de processos distintos e também permite que alocação/utilização de uma quantidade maior de memória do que existe fisicamente, dessa forma a alocação de memória fica limitada a quantidade de “endereços disponíveis” o que em plataformas 32bits estaria limitado a 4GB, ou seja, cada processo independente da quantidade de memória RAM disponível poderia alocar até 4GB memória. Já em plataformas 64bits seria possível alocar até 16EiB de memória virtual por processo.
A grosso modo a memoria de um processo no Linux é dividida da seguinte forma:
- A área “text” é onde é armazenado o código do programa em execução, é de tamanho fixo e não pode ser alterada.
- A área “stack” é onde fica armazenado a pilha de execução das funções e as variáveis com tamanho pré determinado, ou seja, sem alocação dinâmica. Esta área de memoria cresce para baixo sempre que necessário.
- A área “heap” é utilizada para alocação dinâmica de memória, é a área de memoria normalmente utilizada quando chamamos a função malloc em C. Esta área de memória cresce para cima sempre que necessário.
- A área “memory map” é utilizada para mapeamento de arquivos em memória e carregamento de bibliotecas dinâmicas.
Obs: Existem mais algumas áreas de memoria, porém, para simplificar o entendimento aqui, iremos abstrair estas regiões.
Stack
Conforme vimos no post anterior, dentro do /proc/[]/status temos como visualizar quanto estamos gastando de memória em cada uma destas áreas.
VmData: 64 kB //uso de memória de alocação dinâmica, principalmente a área heap
VmStk: 136 kB //uso do stack
VmExe: 4 kB //uso pelo executavel (“text”)
A saída acima é referente a execução do mem.c, o qual não faz nenhuma alocação de memória via malloc, e só contem as variáveis argc e argv da função main.
Agora vamos executar o “mem-variables.c”, onde incluímos uma nova variável, e vamos ver como fica o uso de memória.
#include <stdio.h> int main(int argc, char* argv[]){ int teste =1000; char big[1024*1000]; printf("Up And Running"); getchar(); return 0; }
$ gcc mem-variables.c -o mem-variables.o $ ./mem-variables.o Up And Running
# cat /proc/$(pgrep mem-variables.o)/status ... VmHWM: 788 kB VmRSS: 788 kB VmData: 64 kB VmStk: 1012 kB VmExe: 4 kB ...
É possível observar que a área de memória de stack aumentou.
Seguindo com o nosso estudo da memória de stack, vamos executar o “stackmem-alloc.c”, cujo o propósito é realizar recursões na função “hello” baseado no número passado por argumento durante a execução.
#include <stdio.h> #include <stdlib.h> int hello(int n); int main(int argc, char* argv[]){ int teste =1000; char big[1024*1000]; if(argc<=1){ printf("Usage:\n ./stackmem-alloc.o <recursion number>\n\n"); return 1; } int recursion = atoi(argv[1]); hello(recursion); getchar(); return 0; } int hello(int number){ char big[1000*1024]; printf("Recursion %d\n",number); if(number==0){ printf("->Recursion %d\n",number); getchar(); return number; } return hello(number-1); }
Vamos iniciar executando com 5 recursões:
$ gcc stackmem-alloc.c -o stackmem-alloc.o $ ./stackmem-alloc.o 5 Recursion 5 Recursion 4 Recursion 3 Recursion 2 Recursion 1 Recursion 0 ->Recursion 0
Com o processo ainda rodando vamos checar o como está seu uso de memória:
# cat /proc/$(pgrep stackmem-alloc)/status ... VmHWM: 688 kB VmRSS: 688 kB VmData: 64 kB VmStk: 7016 kB VmExe: 4 kB ...
Para atender as 5 recursões a área de stack precisou ser aumentada para 7016kB. Vale notar que a nossa função “hello” possui uma variável (big) que necessita de 1000kB por execução.
Agora vamos tentar executar novamente, mas com 8 recursões:
$ ./stackmem-alloc.o 8 Recursion 8 Recursion 7 Recursion 6 Recursion 5 Recursion 4 Recursion 3 Recursion 2 Segmentation fault (core dumped)
Ok, temos um “segmentation fault”, mas porque isso ocorreu ? Nossa aplicação esta fazendo alguma confusão na alocação de memória ? Na verdade, não.. esse erro ocorreu porque estouramos um limite aplicado a área de stack.
Para consultar o limite aplicado ao seu usuário basta executar o comando abaixo:
$ ulimit -s 8192
No nosso caso o limite esta setado em 8192kB, por isso a ocorrência do “segfault”
Agora vamos aumentar o nosso limite e executar novamente:
$ ulimit -s 12024 $ ./stackmem-alloc.o 8 Recursion 8 Recursion 7 Recursion 6 Recursion 5 Recursion 4 Recursion 3 Recursion 2 Recursion 1 Recursion 0 ->Recursion 0
Uma dica importante, quando se realiza alterações de limites, é conferir se o processo em questão esta realmente com o limite aplicado, através do /proc/[]/limits:
# cat /proc/$(pgrep stackmem-alloc)/limits Limit Soft Limit Hard Limit Units Max cpu time unlimited unlimited seconds Max file size unlimited unlimited bytes Max data size unlimited unlimited bytes Max stack size 12312576 12312576 bytes Max core file size unlimited unlimited bytes Max resident set unlimited unlimited bytes Max processes 31062 31062 processes Max open files 1024 65536 files Max locked memory unlimited unlimited bytes Max address space unlimited unlimited bytes Max file locks unlimited unlimited locks Max pending signals 31062 31062 signals Max msgqueue size 819200 819200 bytes Max nice priority 0 0 Max realtime priority 95 95 Max realtime timeout unlimited unlimited us
De acordo com a saída acima podemos concluir que o limite que alteramos foi aplicado ao processo (12312576 / 1024 = 12024kB).
Somente por curiosidade vamos checar qual o tamanho atual do stack:
# cat /proc/$(pgrep stackmem-alloc)/status | grep Vm VmPeak: 14108 kB VmSize: 14108 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 716 kB VmRSS: 716 kB VmData: 64 kB VmStk: 10016 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 48 kB VmPMD: 12 kB VmSwap: 0 kB
Ou seja, estamos dentro do limite.
Não achou nada estranho até então ? Como pode o nosso stack estar em aproximadamente 10MB, o Swap em 0 e só termos somente 716kB de memória residente (memória efetiva em uso)?
O que acontece é que as variáveis que estamos utilizando em nosso programa foram somente declaradas, não foram inicializadas em nenhum momento, desta forma a área de Stack da memória virtual do processo precisa crescer para comportar o conteúdo destas variáveis, porém como não existem dados, nenhuma memória física é alocada até que a variável seja inicializada.
Veja a execução do “stackmem-alloc-initialize.c”, cuja única alteração é que a variável big é inicializada com uma string vazia:
#include <stdio.h> #include <stdlib.h> int hello(int n); int main(int argc, char* argv[]){ int teste =1000; char big[1024*1000]; if(argc<=1){ printf("Usage:\n ./stackmem-alloc.o <recursion number>\n\n"); return 1; } int recursion = atoi(argv[1]); hello(recursion); getchar(); return 0; } int hello(int number){ char big[1000*1024]=""; printf("Recursion %d\n",number); if(number==0){ printf("->Recursion %d\n",number); getchar(); return number; } return hello(number-1); }
$ ./stackmem-alloc-initialize.o 8 Recursion 8 Recursion 7 Recursion 6 Recursion 5 Recursion 4 Recursion 3 Recursion 2 Recursion 1 Recursion 0 ->Recursion 0
# cat /proc/$(pgrep stackmem-alloc- )/status | grep Vm VmPeak: 14104 kB VmSize: 14104 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 10244 kB VmRSS: 10244 kB VmData: 64 kB VmStk: 10012 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 48 kB VmPMD: 12 kB VmSwap: 0 kB
Agora podemos ver que essa área da stack esta utilizando 10MB de memória física.
Alocação dinâmica de memória
Entendido o comportamento da área de Stack vamos partir para memória heap (data), onde realizamos alocação dinâmica de memória.
Vamos executar o “heapmem-alloc.c” onde apenas realizamos um “malloc” solicitando 1024kB:
#include <stdio.h> #include <stdlib.h> int hello(int n); int main(int argc, char* argv[]){ char *ptr; ptr=malloc(1024*1024); printf("Memoria alocada, pressiona qualquer tecla para desalocar\n"); getchar(); free(ptr); printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n"); getchar(); return 0; }
$ gcc heapmem-alloc.c -o heapmem-alloc.o $ ./heapmem-alloc.o Memória alocada, pressiona qualquer tecla para desalocar
Observando o status vemos que a área de memória VmData teve sua área aumentada para 1092kB.
# cat /proc/$(pgrep heapmem)/status | grep Vm VmPeak: 5256 kB VmSize: 5256 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 712 kB VmRSS: 712 kB VmData: 1092 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 28 kB VmPMD: 12 kB VmSwap: 0 kB
Após pressionar qualquer tecla a aplicação realiza a liberação de memória chamando a função free e a área de VmData foi reduzida para 64kB.
# cat /proc/$(pgrep heapmem)/status | grep Vm VmPeak: 5256 kB VmSize: 4228 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 1300 kB VmRSS: 1300 kB VmData: 64 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 28 kB VmPMD: 12 kB VmSwap: 0 kB
Nas execuções anteriores tivemos o mesmo comportamento do que vimos com área de stack, a área é expandida, porém não afetou o consumo efetivo de hardware.
Executando o “heapmem-alloc-initialize.c”, onde preenchemos a área alocada com o carácter “a”, a memória física passa efetivamente a ser utilizada.
#include <stdio.h> #include <stdlib.h> #include <string.h> int hello(int n); int main(int argc, char* argv[]){ char *ptr, *tmp_ptr; ptr=malloc(1024*1024); tmp_ptr=ptr; for(int i=0;i<(1024*1024);i++){ strcpy(tmp_ptr,"a"); tmp_ptr+=1; } printf("Memoria alocada, pressiona qualquer tecla para desalocar\n"); getchar(); free(ptr); printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n"); getchar(); return 0; }
$ gcc heapmem-alloc-initialize.c -o heapmem-alloc-initialize.o $ ./heapmem-alloc-initialize.o Memória alocada, pressiona qualquer tecla para desalocar
# cat /proc/$(pgrep heapmem)/status | grep Vm VmPeak: 5256 kB VmSize: 5256 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 2220 kB VmRSS: 2220 kB VmData: 1092 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 24 kB VmPMD: 12 kB VmSwap: 0 kB
Após desalocar:
# cat /proc/$(pgrep heapmem)/status | grep Vm VmPeak: 5256 kB VmSize: 4228 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 2220 kB VmRSS: 1280 kB VmData: 64 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 24 kB VmPMD: 12 kB VmSwap: 0 kB
Além de saber quanto está sendo consumido de memória, qual sua origem e quanto efetivamento esta sendo utilizado, também podemos visualizar o mapeamento da memória de um processo através do /proc/[]/maps.
Nele é mostrado o endereçamento de cada região de memória, do stack, do heap, do programa, das bibliotecas e de áreas de alocação anônimas.
$ ./heapmem-alloc-initialize.o Memória alocada, pressiona qualquer tecla para desalocar
# cat /proc/$(pgrep heapmem)/maps 00400000-00401000 r-xp 00000000 08:02 11934402 /home/iron_trooper/Documents/memory-tests/heapmem-alloc-initialize.o 00600000-00601000 r--p 00000000 08:02 11934402 /home/iron_trooper/Documents/memory-tests/heapmem-alloc-initialize.o 00601000-00602000 rw-p 00001000 08:02 11934402 /home/iron_trooper/Documents/memory-tests/heapmem-alloc-initialize.o 7f1740941000-7f1740b01000 r-xp 00000000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f1740b01000-7f1740d01000 ---p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f1740d01000-7f1740d05000 r--p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f1740d05000-7f1740d07000 rw-p 001c4000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f1740d07000-7f1740d0b000 rw-p 00000000 00:00 0 7f1740d0b000-7f1740d2f000 r-xp 00000000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f1740df8000-7f1740efc000 rw-p 00000000 00:00 0 7f1740f2a000-7f1740f2e000 rw-p 00000000 00:00 0 7f1740f2e000-7f1740f2f000 r--p 00023000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f1740f2f000-7f1740f30000 rw-p 00024000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f1740f30000-7f1740f31000 rw-p 00000000 00:00 0 7ffc5efaf000-7ffc5efd0000 rw-p 00000000 00:00 0 [stack] 7ffc5efdc000-7ffc5efde000 r--p 00000000 00:00 0 [vvar] 7ffc5efde000-7ffc5efe0000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Estranhou não existir [heap] neste cenário.. ??
O que acontece é que a chamada malloc possui uma “inteligência” para decidir se fará uma chamada de sistema “brk” ou se ira realizar uma chamada ao “mmap”. Mas qual a diferença entre as duas chamadas, e qual o motivo dessa escolha ?
Alocação de memoria Heap
Antes de entender o motivo desse comportamento precisamos entender como funciona a alocação da memoria heap.
No Linux para realizar a alocação dessa área de memória, o que fazemos é expandir seu limite, o que é feito realizando a chamada de sistema brk() (onde passamos qual sera o último endereço de memória) ou sua variante sbrk() (onde passamos a quantidade de memória a ser expandida). No caso de desalocação o mesmo procedimento é realizado porém com valor negativo no caso do sbrk() ou com uma posição de menor que o último endereço válido.
No código abaixo iremos utilizar a chamada de sistema sbrk() para realizar alocação e desalocação memória:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int hello(int n); int main(int argc, char* argv[]){ char *ptr; printf("Pressione qualquer tecla para alocar\n"); getchar(); ptr=sbrk(1024*1024); printf("Memoria alocada, pressiona qualquer tecla para desalocar\n"); getchar(); sbrk(-(1024*1024)); printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n"); getchar(); return 0; }
$ gcc brk-alloc.c -o brk-alloc.o $ ./brk-alloc.o Pressione qualquer tecla para alocar Memória alocada, pressiona qualquer tecla para desalocar Memória desalocada, pressiona qualquer tecla para encerrar
Em outro terminal podemos consultar o status e o maps e verificar o seu comportamento:
// Antes da alocação $ cat /proc/$(pgrep brk-alloc.o)/status | grep VmData VmData: 64 kB $ cat /proc/$(pgrep brk-alloc.o)/maps | grep heap // Depois da alocação $ cat /proc/$(pgrep brk-alloc.o)/status | grep VmData VmData: 1088 kB $ cat /proc/$(pgrep brk-alloc.o)/maps | grep heap 01004000-01104000 rw-p 00000000 00:00 0 // Depois de desalocação $ cat /proc/$(pgrep brk-alloc.o)/status | grep VmData VmData: 64 kB $ cat /proc/$(pgrep brk-alloc.o)/maps | grep heap
Nas saídas acima podemos verificar que assim que o processo iniciou-se, não existia memória heap alocada e após a primeira chamada sbrk() a memória heap foi alocada e a VmData cresceu para 1088kB e logo após a segunda chamada sbrk() a memória heap foi completamente desalocada e o VmData voltou para o tamanho inicial do processo (64kB)
Estratégia do malloc()
A questão é que chamadas de sistema são custosas em termos de CPU, por isso a função malloc() tem a estratégia de realizar alocações de áreas maiores utilizando brk()/sbrk() do que o solicitado e manter internamente um mapeamento interno das áreas entregues para aplicação, de forma que nas próximas requisições a malloc() não seja necessário novamente realizar chamadas de sistema para aumentar a área da memória heap. Por outro lado a função free() não pode simplesmente reduzir o tamanho da heap quando é chamado pois a área de memória em questão pode não estar no topo da heap, veja o exemplo abaixo:
Primeiro alocamos 1024 bytes e mantemos seu endereço inicial no ptr_a ptr_a = malloc(1024) ... Em seguida alocamos mais 200 bytes e mantemos seu endereço inicial no ptr_b ptr_b = malloc(200) Agora o que aconteceria se chamarmos o free no ptr_a ? free(ptr_a)
O free não poderia simplesmente executar a chamada de sbrk reduzindo o tamanho da heap, pois quem esta no topo da memória heap a ptr_b. Nesse cenário o free apenas marca esta região de memória como disponível, de forma que uma próxima chamada de malloc com tamanho adequado possa reaproveitar essa área. Esse comportamento pode levar a fragmentação da memória, ou seja, depois de um tempo teremos vários blocos inutilizados dentro da memória heap, o que explica um comportamento em que mesmo o programa executando um free() essa memória não é liberada para o sistema operacional de forma imediata.
Quando tentamos alocar blocos grandes de memória o malloc opta por não utilizar a memória heap para evitar a fragmentação da memória. Nestes cenários o malloc utiliza a área de memória de mapeamento de arquivos em memória através da chamada de sistema mmap, criando o que chamamos de bloco de memória anônima.
O limite de tamanho de bloco de memória a ser alocado no heap é por padrão de 128Kb e ajustado de forma dinâmica durante a execução do programa, porém pode ser alterado através da variável de ambiente M_MMAP_THRESHOLD ou através da função mallopt(), no exemplo abaixo iremos utilizar a variável de ambiente M_MMAP_MAX para desabilitar o uso da memória anônima.
$ export MALLOC_MMAP_MAX_=0 $ ./heapmem-alloc-initialize.o Memória alocada, pressiona qualquer tecla para desalocar
# cat /proc/$(pgrep heapmem)/maps 00400000-00401000 r-xp 00000000 08:02 11934402 /home/iron_trooper/Documents/memory-tests/heapmem-alloc-initialize.o 00600000-00601000 r--p 00000000 08:02 11934402 /home/iron_trooper/Documents/memory-tests/heapmem-alloc-initialize.o 00601000-00602000 rw-p 00001000 08:02 11934402 /home/iron_trooper/Documents/memory-tests/heapmem-alloc-initialize.o 006eb000-0080c000 rw-p 00000000 00:00 0 [heap] 7fb1aa094000-7fb1aa254000 r-xp 00000000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7fb1aa254000-7fb1aa454000 ---p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7fb1aa454000-7fb1aa458000 r--p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7fb1aa458000-7fb1aa45a000 rw-p 001c4000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7fb1aa45a000-7fb1aa45e000 rw-p 00000000 00:00 0 7fb1aa45e000-7fb1aa482000 r-xp 00000000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7fb1aa64c000-7fb1aa64f000 rw-p 00000000 00:00 0 7fb1aa67d000-7fb1aa681000 rw-p 00000000 00:00 0 7fb1aa681000-7fb1aa682000 r--p 00023000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7fb1aa682000-7fb1aa683000 rw-p 00024000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7fb1aa683000-7fb1aa684000 rw-p 00000000 00:00 0 7fff98eb2000-7fff98ed3000 rw-p 00000000 00:00 0 [stack] 7fff98f7e000-7fff98f80000 r--p 00000000 00:00 0 [vvar] 7fff98f80000-7fff98f82000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Neste exemplo definimos o limite de tamanho para memória anônima para 0, o que na prática desabilita o seu uso e o malloc passa a trabalhar somente com a memória Heap.
Alocação de memória anônima
Alocação de memória anônima é realizada através da chamada de sistema mmap() utilizando a flag MAP_ANONYMOUS e definindo o tamanho da área a ser alocada. Após a execução desta chamada o kernel irá realizar a alocação deste bloco e retornar o endereço inicial da área alocada (sempre maior que o valor definido em /proc/sys/vm/mmap_min_addr). Para realizar a desalocação basta realizar a chamada munmap() com endereço e tamanho a ser desalocado.
Vale lembrar que o mmap() foi originalmente concebido para realizar o mapeamento de arquivos em memória, por isso, no cenário de alocação para memória anônima utilizamos “-1” no parâmetro referente ao arquivo e 0 no atributo de offset.
No exemplo abaixo iremos 3 blocos de memória anônima de 4Mb cada:
#include <stdio.h> #include <sys/mman.h> #include <string.h> int main (int argc, char* argv[]){ char *ptr_a, *ptr_b, *ptr_c; size_t size = 1024*1024*4; printf("Pressione qualquer tecla para alocar memoria anonima:"); getchar(); ptr_a=mmap(0,size,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); ptr_b=mmap(0,size,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); ptr_c=mmap(0,size,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); printf("Pressiona qualquer tecla para desalocar prt_b:"); getchar(); munmap(ptr_b,size); printf("Pressiona qualquer tecla para desalocar prt_a:"); getchar(); munmap(ptr_a,size); printf("Pressiona qualquer tecla para desalocar prt_c:"); getchar(); munmap(ptr_c,size); printf("Pressione qualquer tecla para encerrar"); getchar(); }
$ gcc mmap-allocate.c -o mmap-allocate.o $ ./mmap-allocate.o Pressione qualquer tecla para alocar memoria anonima: Pressiona qualquer tecla para desalocar prt_b Pressiona qualquer tecla para desalocar prt_a Pressiona qualquer tecla para desalocar prt_c Pressione qualquer tecla para encerrar
Em outro terminal podemos conferir o mesmo comportamento de uso que vimos com a memória heap, ou seja, a memória é alocada, porém a memória física somente é utilizado quando quando utilizamos essa área de memória.
Antes de alocar a memória podemos conferir que temos apenas 64kB alocados no VmData:
$ grep -i vm /proc/$(pgrep mmap)/status VmPeak: 4412 kB VmSize: 4228 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 708 kB VmRSS: 708 kB VmData: 64 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 28 kB VmPMD: 12 kB VmSwap: 0 kB $ cat /proc/$(pgrep mmap)/maps 00400000-00401000 r-xp 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00600000-00601000 r--p 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00601000-00602000 rw-p 00001000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 7f2ee5525000-7f2ee56e5000 r-xp 00000000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee56e5000-7f2ee58e5000 ---p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e5000-7f2ee58e9000 r--p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e9000-7f2ee58eb000 rw-p 001c4000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58eb000-7f2ee58ef000 rw-p 00000000 00:00 0 7f2ee58ef000-7f2ee5913000 r-xp 00000000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5add000-7f2ee5ae0000 rw-p 00000000 00:00 0 7f2ee5b0e000-7f2ee5b12000 rw-p 00000000 00:00 0 7f2ee5b12000-7f2ee5b13000 r--p 00023000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b13000-7f2ee5b14000 rw-p 00024000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b14000-7f2ee5b15000 rw-p 00000000 00:00 0 7ffdc49d7000-7ffdc49f8000 rw-p 00000000 00:00 0 [stack] 7ffdc49fc000-7ffdc49fe000 r--p 00000000 00:00 0 [vvar] 7ffdc49fe000-7ffdc4a00000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Após alocar os 3 segmentos vemos que o VmData cresceu para ~12MB, porém o uso de memória física se manteve em 708kB. Outra coisa que podemos observar é que no “/proc/XXX/maps” temos um novo segmento de memória iniciando no endereço 7f2ee4925000 e encerrando no 7f2ee5525000, e se fizermos as contas, 7f2ee5525000−7f2ee4925000 é igual a C00000, o que em decimal é 12582912, e se convertermos para MB temos ~12MB, ou seja, se trata de um bloco contíguo de memória contendo os 3 segmentos de 4MB que solicitamos através do mmap().
$ grep -i vm /proc/$(pgrep mmap)/status VmPeak: 16516 kB VmSize: 16516 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 708 kB VmRSS: 708 kB VmData: 12352 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 28 kB VmPMD: 12 kB VmSwap: 0 kB $ cat /proc/$(pgrep mmap)/maps 00400000-00401000 r-xp 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00600000-00601000 r--p 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00601000-00602000 rw-p 00001000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 7f2ee4925000-7f2ee5525000 rw-p 00000000 00:00 0 7f2ee5525000-7f2ee56e5000 r-xp 00000000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee56e5000-7f2ee58e5000 ---p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e5000-7f2ee58e9000 r--p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e9000-7f2ee58eb000 rw-p 001c4000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58eb000-7f2ee58ef000 rw-p 00000000 00:00 0 7f2ee58ef000-7f2ee5913000 r-xp 00000000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5add000-7f2ee5ae0000 rw-p 00000000 00:00 0 7f2ee5b0e000-7f2ee5b12000 rw-p 00000000 00:00 0 7f2ee5b12000-7f2ee5b13000 r--p 00023000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b13000-7f2ee5b14000 rw-p 00024000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b14000-7f2ee5b15000 rw-p 00000000 00:00 0 7ffdc49d7000-7ffdc49f8000 rw-p 00000000 00:00 0 [stack] 7ffdc49fc000-7ffdc49fe000 r--p 00000000 00:00 0 [vvar] 7ffdc49fe000-7ffdc4a00000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Após desalocar o segmento do “ptr_b”, o qual seria a área de memoria intermediária, vemos que o “VmData” caiu para ~8MB. Outro ponto que podemos ver aqui, é que como desalocamos a área de memória intermediária, agora temos 2 segmentos de memória, um referente ao ptr_a e outro ao ptr_c, ou seja, com o mmap é possível desalocar qualquer segmento de memória de forma seletiva, diferente da heap, onde apenas conseguimos expandir o encolher seu tamanho.
$ grep -i vm /proc/$(pgrep mmap)/status VmPeak: 16516 kB VmSize: 12420 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 708 kB VmRSS: 708 kB VmData: 8256 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 28 kB VmPMD: 12 kB VmSwap: 0 kB $ cat /proc/$(pgrep mmap)/maps 00400000-00401000 r-xp 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00600000-00601000 r--p 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00601000-00602000 rw-p 00001000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 7f2ee4925000-7f2ee4d25000 rw-p 00000000 00:00 0 7f2ee5125000-7f2ee5525000 rw-p 00000000 00:00 0 7f2ee5525000-7f2ee56e5000 r-xp 00000000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee56e5000-7f2ee58e5000 ---p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e5000-7f2ee58e9000 r--p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e9000-7f2ee58eb000 rw-p 001c4000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58eb000-7f2ee58ef000 rw-p 00000000 00:00 0 7f2ee58ef000-7f2ee5913000 r-xp 00000000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5add000-7f2ee5ae0000 rw-p 00000000 00:00 0 7f2ee5b0e000-7f2ee5b12000 rw-p 00000000 00:00 0 7f2ee5b12000-7f2ee5b13000 r--p 00023000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b13000-7f2ee5b14000 rw-p 00024000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b14000-7f2ee5b15000 rw-p 00000000 00:00 0 7ffdc49d7000-7ffdc49f8000 rw-p 00000000 00:00 0 [stack] 7ffdc49fc000-7ffdc49fe000 r--p 00000000 00:00 0 [vvar] 7ffdc49fe000-7ffdc4a00000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Após desalocar o “ptr_a” e “ptr_b” o VmData diminui para 4MB e depois para 64kb, e também vemos os segmentos desaparecendo em “/proc/XXX/maps”
// Após desalocar o ptr_a $ grep -i vm /proc/$(pgrep mmap)/status VmPeak: 16516 kB VmSize: 8324 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 1296 kB VmRSS: 1296 kB VmData: 4160 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 28 kB VmPMD: 12 kB VmSwap: 0 kB $ cat /proc/$(pgrep mmap)/maps 00400000-00401000 r-xp 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00600000-00601000 r--p 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00601000-00602000 rw-p 00001000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 7f2ee4925000-7f2ee4d25000 rw-p 00000000 00:00 0 7f2ee5525000-7f2ee56e5000 r-xp 00000000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee56e5000-7f2ee58e5000 ---p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e5000-7f2ee58e9000 r--p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e9000-7f2ee58eb000 rw-p 001c4000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58eb000-7f2ee58ef000 rw-p 00000000 00:00 0 7f2ee58ef000-7f2ee5913000 r-xp 00000000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5add000-7f2ee5ae0000 rw-p 00000000 00:00 0 7f2ee5b0e000-7f2ee5b12000 rw-p 00000000 00:00 0 7f2ee5b12000-7f2ee5b13000 r--p 00023000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b13000-7f2ee5b14000 rw-p 00024000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b14000-7f2ee5b15000 rw-p 00000000 00:00 0 7ffdc49d7000-7ffdc49f8000 rw-p 00000000 00:00 0 [stack] 7ffdc49fc000-7ffdc49fe000 r--p 00000000 00:00 0 [vvar] 7ffdc49fe000-7ffdc4a00000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] // Após desalocar o ptr_c $ grep -i vm /proc/$(pgrep mmap)/status VmPeak: 16516 kB VmSize: 4228 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 1296 kB VmRSS: 1296 kB VmData: 64 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1936 kB VmPTE: 28 kB VmPMD: 12 kB VmSwap: 0 kB $ cat /proc/$(pgrep mmap)/maps 00400000-00401000 r-xp 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00600000-00601000 r--p 00000000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 00601000-00602000 rw-p 00001000 08:02 11927909 /home/iron_trooper/Documents/memory-tests/mmap-allocate.o 7f2ee5525000-7f2ee56e5000 r-xp 00000000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee56e5000-7f2ee58e5000 ---p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e5000-7f2ee58e9000 r--p 001c0000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58e9000-7f2ee58eb000 rw-p 001c4000 08:02 16257350 /lib/x86_64-linux-gnu/libc-2.21.so 7f2ee58eb000-7f2ee58ef000 rw-p 00000000 00:00 0 7f2ee58ef000-7f2ee5913000 r-xp 00000000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5add000-7f2ee5ae0000 rw-p 00000000 00:00 0 7f2ee5b0e000-7f2ee5b12000 rw-p 00000000 00:00 0 7f2ee5b12000-7f2ee5b13000 r--p 00023000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b13000-7f2ee5b14000 rw-p 00024000 08:02 16257322 /lib/x86_64-linux-gnu/ld-2.21.so 7f2ee5b14000-7f2ee5b15000 rw-p 00000000 00:00 0 7ffdc49d7000-7ffdc49f8000 rw-p 00000000 00:00 0 [stack] 7ffdc49fc000-7ffdc49fe000 r--p 00000000 00:00 0 [vvar] 7ffdc49fe000-7ffdc4a00000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Nos exemplos acima podemos ver que o uso de memória anônima com mmap() aparenta ser mais eficiente que uso da memória heap evitando a fragmentação e disperdício da memória, no entanto a alocação de memória anônima é mais custoso em termos de recursos hardware, por isso que o malloc, tende a utilizar heap para alocações pequenas e memória anônima somente para blocos grandes e contíguos.
É possível acessar a memória virtual de um processo?
Sim, sendo root, no /proc/PID/ também temos um pseudo arquivo “mem”, este arquivo é basicamente um acesso a memória virtual do processo onde podemos visualizar ou alterar seu conteúdo.
Para fins de exemplo criamos um programas em C para inspecionar o conteúdo de memória de um determinado processo (mem-sniff.c).
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MMAP 1 #define HEAP 2 #define STACK 3 struct position { size_t start; size_t end; }; struct position parseMaps(char* pid, int type); int main(int argc, char* argv[]){ FILE *mem; char path[255], buff[256]; int count=0; size_t startPos=0x7fa97b122000, endPos=0x7fa97b922000; struct position pos; pos=parseMaps(argv[1],atoi(argv[2])); sprintf(path,"/proc/%s/mem",argv[1]); mem=fopen(path,"r"); if(mem==NULL){ printf("Error: Unable do load file %s\n",path); return 1; } fseek(mem,pos.start,SEEK_SET); while(pos.start<pos.end){ fread(buff,1,1,mem); printf("%s",buff); pos.start+=1; count++; } printf("\n"); fclose(mem); } struct position parseMaps(char* pid, int type){ char path[255], buff[512]; FILE *maps; sprintf(path,"/proc/%s/maps",pid); size_t *start, *end; char *perms,*offset,*dev,*inode,*pathname; struct position pos; pos.start = 0; pos.end = 0; perms=malloc(5); offset=malloc(9); dev=malloc(6); inode=malloc(8); pathname=malloc(256); start=malloc(sizeof(size_t)); end=malloc(sizeof(size_t)); maps=fopen(path,"r"); while(fgets(buff,512,maps)!=NULL){ sscanf(buff,"%lx-%lx %s %s %s %s %s",start,end,perms,offset,dev,inode,pathname); switch(type){ case MMAP: if(strcmp(pathname,"")==0){ printf("Private segment found: %lx-%lx \n",*start,*end); pos.start=*start; pos.end=*end; free(perms); free(offset); free(dev); free(inode); free(pathname); return pos; }; break; case HEAP: if(strcmp(pathname,"[heap]")==0){ printf("Heap segment found: %lx-%lx \n",*start,*end); pos.start=*start; pos.end=*end; free(perms); free(offset); free(dev); free(inode); free(pathname); return pos; }; break; case STACK: if(strcmp(pathname,"[stack]")==0){ printf("Stack segment found: %lx-%lx \n",*start,*end); pos.start=*start; pos.end=*end; free(perms); free(offset); free(dev); free(inode); free(pathname); return pos; }; break; } strcpy(pathname,""); } return pos; }
Seu funcionamento é bem simples, basicamente ele espera 2 parâmetros, o PID do processo que queremos inspecionar e qual região de memória (1 para memória anônima, 2 para heap e 3 para stack). Com base nos parâmetros fornecidos realizamos um parse no arquivo “/proc/PID/maps” para saber os endereços inicial e final da região de memória selecionada. Com os endereços obtidos, abrimos o arquivo “/proc/PID/mem” como um arquivo comum através da função “fopen()” e depois posicionamos o arquivo no endereço inicial e vamos lendo e imprimindo ele byte a byte até atingir o endereço final desta região da memória.
Para testar e mostrar o funcionamento do “mem-sniff.c”, criamos um segundo programa (sniff-example.c) com algumas strings na stack, heap e na memória anônima:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/mman.h> int hello(int n); int main(int argc, char* argv[]){ char *ptr, *ptr_a, *ptr_b; size_t size = 1024*1024*4; char teste2[90] = " ########################### teste stack ############################################# "; int a = 7777777; ptr=malloc(128); strcpy(ptr,"-- Aqui temos a memoria heap --"); ptr_a=mmap(0,size,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); ptr_b=mmap(0,size,PROT_WRITE|PROT_READ,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); strcpy(ptr_a,"mapeado a"); strcpy(ptr_b,"mapeado b"); printf("Memoria alocada, pressiona qualquer tecla para desalocar\n"); getchar(); free(ptr); printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n"); getchar(); return 0; }
Inicialmente vamos executar o “sniff-example.c”:
$ gcc sniff-example.c -o sniff-example.o $ ./sniff-example.o Memoria alocada, pressiona qualquer tecla para desalocar
Em outro terminal podemos executar “mem-sniff.c”
$ gcc mem-sniff.c -o mem-sniff.o $ sudo ./mem-sniff.o $(pgrep example) 1 Private segment found: 7f6122d19000-7f6123519000 mapeado bmapeado a $ sudo ./mem-sniff.o $(pgrep example) 2 Heap segment found: a35000-a56000 -- Aqui temos a memoria heap --q $sudo ./mem-sniff.o $(pgrep example) 3 | strings Stack segment found: 7ffd1e3d8000-7ffd1e3f9000 <`<@X< @@@0 C7 ########################### teste stack ############################################# @`
Conforme vimos nas saídas acima foi possível acessar todas regiões de memória e exibir seu conteúdo na tela, no entanto, vale lembrar que também seria possível escrever e alterar o conteúdo da memória.
Agora vamos executar ambos os programas novamente, e consultar a área de stack:
$ ./sniff-example.o Memoria alocada, pressiona qualquer tecla para desalocar // Em outro terminal $ sudo ./mem-sniff.o $(pgrep example) 3 | strings Stack segment found: 7ffca95ce000-7ffca95ef000 <`<@X< @@@0 C7 ########################### teste stack ############################################# @` //Executando novamente $ ./sniff-example.o Memoria alocada, pressiona qualquer tecla para desalocar //Em outro terminal $ sudo ./mem-sniff.o $(pgrep example) 3 | strings Stack segment found: 7ffffffde000-7ffffffff000 <`<@X< @@@0 C7 ########################### teste stack ############################################# @
Note que o endereço região da stack mudou, mas porque isso acontece? estamos executando o mesmo binário em mesmas condições.
Porque os endereços de memória mudam ?
Na grande maioria das distribuições modernas Linux, os endereços da memória virtual de um processo são randomizados a cada execução, isso se deve ao recurso do Kernel chamado ASLR, o qual foi introduzido a partir da versão 2.6.12 como um recurso de segurança para mitigar ataques de exploits que se utilizam de vulnerabilidades de corrupção de memória, de forma que se o atacante encontrar esse tipo de vulnerabilidade ele pelo menos não sabe o seu endereçamento, tornando mais difícil a sua manipulação para execução de funções arbitrárias (no futuro teremos um post somente para demonstrar o funcionamento de um exploit desse tipo).
No entanto é possível alterar o comportamento dessa funcionalidade e até desabilitá-la através do /proc/sys/kernel/randomize_va_space. No exemplo abaixo iremos desabilitar o ASLR e executar novamente o “sniff-example” e inspecionar a sua área de stack.
$ sudo echo 0 > /proc/sys/kernel/randomize_va_space $ ./sniff-example.o Memoria alocada, pressiona qualquer tecla para desalocar //Em outro terminal $ sudo ./mem-sniff.o $(pgrep example) 3 | strings Stack segment found: 7ffffffde000-7ffffffff000 <`<@X< @@@0 C7 ########################### teste stack ############################################# @ /Executando novamente $ ./sniff-example.o Memoria alocada, pressiona qualquer tecla para desalocar //Em outro terminal $ sudo ./mem-sniff.o $(pgrep example) 3 | strings Stack segment found: 7ffffffde000-7ffffffff000 <`<@X< @@@0 C7 ########################### teste stack ############################################# @
Agora sim, o endereçamento permaneceu igual, porém o ideal é sempre manter essa funcionalidade ligada por questões de segurança, só recomendamos desabilitar esse recurso em casos pontuais de debug.
É possível limitar a memória de um processo ?
Sim, inclusive já vimos como isso é feito com o segmento de stack, no entanto ainda é possível limitar o tamanho da área de alocação dinâmica (heap+memory maps e até o tamanho máximo da memoria virtual de um processo.
Limitando área de alocação dinâmica
Para limitar a área de alocação dinâmica utilizamos o ulimit com a opção “-d” especificando o limite em kB. No exemplo abaixo vamos executar o “heapmem-alloc.o” e o “heapmem-alloc-initialize.o” com o limite reduzido para 600kB.
$ ./heapmem-alloc.o Memoria alocada, pressiona qualquer tecla para desalocar Memoria desalocada, pressiona qualquer tecla para encerrar $ ulimit -d 600 $ ./heapmem-alloc.o Memoria alocada, pressiona qualquer tecla para desalocar Memoria desalocada, pressiona qualquer tecla para encerrar $ ./heapmem-alloc-initialize.o Segmentation fault (core dumped)
Em outro terminal, em paralelo, monitoramos o tamanho do VmData através do arquivo status:
//Durante a primeira execução (sem limite aplicado) $ cat /proc/$(pgrep heap)/status | grep VmData VmData: 1204 kB //Durante a segunda execução (com o limite aplicado) $ cat /proc/$(pgrep heap)/status | grep VmData VmData: 176 kB $ cat /proc/$(pgrep heap)/limits | grep "data" Max data size 614400 614400 bytes
Nos exemplos acima podemos ver que a limitação de memória foi aplicada, no entanto é interessante notar que na execução do “heapmem-alloc-initialize” onde inserimos dados na região alocada temos um segmentation fault, isso acontece porque o nosso código não esta validando o retorno da função malloc (que em caso de falha retorna NULL) e dessa forma acabamos tentando escrever em uma região de memória que não foi alocada. Veja execução do “heapmem-alloc-initialize-ng“, uma versão melhorada onde validamos o retorno do “malloc()”:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int hello(int n); int main(int argc, char* argv[]){ char *ptr, *tmp_ptr; ptr=malloc(1024*1024); if(ptr==NULL){ printf("Falha ao alocar esta quantidade de memoria\n"); return 1; } tmp_ptr=ptr; for(int i=0;i<(1024*1024);i++){ strcpy(tmp_ptr,"a"); tmp_ptr+=1; } printf("Memoria alocada, pressiona qualquer tecla para desalocar\n"); getchar(); free(ptr); printf("Memoria desalocada, pressiona qualquer tecla para encerrar\n"); getchar(); return 0; }
$ gcc heapmem-alloc-initialize-ng.c -o heapmem-alloc-initialize-ng.o $ ulimit -d 600 $ ./heapmem-alloc-initialize-ng.o Falha ao alocar esta quantidade de memoria
Nota: Este limite passa a considerar os segmentos de memória mapeado somente a partir da versão 4.7, antes dessa versão se aplica somente a área da memória heap.
Limitando o tamanho da memória virtual
Para determinar um limite para a memória virtual dos processos utilizamos o ulimit com a opção “-v” especificando o limite em 5000kB. No exemplo abaixo vamos executar o “heapmem-alloc.o” e o “heapmem-alloc-initialize.o” com o limite reduzido para 600kB.
$ ./heapmem-alloc.o Memoria alocada, pressiona qualquer tecla para desalocar $ ulimit -v 5000 $ ./heapmem-alloc.o Memoria alocada, pressiona qualquer tecla para desalocar $ ./heapmem-alloc-initialize.o Segmentation fault (core dumped) $ ./heapmem-alloc-initialize-ng.o Falha ao alocar esta quantidade de memória
Em outro terminal, em paralelo, monitoramos o tamanho do VmSize e VmPeak através do arquivo status:
//Durante a primeira execução (sem limite aplicado) $ cat /proc/$(pgrep heap)/status | egrep "VmPeak|VmSize" VmPeak: 5536 kB VmSize: 5536 kB //Durante a segunda execução (com o limite aplicado) $ cat /proc/$(pgrep heap)/status | egrep "VmPeak|VmSize" VmPeak: 4544 kB VmSize: 4508 kB $ cat /proc/$(pgrep heap)/limits | grep "address" Max address space 5120000 5120000 bytes
Aqui temos um comportamento bem similar ao limite anterior, chamadas para alocação de memória irão falhar quando ultrapassarem o limite definido, só que neste caso temos área de stack também limitada, e caso o programa precise de mais memória para stack teremos um “segmentation fault”, conforme o exemplo abaixo:
$ ulimit -v 7000 $ ./stackmem-alloc.o 0 Recursion 0 ->Recursion 0 $ ./stackmem-alloc.o 1 Recursion 1 Segmentation fault (core dumped)
Em outro terminal, em paralelo, monitoramos o tamanho do VmSize e VmPeak através do arquivo status:
// Com 0 recursoes $ cat /proc/$( pgrep stack)/status | egrep "VmPeak|VmSize" VmPeak: 6104 kB VmSize: 6104 kB
É possível limitar o uso de memoria residente?
Não de maneira simples, mas existe um ulimit (-m) para este fim, mas não é efetivo nas versões de kernel atuais, ele só funciona nas versões 2.4.x, onde x < 30. Para realizar este tipo de limite atualmente temos os recursos do cgroup, no entanto não é nosso foco agora.
Próximos passos
Aguardem … no próximo post iremos nos aprofundar sobre outros comportamentos de memória no Linux, como o memory overcommit, memória compartilhada, tradução de memória, huge pages, etc …
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
Guia definitivo: otimize o acesso a dados com Redis em Alta Disponibilidade
Aprenda como implantar a solução Redis para otimização de acessos em memória em Alta Disponibilidade e com a criação de um cluster com failover automatizado com Redis Sentinel Redis é
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
Moodle – Descomplicando a instalação
Caro leitor, nesse artigo vamos tentar descomplicar a instalação do ambiente virtual de aprendizado Moodle para quem está iniciando o uso desta plataforma. Para isso, utilizaremos o Vagrant para provisionar