Nel precedente articolo, abbiamo evidenziato la necessità di astrarre dati e codice di un programma, servendoci del concetto di task.
Analizziamo adesso un esempio di implementazione dei task: il modello dei processi introdotto da UNIX, adottato poi in varie forme da tutti gli OS moderni.
I processi di UNIX presentano una struttura molto complessa, che riflette il livello di sofisticazione necessario per un OS come UNIX, dotato di multiutenza e multitasking. In questo articolo non scenderemo nei particolari delle varie implementazioni di UNIX, ma ci limiteremo ad illustrarne a grandi linee alcuni aspetti.
Un processo contiene, innanzitutto, le informazioni relative alla struttura di codice e dati, organizzati nei cosiddetti segmenti. Abbiamo un segmento chiamato text, contenente il testo del programma (il codice); un segmento chiamato data, contenente variabili inizializzate e costanti (le costanti possono essere poste su un segmento a parte, chiamto Read Only Data); un segmento chiamato bss, contenente le variabili non inizializzate e infine un segmento heap contenente le strutture dinamicamente allocate e lo stack contenente le variabili locali di procedure e funzioni.
Ciascuno di questi segmenti è descritto da almeno due parametri: l’indirizzo di base e la lunghezza del segmento. Ulteriori informazioni possono essere associate a ciascun segmento, come i permessi di accesso e d’esecuzione, ma questi dettagli dipendono strettamente dal Memory Model adottato dal sistema, argomento che tratteremo in un prossimo articolo.
Concentriamoci, invece, su un meccanismo particolare, che determina una netta separazione tra il Sistema Operativo e il codice utente.
Il meccanismo in questione prende il nome di System Call, e serve al codice utente per richiedere i servizi dell’OS.
A differenza delle normali istruzioni di call usate, come abbiamo già avuto modo di vedere, in rudimentali OS quali il KERNAL di Commodore, le System Calls (syscalls, da qui in poi) si svolgono sostanzialmente in 4 fasi:
- Invocazione di un Software Interrupt
- Ingresso in System Mode da parte della CPU
- Gestione dell’interruzione ed erogazione del servizio, da parte del kernel
- Ritorno in User Mode da parte della CPU
System e User Mode sono due modalità di funzionamento che la CPU deve supportare. Tale meccanismo richiede, quindi, un supporto specifico da parte dell’hardware.
La differenza principale tra System e User mode consiste nella presenza di alcune istruzioni privilegiate, cioè che possono essere eseguite soltanto in System Mode, mentre le stesse istruzioni generano un errore se eseguite in User Mode. Ci possono essere ulteriori differenze, come registri speciali o altro, ma per i nostri scopi è sufficiente notare che in User Mode vengono solitamente disabilitate le istruzioni per la manipolazione degli Interrupt.
Il passaggio tra le due modalità di funzionamento è, per così dire, asimmetrico. Infatti la CPU può passare da System a User Mode tramite un semplice salto (un jump o un return), mentre il passaggio opposto avviene per via indiretta tramite Interrupt. A loro volta, gli Interrupt possono essere generati da hardware esterno, come un timer o una periferica, dalla stessa CPU in risposta ad alcuni eventi particolari, come una divisione per zero, oppure può essere invocato dal software tramite particolari istruzioni.
Queste istruzioni speciali, dette Software Interrupts, sono quelle che vengono usate dal software per richiedere i servizi dell’OS. Il tipo di servizio richiesto, ed i valori di eventuali parametri, sono solitamente posizionati nei registri della CPU, seguendo una convenzione che varia da un sistema all’altro. In genere la funzione richiesta è codificata da un numero intero, ad esempio OpenFile = 1, ReadByte = 2, WriteByte = 3, e così via…
L’insieme delle informazioni relative all’invocazione delle syscalls, alla struttura del processo (con la sua suddivisione in segmenti) ed alla gestione della memoria, prende il nome di Application Binary Interface (abbreviato in ABI). Come facilmente intuibile, le ABI cambiano in base alla CPU ed al Sistema Operativo.
Ricapitolando, abbiamo visto come il processo ed il Sistema Operativo possano interagire tramite un meccanismo di interruzioni, che separa nettamente l’ambito applicativo in User Mode, dal Sistema Operativo vero e proprio che gira in System Mode.
A voler essere precisi, non tutto il Sistema Operativo gira necessariamente in System Mode. Per semplificare la struttura dell’OS, tutto il codice di gestione a basso livello, cioè quello che interagisce direttamente con l’hardware, viene relegato in un modulo centrale detto nucleo del sistema, in gergo system kernel, o semplicemente kernel. Le altre parti del Sistema Operativo, come le utilità di sistema, la shell dei comandi, e gli altri componenti di contorno possono girare in User Mode come qualsiasi altro processo, e sfruttare quindi le risorse del kernel al pari degli altri software. In gergo, la parte del Sistema Operativo esterna al kernel viene definita userland.
Nei prossimi articoli approfondiremo i temi appena accennati, applicandoli ad un sistema concreto, basato su CPU di tipo ARM, architettura che scelgo per la sua semplicità, perfetta per i nostri scopi didattici). Illustreremo quindi il meccanismo delle syscalls nel dettaglio e mostreremo il funzionamento di un Memory Model semplice ma molto diffuso, il Flat Memory Model implementato con Memory Protection Unit (MPU).