Pochi dibattiti nella progettazione dei sistemi operativi sono durati quanto il dibattito tra microkernel e kernel monolitici.

In superficie, la distinzione sembra semplice:

  • i kernel monolitici tengono la maggior parte dei servizi del sistema operativo dentro il kernel
  • i microkernel spostano la maggior parte dei servizi nello spazio utente

In pratica, il compromesso è più sottile.

La vera domanda non è se una struttura sia universalmente più veloce, più pulita o più sicura. La vera domanda è dove debbano risiedere autorità, complessità, guasti e costi di prestazione.

Questo articolo rivisita quel compromesso, spiega perché molti vecchi argomenti sui microkernel sono stati semplificati troppo e mostra perché sistemi moderni come EriX rendono di nuovo pratico il modello microkernel.


La forma storica del dibattito

I primi sistemi operativi furono costruiti sotto vincoli hardware severi.

La memoria era limitata. Le CPU erano più lente. I cambi di contesto erano costosi. Cache, TLB, sistemi multiprocessore e percorsi syscall veloci erano molto meno capaci di oggi.

In quelle condizioni, i kernel monolitici erano una scelta naturale.

I sistemi simili a Unix collocavano filesystem, driver di dispositivo, stack di rete, gestione dei processi e molti altri servizi dentro un unico spazio di indirizzamento privilegiato del kernel. Questo progetto rendeva economiche molte operazioni:

  • un filesystem poteva chiamare direttamente il livello a blocchi
  • uno stack di rete poteva accedere direttamente alle strutture del driver
  • i sottosistemi del kernel potevano condividere dati senza IPC

Il risultato era efficiente e pragmatico.

Significava anche che grandi quantità di codice venivano eseguite con pieni privilegi di kernel.


Perché apparvero i microkernel

I microkernel nacquero da un’osservazione diversa:

La maggior parte del codice di un sistema operativo non ha bisogno di piena autorità sulla macchina.

Un filesystem non deve modificare tabelle delle pagine arbitrarie. Un driver per tastiera non deve accedere a tutti i processi. Uno stack di rete non deve controllare lo scheduler.

I microkernel mantengono nel kernel solo i meccanismi più fondamentali, di solito:

  • scheduling
  • gestione degli spazi di indirizzamento
  • comunicazione tra processi
  • gestione di capacità o handle
  • consegna di interrupt ed eccezioni

I servizi di livello superiore vengono eseguiti come normali processi in spazio utente.

Questo offre al sistema un isolamento più forte. Il crash di un driver non deve diventare un crash del kernel. Un bug in un filesystem non diventa automaticamente corruzione arbitraria della memoria del kernel. L’autorità può essere distribuita con maggiore precisione.

L’idea era convincente, ma le prime implementazioni spesso ebbero difficoltà di prestazioni e compatibilità.


Il primo problema di prestazioni

La critica classica ai microkernel è che sono lenti.

Questa critica non è nata dal nulla.

Alcuni primi sistemi microkernel collocavano servizi tradizionali del sistema operativo dietro molti server separati in spazio utente, poi cercavano di preservare interfacce Unix familiari sopra di essi. Un’operazione semplice poteva diventare una catena di messaggi:

  1. applicazione al file server
  2. file server al gestore della memoria
  3. gestore della memoria al pager
  4. pager al servizio a blocchi
  5. servizio a blocchi al driver

Ogni passo poteva comportare un cambio di contesto, validazione del messaggio, una decisione di scheduling e talvolta una copia.

Se le interfacce sono troppo loquaci, il costo si accumula.

L’errore fu trasformarlo in una regola universale:

I microkernel sono lenti.

Una regola più accurata è:

Percorsi IPC progettati male e confini di servizio troppo loquaci sono lenti.

Questa distinzione conta.


Il percorso veloce monolitico

I kernel monolitici possono essere estremamente veloci perché evitano molti confini di protezione.

Un filesystem nel kernel può chiamare un livello a blocchi nel kernel con una normale chiamata di funzione. Un driver può condividere memoria direttamente con un altro sottosistema. Non serve serializzare ogni richiesta in un formato di messaggio.

Questo è un vantaggio reale.

Ma non è gratuito.

Il percorso veloce monolitico spesso porta con sé:

  • più codice privilegiato
  • più stato mutabile condiviso
  • più complessità di locking interno al kernel
  • più modi in cui un sottosistema può corromperne un altro
  • una base di calcolo fidata più grande

Le prestazioni non riguardano solo il conteggio delle istruzioni. Riguardano anche comportamento della cache, contesa dei lock, contenimento dei guasti, ripristino e costo di mantenere la correttezza nel tempo.

Un kernel monolitico può vincere un microbenchmark grezzo e rendere comunque più difficili isolamento e auditabilità.


Mito prestazionale: ogni confine è fatale

Un mito comune è che ogni confine di microkernel sia così costoso da impedire al progetto di competere.

Questa visione è superata.

Un confine ha un costo, ma i sistemi moderni possono renderlo gestibile:

  • percorsi syscall e return veloci
  • euristiche di scheduling migliori
  • percorsi dati con memoria condivisa
  • mapping di pagine invece di copie massive
  • richieste in batch
  • consegna asincrona degli eventi
  • ABI IPC progettate con cura

L’obiettivo importante di progetto è mantenere la politica fuori dal kernel senza costringere ogni byte di dati a passare attraverso il kernel.

Il kernel deve mediare l’autorità. Non deve necessariamente spostare tutti i dati.


Mito prestazionale: IPC significa copiare tutto

IPC viene spesso immaginato come “copia questo intero buffer dal processo A al processo B”.

È solo un possibile progetto.

Un microkernel può passare piccoli messaggi di controllo mentre trasferisce autorità su memoria condivisa, frame, endpoint o oggetti dispositivo. Il percorso dati costoso può rimanere mappato, mentre il kernel valida solo chi è autorizzato ad accedervi.

Questo è centrale nel design basato su capacità.

Invece di copiare grandi strutture dati attraverso un sottosistema privilegiato, un processo può ricevere una capacità che autorizza l’accesso a un oggetto specifico con diritti specifici.

Il kernel resta responsabile dell’applicazione del trasferimento. Non deve comprendere ogni protocollo di alto livello costruito sopra quel trasferimento.


Mito prestazionale: i driver in spazio utente non sono pratici

I driver in spazio utente vengono spesso trattati come un’idea di ricerca.

La preoccupazione è comprensibile. L’accesso all’hardware è sensibile, gli interrupt dipendono dal tempo e i driver spesso si trovano su percorsi caldi.

Ma la maggior parte dei driver non ha bisogno di piena autorità di kernel.

Un driver di solito ha bisogno di accedere a:

  • uno specifico intervallo di porte I/O
  • una specifica regione MMIO
  • una specifica linea di interrupt
  • una specifica disposizione di DMA o buffer

Queste sono forme di autorità più ristrette di “l’intero kernel”.

Se il kernel può delegare esattamente quelle risorse, un driver può eseguirsi fuori dal kernel e svolgere comunque lavoro utile. Se fallisce, il sistema ha la possibilità di fermare, riavviare o sostituire quel driver senza trattare il guasto come corruzione della memoria del kernel.

Il compromesso è reale: i driver in spazio utente richiedono buon IPC, consegna accurata degli interrupt e proprietà esplicita delle risorse. Ma il modello non è intrinsecamente impraticabile.


Che cosa EriX mette nel kernel

EriX è progettato come un microkernel a capacità.

Il kernel di EriX è intenzionalmente minimo nella politica. I suoi documenti di architettura definiscono il kernel come responsabile di:

  • validare il passaggio dal bootloader al kernel
  • gestire gli oggetti fondamentali del kernel e la semantica delle capacità
  • creare il task root
  • esporre punti di ingresso per trap, syscall e interrupt

Il kernel esplicitamente non è responsabile di:

  • politica di sistema
  • politica di orchestrazione dei processi
  • politica di memoria di alto livello
  • politica del ciclo di vita dei servizi

Questa è la linea del microkernel nella pratica.

Il kernel parte con autorità sulla macchina, ma deve convertire quell’autorità in oggetti kernel espliciti e riferimenti di capacità. Nessuna autorità ambientale deve trapelare nello spazio utente.


Che cosa EriX sposta fuori dal kernel

EriX colloca le funzionalità che portano politica nei servizi in spazio utente.

Per esempio:

  • rootd è la prima autorità in spazio utente che porta politica
  • procd possiede la gestione del ciclo di vita dei processi
  • deviced possiede la politica dei driver e l’orchestrazione del loro avvio
  • vfsd possiede il namespace pubblico del filesystem
  • provider di filesystem come ramfsd, e2fsd e fatd restano peer backend privati dietro vfsd

Questo non è solo “spostare codice fuori dal kernel” come scelta estetica.

Ogni confine di servizio definisce un confine di autorità.

rootd distribuisce capacità di avvio a privilegio minimo. procd crea e avvia processi tramite creazione staged di figli e grant di installazione. deviced non diventa direttamente il kernel; chiede a procd di gestire processi driver e passa solo l’autorità driver richiesta per ciascun ruolo.

Questa struttura è più verbosa di un grafo di chiamate di kernel monolitico, ma rende visibile il flusso di autorità.


Autorità ristretta invece di privilegio ampio

Uno dei dettagli implementativi più importanti di EriX è l’allontanamento da un ampio endpoint root come normale superficie di controllo a runtime.

Il kernel attuale espone famiglie ristrette di endpoint di controllo del kernel per compiti specifici:

  • controllo del tempo
  • controllo degli interrupt
  • eventi hotplug
  • letture della configurazione PCI
  • accesso a console e framebuffer
  • I/O COM1
  • I/O i8042
  • retyping della memoria
  • mapping di VSpace
  • risoluzione dei fault del pager
  • controllo dei processi
  • letture ACPI

Il dispatch a runtime è determinato dall’oggetto endpoint e dal suo tipo, non da un numero di slot globale privilegiato.

Questo importa perché un task non ottiene autorità solo conoscendo un valore di slot convenzionale. Deve possedere davvero la capacità corretta nel proprio spazio locale delle capacità.

Per esempio, drv-serial riceve autorità I/O specifica per COM1. drv-i8042 riceve autorità I/O specifica per i8042. drv-acpi riceve autorità di lettura ACPI. probed riceve autorità di lettura della configurazione PCI.

Questa è una forma di sicurezza diversa dal mettere tutte quelle operazioni dietro un unico handle kernel ampio.


Memoria di dispositivo come oggetto esplicito

EriX tratta anche l’autorità sulla memoria di dispositivo come esplicita e tipata.

Il kernel ha un CAP_TYPE_DEVICE_FRAME distinto per memoria di dispositivo validata. Nel percorso di storage, un frame MMIO basato su BAR può essere derivato per deviced, e deviced può poi installare solo quel frame di dispositivo derivato nel bundle di avvio staged del driver.

Il punto non è che i driver di dispositivo diventino semplici.

Il punto è che l’autorità MMIO non viene confusa con frame RAM ordinari e non viene esposta attraverso una via generica per “fare qualsiasi cosa con la memoria di dispositivo”.

Questo è esattamente il tipo di dettaglio che rende praticabili i microkernel moderni: l’accesso all’hardware è delegato come un oggetto preciso con diritti precisi.


IPC come ABI, non come incidente

In un kernel monolitico, molte interfacce interne sono normali chiamate di funzione.

In un microkernel, IPC diventa parte dell’ABI del sistema. Questo lo rende più importante, non meno.

EriX tratta IPC come un contratto condiviso:

  • gli header dei messaggi sono versionati
  • i layout sono fissi
  • il parsing usa aritmetica controllata
  • payload malformati falliscono chiusi
  • i trasferimenti di capacità sono espliciti
  • i messaggi runtime che portano trasferimenti richiedono GRANT

È l’opposto di trattare IPC come un ripensamento.

Il costo dell’IPC è controllato in parte dall’implementazione, ma anche dal design dell’interfaccia. Un’ABI progettata con cura evita round trip inutili, mantiene i messaggi limitati e separa il trasferimento di controllo dal movimento dei dati.


Perché i microkernel sono di nuovo praticabili

I microkernel sono oggi più praticabili per diverse ragioni.

1. L’hardware è cambiato

Il costo relativo di un confine di protezione è cambiato.

Cambi di contesto e syscall non sono ancora gratuiti, ma CPU moderne, sistemi di memoria e meccanismi di interrupt rendono il costo grezzo meno decisivo di quando furono giudicati i primi esperimenti microkernel.

Allo stesso tempo, i sistemi moderni sono più complessi e più esposti. Il costo di una compromissione del kernel è aumentato.

L’isolamento ora vale di più.


2. Comprendiamo meglio IPC

La lezione dei sistemi precedenti non è “evitare IPC”.

La lezione è:

  • evitare IPC inutile
  • evitare protocolli troppo loquaci
  • evitare di copiare grandi dati quando basta trasferire autorità
  • progettare confini di servizio attorno a proprietà reali

I microkernel sono praticabili quando IPC viene trattato come un problema di progetto di prima classe.


3. Le capacità rendono utili i confini

Spostare codice nello spazio utente è solo metà della storia.

Se ogni server in spazio utente riceve ancora ampio privilegio implicito, il sistema ha in gran parte ricreato un monolite con cambi di contesto aggiuntivi.

Le capacità danno significato al confine.

In EriX, l’autorità è rappresentata da capacità tipate con diritti espliciti. I servizi validano le capacità che ricevono. I bundle di avvio descrivono l’autorità dichiarata. Il codice del kernel e dei servizi evita di trattare i numeri di slot canonici come permesso ambientale.

Questo rende la decomposizione più che modularità. La rende parte del modello di sicurezza.


4. Linguaggi e strumenti sono migliorati

Anche i linguaggi di implementazione e gli strumenti moderni cambiano il compromesso.

Rust non elimina i bug dei sistemi operativi, ma rende più difficile scrivere accidentalmente molti errori di sicurezza della memoria. Rende anche visibili i confini unsafe durante la revisione.

Per un sistema microkernel, questo è particolarmente utile. Il kernel può rimanere piccolo e auditabile, mentre i servizi in spazio utente possono ancora essere scritti con garanzie di sicurezza più forti dei tradizionali componenti di sistema pesanti in C.

EriX combina questo con un approccio clean-room e senza crate di terze parti, che mantiene il sistema più facile da auditare anche se aumenta il lavoro di implementazione.


I costi rimanenti

I microkernel hanno ancora costi reali.

Richiedono:

  • logica di avvio più esplicita
  • contratti IPC attentamente versionati
  • supervisione robusta dei servizi
  • più attenzione a batching e movimento dei dati
  • proprietà chiara di ogni capacità
  • buon tracing e misurazione delle prestazioni

Spostano anche parte della complessità fuori dal kernel invece di eliminarla.

rootd, procd, deviced e i servizi filesystem richiedono ancora un design attento. Possono essere fuori dal kernel, ma possono comunque essere componenti fidati per parti specifiche del sistema.

La differenza è che la loro autorità può essere più ristretta dell’autorità del kernel, e i loro guasti possono essere contenuti più deliberatamente.


Il compromesso rivisitato

Il vecchio schema era spesso:

  • i kernel monolitici sono veloci
  • i microkernel sono puliti ma lenti

Questo schema è troppo semplice.

Uno schema migliore è:

  • i kernel monolitici ottimizzano la cooperazione diretta dentro il kernel
  • i microkernel ottimizzano autorità esplicita e isolamento dei guasti
  • entrambi i design possono essere veloci o lenti a seconda dell’implementazione
  • entrambi i design possono diventare complessi se i confini sono scelti male

Per EriX, la scelta microkernel deriva dagli obiettivi del sistema:

  • base di calcolo fidata minima
  • autorità esplicita tramite capacità
  • separazione rigorosa tra kernel e spazio utente
  • confini di servizio auditabili
  • bootstrap e comportamento di fallimento deterministici

Questi obiettivi non rendono le prestazioni irrilevanti.

Definiscono dove deve avvenire il lavoro sulle prestazioni: IPC veloce, interfacce di servizio attente, percorsi dati con memoria condivisa, famiglie di endpoint ristrette e trasferimento esplicito di capacità.


Guardando avanti

I microkernel non sono una scorciatoia.

Richiedono più disciplina progettuale iniziale di un semplice grafo di chiamate nel kernel. Costringono il sistema a definire presto autorità, proprietà e comportamento in caso di guasto.

È esattamente per questo che sono interessanti.

EriX usa il modello microkernel non perché sia di moda, ma perché corrisponde all’architettura: un kernel piccolo, autorità mediata da capacità e politica implementata da servizi espliciti in spazio utente.

Il prossimo articolo esaminerà l’idea che motiva gran parte di questa struttura: la base di calcolo fidata.

Vedremo che cosa include davvero la TCB, perché la sua dimensione influenza la superficie di attacco e come EriX cerca di mantenere piccolo il codice fidato spostando la politica in servizi espliciti in spazio utente, vincolati da capacità.