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
TIPOno 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 parachar*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.