SerHack/books

A set of translated and written books.


L'astrazione: I Processi

In questo capitolo, discutiamo una delle astrazioni più fondamentali che il sistema operativo fornisce agli utenti: il processo. La definizione di un processo, informalmente, è abbastanza semplice: è un programma in esecuzione [V+65,BH70]. Il programma in sé è una cosa senza vita: se ne sta lì sul disco, un mucchio di istruzioni (e forse qualche dato statico), in attesa di entrare in azione. È il sistema operativo che prende questi byte e li fa funzionare, trasformando il programma in qualcosa di utile. Si scopre che spesso si vuole eseguire più di un programma alla volta; per esempio, considerate il vostro desktop o laptop dove potreste voler eseguire un browser web, un programma di posta, un gioco, un lettore musicale e così via. In effetti, un tipico sistema può apparentemente eseguire decine o addirittura centinaia di processi allo stesso tempo. Questo rende il sistema facile da usare, poiché non ci si deve mai preoccupare se una CPU è disponibile; si eseguono semplicemente i programmi. Da qui la nostra sfida:

IL NOCCIOLO DEL PROBLEMA: COME FORNIRE L’ILLUSIONE DI MOLTE CPU?

Anche se ci sono solo poche CPU fisiche disponibili, come può il sistema operativo fornire l’illusione di una fornitura quasi infinita di tali CPU?

Il sistema operativo crea questa illusione virtualizzando la CPU. Eseguendo un processo, poi fermandolo ed eseguendone un altro, e così via, il sistema operativo può promuovere l’illusione che esistano molte CPU virtuali quando in realtà c’è solo una CPU fisica (o poche). Questa tecnica di base, nota come condivisione del tempo della CPU, permette agli utenti di eseguire tutti i processi concorrenti che desiderano; il costo potenziale è la performance, poiché ognuno di essi girerà più lentamente se la CPU (o le CPU) devono essere condivise. Per implementare la virtualizzazione della CPU, e per implementarla bene, il sistema operativo avrà bisogno sia di alcuni meccanismi di basso livello che di intelligenza di alto livello. Chiamiamo il macchinario di basso livello, meccanismi; i meccanismi sono metodi o protocolli di basso livello che implementano un pezzo necessario di funzionalità.

CONSIGLIO: USATE LA CONDIVISIONE DEL TEMPO (E DELLO SPAZIO)

La condivisione del tempo (time-sharing) è una tecnica di base utilizzata da un sistema operativo per condividere una risorsa. Permettendo che la risorsa sia usata per un po’ di tempo da un’entità, e poi un po’ di tempo da un’altra, e così via, la risorsa in questione (ad esempio, la CPU, o un collegamento di rete) può essere condivisa da molti. La controparte della condivisione del tempo è la condivisione dello spazio, dove una risorsa è divisa (nello spazio) tra coloro che desiderano usarla. Per esempio, lo spazio su disco è naturalmente una risorsa condivisa nello spazio; una volta che un blocco è assegnato a un file, normalmente non è assegnato a un altro file finché l’utente non cancella il file originale.

Per esempio, impareremo più avanti come implementare un cambio di contesto, che dà al sistema operativo la capacità di fermare l’esecuzione di un programma e iniziarne un altro su una data CPU; questo meccanismo di condivisione del tempo è impiegato da tutti i sistemi operativi moderni. In cima a questi meccanismi risiede una parte dell’intelligenza del sistema operativo, sotto forma di politiche (o anche detto policy). Le politiche sono algoritmi per prendere qualche tipo di decisione all’interno del sistema operativo. Per esempio, dato un certo numero di possibili programmi da eseguire su una CPU, quale programma dovrebbe essere eseguito dal sistema operativo? Una politica di programmazione nel sistema operativo prenderà questa decisione, probabilmente utilizzando informazioni storiche (ad esempio, quale programma è stato eseguito di più nell’ultimo minuto?), la conoscenza del carico di lavoro (ad esempio, quali tipi di programmi vengono eseguiti), e le metriche delle prestazioni (ad esempio, il sistema sta ottimizzando le prestazioni interattive, o il throughput?

L’astrazione: un processo

L’astrazione fornita dal sistema operativo di un programma in esecuzione è qualcosa che chiameremo processo. Come abbiamo detto sopra, un processo è semplicemente un programma in esecuzione; in qualsiasi istante nel tempo, possiamo riassumere un processo facendo un inventario dei diversi pezzi del sistema a cui accede o che influisce nel corso della sua esecuzione. Per capire cosa costituisce un processo, dobbiamo quindi capire il suo stato macchina: ciò che un programma può leggere o aggiornare quando è in esecuzione. In qualsiasi momento, quali parti della macchina sono importanti per l’esecuzione di questo programma? Un componente ovvio dello stato della macchina che comprende un processo è la sua memoria. Le istruzioni si trovano nella memoria; anche i dati che il programma in esecuzione legge e scrive si trovano nella memoria. Così la memoria che il processo può indirizzare (chiamata il suo spazio di indirizzi) è parte del processo. Anche i registri fanno parte dello stato della macchina del processo; molte istruzioni leggono o aggiornano esplicitamente i registri e quindi sono chiaramente importanti per l’esecuzione del processo.

Si noti che ci sono alcuni registri particolarmente speciali che fanno parte di questo stato della macchina. Per esempio, il program counter (PC) (a volte chiamato puntatore di istruzione o IP) ci dice quale istruzione del programma verrà eseguita dopo; allo stesso modo un puntatore di stack e un puntatore associato ad un frame sono utilizzati per gestire lo stack per i parametri di funzione, variabili locali e indirizzi di ritorno. Infine, i programmi spesso accedono anche a dispositivi di memorizzazione persistenti. Tali informazioni I/O potrebbero includere una lista dei file che il processo ha attualmente aperto.

SUGGERIMENTO: SEPARARE POLITICA E MECCANISMO

In molti sistemi operativi, un paradigma comune di progettazione è quello di separare le politiche di alto livello dai loro meccanismi di basso livello [L+75]. Si può pensare al meccanismo come se fornisse la risposta ad una domanda sul come di un sistema; per esempio, come fa un sistema operativo ad eseguire un cambio di contesto? La politica fornisce la risposta ad una domanda “quale”; per esempio, quale processo dovrebbe eseguire il sistema operativo in questo momento? Separare le due cose permette di cambiare facilmente le politiche senza dover ripensare il meccanismo ed è quindi una forma di modularità, un principio generale di progettazione del software.

API per i processi

Anche se rimandiamo la discussione di una vera e propria API di processo ad un successivo capitolo, qui diamo prima qualche idea di ciò che deve essere incluso in qualsiasi interfaccia di un sistema operativo. Queste API, in qualche forma, sono disponibili su qualsiasi sistema operativo moderno.

Creazione del processo: Un po’ più di dettagli

Un mistero che dovremmo smascherare un po’ è come i programmi si trasformano in processi. In particolare, come fa il sistema operativo a far partire un programma? Come funziona effettivamente la creazione dei processi? La prima cosa che il sistema operativo deve fare per eseguire un programma è caricare il suo codice e qualsiasi dato statico (ad esempio, le variabili inizializzate) in memoria, nello spazio degli indirizzi del processo. I programmi inizialmente risiedono su disco (o, in alcuni sistemi moderni, su SSD basati su flash) in qualche tipo di formato eseguibile; così, il processo di caricamento di un programma e dei dati statici in memoria richiede che il SO legga quei byte dal disco e li metta in memoria da qualche parte (come mostrato nella Figura 4.1).

Nei primi (o semplici) sistemi operativi, il processo di caricamento è fatto in modo avido, cioè tutto in una volta prima di eseguire il programma; i sistemi operativi moderni eseguono il processo in modo pigro, cioè caricando pezzi di codice o dati solo quando sono necessari durante l’esecuzione del programma. Per capire veramente come funziona il caricamento pigro di pezzi di codice e dati, dovrete capire meglio il meccanismo di paginazione e swapping, argomenti che tratteremo in futuro quando parleremo della virtualizzazione della memoria. Per ora, ricordate solo che prima di eseguire qualsiasi cosa, il sistema operativo deve chiaramente fare del lavoro per portare i bit importanti del programma dal disco alla memoria.

Una volta che il codice e i dati statici sono caricati in memoria, ci sono alcune altre cose che il sistema operativo deve fare prima di eseguire il processo. Un po’ di memoria deve essere allocata per lo stack di run-time del programma (o semplicemente stack). Come probabilmente già sapete, i programmi C usano lo stack per variabili locali, parametri di funzione e indirizzi di ritorno; il sistema operativo alloca questa memoria e la dà al processo. Il sistema operativo probabilmente inizializzerà anche lo stack con argomenti; in particolare, riempirà i parametri della funzione main(), cioè argc e l’array argv.

Il sistema operativo può anche allocare un po’ di memoria per l’heap del programma. Nei programmi C, l’heap è usato per i dati allocati dinamicamente richiesti esplicitamente; i programmi richiedono tale spazio chiamando malloc() e lo liberano esplicitamente chiamando free(). L’heap è necessario per strutture di dati come liste collegate, tabelle hash, alberi e altre strutture di dati interessanti. L’heap sarà piccolo all’inizio; man mano che il programma viene eseguito e richiede più memoria tramite l’API della libreria malloc(), il sistema operativo può essere coinvolto e assegnare più memoria al processo per aiutare a soddisfare tali richieste.

Il sistema operativo farà anche altri compiti di inizializzazione, in particolare per quanto riguarda l’input/output (I/O). Per esempio, nei sistemi UNIX, ogni processo ha di default tre descrittori di file aperti, per l’input standard, l’output e l’errore; questi descrittori permettono ai programmi di leggere facilmente l’input dal terminale e stampare l’output sullo schermo. Impareremo di più su I/O, descrittori di file e simili nella terza parte del libro sulla persistenza. Caricando il codice e i dati statici in memoria, creando e inizializzando uno stack, e facendo altro lavoro relativo all’impostazione dell’I/O, il sistema operativo ha ora (finalmente) preparato la scena per l’esecuzione del programma. Ha quindi un ultimo compito: avviare il programma in esecuzione al punto di ingresso, cioè main(). Saltando alla routine main() (attraverso un meccanismo specializzato che discuteremo nel prossimo capitolo), il sistema operativo trasferisce il controllo della CPU al processo appena creato, e così il programma inizia la sua esecuzione.

Stati di processo

Ora che abbiamo un’idea di cosa sia un processo (anche se continueremo continueremo a raffinare questa nozione), e (approssimativamente) come viene creato, parliamo dei diversi stati in cui un processo può trovarsi in un dato momento. La nozione che un processo può essere in uno di questi stati è nata nei primi sistemi informatici [DV66,V+65]. In una visione semplificata, un processo può essere in uno dei tre stati:

Se dovessimo mappare questi stati in un grafico, arriveremmo al diagramma della figura 4.2. Come potete vedere nel diagramma, un processo può essere spostato tra gli stati pronto e in esecuzione a discrezione del sistema operativo. Essere spostato da pronto a in esecuzione significa che il processo è stato pianificato; essere spostato da in esecuzione a pronto significa che il processo è stato deschedulato. Una volta che un processo è stato bloccato (ad esempio, iniziando un’operazione di operazione di I/O), il sistema operativo lo manterrà come tale finché non si verifica qualche evento (ad es, completamento dell’I/O); a quel punto, il processo si sposta di nuovo nello stato di pronto (e potenzialmente immediatamente di nuovo in esecuzione, se il SO lo decide). Vediamo un esempio di come due processi potrebbero transitare attraverso alcuni di questi stati. Per prima cosa, immaginate due processi in esecuzione, ognuno dei quali utilizzano solo la CPU (non fanno I/O). In questo caso, una traccia dello stato di ogni processo potrebbe assomigliare a questo (Figura 4.3).