Microkernels vs kernels monolíticos: revisitando os compromissos
Poucos debates de projeto de sistemas operacionais duraram tanto quanto o debate entre microkernels e kernels monolíticos.
Na superfície, a distinção parece simples:
- kernels monolíticos mantêm a maior parte dos serviços do sistema operacional dentro do kernel
- microkernels movem a maior parte dos serviços para o espaço de usuário
Na prática, o compromisso é mais sutil.
A verdadeira pergunta não é se uma estrutura é universalmente mais rápida, mais limpa ou mais segura. A verdadeira pergunta é onde autoridade, complexidade, falhas e custos de desempenho devem viver.
Este post revisita esse compromisso, explica por que muitos argumentos antigos sobre microkernels foram simplificados demais e mostra por que sistemas modernos como o EriX tornam o modelo de microkernel prático novamente.
A forma histórica do debate⌗
Os primeiros sistemas operacionais foram construídos sob restrições severas de hardware.
A memória era limitada. As CPUs eram mais lentas. Trocas de contexto eram caras. Caches, TLBs, sistemas multiprocessados e caminhos rápidos de syscall eram muito menos capazes do que são hoje.
Sob essas restrições, kernels monolíticos eram uma escolha natural.
Sistemas semelhantes ao Unix colocavam sistemas de arquivos, drivers de dispositivo, redes, gerenciamento de processos e muitos outros serviços dentro de um único espaço de endereçamento privilegiado do kernel. Esse projeto tornava muitas operações baratas:
- um sistema de arquivos podia chamar diretamente a camada de blocos
- uma pilha de rede podia acessar diretamente estruturas do driver
- subsistemas do kernel podiam compartilhar dados sem IPC
O resultado era eficiente e pragmático.
Também significava que grandes quantidades de código rodavam com privilégio total de kernel.
Por que os microkernels apareceram⌗
Microkernels surgiram de uma observação diferente:
A maior parte do código de um sistema operacional não precisa de autoridade total sobre a máquina.
Um sistema de arquivos não precisa modificar tabelas de páginas arbitrárias. Um driver de teclado não precisa acessar todos os processos. Uma pilha de rede não precisa controlar o escalonador.
Microkernels mantêm no kernel apenas os mecanismos mais fundamentais, normalmente:
- escalonamento
- gerenciamento de espaços de endereçamento
- comunicação entre processos
- gerenciamento de capacidades ou handles
- entrega de interrupções e exceções
Serviços de nível mais alto rodam como processos comuns no espaço de usuário.
Isso dá ao sistema isolamento mais forte. Uma falha de driver não precisa ser uma falha do kernel. Um bug em um sistema de arquivos não vira automaticamente corrupção arbitrária de memória do kernel. A autoridade pode ser distribuída com mais precisão.
A ideia era convincente, mas as primeiras implementações muitas vezes tiveram dificuldades de desempenho e compatibilidade.
O primeiro problema de desempenho⌗
A crítica clássica aos microkernels é que eles são lentos.
Essa crítica não apareceu do nada.
Alguns sistemas microkernel iniciais colocavam serviços tradicionais do sistema operacional atrás de muitos servidores separados no espaço de usuário, e então tentavam preservar interfaces Unix familiares por cima. Uma operação simples podia virar uma cadeia de mensagens:
- aplicação para servidor de arquivos
- servidor de arquivos para gerenciador de memória
- gerenciador de memória para pager
- pager para serviço de blocos
- serviço de blocos para driver
Cada passo podia envolver uma troca de contexto, validação de mensagem, decisão de escalonamento e às vezes cópia.
Se as interfaces são conversadoras demais, o custo se acumula.
O erro foi transformar isso em uma regra universal:
Microkernels são lentos.
Uma regra mais precisa é:
Caminhos IPC mal projetados e fronteiras de serviço conversadoras demais são lentos.
Essa distinção importa.
O caminho rápido monolítico⌗
Kernels monolíticos podem ser extremamente rápidos porque evitam muitas fronteiras de proteção.
Um sistema de arquivos dentro do kernel pode chamar uma camada de blocos dentro do kernel com uma chamada de função normal. Um driver pode compartilhar memória diretamente com outro subsistema. Não é necessário serializar cada requisição em um formato de mensagem.
Essa é uma vantagem real.
Mas ela não é gratuita.
O caminho rápido monolítico costuma vir com:
- mais código privilegiado
- mais estado mutável compartilhado
- mais complexidade de bloqueios internos do kernel
- mais maneiras de um subsistema corromper outro
- uma base de computação confiável maior
Desempenho não é apenas contagem de instruções. Também envolve comportamento de cache, contenção de bloqueios, contenção de falhas, recuperação e o custo de manter correção ao longo do tempo.
Um kernel monolítico pode vencer um microbenchmark bruto e ainda assim tornar isolamento e auditabilidade mais difíceis.
Mito de desempenho: toda fronteira é fatal⌗
Um mito comum é que cada fronteira de microkernel é tão cara que o projeto não consegue competir.
Essa visão está ultrapassada.
Uma fronteira tem custo, mas sistemas modernos podem tornar esse custo gerenciável:
- caminhos rápidos de syscall e retorno
- melhores heurísticas de escalonamento
- caminhos de dados com memória compartilhada
- mapeamento de páginas em vez de cópia em massa
- requisições em lote
- entrega assíncrona de eventos
- ABIs de IPC cuidadosamente projetadas
O objetivo de projeto importante é manter política fora do kernel sem forçar cada byte de dados a passar pelo kernel.
O kernel deve mediar autoridade. Ele não precisa necessariamente mover todos os dados.
Mito de desempenho: IPC significa copiar tudo⌗
IPC muitas vezes é imaginado como “copiar este buffer inteiro do processo A para o processo B”.
Esse é apenas um projeto possível.
Um microkernel pode passar pequenas mensagens de controle enquanto transfere autoridade sobre memória compartilhada, frames, endpoints ou objetos de dispositivo. O caminho de dados caro pode permanecer mapeado, enquanto o kernel apenas valida quem tem permissão para acessá-lo.
Isso é central para o projeto baseado em capacidades.
Em vez de copiar grandes estruturas de dados por um subsistema privilegiado, um processo pode receber uma capacidade que autoriza acesso a um objeto específico com direitos específicos.
O kernel continua responsável por impor a transferência. Ele não precisa entender todos os protocolos de alto nível construídos sobre essa transferência.
Mito de desempenho: drivers em espaço de usuário não são práticos⌗
Drivers em espaço de usuário muitas vezes são tratados como uma ideia de pesquisa.
A preocupação é compreensível. Acesso a hardware é sensível, interrupções dependem de tempo, e drivers muitas vezes ficam em caminhos quentes.
Mas a maioria dos drivers não precisa de autoridade total de kernel.
Um driver geralmente precisa de acesso a:
- uma faixa específica de portas de E/S
- uma região MMIO específica
- uma linha de interrupção específica
- um arranjo específico de DMA ou buffers
Essas são formas de autoridade mais estreitas do que “o kernel inteiro”.
Se o kernel puder delegar exatamente esses recursos, um driver pode rodar fora do kernel e ainda realizar trabalho útil. Se ele falhar, o sistema tem a chance de parar, reiniciar ou substituir esse driver sem tratar a falha como corrupção de memória do kernel.
O compromisso é real: drivers em espaço de usuário precisam de bom IPC, entrega cuidadosa de interrupções e propriedade explícita de recursos. Mas o modelo não é inerentemente impraticável.
O que o EriX coloca no kernel⌗
O EriX é projetado como um microkernel baseado em capacidades.
O kernel do EriX é intencionalmente mínimo em política. Seus documentos de arquitetura definem que o kernel é responsável por:
- validar a passagem do bootloader para o kernel
- gerenciar objetos básicos do kernel e semântica de capacidades
- criar a tarefa raiz
- expor pontos de entrada de traps, syscalls e interrupções
O kernel explicitamente não é responsável por:
- política do sistema
- política de orquestração de processos
- política de memória de alto nível
- política de ciclo de vida de serviços
Essa é a linha do microkernel na prática.
O kernel começa com autoridade de máquina, mas precisa converter essa autoridade em objetos explícitos do kernel e referências de capacidade. Nenhuma autoridade ambiente deve vazar para o espaço de usuário.
O que o EriX move para fora do kernel⌗
O EriX coloca funcionalidade que carrega política em serviços de espaço de usuário.
Por exemplo:
rootdé a primeira autoridade de espaço de usuário que carrega políticaprocdé dono do gerenciamento do ciclo de vida de processosdevicedé dono da política de drivers e da orquestração de inicialização de driversvfsdé dono do namespace público do sistema de arquivos- provedores de sistema de arquivos como
ramfsd,e2fsdefatdpermanecem como pares backend privados atrás devfsd
Isso não é apenas “mover código para fora do kernel” como escolha estética.
Cada fronteira de serviço define uma fronteira de autoridade.
rootd distribui capacidades de inicialização de menor privilégio. procd cria
e inicia processos por criação de filhos em etapas e concessões de instalação.
deviced não se torna diretamente o kernel; ele pede a procd para gerenciar
processos de driver e passa apenas a autoridade de driver exigida para cada
papel.
Essa estrutura é mais verbosa do que um grafo de chamadas de kernel monolítico, mas torna o fluxo de autoridade visível.
Autoridade estreita em vez de privilégio amplo⌗
Um dos detalhes de implementação mais importantes do EriX é afastar-se de um endpoint raiz amplo como superfície normal de controle em tempo de execução.
O kernel atual expõe famílias estreitas de endpoints de controle do kernel para trabalhos específicos:
- controle de tempo
- controle de interrupções
- eventos de hotplug
- leituras de configuração PCI
- acesso a console e framebuffer
- E/S COM1
- E/S i8042
- retyping de memória
- mapeamento de VSpace
- resolução de falhas do pager
- controle de processos
- leituras ACPI
O despacho em tempo de execução é determinado pelo objeto endpoint e seu tipo, não por um número de slot global privilegiado.
Isso importa porque uma tarefa não ganha autoridade apenas por conhecer um valor de slot convencional. Ela precisa realmente possuir a capacidade correta em seu próprio espaço local de capacidades.
Por exemplo, drv-serial recebe autoridade de E/S específica de COM1.
drv-i8042 recebe autoridade de E/S específica de i8042. drv-acpi recebe
autoridade de leitura ACPI. probed recebe autoridade de leitura de
configuração PCI.
Essa é uma forma de segurança diferente de colocar todas essas operações atrás de um único handle amplo do kernel.
Memória de dispositivo como objeto explícito⌗
O EriX também trata autoridade sobre memória de dispositivo como explícita e tipada.
O kernel tem um CAP_TYPE_DEVICE_FRAME distinto para memória de dispositivo
validada. No caminho de armazenamento, um frame MMIO apoiado por BAR pode ser
derivado para deviced, e deviced pode então instalar apenas esse frame de
dispositivo derivado no pacote de inicialização em etapas do driver.
A questão não é que drivers de dispositivo se tornem simples.
A questão é que autoridade MMIO não se confunde com frames de RAM comuns e não é exposta por uma saída genérica de “fazer qualquer coisa com memória de dispositivo”.
Esse é exatamente o tipo de detalhe que torna microkernels modernos viáveis: acesso a hardware é delegado como um objeto preciso com direitos precisos.
IPC como ABI, não como acidente⌗
Em um kernel monolítico, muitas interfaces internas são chamadas de função comuns.
Em um microkernel, IPC passa a fazer parte da ABI do sistema. Isso o torna mais importante, não menos.
O EriX trata IPC como um contrato compartilhado:
- cabeçalhos de mensagem são versionados
- layouts são fixos
- parsing usa aritmética verificada
- payloads malformados falham de modo fechado
- transferências de capacidades são explícitas
- mensagens de runtime que carregam transferências exigem
GRANT
Isso é o oposto de tratar IPC como uma reflexão tardia.
O custo de IPC é controlado em parte pela implementação, mas também pelo projeto de interface. Uma ABI cuidadosamente projetada evita idas e voltas desnecessárias, mantém mensagens limitadas e separa transferência de controle de movimento de dados.
Por que microkernels são viáveis novamente⌗
Microkernels são mais viáveis hoje por várias razões.
1. O hardware mudou⌗
O custo relativo de uma fronteira de proteção mudou.
Trocas de contexto e syscalls ainda não são gratuitas, mas CPUs modernas, sistemas de memória e mecanismos de interrupção tornam o custo bruto menos decisivo do que era quando os primeiros experimentos com microkernels foram julgados.
Ao mesmo tempo, sistemas modernos são mais complexos e mais expostos. O custo de comprometer o kernel aumentou.
Isolamento é mais valioso agora.
2. Entendemos IPC melhor⌗
A lição dos sistemas anteriores não é “evitar IPC”.
A lição é:
- evitar IPC desnecessário
- evitar protocolos conversadores demais
- evitar copiar grandes dados quando transferência de autoridade basta
- projetar fronteiras de serviço em torno de propriedade real
Microkernels são viáveis quando IPC é tratado como um problema de projeto de primeira classe.
3. Capacidades tornam fronteiras úteis⌗
Mover código para o espaço de usuário é apenas metade da história.
Se todo servidor de espaço de usuário ainda recebe privilégio implícito amplo, o sistema basicamente recriou um monólito com trocas de contexto extras.
Capacidades tornam a fronteira significativa.
No EriX, autoridade é representada por capacidades tipadas com direitos explícitos. Serviços validam as capacidades que recebem. Pacotes de inicialização descrevem autoridade declarada. O código do kernel e dos serviços evita tratar números canônicos de slot como permissão ambiente.
Isso faz da decomposição mais do que modularidade. Faz da decomposição parte do modelo de segurança.
4. Linguagens e ferramentas melhoraram⌗
Linguagens de implementação e ferramentas modernas também mudam o compromisso.
Rust não elimina bugs de sistemas operacionais, mas torna mais difícil escrever acidentalmente muitos erros de segurança de memória. Ele também torna fronteiras unsafe visíveis durante revisão.
Para um sistema microkernel, isso é especialmente útil. O kernel pode permanecer pequeno e auditável, enquanto serviços de espaço de usuário ainda podem ser escritos com garantias de segurança mais fortes do que componentes de sistema tradicionais pesados em C.
O EriX combina isso com uma abordagem de sala limpa e sem crates de terceiros, o que mantém o sistema mais fácil de auditar mesmo aumentando o trabalho de implementação.
Os custos restantes⌗
Microkernels ainda têm custos reais.
Eles exigem:
- lógica de inicialização mais explícita
- contratos IPC cuidadosamente versionados
- supervisão robusta de serviços
- mais atenção a batching e movimento de dados
- propriedade clara de cada capacidade
- bom tracing e medição de desempenho
Eles também movem parte da complexidade para fora do kernel em vez de eliminá-la.
rootd, procd, deviced e serviços de sistema de arquivos ainda precisam de
projeto cuidadoso. Eles podem estar fora do kernel, mas ainda podem ser
componentes confiáveis para partes específicas do sistema.
A diferença é que sua autoridade pode ser mais estreita do que a autoridade do kernel, e suas falhas podem ser contidas de forma mais deliberada.
O compromisso revisitado⌗
O enquadramento antigo muitas vezes era:
- kernels monolíticos são rápidos
- microkernels são limpos, mas lentos
Esse enquadramento é simples demais.
Um enquadramento melhor é:
- kernels monolíticos otimizam cooperação direta dentro do kernel
- microkernels otimizam autoridade explícita e isolamento de falhas
- qualquer projeto pode ser rápido ou lento dependendo da implementação
- qualquer projeto pode se tornar complexo se fronteiras forem mal escolhidas
Para o EriX, a escolha por microkernel segue dos objetivos do sistema:
- base de computação confiável mínima
- autoridade explícita por capacidades
- separação estrita entre kernel e espaço de usuário
- fronteiras de serviço auditáveis
- bootstrap e comportamento de falha determinísticos
Esses objetivos não tornam desempenho irrelevante.
Eles definem onde o trabalho de desempenho deve acontecer: IPC rápido, interfaces de serviço cuidadosas, caminhos de dados com memória compartilhada, famílias estreitas de endpoints e transferência explícita de capacidades.
Olhando adiante⌗
Microkernels não são um atalho.
Eles exigem mais disciplina de projeto inicial do que um simples grafo de chamadas dentro do kernel. Eles forçam o sistema a definir autoridade, propriedade e comportamento de falha cedo.
É exatamente por isso que são interessantes.
O EriX usa o modelo de microkernel não porque está na moda, mas porque combina com a arquitetura: um kernel pequeno, autoridade mediada por capacidades e política implementada por serviços explícitos de espaço de usuário.
O próximo post examinará a ideia que motiva grande parte dessa estrutura: a base de computação confiável.
Veremos o que a TCB realmente inclui, por que seu tamanho afeta a superfície de ataque e como o EriX tenta manter pequeno o código confiável movendo política para serviços explícitos de espaço de usuário, restringidos por capacidades.