Estrutura de Memória e Gerenciamento de Heap no Kernel Linux

Em sistemas Linux moderons de 64 bits, o kernel gearlmente utiliza 48 bits para o endereçamento virtual e cerca de 45 bits para o endereçamento físico. Essas informações podem ser validadas inspecionando o arquivo /proc/cpuinfo, que detalha as capacidades de hardware e software do processador.

Organização da Memória no Kernel x86

O gerenciamento de memória é exposto ao usuário através do sistema de arquivos /proc/meminfo. Abaixo, categorizamos os principais indicadores de estado da memória:

  • Memória Física:
    • MemTotal: Soma total da RAM física disponível.
    • MemFree: Memória que não está sendo utilizada para nada no momento.
    • MemAvailable: Estimativa de quanta memória está disponível para iniciar novos aplicativos sem causar swapping.
  • Cache e Buffers:
    • Buffers: Armazenamento temporário para blocos de disco brutos.
    • Cached: Cache de páginas de arquivos lidos do disco.
  • Segmentação de Atividade:
    • Active/Inactive: Memória que foi usada recentemente versus memória que é candidata à remoção ou swap.
    • AnonPages: Páginas mapeadas em espaço de usuário que não possuem suporte em arquivo (como pilhas e heaps).
  • Slab Allocator:
    • Slab: Cache do kernel para estruturas de dados internas.
    • SReclaimable: Parte do Slab que pode ser recuperada (como caches de inodes).

Alocação de Memória Dinâmica no Kernel

O kernel oferece diferentes interfaces para alocação, dependendo da necessidade de contiguidade física ou virtual:

  • alloc_pages: Aloca memória em unidades de páginas.
  • vmalloc: Aloca memória virtualmente contígua, mas fisicamente dispersa. Útil para grandes buffers onde a contiguidade física não é crítica.
  • kmalloc: Aloca memória fisicamente contígua. É a interface principal para pequenos objetos, operando sobre o sistema Slab.

Na arquitetura ARM64, o espaço de endereçamento é dividido de forma eqiulibrada:

  • User Space: 0x0000_0000_0000_0000 até 0x0000_FFFF_FFFF_FFFF.
  • Kernel Space: 0xFFFF_0000_0000_0000 até 0xFFFF_FFFF_FFFF_FFFF.

Dentro do espaço do kernel, encontramos regiões específicas como KASAN (detecção de erros de memória), modules (código de drivers dinâmicos), .text (instruções do kernel) e a área de vmemmap (utilizada para gerenciar o array de estruturas page em sistemas com memória esparsa).

Gerenciamento do Heap em Espaço de Usuário

O heap é a região onde os processos alocam dados dinamicamente. No Linux, o gerenciamento dessa área ocorre principalmente através de duas chamadas de sistema que expandem o limite superior do segmento de dados (brk) ou criam novos mapeamentos (mmap).

1. Manipulação via brk/sbrk

A função brk() altera o program break, que define o fim do segmento de dados do processo.

#include <stdio.h>
#include <unistd.h>

int main() {
    // Obtém o endereço atual do break
    void *ptr_inicial = sbrk(0);
    
    if (ptr_inicial == (void *)-1) return 1;

    // Incrementa o heap em 2048 bytes
    void *bloco_novo = sbrk(2048);
    if (bloco_novo == (void *)-1) return 1;

    printf("Início do bloco alocado: %p\n", bloco_novo);

    // Reduz o heap de volta ao estado original
    if (brk(ptr_inicial) == -1) {
        perror("Erro ao ajustar brk");
        return 1;
    }

    return 0;
}

2. Alocação via mmap

Para alocações maiores, o mmap() é preferido, criando um mapeamento anônimo que não está vinculado a um arquivo.

#include <stdio.h>
#include <sys/mman.h>

int main() {
    size_t tamanho = 4096; // 1 página típica
    
    // Alocação de memória anônima e privada
    void *area_mem = mmap(
        NULL, 
        tamanho, 
        PROT_READ | PROT_WRITE, 
        MAP_PRIVATE | MAP_ANONYMOUS, 
        -1, 
        0
    );

    if (area_mem == MAP_FAILED) {
        perror("mmap falhou");
        return 1;
    }

    printf("Memória mapeada em: %p\n", area_mem);

    // Liberação da região
    if (munmap(area_mem, tamanho) == -1) {
        return 1;
    }

    return 0;
}

Contadores Per-CPU e Escalabilidade

Em sistemas SMP (Symmetric Multi-Processing), a contenção de locks pode degradar o desempenho se vários núcleos tentarem atualizar o mesmo contador global. O Linux resolve isso com per-CPU counters.

A ideia fundamental é manter uma cópia do contador para cada CPU. Cada núcleo incrementa apenas sua variável local, eliminando a necessidade de travas (locks) durante a escrita. A soma total só é calculada quando uma leitura do valor global é solicitada.

#include <linux/percpu_counter.h>

struct percpu_counter estatistica_global;

// Inicialização (geralmente no init do módulo)
void iniciar_contagem(void) {
    percpu_counter_init(&estatistica_global, 0, GFP_KERNEL);
}

// Incremento rápido sem contenção de lock
void registrar_evento(void) {
    percpu_counter_inc(&estatistica_global);
}

// Leitura agregada dos valores de todas as CPUs
void mostrar_total(void) {
    s64 total = percpu_counter_read_positive(&estatistica_global);
    pr_info("Total acumulado: %lld\n", total);
}

// Limpeza
void finalizar_contagem(void) {
    percpu_counter_destroy(&estatistica_global);
}

Tags: Linux Kernel Memory Management ARM64 Systems Programming C programming

Publicado em 9-16 06:22