Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
PREFAZIONE...........................................................................................................................................2
PROCESSI..............................................................................................................................................10
THREADS..............................................................................................................................................14
IL MODELLO A THREAD 14
IMPLEMENTAZIONE DEI THREADS 14
USER VS KERNEL SPACE 15
SCHEDULER ACTIVATIONS 16
POP-UP THREADS 16
USO DEI THREADS 16
INTERPROCESS COMMUNICATION........................................................................................................19
SCHEDULING.........................................................................................................................................31
MEMORY MANAGEMENT.....................................................................................................................46
FILE SYSTEM..........................................................................................................................................51
FILES 51
STRUTTURA 51
ACCESSO 52
DIRECTORIES 52
IMPLEMENTAZIONE 52
DISCO 52
FILE 52
ALLOCAZIONE CONTIGUA 52
LINKED LIST 52
FILE ALLOCATION TABLE 53
I-NODES 53
INPUT/OUTPUT....................................................................................................................................56
DEADLOCKS..........................................................................................................................................66
RISORSE 67
RISORSE PRERILASCIABILI - NON PRERILASCIABILI 67
ACQUISIZIONE DELLA RISORSA 68
RESOURCE DEADLOCKS - CONDIZIONI PER LO STALLO 68
MODELLI PER LO STALLO 68
IGNORARE LO STALLO - ALGORITMO DELLO STRUZZO 69
RICONOSCERE LO STALLO ED ELIMINARLO 70
RICONOSCERE LO STALLO CON UNA SOLA RISORSA DI OGNI TIPO 70
RICONOSCERE LO STALLO CON PI RISORSE DI OGNI TIPO 70
ESERCIZI................................................................................................................................................77
ESERCIZIO 1 77
ESERCIZIO 2 77
ESERCIZIO 3 77
ESERCIZIO 4 78
ESERCIZIO 5 79
ESERCIZIO 6 79
ESERCIZIO 7 80
ESERCIZIO 8 81
ESERCIZIO 9 81
ESERCIZIO 10 81
ESERCIZIO 11 81
ESERCIZIO 12 81
ESERCIZIO 13 81
ESERCIZIO 14 81
ESERCIZIO 15 81
ESERCIZIO 16 82
ESERCIZIO 17 82
ESERCIZIO 18 82
ESERCIZIO 19 82
Sommario hardware
CPU
Il cervello di un computer la CPU, prende le istruzioni dalla memoria e le esegue. In generale
le CPU hanno molti registri, usati dai programmatori per poter eseguire i loro programmi.
Alcuni di questi registri, quelli non general purpose, sono i seguenti:
PC (Program counter): registro che punta all'indirizzo di memoria della prossima
istruzione da eseguire.
SP (Stack pointer): registro che punta all'indirizzo dello stack in memoria.
PSW (Program Status Word): questo registro contiene alcune informazioni riguardo il
processo: per esempio la priorit, bits di controllo, bits di condizione e se il processo in
User o Kernel mode (il significato sar spiegato pi avanti, almeno) . E' molto importante
nelle system calls e nell'I/O.
Senza dilungarci sulla CPU (trattato nel I semestre), possiamo dire cosa significa eseguire un
processo in kernel o user mode:
Kernel mode: Quando eseguito in kernel mode la CPU pu eseguire tutte le istruzioni
del suo Instruction Set e usare qualsiasi feature dell'hardware. Il O.S eseguito in kernel
mode.
User mode: i programmi utenti sono eseguiti in user mode, il quale permette alla CPU di
poter eseguire solo una parte dei comandi del suo Instruction Set. Genericamente in
questa modalit i programmi non possono interagire con l'hardware (devono passare dal
O.S). Naturalmente non possono modificare il PSW per passare in kernel mode.
Un programma in modalit utente per parlare con il O.S deve far uso delle system call, le quali
sono funzioni speciali che eseguono un'istruzione TRAP e invocano il O.S. Quello che fa una
Memoria
In generale con memoria primaria si intende la RAM, mentre con memoria secondaria i dischi
magnetici (hard disks), nastri magnetici, etc...
3. Il terzo metodo quello di usare un chip speciale: un chip DMA (Direct memory
access), il quale permette il controllo del flusso di bit tra la memoria e il controllore del
dispositivo, senza aver bisogno della CPU. La CPU dice al DMA quanti byte trasferire, il
dispositivo e gli indirizzi di memoria coinvolti. Una volta che il chip DMA ha finito, esso
genera un interrupt.
Solitamente se ci sono pi interrupt insieme, vince quello del dispositivo con priorit pi alta.
System calls
La system call una funzione speciale che permette al programma in user mode di poter fruire
dei servizi di sistema (leggere un file, etc...).
Quando viene chiamata una system call vengono eseguiti i seguenti step:
1. Push nello stack dei parametri della chiamata a funzione
2. Viene eseguita la chiamata a funzione a una libreria (ancora in user mode)
3. La funzione chiamata, solitamente scritta in assembly, inserisce il numero della system
call in un registro dove l'O.S si aspetta di trovare il numero, poi esegue un'istruzione
TRAP, che fa passare da user mode a kernel mode, e inizia l'esecuzione delle istruzioni
da un indirizzo di memoria fissato nel kernel. La TRAP come fare una chiamata a una
funzione, ma ha due differenze: prima di tutto passa in kernel mode, secondo la trap non
fa un JUMP a una locazione di memoria arbitraria, ma a una zona di memoria fissata.
4. Una volta eseguita la TRAP si arriva in una zona di memoria di una funzione che in base
al numero salvato nel registro (vedi step 3) sceglie di eseguire il gestore delle chiamate
di sistema opportuno.
5. Una volta che il gestore ha finito esso ritorna all'istruzione successiva alla TRAP e torna
in user mode.
6. Il programma libera lo stack.
Sistemi monolitici: sono quei sistemi operativi composti da un solo programma che
gira in kernel mode.
Sistemi a livelli: sono sistemi monolitici ma strutturati a livelli, e ogni livello comunica
solo quelli adiacenti.
Microkernels: con il sistema a livelli solitamente sono tutti nel kernel, ma questo non
necessario. L'idea di splittare (spezzettare) il programma unico del sistema operativo
in tanti moduli, di cui solo uno di questi, il microkernel, viene eseguito in modalit kernel,
e gli altri vengono eseguiti in modalit utente. In pratica il microkernel e quel modulo
essenziale per permettere la comunicazione hardware-software.
Esokernels : Nessuno meglio di uno sviluppatore sa come rendere efficiente l'uso dell'hardware
disponibile, quindi l'obiettivo dargli la possibilit di prendere le decisioni. Gli esokernels sono
estremamente piccoli e compatti, poich le loro funzionalit sono volutamente limitate alla
protezione e al multiplexing delle risorse.
I kernel "classici" (sia monolitici che microkernel) astraggono l'hardware, nascondendo le risorse
dietro a un livello di astrazione dell'hardware (hardware abastraction level o HAL) In questi
sistemi "classici", se ad esempio viene allocata della memoria, il programma non pu sapere in
quale pagina fisica questa verr riservata dal sistema operativo, e se viene scritto un file non c'
modo di sapere direttamente in quale settore del disco stato allocato. questo il livello di
astrazione che un esokernel cerca di evitare. Esso permette ad un'applicazione di richiedere aree
specifiche di memoria, settori specifici su disco e cos via, e si assicura solamente che le risorse
richieste siano disponibili e che le applicazioni vi possano accedere. (Preso da wikipedia)
Con questo modello tutto il software che viene eseguito sul computer organizzato come
un'insieme di processi sequenziali cooperanti.
Un processo solamente l'istanza di un programma in esecuzione, con i registri e le varie
risorse che potrebbero servire al programma: concettualmente ogni processo ha la sua CPU
virtuale. In realt la CPU passa da un processo all'altro molto velocemente
(multiprogramming) grazie al process scheduler: quel componente software che decide
quale processo deve venir eseguito.
Si dice a processi sequenziali perch viene eseguito un processo alla volta in sequenza.
Da ora in poi consideriamo calcolatori con una sola CPU (senno si parla di multiprocessor).
int main()
{
int pid = fork(); /* creo un processo clone */
if(pid == 0)
{
/* processo figlio */
}
else if(pid > 0)
{
/* processo padre */
}
else
{
/* Errore. */
}
return 0;
}
Stato
I processi una volta mandati in esecuzione, possono ritrovarsi in 3 stati:
1. RUNNING, se il processo sta impegnando la CPU.
2. BLOCKED, se il processo in attesa di un evento.
3. READY, se (in memoria) pronto per essere eseguito ( un altro processo in
esecuzione pero).
Un nuovo processo viene caricato in memoria dove nello stato READY ovvero pronto per
essere eseguito. Successivamente quando lo scheduler decide di assegnargli la CPU passa
allo stato RUNNING. A questo punto possono accadere diverse cose:
1. se il processo richiede accesso allI/O o comunque necessita di un evento per poter
continuare la sua esecuzione, passa allo stato BLOCKED. Il processo non pu essere
eseguito finch levento non occorrer, nel qual caso passerebbe allo stato READY.
2. se lo scheduler decide di assegnare la CPU ad un altro processo, passa allo stato
READY. Il processo pronto per essere eseguito nuovamente.
Il sistema operativo tiene una tabella (process table) dove in ogni riga c un processo e tutti i
suoi attributi, compresi quelli che gli permettono di andare nuovamente in esecuzione (program
counter, stack pointer, ecc.) o che permettono al process scheduler di prendere decisioni
(priorit, ecc.).
Il cambio di processo in esecuzione oneroso, si devono salvare i registri, PC, stack pointer, la
cache, TLB, ecc.. Context Switch.
Sono flussi di esecuzione allinterno di uno stesso processo che procedono in parallelo come
accade tra due processi diversi, ma a differenza loro condividono lo spazio di indirizzamento del
processo a cui appartengono (sono come processi all'interno di un processo che vengono
eseguiti in modo parallelo).
Pro:
1. I thread possono scambiarsi semplicemente informazioni tra loro
2. Sono molto veloci da creare (100 volte)
3. Velocizzano il programma se sono I/O Bound, poich solo il thread interessato si blocca
mentre gli altri possono continuare la loro esecuzione.
Immaginare come in un word processor il salvataggio del file interromperebbe tutto il
programma.
Il modello a thread
I thread sono quelle entit di un processo assegnate per far eseguire istruzioni dalla cpu. Di
conseguenza un thread ha bisogno di meno informazioni, ha bisogno delll'indirizzo del Program
Counter (PC), dello stack, etc... Se un processo ha pi thread, ogni thread ha il suo stack, il suo
PC, il suo stato, etc...
La libreria Posix mette a disposizione diversi metodi per creare, distruggere un thread. Da
segnalare i metodi Pthread_yield e Pthread_join che permettono nellordine di rilasciare la CPU
in favore di un altro thread e di aspettare che il thread passato come paramentro termini.
Quando un processo si blocca in attesa che un altro thread finisca il lavoro, chiama una
procedura del run-time system che decide quale altro thread mandare in esecuzione. Questo
avviene molto pi rapidamente comparato con una chiamata a sistema (non ho context switch).
C per un problema, una system call interrompe lintero processo. Perdo quindi il parallelismo
per il quale il thread nasce. Inoltre un thread non pu essere interrotto dal run-time system
poich non ci sono clock interrupts nello user space.
Poich i thread portano benefici solo quando sono I/O Bound(ci sono molte operazioni I/O), e
quindi quando effettuano molte chiamate a sistema, limplementazione nello user space non
efficace.
Laltro approccio prevede che sia il kernel a gestire i thread. Il kernel materr quindi una thread
table globale. Quindi per gestire i thread si utilizzeranno delle chiamate a sistema (p lente). Lo
scheduling effettuato tra tutti i thread non solo quelli di un processo. Per velocizzare i thread
possono essere disabilitati invece che distrutti, in modo da riattivarli se servono nuovamente.
Una chiamata a sistema in questo caso blocca solo il thread interessato.
PRO KERNEL:
1. Gestione da parte del SO, Scheduling tra thread di diversi processi.
2. Chiamate a sistema bloccano solo il thread interessato
CONTRO KERNEL:
1. Gestione tramite chiamate a sistema (LENTE)
E' possibile pure avere un'implementazione ibrida, con alcuni thread nello user space e altri nel
kernel space: in pratica ogni kernel thread ha pi user thread al suo interno.
Scheduler Activations
E una soluzione ibrida. Il thread user space effettua una chiamata a sistema che ne blocca la
sua esecuzione. Il kernel ne prende atto e notifica (invece di bloccare) il run-time system con
una upcall (viene eseguita listruzione ad un indirizzo prefissato, tipo interrupt). Il run-time
system prende quindi il controllo e decide quale altro thread mandare in esecuzione.
Quando il vecchio thread sar in grado di riprendere lesecuzione, il kernel notificher
nuovamente con una upcall il run-time system.
E un approccio non usuale poich solitamente un livello pi basso (il O.S in kernel mode) non
notifica livelli pi alti.
Pop-up Threads
Esempio attesa di un messaggio.
Supponiamo che il thread in esecuzione aspetti l'arrivo di un messaggio, quindi deve essere
svegliato ( In blocked). Una volta svegliato avviene un context switch, prende il possesso
della CPU e fa il suo dovere.
Un pop-up thread invece viene creato nel momento in cui arriva il messaggio. Il vantaggio che
non ho bisogno di recuperare le sue vecchie informazioni (context switch) poich il thread
nuovo!
Nel 1 approccio il thread si blocca aspettando il messaggio.
Invece usando I Pop-Up threads il thread non si blocca e continua l'esecuzione, e verr creato
un nuovo thread (Pop-Up thread) all'arrivo del messaggio, occupandosene.
Immaginiamo un server web a thread singolo: il server ascolta per messaggi in arrivo
invocando una system call => il processo si blocca finch non succede un evento. Una volta
arrivato un messaggio potr elaborarlo.
Invece implementandolo con pi threads si ha la seguente situazione: si ha un thread che
ascolta per messaggi in arrivo, ma solo questo thread si blocca. Una volta arrivato un
messaggio questo viene passato a un altro thread dormiente che viene svegliato =>
parallelismo.
Il terzo metodo di usare delle system call non bloccanti (insieme al polling a volte: il polling
un'operazione (non bloccante) che permette di conoscere lo status di una periferica). In pratica
succede questo: il server (thread singolo) fa uso di una read non bloccante per vedere se sono
arrivati messaggi, nel caso non siano arrivati continua con il proprio lavoro e poi ricontroller
dopo. Nel caso invece sia arrivato un messaggio, questo viene esaminato ed elaborato con
altre system call non bloccanti, la cui risposta verr segnalata tramite signal o interrupt. Ogni
volta che arriva una richiesta bisogna salvare la richiesta precedente e il suo status,
permettendo poi dopo al programma di poterci ritornare a lavorare. Questo sistema viene
chiamato MSF (macchina a stati finiti).
Variabili di lock
E' possibile avere una variabile condivisa in memoria, chiamata variabile di lock, inizialmente a
0. Quando un processo entra la sua regione critica questa variabile viene messa a 1, quando ci
esce viene messa a 0.. Se la variabile gi a 1 il processo aspetta (busy waiting) finch non
va a 0.
Sfortunatamente questa idea ha il problema che cerchiamo di risolvere, cosa succede se due
processi vedono contemporaneamente che la variabile a 0 e quindi entrano insieme nella
regione critica?
/* Processo A */
while(1)
{
while(turn != 0) ; /* loop Busy Waiting*/
critical_region();
turn = 1;
noncritical_regions();
}
/* Processo B */
while(1)
{
while(turn != 1) ; /* loop Busy Waiting*/
critical_region();
turn = 0;
noncritical_regions();
}
In questo caso si fa uso sempre di una variabile condivisa ma a causa dell'alternanza non ci
sono problemi.
Il problema che occorre nel ciclo che controlla la variabile: questo ciclo spreca tanti colpi di
clock e usa la CPU in maniera inutile. Inoltre questa soluzione prevede che I due processi si
alternino: potrebbero esserci problemi se un processo pi lento dell'altro.
Una variabile di lock usata in questo modo si chiama spin lock.
#define FALSE 0
#define TRUE 1
#define N 2
Questa soluzione prevede di implementare due funzioni, che devono essere chiamate
all'entrata e all'uscita di una regione critica: enter_region() e leave_region() nell'esempio.
Con due processi, A e B, nel caso A voglia entrare nella regione critica e B gi nella regione
critica, A dovr aspettare finch B non esce, causando busy waiting per via del loop while.
La soluzione di peterson pu essere estesa a pi processi.
enter_region:
TSL REGISTER, LOCK ; copia LOCK in REGISTER e mette LOCK a 1
CMP REGISTER, 0 ; if (REGISTER == 0)
JNE enter_region ; REGISTER == 0? se non lo ripeti il ciclo
RET ; entrato nella regione critica
leave_region:
MOVE LOCK, 0 ; mette LOCK a 0
RET
Questa soluzione prevede l'implementazione a livello hardware dell'istruzione TSL Test and
Set Lock.
Questa istruzione prende la variabile di lock che gli viene passata e la copia in un registro,
dopodich mette la variabile di lock a 1. Questa un'operazione atomica, ovvero viene fatta in
un solo colpo visto dall'utente e non ci sono rischi che qualcun'altro possa modificare la
variabile di lock nello stesso momento.
Realmente questo viene fatto attraverso il seguente meccanismo: viene bloccato il bus di
memoria finch non stata eseguita la TSL. Questo per far si che altri processori non possano
leggere o scrivere nello stesso momento.
Anche questo metodo presenta Busy Waiting, perch c' un test che viene ripetuto sul registro.
void producer()
{
int item;
while(1)
{
item = produce_item(); /* produci un oggetto */
if (count == N) sleep(); /* sleep se numero oggetti
prodotti=100*/
insert_item(item); /* inserisci oggetto */
count++;
if (count == 1) wakeup(consumer); /* count era a 0, sveglia
consumer*/
}
}
void consumer()
{
int item;
while(1)
{
if (count == 0) sleep(); /* dormo finch non mi svegliano */
item = remove_item(); /* c' un oggetto, lo tolgo */
count--;
if (count == N-1) wakeup(producer); /*sveglio producer se il
buffer era full */
consume_item(item); /* uso l'oggetto */
}
}
semaphore mutex = 1;
semaphore full = 0;
sempahore empty = N;
void producer()
{
int item;
while(1)
{
item = produce_item();
down(&empty);
down(&mutex);
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer()
{
int item;
while(1)
{
down(&full);
down(&mutex);
item = remove_time();
up(&mutex);
up(&empty);
Mutex
Quando non richiesta l'abilit di contare dei semafori si possono usare I mutex.
Un mutex una variabile che pu essere solo in due stati: locked e unlocked => ha bisogno di
1 solo bit o pi bits, dove 0 indica unlocked e tutti gli altri valori locked (contrario dei semafori).
Quando un processo o un thread hanno bisogno di accedere a una regione critica chiamano
mutex_lock(), mentre invece quando escono chiamano mutex_unlock(). Se pi thread o
processi sono bloccati sullo stesso mutex, uno di essi scelto a random gli viene concesso di
acquisire il lock. I mutex possono essere implementati in user space con l'istruzione TSL e con
il thread_yeld() (permette di evitare busy waiting passando nello stato blocked).
mutex_lock:
TSL REGISTER, mutex ; copia mutex in REGISTER e mette mutex a 1
CMP REGISTER, 0 ; if (REGISTER == 0)
BEQ ok ; se era 0, unlocked
CALL thread_yeld ; non era 0, vado in stato blocked
JMP mutex_lock ; riprovo
RET ; entrato nella regione critica
mutex_unlock:
MOVE mutex, 0 ; mette mutex a 0 => unlocked
RET
Inoltre nella libreria pthread oltre ai mutex ci sono delle funzioni speciali, pthread_cond_wait
e pthread_cond_signal, le quai offrono un ulteriore meccanismo di sincronizzazione chiamato
condition variables: infatti queste funzioni vengono chiamate se certe variabili soddisfano
certe condizioni.
La prima funzione blocca il thread in attesa di un segnale, la seconda invia un segnale al thread
bloccato e lo sveglia (simile al wakeup & sleep).
Message passing
Fa utilizzo delle primitive send e receive per inviare e ricevere messaggi, cosi I processi
possono sincronizzarsi in base ai messaggi. Potrebbero sorgere vari problemi.
Sono primitive usate spesso per fare comunicazioni attraverso la rete (internet, lan, etc..:)
Barriers
Questo tipo di sincronizzazione si intende per gruppi di processi.
Alcune applicazioni sono divise in fasi o stages, e vogliamo che questi processi non possano
continuare alla prossima fase se non prima che tutti I processi di queli gruppo abbiano raggiunto
la fine della fase corrente: questo si pu implementare mettendo una barriera alla fine di ogni
fase, quando un processo raggiunge la barriera, questo va in stato blocked finch non abbiano
raggiunto la barriera tutti gli altri processi del gruppo.
Si dice CPU Bound process un processo che fa un uso intensivo della CPU e che fa poco uso
delle funzionalit di Input/Output che possono bloccare il processo.
Si dice I/O Bound process un processo che fa il contrario di quello CPU Bound.
Le CPU migliorano pi velocemente rispetto ai dischi e alle periferiche di input/output => I
processi diventano pi di tipo I/O Bound.
In generale meglio dare precedenza I processi I/O Bound: questo perch quando viene
eseguita un'operazione di Input/Output il processo si blocca e un altro processo viene eseguito,
e se tutti sono I/O Bound molti processi vengono eseguiti.
Ci sono 3 tipi di sistemi dove in ognuno di questi ci sono diverse priorit da soddisfare:
NB: negli esercizi la schedulazione dei processi pu essere visualizzata facendo un grafico di
Gantt (http://it.wikipedia.org/wiki/Diagramma_di_Gantt ).
Sistemi Batch
Devono fare una grande quantit di lavoro. Massimizzare il throughput, garantendo che la CPU
venga utilizzata il massimo possibile.
Round-Robin
A ogni processo gli viene assegnato un quanto, ovvero un intervallo di tempo, durante il quale
ha il permesso di eseguire il programma. Se il processo ancora in esecuzione alla fine del
quanto, questo viene prerilasciato e il quanto dato a un altro processo. Se il processo si
blocca o finisce prima del quanto, quest'ultimo viene dato a un altro processo.
L'unico problema con questo algoritmo quanto deve essere grande il quanto di tempo,
perch passare da un processo all'altro genere overhead, ovvero bisogna fare operazioni che
non riguardando con l'obiettivo dell'algoritmo, quali: salvataggio dello stato del processo,
aggiornare tabelle, etc...si chiama process switch o context switch.
Quanto troppo piccolo implica tanto overhead, quanto troppo grande ne perdo in reattivit.
Non favorisce I processi I/O Bound.
Code multiple
Si creano N priority class, ogni processo viene assegnato a una di queste classi di priorit. I
processi nella classe di priorit pi alta vengono eseguiti per un quanto di tempo. I processi
nella seconda classe di proprit pi alta vengono eseguiti per due quanti di tempo. I processi
T1
comando duri secondi.
Sistemi real-time
I processi devono essere eseguiti categoricamente entro un certo limite di tempo (Deadlines).
Solitamente questo tipo di sistemi sono divisi in due tipi:
1. Hard real time :ci sono deadlines che devono essere rispettate assolutamente.
2. Soft real time: perdere una deadline non la fine del mondo.
In questo tipo di sistemi ogni programma diviso in un numero di processi, di cui ogni processo
conosciuto in anticipo il comportamento. Questi processi generalmente sono completati entro
1 secondo.
Kernel threads: lo scheduler decide fra tutti I threads di tutti I processi quale eseguire in
base all'algoritmo. Deve tenere da conto che pi veloce passare da un thread a un
altro thread dello stesso processo che fra thread di processi diversi, a causa del
context -switch.
void produce()
{
int i = 0;
while(i++<MAX)
{
pthread_mutex_lock(&lock); /*accesso esclusivo a questarea*/
if(size==BUFF_SIZE)
pthread_cond_wait(&buff_controller, &lock); /*aspetto che il
buffer si svuoti*/
buffer[size++] = rand();
printf("P\titem inserted\t%d\tsize=%d\n", buffer[size-1], size);
if(size==1)
pthread_cond_signal(&buff_controller); /*sveglio il produttore*/
pthread_mutex_unlock(&lock);
}
pthread_exit(0);
}
void consume()
{
int i = 0;
while(i++<MAX)
{
pthread_mutex_lock(&lock);
if(size==0)
pthread_cond_wait(&buff_controller, &lock);
size--;
printf("C\titem removed\t%d\tsize=%d\n", buffer[size], size);
if(size==BUFF_SIZE-1)
pthread_cond_signal(&buff_controller);
pthread_mutex_unlock(&lock);
pthread_yield(); /*libero la CPU (consumo l'oggetto)*/
}
pthread_exit(0);
}
void philosopher(int i)
{
while(1)
{
think();
take_forks(i);
eat();
put_forks(i);
}
}
void take_forks(int i)
{
down(&mutex);
state[i] = HUNGRY;
test(i);
up(&mutex);
down(&s[i]); /* blocca se non sono state acquisite le forchette */
}
void put_forks(int i)
{
down(&mutex);
state[i] = THINKING;
test(LEFT);
test(RIGHT);
up(&mutex);
}
void test(int i)
{
if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT]!= EATING)
{
state[i] = EATING;
up(&s[i]);
Lettori e Scrittori
Database che ammette pi lettori in contemporanea, ma uno scrittore deve avere accesso
esclusivo. Quando uno scrive nessunaltro pu scrivere o leggere.
Il problema sta nel fatto che se permettiamo ai lettori di poter leggere sempre, lo scrittore non
avr mai accesso al database a meno che tutti i lettori non escano e non ne arrivino altri.
Presentiamo una soluzione in C che per ha il seguente problema: supponiamo che un lettore
stia leggendo il database, e ci sia uno scrittore in attesa. Nel frattempo arrivano altri lettori che
leggono il database (senza problemi), e cosi via finch lo scrittore non attende per un tempo
indeterminato. Per prevenire ci si pu inserire una coda FIFO, il primo che arriva fa qualcosa
(scrittore o lettore), per c' meno concorrenza => meno performance. Sono possibili altre
soluzioni.
void reader()
{
while(1)
{
down(&mutex);
rc++;
if (rc == 1) down(&db); /* c' almeno un lettore, metti in lock db */
up(&mutex);
read_database();
down(&mutex);
rc--;
if (rc == 0) up(&db); /* non ci sono pi lettori, lascia db */
up(&mutex);
use_data();
}
}
void writer()
{
while(1)
{
think_up_data();
down(&db); /* se nessuno sta facendo niente su db, lock */
write_data_to_database();
up(&db);
}
}
In sistemi che non utilizzano virtualizzazione sulla memoria possibile avere un solo
programma alla volta in memoria, con conseguente context switch che prevede un swap out su
disco del vecchio processo e swap in di quello nuovo.
Un altro approccio (IBM) quello della static relocation. Il programma quando viene caricato in
memoria viene modificato in modo che le istruzioni che fanno riferimento a salti a indirizzi
assoluti vengano aggiornati in base a dove viene caricato il programma in memoria. Questo
approccio rallenta evidentemente loperazione di caricamento.
Swapping
Non tutti i processi riescono a stare in memoria. Serve quindi un meccanismo che permetta di
spostare i processi da memoria a disco, e viceversa, in modo efficiente.
Allinizio la memoria immaginiamola vuota. Carico processi in memoria finch ho spazio per
farlo. Immaginiamo ora che un processo abbia terminato la sua esecuzione, lo dovr quindi
spostare su disco (swap out); si sar creato un buco in memoria che pu essere usato da
qualunque nuovo processo (basta che ci stia!).
Dopo svariati swap, la memoria sar piena di buchi ovvero si dice che sar frammentata. Per
evitare questo ogni tot secondi si potrebbe deframmentarla, ovvero compattarla, ma questa
operazione dispendiosa (circa 5sec per 1GB). Inoltre un programma non ha dimensione fissa,
potrebbe richiedere altra memoria dinamicamente; in questo caso il SO dovrebbe trovargli un
buco pi grande o togliere momentaneamente il processo dalla memoria.
Memoria Virtuale
Il concetto alla base che i programmi non necessitano dellintero spazio di indirizzi per essere
eseguiti. Un primo approccio stato quindi quello degli overlays, ovvero il programmatore
doveva dividere il programma in diverse parti che potevano essere caricate ed eseguite
separatamente in memoria; questo per appesantiva il lavoro del programmatore e induceva a
errori. Il secondo approccio stato il paging.
Paging
Lo spazio di indirizzamento virtuale di un processo viene diviso in pagine di dimensione fissata
(4KB). Ogni pagina pu essere caricata in memoria, anchessa divisa in page frames della
stessa dimensione.
Il processo fa quindi uso di indirizzi virtuali: lindirizzo 1024 virtuale non corrisponde al 1024
fisico, bisogna sapere in quale page frame stata caricata la pagina corrispondente allinidirizzo
virtuale 1024! Si evince che deve esistere una tabella di corrispondenza pagina - page frame
per ogni processo.
La MMU (memory manegement unit) si occupa di tradurre gli indirizzi virtuali in indirizzi fisici e
inviarli sul bus (la memoria non a conoscenza della virtualizzazione). Non tutte le pagine
saranno caricate in memoria quindi si deve tenere conto, tramite un bit, della presenza o
assenza di una pagina in memoria. Quando viene chiamata una pagina che non presente in
memoria si parla di page fault, e sar il SO ad occuparsene.
Un indirizzo virtuale (16 bit) quindi diviso in 2 parti: numero di pagina virtuale (4 bit) e offset
interno alla pagina (12 bit). Loffset lo stesso poich pagine e page frames hanno la stessa
dimensione, mentre il numero di pagina viene fatto corrispondere al page frame in memoria
tramite la page table. In questo esempio ho uno spazio di indirizzamento di indirizzi diviso
in pagine di dimensione .
Notare che se la tabella delle pagine fosse interamente in memoria ad ogni chiamata di
lettura/scrittura in memoria si trasforma in una ricerca nella page table dellindirizzo fisico.
Diversamente se fosse interamente in HW sarebbe immensamente costosa una tabella di
righe.
TLB
Il Translation Lookaside Buffer una cache completamente associativa (ovvero in una riga
vi scritto esplicitamente il numero di pagina e il numero di page frame) dove sono contenute
alcune (64) righe della tabella delle pagine. Funziona come una cache, ovvero tramite il
principio di localit nel tempo e nello spazio, dove una MISS comporta lo spostamento dalla
memoria al TLB della pagina richiesta. Si deve distinguere tra MISS e page fault!
La gestione del TLB pu essere fatta tramite HW, ovvero la MMU conosce dove sono
memorizzate le pagine in memoria e aggiorna il TLB in caso di MISS; oppure via SW dove il
sistema operativo ad occuparsi di aggiornare il TLB. Si scoperto che se il TLB abbastanza
grande non vi molta differenza in termini di prestazioni e inoltre nel secondo caso la MMU
molto pi semplice (risparmio spazio sul chip).
Paginazione a pi livelli
Si divide lindirizzo virtuale (32 bit) in 3 parti: PT1 (10), PT2 (10), OFFSET (12).
In memoria tengo quindi una tabella che viene indicizzata da PT1 ( righe) contenente il
numero della pagina di secondo livello da accedere (ho quindi 1024 pagine di secondo livello).
PT2 ( righe) viene usato per accedere alla riga della pagina di secondo livello che contiene il
page frame interessato.
Questo funziona perch un programma non utilizza mai tutto il proprio spazio di indirizzamento,
quindi molte pagine non saranno mai usate! In memoria tengo solo quelle tabelle contenenti le
pagine che utilizzer pi la PT1.
Page replacement
Quando arriva un page fault devo decidere quale pagina togliere dalla memoria per far spazio a
quella mancante. Una volta scelta la pagina devo controllare il dirty bit poich se la pagina stata
modificata devo aggiornare il disco.
Algoritmo Ottimale
Non implementabile. Prevede che sia la pagina che verr usata pi avanti nel tempo ad essere
tolta. Sfortunatamente non si pu prevedere il futuro.
NRU
Prevede di scartare una pagina che non stata usata recentemente analizzando i bit R/M
ovvero guardando se la pagina stata indirizzata (R=1) e/o modificata (M=1). Ad ogni clock
interrupt il bit R viene azzerato cosi da tenere conto solo delle meno recenti.
Lalgoritmo semplice ma fa schifo.
FIFO
First in, First out.
Second Chance
E un FIFO migliorato, ovvero se la pagina che sta per uscire ha R=1 viene spostata in fondo e il
bit R viene azzerato.
Clock
E uguale a second chance ma implementato tramite una lista circolare dove il puntatore non
rimane sulla testa ma allultimo nodo analizzato, velocizzando lalgoritmo.
LRU
Least recently used prevede di utilizzare una lista ordinata dalla pagina utilizzata pi
recentemente a quella pi vecchia. Tenerla aggiornata troppo dispendioso.
NFU
Not frequently used, ogni pagina ha un contatore (SW) che viene incrementato in base al valore
del bit R. La pagina con il contatore pi basso stata letta meno volte. Il problema che se una
pagina stata usata molto 1 ora fa, ha il contatore alto e quindi non viene tolta anche se non
pi usata.
Aging
Come NFU solo che il bit R viene sommato nel bit pi significativo dopo aver shiftato il
contatore. In questo modo processi usati non di recente avranno i bit shiftati via e quindi un
valore del contatore pi basso.Ovviamente il difetto sta nel fatto che il possiamo registrare fino a
tot bit nel passato (8 bit a zero potrebbe significare che non viene usata da 8 tick oppure da 1
ora).
Working Set
Un nuovo approccio quello di capire quali pagine servono ad un processo per essere eseguite
in un certo intervallo di tempo. Quelle pagine fanno parte del working set (nota che il WS evolve
lentamente nel tempo). Ovviamente il WS deve rimanere preferibilmente in memoria, poich
togliere una pagina del WS implica avere molti page fault successivamente (thrashing).
Pagine che il processo utilizzava prima di essere swapped out fanno parte del WS e devono
essere caricate se il processo va di nuovo in esecuzione (prepaging).
Una implementazione efficiente quella di utlizzare il current virtual time invece che dei
contatori. Quando una pagina indirizzata viene aggiornato il suo R bit (azzerato ad ogni tick).
Ad ogni page fault il timer delle pagine con R=1 viene aggiornato al tempo corrente, invece le
pagine con R=0 vengono scartate se il loro timer ha un valore troppo indietro nel tempo
(confrontato con quello attuale) altrimenti non viene scartato ma non viene neanche aggiornato.
WSClock
Funziona come sopra, ma implementato come lalgoritmo di Clock. Una nota in pi che
quando trovo una pagina che non fa parte del WS controlla anche il dirty bit: se a uno non
posso rimuovere la pagina perch prima devo aggiornare quella su disco; quindi faccio partire
una scrittura e intanto cerco altre pagine che abbiano il dirty bit a zero (veloci da sostituire).
Note Generali
Locale vs Globale
Thrashing
Se un processo genera troppi page fault, il PFF prova a dargli pi spazio ma non ce n
abbastanza; quindi il SO pu decidere di spostarlo in memoria per un periodo di tempo.
Dimensione pagina
Pagine grandi implicano frammentazione interna. Pagine piccole implicano tabelle grandi.
Condivisione
Condividere il testo di un programma tra pi processi semplice se si divide lo spazio di
indirizzamento tra Dati e Istruzioni.
Condividere i dati deve avere delle accortezze in pi. La pagina marcata come READ ONLY,
cos se un processo prova a scrivere il SO interviene, divide le 2 parti e permette la scrittura
(copy on write).
Librerie condivise
Quando si compila un programma non vengono linkate le librerie ma una subroutine. Leffettivo
link avviene al runtime. Questo permette di avere solo un testo della libreria usato da pi
processi.
Mapped Files
I file vengono spostati da disco in memoria. Le scritture su file sono scritture su memoria non su
disco finch il file non viene chiuso. In questo modo si possono condividere i file tra processi.
Bloccare le pagine
Immagina un processo che permette ad un dispositivo I/O di riempire un buffer in memoria.
Quel processo viene bloccato (aspetta che il dispositivo finisca) e un altro processo genera un
page fault. La pagina contenente il buffer non deve essere rimossa per nessun motivo altrimenti
perdo i dati! Esiste un modo per bloccare le pagina in memoria (pinning).
Segmentazione
Si assegna uno spazio di indirizzamento personale ad ogni parte di un programma (anche uno
spazio per ogni procedura se si vuole). La paginazione quindi un segmento unico. Il vantaggio
che i segmenti possono avere dimensione non fissata, il che per porta a delle conseguenze.
Indirizzare una memoria a segmenti implica il fatto di avere nellindirizzo una parte che indichi
qual il segmento e loffset allinterno del segmento. Semplifica il linking e la condivisione.
Files
Un File sempre creato da un processo, ma a differenza della parte dati persiste in memoria
secondaria in modo da essere utilizzato pi avanti nel tempo (anche da altri processi).
Lestensione di un file pi una convenzione che una regola in UNIX. Windows invece associa
ad ogni estensione un programma per aprire il file.
Struttura
I file in UNIX e Windows sono semplicemente una sequenza di byte, sta al processo che li
utilizza dare un significato.
Un alternativa strutturare in record di n byte. In questo modo si semplifica la scrittura o lettura
in certi ambienti dove comodo poter leggere o scrivere degli interi record per volta.
In ultima analisi i dati potrebbero essere strutturati come un albero di record contententi una
chiave, utili nella ricerca e utilizzati in alcuni ambienti.
I file eseguibili invece hanno generalmente un formato particolare per essere riconosciuti:
Entry Point
Flags Attributi
Data Dati
Relocation bits
Accesso
Directories
Utilizzate per raggruppare insiemi di file. Esiste una gerarchia in modo che lutente sia facilitato
nel trovare il file che cerca.
Per inoltrarsi nelle directory si ha bisogno di path che indicano dove si trova un file. Esistono
due tipi di path:
1. assoluti, si inizia a elencare il percorso dalla cartella root
2. relativi, si inizia dalla working directory
Implementazione
Disco
I File System sono implementati su disco.
Il settore 0 di un disco viene chiamato Master Boot Record (MBR) e serve a far partire il SO.
La fine dellMBR contiene la tabella delle partizioni dove una in particolare denominata come
attiva. Il BIOS esegue il boot block (primo blocco della partizione attiva) che a sua volta carica
il SO in memoria.
File
I file sono divisi, un po come accade per i processi, in blocchi di dimensione fissata.
La regola : blocchi piccoli implica spreco di tempo, blocchi grandi spreco di spazio
(frammentazione interna).
Allocazione contigua
I file sono salvati in blocchi contigui. Se un blocco ha dimensione di 1KB, un file di 50 KB viene
memorizzato in 50 blocchi consecutivi.
Questo approccio semplice e molto performante (una volta trovato il blocco iniziale la testina
legge tutto il file in una volta).
Uno svantaggio pesante per la frammentazione.
Viene utilizzato nei CD-ROM, dove i file una volta scritti non possono essere pi cancellati.
Linked List
Il file rappresentato da una lista dove ogni nodo contiene linformazione della posizione del
blocco successivo. Non ho frammentazione ma una lettura impiega troppo tempo; inoltre utilizzo
una parte di blocco per il puntatore invece che per il dato stesso.
Un vantaggio che basta salvare nella directory lindirizzo del primo blocco per recuperare tutto
il file.
I-nodes
Il file strutturato come un array (dimensione fissata) di indirizzi ai blocchi di memoria occupati
da quel file. Se un file ha bisogno di pi indirizzi, lultimo elemento della lista punter ad un
blocco in memoria dove saranno contenuti altri indirizzi.
Li-node deve essere caricato in memoria solo quando il file aperto quindi non occupa sempre
spazio. Ecco un esempio di accesso a file tramite i-node.
Directories
Devono contenere linformazione necessaria a localizzare il file su disco; il puntatore al primo
blocco occupato dal file sufficiente a questo scopo. Inoltre la directory deve contenere gli
attributi di tutti i file che contiene.
Blocchi liberi
Esistono due approcci:
1. Liste linkate
2. bitmap
Analizziamo il primo, immaginando di avere blocchi di 1KB e che per indirizzare un blocco in
memoria servano 32bit. La lista viene implementata usando come nodi i blocchi su disco, dove
ogni blocco contiene 1KB di indirizzi di blocchi liberi (1KB/32bit=255 indirizzi + indirizzo al nodo
successivo). In un disco da 500GB con blocchi da 1KB si hanno blocchi da mappare,
In memoria primaria si tiene solo 1 blocco della lista (occupo 1KB che corrisponde a conoscere
255KB di disco libero) in modo che se un processo crea un file sa dove memorizzarlo e pu
aggiornare velocemente la lista, analogamente se deve cancellarlo. Mantendendo in memoria
un blocco pieno a met si ha lefficienza ottimale. Tenendolo quasi pieno (di blocchi liberi) se un
file viene eliminato il blocco in memoria sar saturato e quindi dovr salvarlo in memoria e
crearne uno nuovo; se subito dopo un file viene creato dovr cedere dei blocchi quindi dovr
caricare dalla memoria il vecchio blocco. Come si evince c troppo I/O inutile che si pu evitare
tenendo il blocco pieno a met.
I bitmap invece memorizzano un bit per blocco e mappano tutta la memoria. Per mappare
500GB divisi in blocchi da 1KB ho bisogno di bit (circa 65MB), molto meno che nel
caso precedente anche se quando la memoria quasi piena la differenza minima.
Ottimizzazione
1. Cache per ridurre gli accessi al disco.
2. Block Read Ahead, leggo dei blocchi anche se non sono stati richiesti e li metto in
cache.
3. Ridurre i movimenti del braccio del disco cercando di collocare i blocchi di un file in
modo intelligente.
4. Deframmentare il disco in modo da riallocare i file in modo contiguo.
Programmed I/O
Se un processo vuole scrivere su un dispositivo di I/O in questa modalit esegue i seguenti
step:
1. System call per acquisire il dispositivo di I/O (error se non disponibile).
2. LO.S. copia i dati da scrivere su un buffer nel kernel space.
3. LOS. aspetta finch il dispositivo di I/O disponibile e scrive i dati. Se non riesce a finire
aspetta di nuovo finch il dispositivo non di nuovo disponibile: questa modalit si
chiama polling o busy waiting.
Semplice ma usa la CPU full time finch non ha finito.
Interrupt-driven I/O
E come il programmed I/O, ma la CPU al posto di aspettare il dispositivo fa altro, ovvero
esegue altri programmi (context switch). Una volta che il dispositivo di I/O ha finito invia un
interrupt.
DMA I/O
Lo svantaggio dellinterrupt-driven I/O che potrebbero esserci molti interrupt. Una soluzione
di usare il DMA che si occupa di gestire tutto quanto. Il DMA programmato come Programmed
I/O. Solo che il DMA controller a fare il lavoro e non la cpu. Svantaggi? E pi lento.
Le interfacce astratte fornite dai driver vengono classificate in due categorie principali :
1. interfacce a blocchi (block-oriented) :
1. la lettura/scrittura sul dispositivo fisico avviene un blocco alla volta
2. tipicamente i dati scritti vengono bufferizzati nel SO finch non si raggiunge lampiezza
di un blocco ( es : dischi, nastri)
2. interfacce a caratteri (character-oriented) :
3. la lettura/scrittura sul dispositivo fisico avviene un carattere alla volta
4. Non c bufferizzazione (tastiera, mouse, )
Magnetic disks
I dischi magnetici sono organizzati in cilindri, di cui ogni cilindro contiene tante traccie quanti
sono i piatti del disco impilati uno sopra laltro. Le traccie sono divise in settori (Ogni settore
numerato attorno alla circonferenza del disco).
Raid Level 0 Il disco virtuale diviso in strips (striscie), dove ogni strip contiene settori. Lo strip 0 conterr
i settori da 0 a . Se lo strip corrisponde al settore. Nel livello 0 il RAID mette gli
strips consecutivi nei drive seguendo la strategia RoundRobin (Vedi immagine sotto).
Performance eccellente e di facile implementazione. Reliability molto bassa, non c ridondanza
=> non un vero RAID.
Raid Level 1 Come raid di livello 0 ma c una copia di backup di tutti i dischi. Quindi c ridondanza.
Raid Level 2 Il RAID level 2 non lavora con strips (strisce) di settori, ma lavora in base alla WORD, o
addirittura in base al byte. Per esempio: si supponga di lavorare su dati di 4 bit. Su questi 4 bit ci
aggiungiamo il codice di hamming => diventa di 7 bit. Poi ogni bit viene scritto su un drive
diverso in parallelo. Questo schema richiede che tutti i drive siano sincronizzati, inoltre vengono
eseguiti molti calcoli per il codice di hamming.
Raid Level 3 E simile al raid livello 2, ma al posto di usare un codice di hamming viene usato un bit di parit e
scritto nel parity drive.
Raid Level 4 Funziona come il raid di livello 0, ma ogni X strips viene calcolato il parity byte degli strips. Per
esempio se voglio fare il parity bytedi 4 strips e ogni strips grande Y bytes, far lo XOR di
questi strips, che risulter in un parity strip di lunghezza Y bytes.
Raid Level 5 Come livello 5 ma i parity byte sono sparsi fra i drive (vedi immagine).
Solitamente un settore contiene 512 byte di dati. La dimensione di questi campi dipende da chi
produce i dischi, anche se per esempio solitamente lECC ha 16 byte di dimensione.
Inoltre tutti gli hard disks hanno dei settori di riserva (spare sectors) da poter usare per
rimpiazzare i settori danneggiati.
Cylinder Skew
La posizione del settore 0 di ogni traccia spostata rispetto alla traccia precedente di un certo
numero di settori: questo numero si chiama offset, ed anche chiamato cylinder skew
(pendenza del cilindro). Questo fatto per permettere al braccio meccanico del disco di leggere
pi traccie in un unica operazione contigua sempre perdere dati. Ovvero: se leggo una traccia
intera, e ritorno al settore 0, se mi devo spostare alla traccia successiva nel frattempo che mi
sposto sono gi sul settore 0 della traccia successiva, per via di questo offset.
Leggere senza interruzioni richiede un buffer dati di grandi dimensioni nel controller.
Consideriamo un controller che ha
un buffer di 512 byte, e ogni settore
ha dimensione dati 512 byte. Quindi
il buffer pu contenere al massimo
un settore per volta. Se volessimo
leggere due settori consecutivi, una
volta letto il primo dovremmo
calcolare lECC e trasferire i dati in
memoria, nel frattempo laltro settore
passato via. Questo pu essere
evitato intervallando i settori uno con
laltro come nellimmagine.
Nel primo approccio, prima che il disco sia venduto, testato ed scritta una lista dei settori
danneggiati. Per ogni settore danneggiato c un settore di riserva che lo sostituisce
(naturalmente non sono infiniti).
Ci sono due modi per fare questa sostituzione:
1. A livello logico segnare che uno dei settori di riserva il settore danneggiato, ovver
mappare (vedi immagine b) )-
2. Shifto tutti i settori avanti di uno (vedi immagine c) ).
Lo stable storage funziona con due dischi identici, con i blocchi corrispondenti che lavorano
insieme. In assenza di errori, i blocchi corrispondenti su entrambi i dischi sono identici ( i dati
sono identici) .
Sono definite 3 operazioni:
1. Stable writes: Scrivo un blocco sul primo disco, poi verifico se stato scritto
correttamente. Se non lo stato, allora riprovo finch non va a buon fine. Dopo
fallimenti, il blocco sostituito con uno di riserva, e loperazione ripetuta. Una volta
scritto correttamente sul 1 disco, rifaccio la stessa operazione sul secondo.
2. Stable reads: Prima legge un blocco dal disco 1. Se il checksum calcolato non
combacia con lECC, viene riletto fino a volte. Se dopo volte non ancora stato
letto, allora viene letto dal 2 disco.
3. Crash recovery: Dopo un crash, un programma di recovery confronta entrambi i dischi
per vedere le differenze fra i blocchi. Se due blocchi sono uguali, non fatto niente. Se
uno di questi ha un ECC incorretto, il blocco corrotto sovrascritto con i dati buoni. Se
entrambi i blocchi hanno ECC giusto, ma dati differenti, il blocco dal 1 disco scritto nel
corrispondente blocco del 2 disco.
Supponiamo che un processo occupi la risorsa A e che richieda la risorsa B: se allo stesso
istante un altro processo occupa B e richiede A => entrambi i processi restano bloccati (vanno
in deadlock).
Risorse
Ci sono due tipi di risorse:
1. Un dispositivo hardware (stampante)
2. Uninformazione (es: un record di un database)
Una risorsa qualcosa che deve essere acquisita, usata, e rilasciata nel corso del tempo.
Se una risorsa non disponibile quando richiesta, il processo che la richiede forzato ad
aspettare. In alcuni O.S. il processo bloccato automaticamente quando una richiesta fallisce,
e risvegliato quando la risorsa disponibile.
In altri invece la richiesta fallisce e basta con un codice di errore.
Nellimmagine a destra si vede un esempio di come vengono gestite 2 risorse con un solo
processo.
Le cose si complicano se ci sono pi processi.
seguente: .
Dopodich cancello la 3 riga dalle
due matrici e vedo che posso
soddisfare la richiesta del processo
nella 2 riga .
Ora posso soddisfare anche la
richiesta del 1 processo Non ci
sono stalli.
Evitare lo stallo
Solitamente non si pu far uso dei metodi presentati precedentemente per riconoscere uno
stallo, perch viene fatta una richiesta alla volta, e non tutte insieme.
Si possono comunque evitare gli stalli se ci sono date certe informazioni dallinizio. Purtroppo
sar quasi impossibile implementare in un sistema degli algoritmi per evitare lo stallo.
Facciamo lesempio con un solo tipo di risorsa: supponiamo ci siano al massimo 10 elementi di
questa risorsa.
A lato c la
dimostrazione di
uno stato non
sicuro.
Bisogna notare che uno stato non sicuro non andr sicuramente in stallo. Questo perch sono
decisioni prese sul numero massimo di risorse che ogni processo potrebbe chiedere, in realt
un processo ne richieder molte meno di risorse. Quello che possiamo dire che se uno stato
sicuro, allora garantito che non ci saranno stalli.
Algoritmo del banchiere per una risorsa
Prima di iniziare lesecuzione ogni processo dichiara il massimo numero di risorse che gli sono
necessarie. Ad ogni richiesta di una nuova risorsa lalgoritmo controlla se accogliere la richiesta
porta ad uno stato sicuro o insicuro:
1. Per ogni processo si calcolano le unit di risorsa ancora richiedibili (R = Max - Has)
2. Si considerano i processi in ordine di R crescente controllando che ognuno possa
ancora richiedere R risorse e terminare correttamente
3. Se tutti i processi possono terminare correttamente lo stato sicuro
4. Solo le richieste che portano a stati sicuri sono accolte.
Bisogna evitare di assegnare una risorsa quando non strettamente necessario, e far in modo
che il minor numero possibile di processi possa richiedere una risorsa.
Un problema che i processi non conoscono a priori quante risorse useranno. Infatti se lo
sapessero si potrebbe usare lalgoritmo del banchiere. Un altro problema che con questo
metodo le risorse non sarebbero utilizzate in modo ottimale.
Una variazione di richiedere al processo che ha fatto domanda per una risorsa di rilasciare
temporaneamente tutte le risorse che ha gi acquisito, e quindi richiedere tutte quelle
necessarie.
Si consideri la seguente soluzione SBAGLIATA al problema dei filosofi a cena riportata dal
testo:
#define N 5
void philosopher (int i) {
while (TRUE) {
think();
takeForks(i);
takeForks((i+1)%N);
eat();
putForks(i);
putForks((i+1)%N);
}
}
Le forchette sono risorse condivise dai diversi processi (filosofi) e bisogna quindi garantirne
laccesso mutuamente esclusivo! La soluzione corretta prevederebbe quindi un mutex che
garantisce accesso esclusivo alla regione takeForks e PutForks.
I problemi che si possono verificare sono molteplici. Per esempio tutti i processi possono
venire interrotti dopo la prima takeForks e quindi ogni filosofo ha una forchetta in mano:
nessuno potr pi mangiare. Altre problematiche potrebbero verificarsi allinterno del
metodo takeForks stesso.
Esercizio 5
In un sistema di gestione della memoria con swapping dinamico, che utilizza un registro
base B ed un registro limite L
per assicurare la protezione degli spazi di indirizzamento, abbiamo 32M di RAM, di cui gli
8M di indirizzi bassi occupati dal Sistema
Operativo. Consideriamo i seguenti programmi : A, di 4M, B di 16M, C di 12M e D di 6M.
Supponiamo inoltre che i programmi passino in esecuzione nellordine seguente: 1) A, 2) B,
3) C, 4) D, 5) A .
Ogni volta che un nuovo processo passa in esecuzione discutere :
a) la mappa della memoria centrale
b) che cosa contengono i registri base e limite.
I registri BASE e LIMIT contengono lindirizzo iniziale e finale in memoria del processo in
esecuzione. Immaginiamo che in memoria sia solo presente il sistema operativo.
FREE
FREE FREE FREE FREE
FREE
B
A C D D
OS
A
OS A C C
OS
OS A A
OS OS
NB:
4. Quando A viene eseguito nuovamente non ha bisogno di essere caricato in memoria e i registri BASE e
LIMIT corrispondono a quelli della sua prima esecuzione (poich non stato spostato).
Esercizio 6
Un disco ha un tempo di seek di 0,5ms per ogni cilindro attraversato, un tempo di rotazione
di 6ms e un tempo di trasferimento dei dati di un settore di 12 microsecondi. Inoltre si fanno
le seguenti ipotesi :
- la testina attualmente posizionata sul cilindro 13
- l'attesa media prima che il settore desiderato passi sotto la testina dopo il seek di mezza
rotazione
Supponendo che al tempo attuale arrivino contemporaneamente le seguenti richieste di
lettura di settori (un settore per cilindro):
a- cilindro 14
b- cilindro 11
c- cilindro 19
d- cilindro 2
e- cilindro 31
Calcolare il tempo di completamento delle richieste nel caso in cui venga utilizzato
l'algoritmo SSF e l'algoritmo dell'ascensore (con direzione iniziale verso l'alto).
SSF (Shortest Seek First) sposta la testina del disco sul cilindro pi vicino alla sua posizione
attuale: 13 14 11 19 31 2.
Lalgoritmo dellascensore invece sposta la testina verso lalto finch ho un settore da leggere in
quella direzione, poi la cambia: 13 14 19 31 11 2.
Per calcolare il tempo di completamento si deve tener conto del fatto che per attraversare ogni
cilindro si impiegano 0,5 ms e che per trovare il settore si impiegano 3 ms; quindi per leggere il
settore nel cilindro 11 (partendo dal cilindro 14) si impiegano (0,5*3)ms + 3ms.
Infine bisogna ipotizzare la dimensione del buffer del controllore del disco:
Buffer = 1 settore: bisogna aspettare che ogni volta letto un settore la CPU lo legga dal
buffer per far spazio al successivo. Bisogna quindi aggiungere ad ogni passaggio 12us
di attesa, ovvero (12*5)us di attesa totale.
Buffer > 5 settori: il trasferimento dei dati avviene solo al completamento di tutte le
letture, quindi bisogna aggiungere una sola volta (alla fine) i 12us.
Esercizio 7
Tempo cpu
A 25 ms
B 50 ms
C 10 ms
D 30ms
SJF (Short Job First) esegue i processi in ordine crescente di tempo di CPU e vengono eseguiti
fino a che non hanno terminato lesecuzione stessa: C A D B.
RR (Round Robin) esegue i processi nellordine di arrivo e li interrompe una volta che
esauriscono il loro quanto di tempo: A (25,15) B (50,40) C (10,0) D (30,20) A (15,5)
B (40,30) D (20,10) A (5,0) B (30,20) D (10,0) B (20,0).
La notazione X(t1,t2) significa che il processo X prende controllo della CPU per (t1-t2)ms e una
volta rilasciata la CPU necessita ancora di t2ms per completare lesecuzione.
Il tempo di turnaround lintervallo di tempo che intercorre tra larrivo di un processo nella coda
dei processi READY e il suo completamento.
Esercizio 8
Esercizio 9
Esercizio 10
Esercizio 11
Esercizio 12
Esercizio 13
Esercizio 14
Esercizio 15
Esercizio 16
Esercizio 17
Esercizio 18
Esercizio 19
Riferimenti & Bibliografia