Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

02 - Gerenciamento de Memória (MMU, páginas, swap)

#14
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

1. A Ilusão do Espaço Contíguo e o Hardware (MMU)

O subsistema de Gerenciamento de Memória (MM - Memory Management) do Linux opera com base em uma das maiores abstrações da ciência da computação: a Memória Virtual. Processos em espaço de usuário (Ring 3) nunca interagem com capacitores físicos de memória RAM. Em vez disso, cada processo recebe a ilusão algorítmica de que possui acesso a um vasto, privado e contíguo bloco de endereçamento (ex: até 128 Terabytes em arquiteturas x86_64).

A concretização dessa ilusão exige uma simbiose perfeita entre o kernel e a MMU (Memory Management Unit), um coprocessador de hardware embutido na CPU. A MMU atua como um tradutor implacável: toda vez que uma instrução Assembly tenta ler ou gravar em um ponteiro de memória virtual, a MMU intercepta a chamada em tempo de ciclo de clock e traduz esse endereço virtual para um endereço físico real.

O núcleo de abstração para essa tradução é a Página de Memória (Page). O Linux e a MMU dividem a memória (tanto virtual quanto física) em blocos de tamanho fixo. O tamanho padrão histórico é de 4 Kilobytes (4KB), embora o kernel suporte HugePages (2MB ou 1GB) para otimizar o processamento de bancos de dados massivos e hypervisors, reduzindo a pressão sobre os caches de tradução.

2. Tabelas de Páginas (Page Tables) e a TLB

A tradução de 4KB por 4KB exige um mapa. Este mapa é a Tabela de Páginas (Page Table). Se o kernel alocasse um mapa linear estático para um espaço de 64 bits, a própria tabela consumiria Petabytes de RAM. Para resolver a esparsidade da memória (processos usam endereços muito altos para a stack e muito baixos para o código, com um imenso vazio no meio), o Linux implementa um esquema de Paginação Hierárquica Multinível.

Em arquiteturas x86_64 modernas, o kernel utiliza 4 ou 5 níveis de paginação:

  1. PGD (Page Global Directory)

  2. P4D (Page 4th-level Directory - introduzido para suportar paginação de 5 níveis)

  3. PUD (Page Upper Directory)

  4. PMD (Page Middle Directory)

  5. PTE (Page Table Entry): A folha da árvore que contém o Endereço Físico Base (PFN - Page Frame Number) e flags de permissão (Leitura/Escrita, Execução Proibida - NX bit).

O registrador mestre de gerenciamento de memória da CPU (o CR3 na Intel/AMD) aponta fisicamente para o PGD do processo atual.

A TLB (Translation Lookaside Buffer)

Atravessar 4 ou 5 níveis de ponteiros na memória RAM a cada instrução de CPU destruiria a performance do processador. Para mitigar isso, o hardware possui a TLB, um cache associativo ultra-rápido dentro da CPU que armazena as últimas traduções de PTEs. O kernel Linux deve gerenciar meticulosamente a consistência deste hardware: quando o kernel altera uma permissão de página ou libera memória, ele é obrigado a emitir interrupções inter-processadores (IPIs) chamadas TLB Shootdowns para forçar outros núcleos a invalidarem seus caches obsoletos.

3. A Anatomia do Espaço do Processo: mm_struct e VMAs

A representação do espaço virtual de um processo no espaço do kernel é mantida pela estrutura mm_struct (ancorada na task_struct). Ela contém ponteiros globais para os limites do código do programa (start_code, end_code), dados (start_data), o heap do usuário (start_brk, brk) e o limite da pilha.

Para rastrear faixas de endereços virtuais específicas alocadas (por exemplo, quando um programa chama mmap() para mapear uma biblioteca .so ou um arquivo), o kernel utiliza as VMAs (Virtual Memory Areas, formalmente struct vm_area_struct).

Cada VMA representa um intervalo contíguo de endereços virtuais com as mesmas permissões (ex: uma VMA apenas leitura para o código .text binário, e outra VMA de leitura/escrita para as variáveis).

Historicamente, as VMAs eram gerenciadas através de Árvores Rubro-Negras (Red-Black Trees). A partir do Linux 6.1, o kernel iniciou a transição para as Maple Trees (uma variante otimizada de árvores B-tree orientada a faixas), que reduzem drasticamente a contenção de locks e a fragmentação de cache da CPU durante operações complexas de mmap/munmap.

4. Alocação Física: Buddy System e Slab Allocator

Enquanto a mm_struct lida com endereços virtuais de programas, o kernel precisa de algoritmos para gerenciar a verdadeira silício físico (os pentes de RAM DDR). A memória RAM física é segmentada em Nodos NUMA e Zonas (ex: ZONE_DMA32, ZONE_NORMAL).

Dentro de uma zona, as páginas físicas (Páginas de Quadro - Page Frames) são alocadas e liberadas usando o Buddy System Allocator.

O Buddy System combate a fragmentação externa agrupando a memória livre em listas de blocos de tamanhos que são potências de 2 (Ordem 0 = 1 página de 4KB; Ordem 1 = 2 páginas; Ordem 10 = 1024 páginas contíguas). Se um driver pede 4 páginas (16KB) e só existem blocos de 8 páginas livres, o algoritmo divide o bloco de 8 ao meio (criando dois "buddies" de 4 páginas), entrega um e guarda o outro. Quando liberados, se dois "buddies" contíguos estiverem livres, eles são aglutinados de volta em um bloco maior.

O SLAB/SLUB Allocator

O Buddy Allocator só entrega memória em múltiplos de 4KB. Contudo, o kernel frequentemente precisa alocar milhões de estruturas diminutas (ex: inode de 600 bytes, ou task_struct). Se usasse o Buddy diretamente, ocorreria massiva fragmentação interna (desperdício de 3496 bytes por alocação).

Para resolver isso, o kernel introduz um alocador de objetos de granulometria fina operando por cima do Buddy System: o Slab Allocator (e sua evolução padrão no Linux moderno, o SLUB). O SLUB requisita grandes blocos de páginas do Buddy, divide essas páginas em "fatias" exatas do tamanho da estrutura de dados necessária e mantém caches específicos para cada tipo de objeto do kernel (acessíveis via funções kmalloc() e kmem_cache_alloc()).

5. Exceções e Interrupções: Page Faults e CoW

A maior virtude do subsistema de memória do Linux é a Paginação sob Demanda (Demand Paging). Quando um usuário executa malloc(1 Gigabyte), o kernel responde atualizando as VMAs e retorna sucesso imediatamente, mas não aloca um único byte de RAM física naquele milissegundo. O PTE na Tabela de Páginas é deixado intencionalmente vazio.

Quando o processo finalmente tenta escrever naquela memória, a MMU percebe o PTE inválido e dispara uma interrupção de hardware na CPU: a Page Fault (Falha de Página, ou Exceção #14 no x86). A execução do programa congela no Ring 3 e o kernel assume no Ring 0.

A rotina de tratamento de Page Fault do Linux analisa a falha:

  1. Major Page Fault: O endereço é válido (está dentro de uma VMA do processo), mas os dados não estão na RAM. O kernel bloqueia a thread, aciona a controladora de disco NVMe/SATA, lê a página para a RAM física, atualiza o PTE do processo e acorda a thread para tentar a instrução novamente. É uma operação gravíssima para a performance.

  2. Minor Page Fault: A página já reside na memória física (talvez lida por outro processo compartilhando a mesma biblioteca), ou é uma alocação nova. O kernel apenas aloca uma Página Física via Buddy System, ajusta o PTE para apontar para ela e retorna rapidamente.

  3. Invalid Page Fault (Segfault): O processo tentou acessar um endereço virtual que não pertence a nenhuma VMA mapeada (ponteiro nulo, buffer overflow). O kernel despacha o sinal fatal SIGSEGV (Segmentation Fault) e destrói o processo.

Copy-on-Write (CoW)

O recurso de Page Fault é a base para a criação extremamente rápida de processos (fork()). Em vez de copiar megabytes de memória RAM do processo Pai para o processo Filho, o kernel apenas copia as Tabelas de Páginas. Todas as páginas de ambos os processos são marcadas como Read-Only (Somente Leitura).

Ambos compartilham a mesma RAM física. Se o Filho tentar alterar uma variável, a MMU dispara uma Page Fault de violação de permissão. O kernel intercepta, nota que é uma página marcada para Copy-on-Write, aloca secretamente uma nova página física transparente, copia os dados apenas daquele bloco de 4KB e atualiza o PTE do Filho com permissão de escrita, provendo isolamento completo sob demanda com custo inicial quase nulo.

6. Recuperação de Memória: Page Cache, LRU e Kswapd

O kernel não permite que a RAM física disponível chegue a zero, caso contrário, chamadas críticas do ring 0 e tratamentos de interrupção falhariam. O gerenciamento proativo de recuperação é regido pelo daemon de kernel thread kswapd.

O kernel rastreia todas as páginas alocadas através de Listas de Uso Menos Recente (LRU - Least Recently Used), categorizadas logicamente em:

  • File-backed Pages (Page Cache): Páginas de memória cuja origem exata existe no disco rígido (ex: arquivos de texto lidos, bibliotecas .so).

  • Anonymous Pages: Memória alocada dinamicamente via malloc (variáveis, estado do programa). Não possuem arquivo de origem espelhado no disco de armazenamento primário.

O Fluxo do Kswapd e o Swap

Cada zona NUMA possui marcas de água estritas (watermarks): high, low e min.

Quando a memória livre da máquina atinge a marca low, o thread kswapd desperta agressivamente em background e inicia o escaneamento das listas LRU Inativas, buscando páginas para expulsar da RAM (eviction).

  1. Se o kswapd encontrar File-backed pages inativas, o tratamento é trivial: se a página estiver "limpa" (idêntica ao disco), ela é sumariamente descartada da RAM e seu PTE invalidado. Se estiver "suja" (modificada), é agendada para gravação no sistema de arquivos.

  2. Se o kswapd encontrar Anonymous pages, elas não podem ser destruídas, pois conteriam as variáveis cruciais de um programa rodando. É aqui que o mecanismo de Swap entra. O kernel move compulsoriamente os blocos de 4KB da RAM física para uma partição dedicada de Swap no disco rígido, atualizando a Tabela de Páginas do processo para indicar o deslocamento de I/O.

Se a pressão de memória for tão colossal que a marca min for violada mesmo com o kswapd escrevendo no Swap em velocidade máxima, o kernel invoca o OOM Killer (Out-of-Memory Killer), a guilhotina final que assassina o processo do espaço de usuário que possui a pior pontuação heurística (geralmente o maior consumidor de RAM), expurgando suas páginas de volta ao sistema para garantir a sobrevivência intrínseca do próprio núcleo.