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).
Bell’articolo, grazie.
Mi aspetto anche un articolo sui threads :D.
grazie!
Ai threads ci arriveremo molto presto :-)
@Antonio Barba: mille grazie per questa serie, che mi sta dando l’opportunità di capire finalmente alcune meccaniche che prima mi erano oscure. Ti leggo sempre con molta soddisfazione :)
Ma allora sei recidivo!!! Vuoi metterti in testa ke si dice “kernel” e non “KERNAL” !!! :D
Ad ogni modo quoto in toto quanto detto da banryu! Al prossimo seg fault avrò una spiegazione un pò più rigorosa dell’attuale: “Ecco, ho fatto qualche casino con un puntatore!”
@banryu e @iuza83
Grazie a voi :-)
@tutti:
Ho ricevuto più di una lamentela/suggerimento/richesta da parte di alcuni lettori che vorrebbe più dettagli e meno divulgazione.
Una volta terminata la panoramica superficiale sui meccanismi di base ovviamente metteremo mano al codice, quindi non vi preoccupate ;-)
“Nei prossimi articoli approfondiremo i temi appena accennati, applicandoli ad un sistema concreto, basato su CPU di tipo ARM”
Evvai! :D
L’affare si ingrossa :-)
Non vedo l’ora di mettere mani al codice… suppongo C/C++ vero?
Continua così, grandi articoli!!!
Beh che dire Antonio… i tuoi articoli mi eccitano, dovrei incominciare a farmi delle domande :hehehe:
@Fog76: per tenermi sul semplice, mostrerò alcuni frammenti di codice di un kernel che scrissi 2-3 anni fa per scopi di ricerca.
Le parti interessanti, che studieremo insieme, sono in C e Assembly.
Mostrerò anche parti di altri kernel, anche se in genere le parti a basso livello sono identiche praticamente in tutti gli OS, quindi sarà più che altro qualche frammento per mostrare 2 o più modi alternativi di risolvere lo stesso problema.
@Davide: ahahah! Non dire così in pubblico… la mia Amiga 500 è gelosa :-P