Kernel, il finale dell'avvio di Alessandro Rubini In queste pagine (poche :) si descrivono alcuni aspetti della fase finale dell'avvio del kernel Linux-2.6, quella che avviene in _init/main.c_ * I sottosistemi del kernel Un kernel Linux moderno è un oggetto abbastanza complesso. Gran parte di questa complessità dipende dalla quantità di differenti realtà che devono convivere nella stessa base di codice: processori diversi, macchine diverse (anche quando montano lo stesso processore), protocolli di rete diversificati, bus di comunicazione di vario tipo. Il codice è stato strutturato ordinatamente in "sottosistemi", ognuno dei quali offre funzionalità di alto livello alle varie istanze del codice di più basso livello che deve parlare con le specifiche periferiche. Per esempio, il sottosistema USB implementa il protollo di comunicazione del bus, l'inserimento e la rimozione a caldo delle periferiche, la gestione delle situazioni di errore; il tutto associato ad un'interfaccia verso il basso per permettere ai driver associati al singolo dispositivo USB di integrarsi dinamicamente nell'architettura software. Inoltre il sottosistema definisce un'interfaccia di basso livello verso i "controllori" USB, in modo che l'implementazione del protocollo si possa appoggiare su diverse implementazioni hardware. Altri sottosistemi del kernel sono SCSI, IDE, PCI, come pure sottosistemi puramente software, per esempio la gestione dei dati in ingresso (==drivers/input==). Idealmente, ogni sottosistema ospita una o più classi di moduli di più basso livello, in una struttura ad albero, in cui le istanze dei dispositivi, o dei controllori appartengono ad un unico sottosistema, come mostrato in figura 1. In molti casi, però i driver di livello inferiore devono comunicare con più sottosistemi. Per esempio i controllori SCSI e USB sono spesso periferiche PCI e un disco USB viene visto come una periferica SCSI, come rappresentato in figura 2. * Il problema dell'inizializzazione Uno dei problemi da affrontare nel progetto di sistemi variamente interconnessi come quello descritto è quello dell'avvio. Mentre l'aggiunta di un modulo a sistema avviato non pone problemi poiché tutte le componenti del suo ambiente operativo sono già disponibili, l'avvio del kernel può avvenire solo se i sottosistemi vengano attivati nell'ordine corretto. Sempre riferendosi alla figura 2, non è possibile attivare il disco USB se non sono già stati attivati i sottosistemi SCSI e USB, il sottosistema PCI per poter comunicare con il controllore USB e il modulo scsi-disk per il protocollo di comunicazione verso il disco. Versioni precedenti di Linux usavano un approccio piuttosto semplicistico per l'inizializzazione, sicuramente adatto ad un sistema con pochi componenti ma non più facilmente manutenibile con l'enorme crescita delle periferiche supportate. Originariamente l'inizializzazione del sistema consisteva nella chiamata sequenziale delle funzioni di inizializzazione dei vari moduli, nell'ordine in cui erano inseriti nel codice sorgente, spesso protetti da serie di ==#ifdef==. Con l'introduzione di ==<linux/init.h>== si è permesso ad ogni modulo di dichiarare le sue funzioni di inizializzazione, automatizzando la loro esecuzione. Purtroppo però il corretto avvio del sistema dipendeva in ancora in modo oscuro dall'ordine in cui i file apparivano nelle regole del ==Makefile==; spostare un file o aggiungere un nuovo file nella posizione sbagliata poteva rendere impossibile il boot, così come avrebbe potuto essere impossibile compilare il codice attuale con versioni successive del compilatore. * Il meccanismo delle ==initcalls== Per evitare la lunga lista di chiamate alle varie funzioni di inizializzazione dei vari sottosistemi e driver abilitati nel kernel, gli sviluppatori sono ricorsi all'uso di sezioni ELF speciali, come abbiamo già visto fare per la gestione della modularizzazione. La funzione di inizializzazione di ogni driver, oltre ad essere inserita nella sezione ==.init.text== (di cui abbiamo già parlato nel numero di Febbraio), viene dichiarata come ==initcall== in modo che un suo puntatore venga inserito in una apposita sezione ELF. Durante l'avvio del sistema, la funzione ==do\_initcalls== (in _init/main.c_) scorre la lista di tali puntatori per invocare tali funzioni. Una ==initcall== per un ipotetico driver _ipot_ viene scritta nel seguente modo: #include <linux/init.h> int \_\_init ipot\_init(void) { /* ... */ } \_\_initcall(ipot\_init) Questo meccanismo, disponibile anche nelle ultime versioni di Linux-2.2 è stato introdotto in Linux-2.4 e rimane un uso in Linux-2.6, dove però è stato esteso permettendo un ordinamento tra le varie funzioni. Il file ==<linux/init.h>== ora definisce sette diverse classi di ==initcall==: #define core\_initcall(fn) \_\_define\_initcall("1",fn) #define postcore\_initcall(fn) \_\_define\_initcall("2",fn) #define arch\_initcall(fn) \_\_define\_initcall("3",fn) #define subsys\_initcall(fn) \_\_define\_initcall("4",fn) #define fs\_initcall(fn) \_\_define\_initcall("5",fn) #define device\_initcall(fn) \_\_define\_initcall("6",fn) #define late\_initcall(fn) \_\_define\_initcall("7",fn) La vecchia dichiarazione ==\_\_initcall== è ancora valida ed è equivatente a ==device\_initcall==. Il puntatore ad ogni funzione viene inserito in una sezione ELF specifica, chiamata, per esempio, ==.initcall6.init== . L'ordinamento relativo delle varie chiamate viene garantito dal linker script usato per creare _vmlinux_, cioè _vmlinux.lds.S_ per ciascuna piattaforma, che prescrive la creazione di un'unica sezione ==.initcall.init== che contenga, in ordine numerico, le sette sezioni presenti nei file oggetto di ingresso al linker: \_\_initcall\_start = .; .initcall.init : { *(.initcall1.init) *(.initcall2.init) *(.initcall3.init) *(.initcall4.init) *(.initcall5.init) *(.initcall6.init) *(.initcall7.init) } \_\_initcall\_end = .; I due simboli ==\_\_initcall\_start== e ==\_\_initcall\_end== identificano gli estremi della sezione nel file _vmlinux_ e vengono usati per accedere al vettore di puntatori in ==do\_initcalls==. Lo stesso meccanismo di ==initcall== può venire usato per gestire ordinatamente l'interpretazione degli argomenti sulla linea di comando del kernel. La dichiarazione ==\_\_setup==, mostrata nel riquadro 1 insieme ad una versione semplificata del codice di ==do\_initcalls==, inserisce informazioni in una sezione ELF chiamata ==.init.setup==. * I parametri del kernel L'interpretazione degli argomenti sulla linea di comando del kernel appena introdotta è un'altra funzionalità molto importante per un corretto avvio del sistema. Nonostante _init/main.c_ usi ==\_\_setup== per dichiarare alcuni parametri, come ==quiet== e ==debug== di cui si è parlato il mese scorso, si tratta del meccanismo di Linux-2.4, oggi considerato obsoleto, a favore di quello definito in ==<linux/moduleparm.h>==. Il nuovo meccanismo è più pesante del precedente a livello di implementazione, ma più leggero da usare. Inoltre permette di unificare, a livello di codice sorgente, il passaggio di un parametro ad un modulo caricato dinamicamente, il passaggio dello stesso parametro al driver staticamente incluso nell'immagine del kernel e la modifica del parametro durante il funzionamento del sistema (ma questa funzionalità non è ancora disponibile). Per il programmatore, la dichiarazione di un parametro configurabile si riduce ad una riga di codice. Il kernel contiene già al suo interno il supporto per la lettura e scrittura dei tipi fondamentali e delle stringhe (usando il nome ==charp== come tipo), oltre a vettori di tipi fondamentali e stringhe. Per esempio, se l'ipotetico modulo _ipot_ avesse un parametro stringa e uno intero leggibile da tutti e modificabile a run-time dal solo amministatore di sistema conterrebbe le seguenti righe: char *ipot\_name; module\_param(ipot\_name, charp, 0); int ipot\_param; module\_param(ipot\_param, int, S\_IRUGO|S\_IWUSR); Il terzo argomento di _module\_param_ rappresenta i permessi di accesso per un file che esporti il parametro allo spazio utente in formato ASCII; passando il valore 0 si richiede di non creare alcun file. Trascurando il caso dei vettori, che si differenzia leggermente, la nuova implementazione si basa su ==struct kernel\_param==, una struttura dati che contiene oltre al nome, al puntatore al parametro e ai permessi di accesso, anche i puntatori a due funzioni usate per converire il parametro dalla sua forma testuale a quella interna e viceversa. Le conversioni sono definite in _kernel/params.c_; il scondo argomento passato a _module\_param_ permette di identificare la coppia di funzioni da inserire nella struttura dati. Le strutture ==kernel\_param== vengono raccolte in una sezione ELF apposita, chiamata ==\_\_param== e recuperate al boot tramite due simboli definiti nel linker script, come già visto per ==initcalls==. Poiché i parametri dinamici devono essere assegnati prima dell'inizializzazione di un sottosistema o di un driver, la funzione _parse\_args_ ( definita in _kernel/params.c_) viene chiamata nella parte iniziale di _start\_kernel_. Quando _parse_args_ trova un parametro che non e` definito nel vettore di strutture dati, utilizza un puntatore ==unknown== come soluzione di ripiego. Tale soluzione, chiamata ==\_\_unknown\_bootoption== e definita in _init/main.c_, cerca il parametro nella sezione ==.init.setup==, in modo da continuare a supportare il meccanismo di dichiarazione dei parametri di Linux-2.4; i parametri che non vengono riconosciuti nemmeno in questo modo diventano variabili di ambiente o opzioni sulla riga di comando del processo init, come è sempre stato per gli argomenti di linea di comando non riconosciuti dal kernel. * I parametri di linea di comando modificabili a run-time. Se un parametro dichiarato con ==module\_param== deve essere leggibile o scrivibile durante la vita della macchina, il sistema lo dovrebbe rendere accessibile tramite un file all'interno di ==sysfs==, un filesystem virtuale, come _/proc_, che normalmente viene montato sulla directory _/sys_, ma le versioni attuali di Linux-2.6 non rendono visibili i parametri in ==sysfs==. L'implementazione di questa funzionalità dovrà in ogni caso richiedere una seconda iterazione sulla sezione ==__param== in un momento più avanzato dell'inizializzazione del kernel, dopo la creazione della struttura di ==sysfs== che avviene nella funzione ==driver\_init==, chiamata verso la fine della procedura di avvio. Sarà anche necessario associare i parametri allo specifico oggetto cui appartengono, associazione che non è realizzata dalla struttura ==kernel\_param== come definita ora. ==sysfs== e la definizione di parametri modificabili a run-time (parametri diversi da quelli che appaiono nella linea di comando del kernel) saranno l'argomento del prossimo articolo sul kernel. <Figura 1 - La struttura ideale ad albero>
<Figura 2 - La struttura reale dei sottosistemi -- fig2.ps>
<Riquadro 1 - do_initcalls e __setup> static int __init initcall_debug_setup(char *str) { initcall_debug = 1; return 1; } __setup("initcall_debug", initcall_debug_setup); static void __init do_initcalls(void) { initcall_t *call; for (call = &__initcall_start; call < &__initcall_end; call++) { if (initcall_debug) printk("calling initcall 0x%p\n", *call); (*call)(); } /* Make sure there is no pending stuff */ flush_scheduled_work(); }