Desvendando a Estrutura de Listas Ligadas do Kernel Linux

Em implementações convencionais de ciência da computação, uma lista ligada geralmente consiste em um nó que contém um campo de dados e um ponteiro para o próximo elemento. No entanto, o Kernel Linux utiliza uma abordagem inversa e elegante: em vez de a lista conter os dados, os dados contêm a lista. Essa filosofia permite que qualquer estrutura de dados seja encadeada de forma genérica e eficiente, facilitando operações de travessia e manipulação em larga escala.

A base dessa implementação é uma lista circular duplamente ligada que armazena apenas ponteiros. Para entender como o kernel consegue recuperar o objeto "pai" a partir de um nó da lista, precisamos explorar duas macros fundamentais: offsetof e container_of.

1. A Macro offsetof

A macro offsetof é utilizada para determinar a posição relativa de um membro dentro de uma estrutura, calculando a distância em bytes desde o início da estrutura até o membro específico.

#define obter_offset(TIPO, MEMBRO) ((size_t) &((TIPO *)0)->MEMBRO)

Funcionamento:

  • (TIPO *)0: Simula a existência de uma estrutura do tipo TIPO no endereço de memória zero.
  • ->MEMBRO: Acessa o campo desejado dentro dessa estrutura hipotética.
  • &: Obtém o endereço do campo. Como a base é zero, o endereço resultante é exatamente o deslocamento (offset) em bytes do campo em relação ao início da estrutura.
  • size_t: Garante que o resultado seja tratado como um valor numérico de tamanho de memória.

2. A Macro container_of

Esta é, talvez, a ferramenta mais poderosa do kernel. Ela permite obter o ponteiro da estrutura principal (o "container") a partir de um ponteiro para um de seus membros internos.

#define recuperar_objeto(ptr, tipo, membro) ({                      \
    const typeof( ((tipo *)0)->membro ) *__mptr = (ptr);    \
    (tipo *)( (char *)__mptr - obter_offset(tipo, membro) );})

Funcionamento:

  • typeof: Identifica o tipo de dado do membro para garantir segurança de tipos durante a atribuição a __mptr.
  • Cálculo de Endereço: O ponteiro do membro (__mptr) é convertido para char* para permitir aritmética de bytes. Subtraindo o offset (calculado anteriormente) deste endereço, chegamos exatamente ao endereço inicial da estrutura que o contém.

3. A Estrutura list_head

O núcleo da lista ligada no Linux é definido de forma extremamente minimalista:

struct list_head {
    struct list_head *next, *prev;
};

Para integrra essa lista em seus próprios dados, você deve embutir o struct list_head dentro da sua struct personalizada:

struct sensor_io {
    int id_dispositivo;
    float leitura;
    struct list_head lista_nos;
};

Inicialização da Lista

Existem duas formas principais de inicializar um cabeçalho de lista (head), ambas fazendo com que os ponteiros next e prev apontem para o próprio nó, caracterizando uma lista vazia.

Via Macro:

#define LISTA_ESTATICA(nome) \
    struct list_head nome = { &(nome), &(nome) }

Via Função Inline:

static inline void inicializar_lista(struct list_head *ptr)
{
    ptr->next = ptr;
    ptr->prev = ptr;
}

4. Operações de Inserção

O kernel fornece funções para inserir novos elementos de forma eficiente. Internamente, ambas utilizam uma função auxiliar que manipula os ponteiros dos vizinhos.

static inline void __adicionar_no(struct list_head *novo,
                                 struct list_head *anterior,
                                 struct list_head *proximo)
{
    proximo->prev = novo;
    novo->next = proximo;
    novo->prev = anterior;
    anterior->next = novo;
}

list_add vs list_add_tail

  • list_add: Insere o novo nó imediataemnte após o cabeçalho (comportamento de Pilha/LIFO).
  • list_add_tail: Insere o novo nó antes do cabeçalho (comportamento de Fila/FIFO), aproveitando que a lista é circular.

Exemplo Prático de Uso

Abaixo, um exemplo de como criar uma lista de sensores, inserir dados e percorrer a lista para exibir os valores.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h" // Cabeçalho padrão do kernel ou similar

struct monitor_energia {
    int voltagem;
    char local[16];
    struct list_head list;
};

int main() {
    struct list_head cabecalho_sensores;
    struct monitor_energia *entrada;
    struct list_head *item;
    
    inicializar_lista(&cabecalho_sensores);

    // Simulando a inserção de 3 registros
    for (int j = 1; j <= 3; j++) {
        entrada = malloc(sizeof(struct monitor_energia));
        entrada->voltagem = 110 * j;
        snprintf(entrada->local, 16, "Sala-%d", j);
        
        // Inserção no final da lista
        list_add_tail(&entrada->list, &cabecalho_sensores);
        printf("Inserido: %s com %dV\n", entrada->local, entrada->voltagem);
    }

    printf("\n--- Percorrendo a Lista ---\n");
    
    // Macro que percorre os nós da lista
    list_for_each(item, &cabecalho_sensores) {
        // Recupera o objeto pai (struct monitor_energia) usando list_entry (que usa container_of)
        struct monitor_energia *obj = list_entry(item, struct monitor_energia, list);
        printf("Sensor no %s detectou %dV\n", obj->local, obj->voltagem);
    }

    return 0;
}

Nesta implementação, o poder reside na função list_entry. Mesmo que estejamos iterando sobre ponteiros do tipo list_head, conseguimos acessar qualquer campo da estrutura monitor_energia graças à aritmética de ponteiros baseada no offset, tornando o sistema extremamente flexível para qualquer tipo de objeto no desenvolvimento de drivers e subsistemas do kernel.

Tags: linux-kernel c-programming linked-list data-structures low-level

Publicado em 6-11 08:06 por Thomas