Chamadas de sistema, interrupções e drivers de dispositivo no kernel space
1. Fundamentos do Kernel Space e Modos de Operação
1.1. Diferenças entre kernel space e user space
O kernel space é a área de memória onde o núcleo do sistema operacional executa, com acesso irrestrito a todo o hardware e memória do sistema. O user space, por sua vez, é o ambiente isolado onde processos de usuário rodam, sem acesso direto a recursos críticos. Essa separação é fundamental para a estabilidade e segurança do sistema: um erro em user space afeta apenas o processo atual, enquanto um erro em kernel space pode derrubar todo o sistema.
1.2. Modos de execução: ring 0 vs. ring 3
Na arquitetura x86, existem quatro níveis de privilégio (rings 0 a 3). O kernel opera no ring 0 (mais privilegiado), com acesso total a instruções privilegiadas (como cli, sti, lgdt) e a todo o espaço de endereçamento. Processos de usuário executam no ring 3, com restrições severas: não podem executar instruções privilegiadas nem acessar memória do kernel diretamente. Qualquer tentativa de violação gera uma exceção (page fault ou general protection fault).
1.3. Transição de contexto
A transição entre user space e kernel space é cara em termos de desempenho, pois envolve:
- Salvar registradores do modo usuário
- Trocar a pilha (para a pilha do kernel)
- Alterar o nível de privilégio
- Verificar permissões e validar parâmetros
Essa troca ocorre principalmente via chamadas de sistema, interrupções ou exceções.
2. Chamadas de Sistema: Interface entre Usuário e Kernel
2.1. Mecanismo de chamada
Chamadas de sistema (syscalls) são a porta de entrada para serviços do kernel. Em x86-64, a instrução syscall é usada para transferir controle ao kernel. O kernel mantém uma tabela de syscalls (sys_call_table) indexada por número, onde cada entrada aponta para um handler específico.
2.2. Fluxo de execução
Quando um programa chama open():
1. A biblioteca glibc empacota os argumentos e executa syscall com o número da syscall em rax
2. O CPU muda para ring 0 e pula para o entry point definido no Model-Specific Register (MSR) LSTAR
3. O kernel salva o contexto, consulta sys_call_table[rax] e executa o handler correspondente
4. Após a execução, o kernel restaura o contexto e retorna ao user space via sysret
2.3. Exemplo prático: rastreando uma syscall
Para rastrear chamadas de sistema, podemos usar strace:
$ strace -e openat cat /etc/passwd 2>&1 | head -5
openat(AT_FDCWD, "/etc/passwd", O_RDONLY) = 3
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
Um exemplo simplificado de como uma syscall poderia ser invocada em assembly:
; Exemplo conceitual - chamada syscall write (número 1)
mov rax, 1 ; número da syscall (write)
mov rdi, 1 ; fd = stdout
lea rsi, [msg] ; buffer
mov rdx, 13 ; tamanho
syscall ; invoca o kernel
3. Tratamento de Interrupções e Exceções
3.1. Interrupções de hardware vs. software
Interrupções de hardware são geradas por dispositivos (ex: teclado pressionado, pacote de rede recebido). Exceções são geradas pelo CPU em situações excepcionais (divisão por zero, page fault). Traps são interrupções intencionais geradas por software (ex: int 0x80 em sistemas legados).
3.2. Descritores de interrupção (IDT)
A Interrupt Descriptor Table (IDT) mapeia cada vetor de interrupção (0-255) para um handler. Quando uma interrupção ocorre, o CPU consulta a IDT, verifica o nível de privilégio e executa o handler no ring apropriado.
3.3. Contextos de interrupção
O kernel divide o tratamento em duas partes:
- Top half: executa imediatamente, com interrupções desabilitadas. Deve ser rápido (ex: apenas salvar dados do dispositivo).
- Bottom half: executa posteriormente, com interrupções habilitadas. Implementado via tasklets, workqueues ou softirqs.
// Exemplo de registro de interrupção (conceitual)
request_irq(irq_number, my_interrupt_handler, IRQF_SHARED, "my_device", dev_id);
4. Drivers de Dispositivo: Estrutura e Registro no Kernel
4.1. Tipos de drivers
- Drivers de caractere: dispositivos que transmitem dados byte a byte (ex: teclado, mouse, portas seriais)
- Drivers de bloco: dispositivos que trabalham com blocos de dados (ex: discos rígidos, SSDs)
- Drivers de rede: gerenciam interfaces de rede (ex: Ethernet, Wi-Fi)
4.2. Operações básicas
Todo driver de caractere implementa a estrutura file_operations, que define callbacks para operações de arquivo:
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
.unlocked_ioctl = my_ioctl,
};
4.3. Exemplo de código: esqueleto de um driver de caractere mínimo
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mydevice"
#define CLASS_NAME "myclass"
static int major;
static struct class *my_class;
static int my_open(struct inode *inodep, struct file *filep) {
printk(KERN_INFO "mydevice: device opened\n");
return 0;
}
static ssize_t my_read(struct file *filep, char __user *buffer,
size_t len, loff_t *offset) {
char msg[] = "Hello from kernel!\n";
size_t msg_len = strlen(msg);
if (*offset >= msg_len) return 0;
if (len > msg_len - *offset) len = msg_len - *offset;
if (copy_to_user(buffer, msg + *offset, len)) return -EFAULT;
*offset += len;
return len;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
};
static int __init my_init(void) {
major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) return major;
my_class = class_create(THIS_MODULE, CLASS_NAME);
device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
printk(KERN_INFO "mydevice: registered with major %d\n", major);
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, MKDEV(major, 0));
class_destroy(my_class);
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_INFO "mydevice: unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
5. Comunicação entre Interrupções e Drivers
5.1. Mapeamento IRQ para driver
O driver solicita uma IRQ específica usando request_irq():
static irqreturn_t my_irq_handler(int irq, void *dev_id) {
// Top half: salva dados rapidamente
// Agenda bottom half para processamento posterior
return IRQ_HANDLED;
}
// No init do driver:
if (request_irq(irq_number, my_irq_handler, IRQF_SHARED, "my_device", dev)) {
printk(KERN_ERR "Failed to register IRQ\n");
return -EBUSY;
}
5.2. Buffering e sincronização
O kernel oferece mecanismos de sincronização para evitar condições de corrida:
- Spinlocks: para contextos onde não se pode dormir (interrupções)
- Semáforos/mutexes: para contextos que podem dormir
- Filas de espera (wait queues): para bloquear processos até que um evento ocorra
5.3. Estudo de caso: driver de teclado simplificado
Um driver de teclado processa interrupções geradas pelo controlador PS/2 ou USB HID. O handler top half lê o scancode do registro do hardware e acorda um processo em user space que lê os dados via read().
6. Depuração e Ferramentas no Kernel Space
6.1. Logs do kernel com printk
printk() é a função principal para logging no kernel. Os níveis de mensagem são:
printk(KERN_EMERG "Sistema em pânico!\n"); // Nível 0
printk(KERN_ALERT "Ação imediata necessária\n"); // Nível 1
printk(KERN_CRIT "Condição crítica\n"); // Nível 2
printk(KERN_ERR "Erro\n"); // Nível 3
printk(KERN_WARNING "Aviso\n"); // Nível 4
printk(KERN_NOTICE "Nota\n"); // Nível 5
printk(KERN_INFO "Informação\n"); // Nível 6
printk(KERN_DEBUG "Depuração\n"); // Nível 7
6.2. Inspeção via /proc, sysfs e debugfs
/proc/devices— lista drivers de caractere e bloco registrados/proc/interrupts— mostra estatísticas de interrupções por CPU/sys/class/— expõe dispositivos do kernel organizados por classe/sys/kernel/debug/— interface de depuração (requer montagem do debugfs)
6.3. Ferramentas avançadas
- ftrace: rastreia chamadas de função no kernel, incluindo syscalls e handlers de interrupção
- perf: amostragem de eventos de hardware e software, incluindo interrupções
- kgdb: depurador remoto para kernel Linux via serial ou Ethernet
7. Considerações de Desempenho e Segurança
7.1. Latência de interrupções
Interrupções frequentes podem degradar o desempenho. Técnicas de redução incluem:
- Interrupt coalescing: agrupar múltiplas interrupções em uma
- NAPI (New API): para drivers de rede, alterna entre polling e interrupções
- Threaded IRQs: tratar interrupções como threads do kernel, permitindo preempção
7.2. Segurança em drivers
Drivers são um ponto crítico de segurança. Práticas essenciais:
- Validar toda entrada vinda de user space com copy_from_user()/copy_to_user()
- Verificar limites de buffers para evitar buffer overflows
- Usar capable() para verificar privilégios do processo chamador
- Evitar uso de memcpy() sem verificação de tamanho
7.3. Boas práticas
- devres: gerenciamento automático de recursos (managed device resources)
- DMA API: para acesso direto à memória por dispositivos
- Modularização: dividir drivers em módulos carregáveis para facilitar manutenção
- Uso de APIs modernas: preferir
devm_*functions que liberam recursos automaticamente
Referências
- Linux Kernel Documentation: syscalls — Documentação oficial sobre como adicionar novas chamadas de sistema ao kernel Linux
- Linux Device Drivers, Third Edition — Livro clássico e gratuito sobre desenvolvimento de drivers de dispositivo no Linux
- The Linux Kernel Module Programming Guide — Guia prático para programação de módulos do kernel, incluindo exemplos de drivers
- Understanding the Linux Kernel, by Daniel Bovet and Marco Cesati — Referência abrangente sobre o funcionamento interno do kernel Linux
- Interrupts and IRQs in Linux — Documentação oficial sobre o subsistema de interrupções do kernel
- ftrace - Function Tracer — Guia completo sobre o rastreador de funções ftrace para depuração no kernel
- Linux kernel 'perf' tools — Documentação das ferramentas de performance profiling do kernel Linux