Sistemi Operativi a confronto di Bernardo Innocenti ABSTRACT La ricerca sui sistemi operativi inizio' non appena l'hardware dei computer divenne abbastanza complesso da rendere necessaria l'interposizione di uno strato di software tra le applicazioni e le periferiche di I/O. INTRODUZIONE Da allora sono state scoperte tantissime soluzioni che affrontano e risolvono, talvolta brillantemente, innumerevoli problemi che si incontrano nella progettazione di un OS, molte delle quali vengono comunemente applicate ai sistemi operativi odierni. L'evoluzione dei sistemi operativi procedette rapidamente, di pari passo con l'aumento di prestazioni dei computer, il miglioramento dei linguaggi di programmazione e dei compilatori, giungendo ad un grado di maturita' apprezzabile circa venticinque anni fa, con la comparsa dei primi sistemi operativi multi utente, tra cui VMS e UNIX. I sistemi operativi che sono stati scritti fino ad oggi sono cosi' numerosi che per elencarli tutti non basterebbero queste pagine. Molti di questi sono del tutto sconosciuti, talvolta frutto del lavoro di qualche oscuro ricercatore in una oscura universita'. Oggi una buona parte degli utenti di personal computer ignora del tutto il significato stesso del termine "sistema operativo", pur usandone uno. Altri, appena piu' smaliziati, sanno che il loro sistema operativo si chiama Windows e che prima c'era il DOS. Sebbene Windows (9x ed NT) nel corso degli anni '90 abbiano conquistato una posizione di indiscutibile predominio, nessuno degli altri contendenti e' mai uscito del tutto dalla scena. Anzi, inebriata dalla posizione ormai raggiunta, Microsoft ha trascurato troppo a lungo l'innovazione tecnologica dei componenti fondamentali del sistema, cosicche' la situazione che si era consolidata negli scorsi anni sembra sull'orlo di essere capovolta dall'incalzare di Linux, che si trascina dietro l'intero movimento Open Source. Il campo di battaglia e' quindi tuttora aperto. Contrariamente a quanto ci viene suggerito dal marketing delle societa' che pubblicizzano i propri sistemi operativi, e' ormai da diversi anni che non vengono introdotte innovazioni sostanziali rispetto alle architetture che furono messe a punto nel corso degli anni '70 e '80, grazie allo sforzo congiunto dei centri di ricerca, delle /corporate/ americane e dello stesso governo degli Stati Uniti. Quest'ultimo riteneva infatti che i computer fossero di importanza strategica in ambito militare e pertanto ne finanziava lo sviluppo con decisione. Ma se le basi scientifiche della ricerca sui sistemi operativi erano gia' solide all'inizio degli anni '80, lo stesso non si puo' dire dei prodotti a disposizione del pubblico. I sistemi operativi multiuser richiedevano risorse hardware che poche tasche potevano permettersi in un'epoca in cui avere 128KB di RAM era considerato un vero e proprio lusso. [TODO: kernel e microkernel] Multitasking La possibilita' di eseguire contemporaneamente piu' processi sullo stesso computer e' una caratteristica che oggi diamo praticamente per scontata. In effetti, oggi il multitasking e' presente nella quasi totalita' dei computer e persino nei sistemi /embedded/ presenti nei telefoni cellulari e nelle telecamere digitali. Il multitasking e' stato uno dei primi problemi che venne affrontato e brillantemente risolto nel corso della ricerca sui sistemi operativi. L'idea nacque dalla considerazione che i costosissimi computer installati presso i centri di calcolo erano una risorsa limitata che doveva essere condivisa fra molte persone. Non potendo compiere piu' operazioni contemporaneamente, la maggior parte della potenza di calcolo sarebbe andata sprecata nelle fasi di caricamento dei programmi e di inserimento dei dati. La soluzione del problema consiste in una tecnica detta time-sharing (condivisione del tempo). Il sistema operativo mantiene un contesto diverso per ciascun processo in esecuzione ed esegue ognuno di essi per un periodo di tempo limitato, chiamato /quantum/ (quanto). Quando un processo ha esaurito il proprio quanto di tempo, esso viene interrotto da un interrupt che passa il controllo al supervisore del multitasking (lo /scheduler/), che salva il contesto del processo attivo e ne sceglie un altro al quale cedere la CPU. Questa operazione prende il nome di 'context switch'. La durata di un quanto viene stabilita in modo che l'esecuzione dei processi appaia contemporanea all'utente (in genere alcuni millisecondi). Non tutti i sistemi operativi sono in grado di effettuare automaticamente lo scambio di contesto tra processi (multitasking pre-emptive). Nelle versioni a 16bit di Windows e nell'attuale MacOS, sono gli stessi processi a cedere volontariamente il passo agli altri in alcuni punti prestabiliti del programma (multitasking cooperativo). Lo svantaggio di questa soluzione e' evidente: i programmi devono essere scritti in modo da non occupare la CPU per un tempo eccessivo, pena il blocco degli altri programmi in esecuzione. Purtroppo non sempre e' possibile stabilire a priori quali sono i punti in cui il programma eseguira' elaborazioni pesanti, per non parlare dei casi, tuttaltro che infrequenti, in cui il programma rimane intrappolato in un ciclo infinito a causa di un bug. In effetti sia Windows 3.1 che MacOS contengono diversi accorgimenti per aggirare i limiti del multitasking cooperativo. Il movimento del mouse, per esempio, continua a funzionare anche se un programma si rifiuta di cedere la CPU. Exec, il microkernel di AmigaOS, costituisce un eccellente esempio di sistema multitasking pre-emptive, la cui semplicita' ed eleganza sono rimasti imbattuti dal 1985 ad oggi. Ogni processo e' dotato di una priorita' che ne stabilisce la posizione in una coda di attesa mantenuta da Exec. La CPU viene ceduta ai processi con priorita' piu' elevata finche' essi non decidono spontaneamente di cederla. Se piu' processi con la stessa priorita' si contendono l'uso della CPU, entra in funzione uno scheduling di tipo round-robin: ognuno dei processi ottiene la CPU per un quanto di tempo, dopodiche' essa viene ceduta al processo successivo. Gli scheduler presenti nella maggior parte dei sistemi UNIX costituiscono lo stato dell'arte della tecnologia. Allo scopo di massimizzare l'interattivita' del sistema anche in condizioni di carico elevato, lo scheduler raccoglie continuamente delle statistiche sul funzionamento dei processi. Quelli che richiedono una quantita' esigua di tempo di elaborazione (detti /I/O-bound/) vengono privilegiati rispetto a quelli che elaborano continuamete (/compute-bound/). I processi I/O-bound sono infatti, nella maggior parte dei casi, programmi interattivi come un editor o una shell, ed e' dunque importante minimizzarne il tempo di reazione. Questo a discapito dei processi compute-bound, per i quali il tempo di risposta non e' rilevante. In UNIX ogni processo possiede una priorita' ed ottiene il controllo del microprocessore ad intervalli piu' frequenti (o per tempi piu' lunghi) se la sua priorita' e' alta. Le priorita' su UNIX vengono modificate dinamicamente da uno scheduler di secondo livello. Quest'ultimo viene eseguito ad intervalli di tempo piu' lunghi (per esempio una volta al secondo). I criteri (policy) con cui vengono riassegnate le priorita' sono in genere piuttosto complessi e prendono in considerazione numerosi parametri sul comportamento passato di ogni processo. Per garantire all'operatore un certo grado di controllo sulla priorita' di ciascun processo, e' possibile impostare per ognuno un valore detto "nice", che indica il grado di disponibilita' a cedere il passo agli altri processi (l'antagonista della priorita'). Semafori Le tecniche per implementare il multitasking pre-emptive erano gia' ben consolidate ai tempi in cui furono progettati MacOS e Windows, percio' e' lecito domandarsi come mai i progettisti di Apple e Microsoft optarono per una soluzione tecnicamente inferiore. Se da una parte implementare uno scheduler multitasking e' cosa di poco conto, in un sitema in cui i processi perdono il controllo della CPU in modo incontrollabile sorgono una serie di problemi di sincronizzazione che richiedono un'attenzione particolare nella progettazione dell'intero sistema operativo. L'accesso a tutte le strutture dati di sistema deve essere arbitrato in modo da evitare che un processo possa accedervi proprio mentre un altro sta modificando le stesse. Queste situazioni prendono il nome di /race conditions/ e devono essere evitate perche' conducono inevitabilmente al malfunzionamento o al crash del sistema. L'/API/ dei sistemi operativi odierni viene progettata in modo da nascondere questi dettagli implementativi al programmatore di applicazioni, ma cio' non toglie che il problema esista e debba essere affrontato. Per evitare le race-conditions, sono noti degli accorgimenti di vario tipo, tra cui i "semafori" ed i "mutex". In pratica, prima di modificare una struttura condivisa il processo deve ottenere l'accesso esclusivo ad essa bloccando (/lock/) il semaforo che la protegge. Se un altro processo tenta di fare la stessa cosa, verra' messo in attesa finche' il primo non rilascia il semaforo (unlock). La sezione di programma compresa tra il lock e l'unlock viene detta "critical section". In certi casi puo' essere necessario ottenere l'accesso esclusivo a due o piu' risorse distinte per poter portare a termine un'operazione. Se il processo A tenta di bloccare in successione i semafori di cui ha bisogno per proseguire, prima o poi potrebbe incontrarne uno che e' gia' bloccato da un altro processo B ed essere quindi messo in attesa. Se a questo punto B richiedesse uno dei semafori gia' bloccati da A, entrambi rimarrebbero in attesa senza alcun modo di proseguire (condizione di /deadlock/ o /stallo/). Anche se puo' sembrare una situazione piuttosto rara, l'esperienza ha mostrato che essa si verifica molto piu' spesso di quanto si possa credere e pertanto non e' possibile ignorarla. Esistono diverse tecniche per prevenire o risolvere a posteriori le condizioni di deadlock. Nessuna di queste, pero', e' del tutto soddisfacente. L'unico modo efficace di affrontare il problema consiste nel progettare l'intero sistema operativo in modo che non sia mai necessario ottenere piu' di un semaforo contemporaneamente per svolgere un'operazione. Quando questo e' inevitabile, ci si assicura che l'ordine in cui i semafori vengono bloccati sia sempre il medesimo. Purtroppo nella pratica questa soluzione e' tuttaltro che facile da implementare in un sistema operativo complesso. SMP Torniamo al problema dello scheduling dei processi. Esso diventa molto piu' complesso nei sistemi con architettura Simmetrical Multi Processing, dotati cioe' di piu' microprocessori. In questi, lo scheduler ha anche il compito di stabilire il /mapping/ tra processi e CPU, operazione che diviene sempre piu' complessa al crescere del numero delle CPU e dei processi. Al momento non si conoscono algoritmi che risolvano in modo ottimale questo problema; questo e' uno dei tanti fattori che limitano il numero pratico di CPU di cui possono essere dotati i computer di oggi. Per ammissione dello stesso Linus Torvalds, il supporto SMP di Linux non e' ancora del tutto soddisfacente. In un sistema progettato tenendo conto dell'SMP fin dal principio, come ad esempio il BeOS, si deve porre particolare attenzione nel limitare i casi in cui una parte del sistema in esecuzione su una CPU si blocchi in attesa del completamento di un'operazione a carico di un'altra CPU. Se questa situazione tende a verificarsi troppo di frequente, si vanifica il vantaggio di possedere piu' microprocessori, perche' essi lavorano alternativamente anziche' in parallelo. I kernel UNIX-like di concezione tradizionale, tra cui Linux, le varie incarnazioni del BSD, nonche' la maggior parte degli UNIX commerciali, possiedono un'architettura estermamente svantaggiosa per l'SMP. Difatti il kernel e', con alcune eccezioni, un enorme programma monolitico. In questo caso non ci riferiamo alla modularita' dei driver del kernel (Linux e' molto modulare), ma alla suddivisione logica del kernel in entita' distinte che interagiscono tra loro. I kernel UNIX di oggi, Linux in testa, sono costituiti da un gran numero di moduli, alcuni dei quali possono essere caricati su richiesta. Cio' non toglie che il kernel, con tutti i suoi driver e moduli, resta comunque un'unica entita'. Un caso a parte e' costituito dai sistemi Amiga dotati delle schede acceleratrici PowerUP prodotte da Phase 5. Questi rappresentano uno dei rarissimi esempi di multiprocessing /asimmetrico/. Due CPU diverse, un 68040 o 68060 ed un PowerPC 603e o 604e, condividono la stessa memoria di sistema ed accedono (con alcune limitazioni) agli stessi dati. Inevitabilmente, ognuna delle due CPU possiede il proprio gruppo di processi ed il proprio scheduler. Nonostante la maggior parte del sistema operativo rimanga confinata al 68k, il PPC possiede comunque un kernel completo che offre ai propri processi la maggior parte dei servizi di base. Alcuni programmi contengono al proprio interno codice per entrambe le CPU. Essi utilizzano un processo 68k come frontend (interfaccia utente, I/O e altro) mentre un secondo processo gira sul piu' veloce PowerPC e si occupa dell'esecuzione dei calcoli. La comunicazione tra le due entita' avviene tramite dei buffer di memoria condivisa sui quali sono necessari alcuni accorgimenti che hanno lo scopo di mantenere coerenti i dati contenuti nelle cache dei due microprocessori. Ovviamente questi dettagli dipendenti dall'hardware sono opportunamente nascosti al programmatore per mezzo di funzioni apposite. Nei sistemi SMP "tradizionali", il problema della coerenza della cache viene invece risolto in hardware con alcune lievi penalita' in termini di prestazioni. I programmi che si limitano ad utilizzare le funzioni ANSI/POSIX, come per esempio il compilatore gcc, possono essere compilati interamente per PPC senza alcun accorgimento particolare da parte del programmatore. Sono le funzioni di libreria del compilatore C ad occultare la comunicazione con un processo slave sulla CPU 68k che svolge l'I/O vero e proprio. Questo modello consente di compilare il codice portabile su PowerPC senza modifiche sostanziali, ma ha lo svantaggio di essere estremamente inefficiente qualora il programma esegua un gran numero di chiamate di sistema con altrettanti switch di contesto tra PPC e 68k. Threads Tradizionalmente, esiste una correlazione biunivoca tra programma e processo. Un processo rappresenta lo stato di un programma in fase di esecuzione. Ad ogni processo corrisponde uno stack, un set di registri della CPU (i quali devono essere salvati e ripristinati quando il processo perde e riottiene il controllo) e altre informazioni di stato come la directory corrente e la lista dei file aperti. Tuttavia, alcuni programmi necessitano di un multitasking intrinseco per poter svolgere diverse operazioni in parallelo. Basta pensare ad un browser web, che deve effettuare contemporaneamente numerose connessioni per velocizzare il trasferimento delle pagine. A maggior ragione, i server HTTP necessitano di un parallelismo ancora piu' elevato per poter soddisfare nel minor tempo possibile tutte le richieste di connessione che pervengono. In questo caso piu' processi devono poter accedere agli stessi dati (per esempio la pagina visualizzata dal browser). In UNIX, la protezione della memoria (di cui parleremo piu' avanti) assicura che un processo non possa accedere ai dati di un altro, se non utilizzando dei meccanismi esterni come i segnali ed i file. In questo caso la protezione della memoria rappresenta un ostacolo che deve essere aggirato. Inoltre nei sistemi dotati di memoria protetta, non sempre e' possibile (o efficiente) lanciare un nuovo processo per ogni compito che si deve portare a termine. La creazione di un processo e' un'operazione relativamente costosa, perche' il kernel deve creare un nuovo contesto ed allocare una nuova area di memoria per i dati. Inoltre, tornando all'esempio del browser, i processi non potrebbero cooperare in modo abbastanza flessibile da aggiornare in parallelo la visualizzazione di una pagina web. I thread costituiscono una soluzione ottimale per questa classe di problemi. Un'applicazione /multithreaded/ possiede all'interno di un solo processo numerosi contesti di esecuzione che possono accedere allo stesso insieme di dati e condividono impostazioni globali come ad esempio la directory corrente. E' possibile creare un'applicazione multithreaded senza alcun supporto da parte del kernel, implementando uno scheduler internamente all'applicazione. Come in un sistema operativo, lo scheduler dei thread puo' essere di tipo cooperativo o preemptive. Nel secondo caso esso puo' entrare in azione grazie ad un "segnale" di timer (l'equivalente software di un interrupt hardware). Esistono ovviamente librerie di supporto (come la /pthreads/) che semplificano la gestione dei thread offrendo nel contempo portabilita' verso altri microprocessori. Il principale svantaggio di un modello di multithreading "fatto in casa" e' che il kernel rimane completamente all'oscuro di cio' che sta accadendo all'interno del processo e pertanto non e' in grado di distribuire i thread su piu' microprocessori nel caso di sistemi SMP. In effetti la parallelizzazione di un programma puo' essere anche una tecnica per accelerare la velocita' di esecuzione di calcoli intensivi (come in un programma di ray-tracing) sfruttando il multiprocessing simmetrico. Per questo motivo tutti i sistemi operativi moderni offrono un supporto nativo per i thread. Nelle versioni piu' recenti di Solaris, l'atomo conosciuto dallo scheduler non e' piu' il processo, ma una forma semplificata di esso detta Light Weight Process (processo leggero). Nel caso banale di un programma non multithreaded, il processo possiede un unico LWP. Altrimenti possono esservi tanti LWP quanti sono i thread, ma non necessariamente. Si e' trovato infatti che le migliori prestazioni si ottengono creando al piu' un LWP per ogni CPU. Se i thread sono in numero superiore, essi verranno ripartiti equamente tra gli LWP disponibili. Nei sistemi in cui non esiste protezione della memoria, quali AmigaOS, la distinzione tra thread e processo e' del tutto irrilevante. Un'applicazione multithreaded altro non e' che un gruppo di processi creati dallo stesso programma. Il BeOS, che come abbiamo gia' detto e' stato progettato fin da subito per un sistema SMP quale era il compianto BeBox, utilizza un modello differente. Quelli che in UNIX si chiamano processi si comportano in realta' come dei thread. Un gruppo di processi che fanno capo ad un'applicazione prende il nome di "team" (squadra). Non e' raro che un team sia costituito da 4-5 processi ed in alcuni casi possono esservene decine. Programmando in ambiente Windows ci si accorge molto presto di come la gestione del multithreading sia fastidiosamente limitata. Difatti, in Windows gli oggetti di sistema (come finestre e file) vengono manipolate dai processi con degli identificatori numerici detti /handle/. Per esempio, l'handle 37 puo' corrispondere ad un bottone di una finestra. Il mapping tra handle e oggetti veri e propri e' specifico di ogni thread: lo stesso handle in due thread puo' rappresentare oggetti completamente diversi. Questo significa che i thread, pur potendo accedere liberamente ai dati globali, non possono utilizzare gli stessi handle per aggiornare l'interfaccia utente. Sebbene sia possibile ottenere un duplicato di un handle valido per un altro thread, questa operazione risulta piuttosto complessa per tutti i programmi che utilizzano la libreria orientata agli oggetti di Windows (Microsoft Foundation Classes). Gli handle veri e propri sono infatti nascosti un po' ovunque all'interno delle classi C++ fornite da MFC. Questa architettura rende di fatto impossibile la condivisione degli stessi oggetti tra piu' thread. La barriera tra kernel e processi Alla base della nota stabilita' di UNIX vi e' la totale separazione tra kernel e processi. Da un processo non vi e' alcun modo di alterare le strutture vitali del sistema operativo perche' esse si trovano al di la' di una "barriera" logica. Per poter richiedere un servizio fornito del kernel (per esempio l'apertura di un file) i processi devono eseguire delle chiamate di sistema (dette /syscall/). Le syscall altro non sono che degli interrupt software: la CPU passa dal modo utente al modo supervisore (detto anche modo master o modalita' privilegiata) ed inizia ad eseguire il codice del kernel. In genere durante l'esecuzione di una syscall lo scheduling dei task rimane bloccato. Se l'esecuzione della syscall puo' essere completata immediatamente, il controllo torna al processo chiamante. Nel caso che l'esecuzione della syscall richieda delle operazioni di input/output per le quali e' necessario attendere, il kernel accoda la richiesta, mette in attesa il processo chiamante e consente allo scheduler di passare il controllo ad un altro processo. Alcuni kernel UNIX non sono rientranti: non e' possibile cioe' che piu' processi entrino contemporaneamente nel kernel. Si tratta in sostanza di un /escamotage/ per evitare di riempire il kernel di semafori ad evitare le race conditions che potrebbero verificarsi. Purtroppo questa architettura presenta numerosi svantaggi. Non e' raro, ad esempio, che l'intero sistema si blocchi perche' il kernel e' in attesa di un evento che non avverra' mai, come la risposta ad un comando inviato ad una periferica malfunzionante. Per aggirare questo problema la maggioranza dei kernel UNIX imposta dei timeout prima di iniziare delle attese potenzialmente pericolose. Kernel threads e me Una soluzione piu' concreta, che ha anche il pregio di funzionare bene con i sistemi SMP, consiste nel far svolgere il lavoro del kernel da diversi /thread/, eventualmente creandone continuamente di nuovi per soddisfare le richieste dei processi senza dover attendere il completamento delle operazioni gia' in corso. Per esempio, i filesystem, ed i sottosistemi di networking possono essere fatti girare in parallelo utilizzando i kernel thread. I kernel thread sono stati introdotti da parecchio tempo in Solaris e piu' recentemente anche in Linux e FreeBSD, ove l'uso rimane tuttora piuttosto limitato. Pur essendo da tutti considerato all'avanguardia dal punto di vista tecnologico, il kernel di Linux possiede una struttura di vecchia concezione, piuttosto comune tra i sistemi operativi derivati da UNIX. Il microkernel Mach, sviluppato nel 1985 presso la Carnegie-Mellon University, ottenne un discreto successo all'inizio degli anni '90 grazie alla sua architettura decisamente innovativa. Come suggerisce il nome, un microkernel altro non e' che un kernel che fornisce unicamente le funzionalita' di base di un sistema operativo, quali il multitasking, la gestione degli interrupts e della memoria. Al di sopra di un kernel cosi' semplificato e' possibile sviluppare uno strato ulteriore di software che costituisce il kernel vero e proprio. L'idea consiste nel suddividere i servizi forniti dal kernel in un certo numero di processi indipendenti (chiamati "server" nel gergo Mach). Il microkernel ha la funzione di supervisore dei server e gode rispetto ad essi di un grado di protezione aggiuntivo, di modo che un bug in uno qualsiasi dei server non possa nuocere all'intero kernel. Una tecnica di protezione simile viene utilizzata anche in OS/2 per ottenere una stabilita' migliore (difatti OS/2 e' noto per andare in crash "un po' alla volta"). Il microkernel Mach fu impiegato nel 1992 come sistema operativo delle workstation NeXT (ora conosciuto come NeXTStep), il cui eccezionale design ricevette numerosi riconoscimenti all'epoca. Purtroppo le prestigiose NeXTstation non riuscirono a conquistare uno share di mercato sufficiente nonostante tutti gli sforzi di Steve Jobs. Successivamente, il Mach venne rilasciato pubblicamente ed utilizzato come microkernel per il progetto GNU/Hurd. Si tratta di un sistema operativo di recente concezione, ma tuttora largamente incompleto. Hurd impiega una moltituidine (ovvero un'/orda/) di server per ottenere la massima modularita' possibile. Una volta ultimato, esso dovrebbe offrire un grado di liberta' in piu' agli amministratori di sistema e agli utenti. Grazie a questa modularita' "spinta", e' infatti possibile aggiungere, rimuovere e sostituire a run-time parti anche vitali del sistema operativo, oppure fornire ad ogni utente un sistema operativo virtuale indipendente dagli altri. In linea teorica e' perfino possibile eseguire contemporaneamente due sistemi operativi diversi, purche' entrambi siano basati su Mach. Negli ultimi anni il Mach e' tornato in auge grazie alle intenzioni manifestate da Steve Jobs in seguito al suo ritorno in casa Apple. Il tanto atteso MacOS X, precedentemente conosciuto con il nome in codice Rhapsody, dovrebbe essere basato su un kernel Mach e su una "userland" derivata dal BSD. Mettendo in campo l'elegante GUI che ha da sempre contraddistinto MacOS con la versatilita' tipica di UNIX unitamente all'affidabilita' di Mach, Jobs si aspetta di mettere alla luce il piu' potente sistema operativo che sia mai visto. Mentre i fedelissimi utenti Macintosh attendono il momento della riscossa, Apple ha appoggiato e generosamente finanziato lo sviluppo di MkLinux, il porting di Linux per PPC basato su Mach. Si tratta in verita' di un compromesso piuttosto discutibile, in quanto il kernel di Linux, cosi' com'e', mal si presta ad essere spezzato in server Mach distinti e viene pertanto eseguito come un singolo server. Message passing La suddivisione del kernel in piu' server concorrenti che avviene nel Mach richiede un meccanismo di qualche tipo che permetta ai server di comunicare tra loro. Nei sistemi operativi UNIX-like i processi utente svolgono ognuno la propria funzione interagendo con il kernel e possono dunque ignorarsi reciprocamente. Questa sorta di autismo non e' tuttavia ne' propizia, ne' efficiente nel caso di una quantita' di server che /devono/ cooperare all'interno del kernel. Trattandosi di processi distinti, non possiamo aspettarci di cavarcela con delle normali chiamate a funzione. Il microkernel deve fornire dunque delle primitive di comunicazione interprocesso (IPC). I segnali costituiscono la forma piu' rudimentale di IPC. Un processo puo' segnalare un evento ad un altro processo, come lo scadere di un timer o il termine di un'operazione in corso). Una forma di IPC piu' elaborata si basa sui semafori, che possono essere usati per regolare lo scambio di comandi e informazioni tra processi in un buffer condiviso. Una forma piu' versatile di comunicazione interprocesso prende il nome di message passing (scambio di messaggi) ed e' impiegata in molti sistemi operativi con architettura a microkernel. Un messaggio e' un pacchetto di informazioni che un processo puo' inviare ad un altro utilizzando una primitiva del microkernel chiamata Send(), Post() o Put(), a seconda del gergo in uso. Piu' messaggi possono essere inviati contemporaneamente da piu' processi allo stesso destinatario, nel qual caso essi vengono inseriti in una coda di attesa (message queue, FIFO o port). Il processo destinatario puo', in qualsiasi momento, controllare la presenza di messaggi accodati con una seconda primitiva chiamata Receive(), Get() o Peek(), entrando cosi' in possesso delle informazioni contenute nel messaggio. A questo punto il destinatario elabora la richiesta che ha ricevuto e al termine risponde al mittente del messaggio usando la primitiva Reply(). La risposta puo' contenere dei dati elaborati o un codice di errore che il mittente puo' controllare per verificare l'esito dell'operazione. Se nell'attesa della risposta il processo rimane bloccato, il modello di IPC si dice /sincrono/, altrimenti /asincrono/. Anche Windows possiede una forma limitata di message passing che consiste in un "loop" di messaggi tra le applicazioni (i processi) ed un'entita' non bene identificata che rappresenta il sistema operativo. A sottolineare il fatto che il kernel di Windows e' stato progettato intorno alle sue finestre, le applicazioni non possono ricevere o inviare messaggi finche' non hanno aperto la loro finestra principale, ma anche una finestra invisibile (hidden) puo' servire allo scopo. Anziche' trasportare un pacchetto di dati a lunghezza variabile, i messaggi di Windows si accontentano di due parametri interi, appena sufficienti a contenere un codice numerico e l'immancabile handle che identifica la finestra interessata. Nelle versioni a 16bit di Windows la funzione di prelievo dei messaggi conteneva al proprio interno anche il codice che implementa il task switching cooperativo, percio' ancora oggi non e' raro vedere programmi Windows che prelevano i messaggi nel bel mezzo dei loop piu' intensivi. L'evidente asimmetria nel rapporto tra Windows e le proprie applicazioni rende difficoltosa la comunicazione tra processi, che viene implementato quando serve usando un ventaglio di soluzioni diverse, tra cui l'inefficiente e poco raccomandabile DDE (Direct Data Exchange), che ci asterremo dal descrivere ulteriormente. AmigaOS e' un sistema operativo piuttosto singolare in quanto non esiste una netta seprazione tra kernel e processi. Il Kickstart contiene una serie di librerie, device e handler, ciascuno dei quali fornisce una parte delle funzioni tipiche di un sistema operativo. L'interazione tra questi moduli avviene per lo piu' con chiamate a funzione e scambio di messaggi. Alcuni di questi moduli, in particolare i device ed i filesystem, creano un task al momento dell'inizializzazione del sistema e rimangono in attesa che giungano dei comandi alle rispettive /message port/. Le message port sono oggetti a se' stanti che possono essere configurati per inviare un segnale al proprio task ogni volta che viene recapitato un messaggio, oppure eseguire una routine di interrupt software. Ogni task puo' possedere piu' di una message port e puo' rimanere in attesa dell'arrivo di messaggi da piu' porte contemporaneamente. E' facile rendersi conto di come un'architettura di questo tipo sia estremamente modulare ed adatta ai sistemi SMP (in realta' esistono altri problemi nel design di AmigaOS che ne impediscono la realizzazione pratica). QNX e' un sistema operativo a microkernel per applicazioni embedded, la cui scalabilita' e' pressoche' imbattibile. Il suo microkernel e' eccezionalmente ridotto e demanda un gran numero di funzionalita' di basso livello ai comuni processi. Grazie ad un /overhead/ molto ridotto sul context switching e sulle primitive IPC, QNX puo' permettersi di utilizzare il message passing laddove gli altri sistemi hanno dovuto rinunciarvi per ragioni di efficienza. Alcune funzioni di gestione dei processi sono demandate al "Process Manager", che e' a sua volta un normale processo. Persino gli interrupt hardware vengono rediretti dal microkernel ai processi che sono incaricati di servirli. Questa scomposizione del kernel in processi messi in comunicazione tra loro esclusivamente tramite le funzioni di message passing permette di escludere o sostituire in qualsiasi momento le parti fondamentali del sistema operativo. Sebbene i messaggi in QNX siano esclusivamente di tipo sincrono, essi possono essere spediti indirettamente utilizzando degli oggetti detti "proxy". I proxy hanno la funzione di ripetitori di messaggi e ne gestiscono autonomamente l'invio e la ricezione della risposta, lasciado cosi' il processo libero di proseguire. Ma la caratteristica di maggior rilievo nel sistema di message passing di QNX consiste nella possibilita' di scambiare messaggi tra processi residenti su computer diversi. Il Network Manager di QNX puo' stabilire un circuito virtuale che mette in comunicazione i due processi attraverso la rete locale. Dalla prospettiva dei programmi non sussite alcuna differenza tra l'invio di messaggi ad un processo locale o ad uno remoto. Le implicazioni di questa architettura sono sbalorditive: dal momento che in QNX l'interfaccia con i filesystem e con i driver delle periferiche e' basata sui messaggi, si ottiene gratuitamente la condivisione dei file in rete e la redirezione dell'output grafico su sistemi remoti. Tutto questo senza aver dovuto implementare protocolli appositi come NFS, RPC e X11. Le possibilita' vanno ben oltre: e' possibile distribuire l'hardware ed il software in una rete di sistemi QNX ed accedere a tutte queste risorse come se fossero fisicamente connesse ad un unico computer. Tutto questo con QNX e' possibile gia' da alcuni anni, mentre i produttori dei sistemi operativi di vecchia concezione promettono di implementare funzioni limitate di /clustering/ che non diventeranno mai realta'. Una regola di design troppo spesso sottovalutata nella progettazione dei sistemi operativi e' che la semplicita' e la simmetria possono ripagare in modo inaspettato. Conclusioni Abbiamo visto come ogni sistema operativo, per certi versi, sia migliore degli altri perche' rappresenta una soluzione differente ad un comune insieme di problemi. Proprio come in natura, la "biodiversita'" nell'informatica e' alla base dell'evoluzione, pertanto deve essere considerata un bene prezioso che e' importante preservare anche quando non vi e' convenienza economica o interesse da parte degli utenti finali. In questo articolo abbiamo tralasciato un importante aspetto dei sistemi operativi: la gestione della memoria. Si tratta di un argomento quantomai vasto, la cui trattazione esaustiva avrebbe sottratto troppo spazio agli altri argomenti di cui ci siamo occupati questo mese. Non e' escluso, dunque, che in futuro torneremo ad occuparci di questo interessante argomento per fare luce sulle numerose tecniche che i sistemi operativi utilizzano per implementare la protezione della memoria, la memoria virtuale e la shared memory. Bibliografia [1] "QNX Operating System, System Architecture", QNX Software Systems Ltd. [2] "The Linux Kernel Book", Remy Card, Eric Dumas, Franck Mevel, John Wiley & Sons. [3] "The Be Book 4.5", Be Inc. http://www-classic.be.com/documentation/be_book/index.html [4] "Amiga ROM Kernel Reference Manuals, 2nd edition", Addison Wesley [5] "Design Elements of the FreeBSD VM System", Matt Dillon, Daemon News, http://www.daemonnews.org/200001/freebsd_vm.html [6] "Multithreaded Programming with Pthreads", Bill Lewis, Daniel Berg, Prentice Hall [7] "NetBSD Documentation: Kernel", The NetBSD Foundation, http://www.netbsd.org/Documentation/kernel/ [8] "Solaris 7 Developer Collection", Sun Microsystems Inc. Si ringrazia Simone Lo Gioco per la realizzazione dei diagrammi che accompagnano questo articolo. Glossario API - Application Programmer Interface (interfaccia per il programmatore di applicazioni) BSD - Berkeley Software Distribution. Sistema operativo UNIX-like dal quale sono derivati numerosi sistemi UNIX commerciali e non. clustering - Raggruppamento logico di sistemi collegati in rete atto a costituire un unico computer virtuale. concurrent, concorrente - Esecuzione parallela (vedi /time-sharing/) di flussi di elaborazione distinti. deadlock - blocco del sistema (o di un processo) causato dal tentativo di bloccare un semaforo che per qualche ragione non diverra' mai libero. IPC - Interprocess Communication (comunicazione interprocesso) kernel - (lett. 'nocciolo') componente principale di un sistema operativo che interagisce con l'hardware per offrire alle applicazioni delle funzioni di alto livello. kernel space - spazio di memoria riservato al kernel, al quale i processi utente non possono accedere. microkernel - kernel di dimensioni ridotte che incorpora unicamente le funzioni di base (multitasking, IPC, memoria), demandando ad altre parti del sistema operativo i compidi di livello piu' elevato. multitasking - possibilita' di eseguire contemporaneamente piu' compiti (task) diversi. mutex - oggetto di sincronizzazione che grantisce l'accesso mutualmente esclusivo a dei dati condivisi tra piu' processi in un ambiente multitasking. race-condition - condizione in cui due o piu' processi possono agire contemporaneamente su di una risorsa condivisa. rientrante - codice scritto in modo da poter essere eseguito contemporaneamente da parte di piu' processi. round-robin - modello di multitasking basato sulla spartizione della CPU tra N processi ad intervalli di tempo omogenei. scheduler - gestore del multitasking che controlla l'assegnazione della CPU ai processi segnale - Evento asincrono che puo' essere comunicato ad un processo. I segnali possono essere immaginati come degli interrupt al livello di processo. semaforo - oggetto di sincronizzazione che garantisce l'accesso condiviso o esclusivo ad uno o piu' processi o thread (vedi anche mutex). time-sharing - suddivisione del tempo di elaborazione della CPU tra piu' processi. userland - (lett. "terra degli utenti"). Collezione di programmi e librerie residenti su disco che formano un sistema operativo, escluso il kernel. user space - spazio di memoria esterno al kernel (vedi anche /kernel space/).