Il kernel Linux 2.2 fa girare driver, filesystem e stack di rete nello stesso spazio di indirizzamento protetto del resto del kernel, e nello stesso tempo carica e scarica quel codice mentre la macchina è accesa. Le due cose sembrano stridere: la prima è la definizione di kernel monolitico, la seconda è quello che i progetti a microkernel rivendicavano come loro. Guardo da vicino come si tengono insieme, perché lì sta gran parte del motivo per cui Linux è finito sui server connessi a internet invece di restare un esercizio universitario.

La disputa del 1992

La differenza tra kernel monolitico e microkernel era già materia di discussione pubblica quando Linux aveva pochi mesi. Il 29 gennaio 1992 Andrew Tanenbaum, autore di MINIX, aprì su comp.os.minix un thread intitolato “LINUX is obsolete”. L’obiezione di fondo era di progetto: un kernel monolitico, dove tutti i sottosistemi condividono un’unica area di memoria, era considerato una scelta arretrata rispetto a un microkernel, dove driver, filesystem e gestione della memoria girano come processi separati in user space e si parlano scambiandosi messaggi. Tanenbaum aggiungeva che Linux era troppo legato alla famiglia 386 per avere un futuro.

Torvalds rispose ammettendo che il microkernel era preferibile “da un punto di vista teorico ed estetico”, ma difese il monolite su due fronti concreti: MINIX aveva i suoi difetti di progetto, e Linux esisteva ed era usabile subito. Alla discussione presero parte anche Theodore Ts’o e David Miller, che sarebbero poi diventati tra i manutentori del kernel.

Il punto su cui torno è che la disputa non si chiuse con un vincitore sul piano dell’architettura. Si chiuse, di fatto, con una terza posizione, ed è quella che il kernel 2.2 incarna nella sua forma attuale.

Tutto in kernel space, ma scomponibile

In Linux 2.2 il kernel è un singolo binario che gira in modalità privilegiata del processore. Un driver di rete, il codice di un filesystem e lo scheduler condividono lo stesso spazio di indirizzamento e si chiamano tra loro con normali chiamate di funzione, non scambiandosi messaggi. Questa è la parte monolitica, e ha un costo preciso: un difetto in un qualsiasi pezzo di codice in kernel space può corrompere le strutture dati di qualunque altro pezzo. Isolamento non ce n’è.

La parte modulare sono i loadable kernel module (LKM). Un modulo è un oggetto compilato a parte che viene collegato dentro al kernel in esecuzione con insmod e rimosso con rmmod. Una volta caricato, il codice del modulo gira esattamente come se fosse stato compilato dentro al kernel: stesso spazio di indirizzamento, stessi privilegi. Qui la modularità non è isolamento, è collegamento dinamico (linking) ritardato.

Su questo si appoggia il caricamento automatico. Quando al kernel serve una funzione che non ha sotto mano — montare un filesystem mai usato prima, accendere una scheda di rete — può lanciare modprobe, che a sua volta chiama insmod per inserire il modulo che manca. In 2.2 a gestirlo è kmod, un thread interno al kernel, che ha preso il posto del demone kerneld della serie 2.0: là la richiesta usciva verso un processo in user space passando per la comunicazione tra processi (IPC, inter-process communication). La differenza pratica è che con kmod la richiesta resta dentro al kernel e non dipende da un processo utente esterno sempre vivo. Il meccanismo è documentato in Documentation/kmod.txt nei sorgenti.

Cosa risolvono i moduli e cosa no

Meglio dire chiaro fin dove arrivano i moduli e dove si fermano, perché è facile scambiarli per i vantaggi del microkernel.

Quello che danno è gestione pratica. Una distribuzione può spedire un kernel minimo più qualche centinaio di moduli e caricare solo quelli che servono all’hardware presente. Il driver di un controller SCSI esotico non occupa memoria su una macchina che quel controller non ce l’ha. Chi sviluppa un driver può ricompilare e ricaricare il solo modulo invece di ricostruire e riavviare l’intero kernel, e il ciclo di lavoro si accorcia parecchio. E un produttore di hardware può distribuire un driver come modulo senza doverlo far entrare nei sorgenti ufficiali del kernel.

Quello che non danno è il contenimento dei guasti. Un modulo difettoso manda in panico la macchina esattamente come potrebbe farlo del codice compilato staticamente. Manca la barriera di protezione che, in un microkernel, fa sì che un driver in user space possa andare in crash mentre il resto del sistema resta in piedi. Era la proprietà che Tanenbaum metteva al centro, e Linux vi ha rinunciato in cambio della semplicità e della velocità delle chiamate dirette.

L’alternativa rigorosa esisteva ed esiste. Il progetto GNU Hurd costruisce un sistema operativo come insieme di server in user space sopra il microkernel Mach; nel novembre 1999 Roland McGrath ha presentato la variante oskit-mach di GNU Mach proprio per migliorarne il riconoscimento dell’hardware. Lo cito come termine di paragone: la strada del microkernel veniva battuta attivamente nello stesso periodo, e a frenarla era il problema dei driver, non l’eleganza del progetto.

Cosa cambia per chi gestisce server

Chi usa Linux in produzione si porta dietro una disciplina sul codice in kernel space. Ogni modulo caricato è codice fidato con pieni privilegi: fare insmod di un binario di dubbia provenienza vuol dire dargli il controllo della macchina. La catena modprobe/insmod e i percorsi da cui i moduli vengono letti vanno controllati con la stessa cura di qualsiasi altra parte privilegiata del sistema.

Lo stesso ragionamento spiega una scelta ricorrente: per i sottosistemi sempre attivi su una macchina dedicata — il driver di rete del server, il filesystem di root — molti amministratori preferiscono compilare statico dentro al kernel e tengono i moduli per l’hardware variabile o usato di rado. Il risparmio di memoria dei moduli è marginale quando una funzione serve sempre, e un kernel più chiuso ha una superficie di caricamento dinamico più piccola.

Limiti di questa lettura

Sto descrivendo la serie 2.2, che è quella in produzione oggi. La serie 2.4, ormai a sviluppo avanzato, ridisegna parti importanti — il sottosistema di rete netfilter, il journaling, la scalabilità sul multiprocessore simmetrico (SMP, symmetric multi-processing) — e con esse cambieranno dettagli di come i moduli si incastrano con il resto. Il confronto con il microkernel resta sul piano dell’architettura: non ho misurato l’overhead dell’IPC di un microkernel contro le chiamate dirette di Linux, e i numeri pubblicati in letteratura cambiano molto a seconda del carico e di come è fatto il passaggio dei messaggi. Chi vuole farsi un’idea quantitativa deve guardare benchmark precisi, non la teoria.

Quello che è verificabile, e che volevo fissare, è la forma della scelta: Linux non ha risolto la disputa del 1992 dando ragione a una delle due parti. Ha spostato la domanda dal piano dell’isolamento a quello del collegamento dinamico.


Immagine di copertina: Tux, la mascotte pinguino di Linux: un pinguino stilizzato seduto, dal corpo nero e ventre bianco, con becco e zampe gialle — illustrazione di Larry Ewing, Simon Budig, Garrett LeSage, CC0 — https://commons.wikimedia.org/wiki/File:Tux.svg