Capitolo 6
Chiamate di Sistema
6.1 Organizzazione
Le chiamate di sistema sono usate dai processi per richiamare alcune funzioni
di utilità messe a disposizione dal sistema operativo.
Per i processori della famiglia 68000 della Motorola le chiamate di sistema
rientrano nel più ampio capitolo delle eccezioni (exception).
Un'eccezione avviene ogni volta che si interrompe in modo asincrono il flusso normale delle istruzioni per eseguire una particolare routine di sistema ed
eventualmente ritornare ad eseguire il programma di partenza dal punto in cui
era stato interrotto.
Esistono tre categorie di eccezioni in funzione dell'evento generato:
- Eccezioni generate esternamente: errori di bus, reset e richiesta di interrupt provenienti dalla logica esterna.
- Eccezioni generate internamente, che risultano dall'esecuzione di determinate istruzioni o da errori interni come errori di indirizzamento, violazione di privilegi e l'utilizzo di codici operativi illegali.
- Eccezioni richieste da un processo in esecuzione tramite l'istruzione TRAP. Queste eccezioni vengono chiamate Trappole.
Quando il microprocessore riconosce un'eccezione, attiva una subroutine il cui indirizzo è contenuto in una particolare tabella. Questa tabella viene chiamata Tabella dei Vettori d'Eccezione. Essa occupa i primi 1024 byte della memoria, cioè 512 word a 16 bit. Le word sono raggruppate in coppie e ciascuna coppia viene chiamata vettore. I 256 vettori non sono altro che gli indirizzi delle subroutine che verranno eseguite al verificarsi dell'eccezione corrispondente.
Prima di proseguire nella descrizione delle chiamate di sistema è necessario fare una breve introduzione dei Modi Operativi.
6.1.1 I Modi Operativi
I microprocessori della famiglia Motorola 68000 possiedono due modi operativi: sistema o supervisore e utente.
Il modo operativo è determinato dal bit 13 del registro di stato SR, questo bit viene chiamato bit S; se il bit S è posto a 1
il microprocessore si trova nello stato supervisore altrimenti è nello
stato utente.
Lo stato supervisore è caratterizzato dal fatto di avere tutti i
privilegi e di poter eseguire tutte le istruzioni disponibili. Nello stato
utente, invece, alcune istruzioni dette protette non possono essere eseguite.
Per passare da supervisore a utente (cioè cambiare il bit 13 da 1 a 0)
si possono usare vari modi compreso quello diretto
ANDI #$DFFF,SR
Invece per passare dalla modalità utente a quella sistema (cioè
cambiare il bit 13 da 0 a 1) l'unico modo è fare una chiamata di sistema
(TRAP). Tutte le eccezioni passano al modo supervisore, qualsiasi sia il valore precedente del bit S. Si noti che se il bit S era posto a 0 si ha contemporaneamente al cambio di modo anche il cambio dello stack pointer.
Esistono due stack pointer SP, uno per ogni stato. Nel modo sistema si può
accedere allo SP del modo utente utilizzando l'etichetta USP, invece nel modo utente non solo non si può accedere in nessun modo allo stack pointer del modo sistema (SSP), ma non si può neanche accedere direttamente all'USP, esso può essere modificato solo mettendo o togliendo dallo stack.
6.2 Descrizione
Esistono 16 trappole numerate da 0 a 15; i vettori associati alle trappole sono compresi tra il vettore 32 e il vettore 47 nella tabella dei vettori di eccezione. Questi vettori vengono inizializzati dal sistema operativo alla partenza e non vengono più modificati.
Ciascuna trappola genera la seguente successione di operazioni:
- il contenuto del registro di stato (SR) viene copiato in un registro interno.
- Il bit S del registro di stato viene posto a 1, abilitando il funzionamento in modo supervisore.
- Il numero di trappola, esteso a 16 bit, viene messo sullo stack supervisore.
- Il Contatore di Programma (PC) viene posto sullo stack supervisore.
- Il registro di stato viene posto sullo stack supervisore.
- Il nuovo contatore di programma viene prelevato dall'appropriata locazione nella tabella dei vettori di eccezione.
Spesso il sistema realizza molto più di 16 utilità disponibili per i processi. Perciò ogni trappola potrà corrispondere a più chiamate di sistema; ciò è possibile utilizzando un parametro come switch fra le diverse chiamate.
Nel presente sistema operativo le trappole sono state utilizzate per servire quattro tipi di chiamate a sistema: file, heap, processi e thread.
6.3 Chiamate di sistema per la gestione dei file
Le trappole riguardanti i file permettono di svolgere l'ingresso e l'uscita di dati sia su file che su console. Le chiamate sono riportate nella tabella 6.1.
trap | switch | nome |
0 | - | read |
1 | - | write |
2 | - | open |
3 | 0 | close |
3 | 1 | unlink |
3 | 2 | rewind |
10 | - | chname |
|
Tabella 6.1: Chiamate di sistema per i file
Un processo per gestire l'ingresso e l'uscita di dati sia su file che su console deve avvalersi di opportune strutture dati chiamati flussi. Ogni processo ha a disposizione 20 flussi, i primi tre sono aperti automaticamente e si riferiscono allo standard input, output, error. Un flusso che abbia per oggetto un file deve essere aperto; solo dopo si potrà leggere o scrivere sul flusso, si potrà anche riavviare il flusso e infine chiuderlo.

Figura 6.1: Tabelle per l'apertura dei file
6.3.1 Apertura di un flusso
Per poter accedere al contenuto di un file da un processo si deve prima aprire il file come un flusso utilizzando la chiamata di sistema open. Questa funzione per prima cosa verifica se esiste il file che è stato specificato, in caso negativo crea un nuovo file se specificato. Se la funzione access verifica i permessi viene allocato un posto nella tabella globale dei descrittori di file (FT) e qui vengono copiati tutti i parametri del file. Un puntatore a questa tabella viene messo nella tabella locale dei descrittori di file.
int open(path, flag, mode)
/* apre un file come un flusso */
{
se non e' stato indicato nessun file esci
se il file non esiste
se O_CREAT e' vero crea il file altrimenti esci
se non e' stato chiesto nessun permesso
restituisci l'esistenza del file
se !access() /* verifica i permessi */
esci
altrimenti {
se trovi un posto libero nella tabella globale dei file {
occupa una riga della tabella inserendo un puntatore al
file, la dimensione del file, offset zero o uguale alla
dimensione, l'indice del file nella directory, il permesso
con cui e' aperto
se c'e' un posto libero nella tabella locale dei file {
riempilo con l'indice della tabella globale
restituisci l'indice della tabella locale
}
altrimenti esci
}
}
}
6.3.2 Lettura da un flusso
Per leggere da un flusso si usa la funzione read. Questa funzione verifica se esiste il flusso; se il flusso è la console chiama la funzione readch che legge un tasto da tastiera. Altrimenti verifica che il flusso sia stato aperto in lettura; infatti il flusso potrebbe essere stato aperto solo in scrittura o in esecuzione; in caso affermativo legge le caratteristiche del flusso: cioè quanto è lungo, a che punto è arrivato il puntatore, verifica se nel file ci sono abbastanza byte per soddisfare il numero di byte richiesti, altrimenti legge fino alla fine del file e comunque restituisce il numero di byte letti.
int read(fd, buf, nbyte)
/* legge da un flusso aperto */
{
Calcolo indice della tavola globale dei file usando fd
Se il flusso e' lo standard input chiama la getch ed esci
Se l'indice e' incorretto esci
Se il flusso e' aperto in lettura {
Leggi le caratteristiche del flusso
Leggi i dati e copiali nella stringa passata
aggiorna i parametri del flusso
restituisci il numero di byte letti
}
}
6.3.3 Scrittura su un flusso
Per scrivere su un flusso si usa la funzione write. Questa funzione verifica se esiste il flusso, se il flusso è la console chiama la funzione printf che scrive una stringa su schermo. Altrimenti verifica che il flusso sia stato aperto in scrittura, in caso affermativo legge le caratteristiche del flusso: cioè quanto è lungo, a che punto è arrivato il puntatore, verifica se nel file ci sono abbastanza byte per soddisfare il numero di byte richiesti, altrimenti tenta di allungare il file fino alla fine del blocco. Arrivato alla fine del blocco tenta di allocare un altro blocco, infine restituisce il numero di byte scritti.
int write(fd, buf, nbyte)
/* scrive su un flusso aperto */
{
Calcolo indice della tavola globale dei file usando fd
Se il flusso e' lo standard output chiama la printf ed esci
Se l'indice e' incorretto esci
Se il flusso e' aperto in scrittura {
Leggi le caratteristiche del flusso
se siamo alla fine di un blocco
alloca il blocco di memoria successivo se libero
scrivi la stringa passata
aggiorna i parametri del flusso
restituisci il numero di byte scritti
}
}
6.3.4 Chiusura e riavvio di un flusso
La chiusura di un file si effettua mediante la funzione close. Questa funzione non fa altro che verificare che il descrittore di flusso sia corretto e libera la locazione corrispondente sia nella tabella locale dei file che in quella globale. Si noti che possono essere chiusi anche lo standard input o lo standard output o anche lo standard error. Aprendo subito dopo un opportuno file, si può ottenere una ridirezione dell'ingresso-uscita.
int close(fd)
/* chiude un flusso */
{
Se il descrittore fd e' corretto {
Calcolo l'indice della tabella globale dei file usando fd
Libero la locazione nella tabella globale dei file
Libero la locazione nella tabella locale dei file
}
Altrimenti esci
}
Il riavvio di un file si ottiene mediante la funzione rewind. Questa funzione non fa altro che verificare che il descrittore di flusso sia associato ad un file (non avrebbe senso per lo standard input o output) e, in caso affermativo, azzera il puntatore alla posizione attuale del flusso.
int rewind(fd)
/* riavvia un flusso */
{
Se il descrittore fd e' associato ad un file {
Calcolo l'indice della tabella globale dei file usando fd
Poni a zero la posizione attuale
}
}
Per modificare il nome di un file si usa la funzione chname, la quale verifica semplicemente se esiste il file con il nome vecchio e non esiste nessun file con il nome nuovo.
int chname(oldname, newname)
/* chiamata di sistema */
/* Cambia il nome di un file. */
{
se il file con il nome vecchio esiste
se non esiste nessun file con il nome nuovo
cambia il nome al file
}
La chiamata di sistema unlink toglie l'unico legame che un file può avere e quindi avvia la funzione delete.
6.4 Chiamate di sistema per la gestione dei processi
Le trappole riguardanti i processi permettono di svolgere le operazioni fondamentali di creazione e distruzione dei processi. Le chiamate sono le seguenti:
trap | switch | nome |
4 | - | exec |
5 | - | kill |
6 | - | exit |
Tabella 6.2: Chiamate di sistema per i processi
La chiamata exec permette di creare un nuovo processo. In sostanza vengono disattivati gli interrupt, viene chiamata la funzione ParseDetect che provvede ad accodare il nuovo processo. Infine vengono riattivati gli interrupt.
La chiamata kill permette di eliminare un processo in corso d'esecuzione. Può essere chiamata in due modi: o fornendo il PID (Process ID) o il nome del processo da eliminare. Nel primo caso vengono eliminati tutti i thread che appartengono al processo specificato. Altrimenti vengono cercati tutti i thread che hanno il nome specificato e si verifica se hanno tutti lo stesso PID; in caso affermativo vengono eliminati tutti, altrimenti viene visualizzato il PID e il TID di ciascun thread e si invita ad effettuare la cancellazione mediante il PID.
void Kill (param, switch)
{
se switch indica che e' stato indicato il PID {
per tutte le priorita' dei thread
se nella coda di priorita' c'e' un thread con PID dato
DiscardTCB /* elimina il thread */
}
altrimenti se e' indicato il nome {
per tutte le priorita' dei thread
se nella coda di priorita' c'e' un thread con nome dato
aggiungi il thread alla lista dei thread trovati
se e' stato trovato almeno un thread {
se i thread trovati appartengono a piu' processi
visualizza il PID di ciascun thread
altrimenti {
per tutti i thread trovati
DiscardTCB /* elimina il thread */
}
}
altrimenti
visualizza messaggio
}
}
La funzione kill individua solamente i thread che sono da eliminare. L'eliminazione effettiva viene effettuata dalla funzione DiscardTCB. Questa funzione elimina le risorse allocate dal thread. Per quanto riguarda la zona dati e la zona codice viene liberata solo nel caso del thread principale (TID nullo). In questo caso, si verifica anche se l'input veniva passato al processo e, in caso affermativo, ritorna al sistema. Come ultima operazione il TCB associato al thread viene tolto dalla coda in cui si trovava e viene accodato a quella libera.
void DiscardTCB(p,i)
/* libera le risorse occupate da un thread */
{
libera i blocchi di Heap del processo
se il TID e' nullo
libera la memoria dati e codice
se il thread e' in foreground
resetta l'input al sistema
libera lo Stack
togli Il TCB dalla coda in cui si trova e
mettilo nella coda dei TCB liberi
}
La chiamata exit è a disposizione dei processi che vogliono terminare l'esecuzione prima di raggiungere l'ultima riga di codice. In sostanza questa chiamata utilizza la funzione DiscardTCB e poi disattiva il timer e torna allo schedulatore.
6.5 Chiamate di sistema per la gestione dello Heap
Le trappole riguardanti la heap permettono l'utilizzo della memoria heap per ottenere l'allocazione di zone di memoria di lunghezza generica. Le chiamate sono le seguenti:
trap | switch | nome |
8 | - | alloc |
9 | - | free |
Tabella 6.3: Chiamate di sistema per la gestione della Heap
Ad ogni chiamata di sistema ``alloc'' si verifica innanzitutto se le
dimensioni di memoria richieste possono essere soddisfatte (cioè sono comprese 1 e
8192), in caso favorevole si cerca il primo blocco di heap puntato dal TCB corrente, si verifica se
all'interno c'è uno spazio libero abbastanza grande, altrimenti si cerca il blocco successivo; se si arriva alla fine della lista di blocchi senza soddisfare la richiesta, allora si alloca un nuovo blocco, lo si aggiunge alla coda di blocchi di Heap posseduti dal processo e si occupa lo spazio richiesto e viene restituito il puntatore alla memoria allocata.
void * alloc (size)
/* alloca una zona di memoria lunga size byte */
{
se la dimensione richiesta non rientra nei limiti esci
carica il primo blocco di Heap del processo attuale
finche' il puntatore di blocco non e' nullo
per tutte le posizioni libere dentro al blocco
se le dimensioni sono sufficienti {
occupa lo spazio che occorre
restituisci puntatore allo spazio allocato
}
se e' possibile allocare un altro blocco di Heap {
concatena il nuovo blocco alla coda
occupa lo spazio che occorre
restituisci puntatore allo spazio allocato
}
}
Per occupare un nuovo blocco di Heap viene utilizzata la funzione GetHeapBlock. Questa funzione esamina tutti i blocchi di Heap disponibili e si ferma appena ne trova uno libero; questo blocco viene marcato come occupato e ne viene restituito il puntatore.
int GetHeapBlock ()
/* alloca un blocco dalla memoria heap */
{
per tutti i blocchi di Heap
se trovi un blocco libero {
marca il blocco come occupato
restituisci il puntatore al blocco
}
}
Quando si usa la chiamata di sistema ``free'', viene attivata la funzione free. Questa funzione calcola il blocco di Heap a cui appartiene la zona da liberare; verifica se ci sono zone adiacenti libere e in caso affermativo le fonde insieme, verifica poi se il blocco di heap è rimasto completamente libero e in caso affermativo chiama la funzione FreeHeapBlock per liberarlo.
void free (i)
/* libera una zona di memoria puntata da i */
{
calcola il blocco di Heap che contiene la zona da liberare
cerca la zona precedente e la successiva
se la zona precedente o la successiva sono libere fondile assieme
libera la zona attuale
se il blocco e' rimasto vuoto
chiama FreeHeapBlock /* libera il blocco di Heap */
}
La funzione FreeHeapBlock si occupa di liberare un blocco di Heap mantenendo corretta la catena di blocchi di Heap. Per fare ciò cerca il blocco precedente a quello da liberare (se esiste) e lo fa puntare a quello successivo a quello da liberare. Infine marca come inutilizzato il blocco in questione.
void FreeHeapBlock (i)
/* libera un blocco dalla memoria heap */
{
se il blocco da liberare non e' il primo della lista
cerca il blocco precedente e collegalo al successivo
libera il blocco indicato
}
Si noti che quando termina l'esecuzione di un processo per liberare tutti i blocchi di heap da esso puntati non si utilizza la funzione FreeUserHeap perché questa ricostruirebbe la catena di blocchi di Heap per ogni blocco che vi toglie. La funzione FreeUserHeap evita questo lavoro inutile; questa funzione semplicemente scorre tutti i blocchi di Heap della catena puntata dal processo da terminare e li libera.
void FreeUserHeap (p)
/* libera tutti i blocchi dalla memoria heap occupati da un processo */
{
carica il primo blocco di Heap del processo puntato da p
finche' esiste un blocco successivo
libera il blocco attuale e passa al successivo
}
6.6 Chiamate di sistema per la gestione dei Thread
Le trappole riguardanti i thread gestiscono gli accessi alla libreria dei thread: permettono di creare, coordinare e distruggere i thread. Le chiamate sono riportate nella tabella 6.4.
trap | switch | nome |
11 | >0 | thread_create |
11 | 0 | thread_self |
12 | 0 | thread_mutex_init |
12 | 1 | thread_mutex_lock |
12 | 2 | thread_mutex_unlock |
12 | 3 | thread_mutex_destroy |
13 | 0 | thread_exit |
13 | 1 | thread_cancel |
Tabella 6.4: Chiamate di sistema per i thread
La chiamata di sistema thread_create alloca un TCB (la struttura utilizzata dallo schedulatore per memorizzare un thread), inizializza tutti i parametri del TCB e lo inserisce nella coda d'attesa.
thread_create(fun)
/* crea un nuovo thread all'interno di un processo */
{
GetTCB(); /* alloca un TCB */
azzera tutti i registri;
copia il puntatore alla memoria, il registro A6, il nome del
processo, il PID, la priorita'
calcola e setta il TID e il tempo di creazione
QueueIMP(); /* inserisci il TCB nella coda di schedulazione */
restituisci il TID
}
La chiamata di sistema thread_self restituisce l'identificatore del thread TID ricavato dal TCB.
thread_self()
/* restituisce il TID del thread in esecuzione */
{
restituisci il TID
}
La chiamata di sistema thread_exit libera tutti i mutex posseduti dal thread e poi elimina il thread chiamando la funzione DiscardTCB.
thread_exit()
/* esce da un thread */
{
per tutti i mutex disponibili {
se il mutex e' posseduto da questo thread
libera il mutex
}
DiscardTCB() /* libera il TCB utilizzato */
ritorna allo schedulatore
}
La chiamata di sistema thread_cancel verifica se esiste qualche mutex posseduto dal thread. In caso affermativo marca questi mutex in modo da segnalare che hanno bloccato una cancellazione, altrimenti elimina il thread chiamando la funzione kill.
thread_cancel (TID)
/* cancella un thread */
{
per tutti i mutex disponibili {
se il mutex e' posseduto da TID
marca il mutex come bloccante la cancellazione
}
se il thread non possiede nessun mutex
kill (PID) /* elimina il thread */
}
La chiamata di sistema thread_mutex_init inizializza un mutex (semaforo binario). Questa funzione verifica se c'è un mutex inutilizzato. Se lo trova lo marca come utilizzato e ne restituisce il numero identificatore.
thread_mutex_init()
/* crea un nuovo mutex (semaforo binario) */
{
per tutti i mutex disponibili {
se il mutex e' inutilizzato {
marca il mutex come utilizzato
restituisci il numero di mutex
}
}
restituisci -1
}
La chiamata di sistema thread_mutex_lock pone un mutex allo stato down utilizzando la funzione omonima.
thread_mutex_lock (mutex)
/* blocca un mutex */
{
se mutex esiste {
down(&mutex)
copia il TID del proprietario
}
}
La chiamata di sistema thread_mutex_unlock verifica che il semaforo sia down e che il richiedente sia il proprietario. In caso affermativo libera il mutex e se il mutex ha bloccato la cancellazione chiama nuovamente la funzione thread_cancel.
thread_mutex_unlock (mutex)
/* sblocca un mutex */
{
se mutex e' bloccato {
se il richiedente e' il proprietario {
se il mutex ha bloccato una cancellazione
thread_cancel(TID)
libera il mutex
}
}
}
La chiamata di sistema thread_mutex_destroy cancella il mutex se esiste ed è libero.
thread_mutex_destroy()
/* distrugge un mutex */
{
se mutex esiste {
se mutex e' libero
cancella mutex
}
}
Back to Lucio's Home page.