Conhecendo o Kernel Linux pelo /proc (Parte 2) – Memória

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 …

 

CURSOSCONSULTORIACONTATO

Anterior Procura por profissionais de Big Data dobrará nos próximos 5 anos.
Próxima Você conhece o Redis?

About author

william.welter
william.welter 2 posts

Formado em ciência da computação, com as certificações LPIC-3, LPI DevOps Tools Engineer, LFCS, LFCE, ZCE-PHP5.3, ZFCA, PP9A, desenvolvedor PHP e C, com foco em Linux e software livre. Atualmente é gerente e líder técnico do time de consultoria e suporte na 4Linux, a qual atua principalmente com ferramentas DevOps, containers, cloud, monitoramento e sistemas de missão critica.

View all posts by this author →

Você pode gostar também

Infraestrutura

PAGILA – Base de dados educativa para PostgreSQL

Quando comecei aprender sobre banco de dados, senti falta de bases de dados prontas para fins de teste com o PostgreSQL. Eu queria aplicar conceitos aprendidos sobre SELECT, INSERT, UPDATE

Infraestrutura

Gerenciamento de filas assíncronas com Celery e Redis

O Celery é uma fila de tarefas assíncrona de trabalho, implementada em python, com base na passagem de mensagens distribuídas. Está focado na operação em tempo real, mas também oferece

Carreiras

Procura por profissionais de Big Data dobrará nos próximos 5 anos.

A “caça” aos profissionais de Big Data será inevitável … esteja preparado! Os estudos mostram que aproximadamente 90% dos dados existentes atualmente foram gerados nos últimos 2 anos. Através desta