Definizione e classificazioni Il sistema operativo di un computer è

Definizione e classificazioni
Il sistema operativo di un computer è quel software di base che permette di gestire le risorse
del computer, in pratica soprattutto le parti hardware di un computer: CPU, memoria centrale e
periferiche. Verrebbe naturale oggi dire che il sistema operativo è quel software che permette
di utilizzare il nostro computer, se non fosse che i primi programmatori non avevano nessun
sistema operativo a disposizione, anzi ai tempi pionieristici dell’informatica non esistevano
neanche i linguaggi di programmazione e chi scriveva programmi lo faceva direttamente
conoscendo il linguaggio della macchina che utilizzavano e tutte le caratteristiche hardware
della macchina stessa.
Dal punto di vista dell’utente il sistema operativo è quel software che permette l’esecuzione dei
programmi lanciati; dal punto di vista del computer il sistema operativo permette la gestione
delle sue risorse, attraverso la virtualizzazione delle stesse. Per virtualizzazione intendiamo
quel processo che mette a disposizione di ogni programma in esecuzione una macchina
virtuale composta da CPU, memoria centrale e periferiche dedicate a quel programma, quando
in effetti esiste una sola CPU all’interno del computer e la memoria centrale, per quanto
estesa, è comunque limitata.
I primi sistemi operativi (vedi DOS) avevano una interfaccia a caratteri per dialogare con
l’utente, mentre ora i sistemi operativi mettono a disposizione una interfaccia grafica (GUI) che
facilita l’utilizzo del computer da parte dell’utente.
Si possono avere diverse classificazioni dei sistemi operativi basati sul loro modo di lavorare
piuttosto che sull’utilizzo che se ne fa.
Una prima classificazione basata sul modo di lavorare dei sistemi operativi è la seguente:
•
sistemi monoprogrammati
•
sistemi multiprogrammati
•
sistemi distribuiti
I sistemi monoprogrammati sono quei sistemi operativi che permettono l’esecuzione di un
programma per volta. Quindi tutte le risorse dell’elaboratore sono a disposizione dell’unico
programma in esecuzione, le quali rimangono inutilizzate, se il programma non ne necessita.
I sistemi multiprogrammati sono di due tipologie differenti a seconda del fatto che sono
monoutente o multiutente. I primi, come gli attuali PC, permettono l’esecuzione di più
programmi contemporaneamente eseguiti da un singolo utente, attraverso l’apertura e la
gestione di differenti finestre di lavoro, in ognuno delle quali lavora una applicazione.
Il secondo tipo di sistemi multiprogrammati, quelli multiutente, sono quelli chiamati
“centralizzati” in cui l’elaboratore è composto da un “mainframe”, un’unità dotata di capacità
elaborativi (CPU e memoria centrale) e da tanti terminali, postazioni composte da tastiera e
video, senza capacità elaborativi. Gli utenti attraverso i terminali mandano in esecuzione i
programmi verso il mainframe e nello stesso istante il mainframe esegue contemporaneamente
programmi di diversi utenti.
I sistemi distribuiti sono le reti di calcolatori, ovvero sono composti da elaboratori che hanno
capacità di elaborazione autonoma, connessi fra loro ed in grado quindi di utilizzare risorse fra
loro condivise (file, cartelle, dischi, stampanti).
Una seconda classificazione è basata sul modo di utilizzo del sistema operativo. Mettiamo fra
questi in evidenza i seguenti tipi di sistemi operativi:
•
batch
•
time sharing
•
real time
I sistemi batch sono quelli che non hanno interazioni fra utente e macchina. L’utente deposita il
suo programma in ingresso al sistema (compresi gli input per la sua esecuzione) e in tempi
successivi potrà vedere il risultato dell’elaborazione.
I sistemi time-sharing sono quelli che suddividono il tempo di lavoro dell’elaboratore in modi
equo fra i vari programmi che sono in esecuzione. Ad ogni programma viene assegnato un
quanto di tempo fisso della CPU, chiamato time-slice. Il time-slice assegnato ad ognuno dei
programma è lo stesso. L’utente in questo modo vede avanzare il proprio programma in
continuazione e ha l’impressione di avere l’intero sistema a disposizione, quando in realtà in
ogni piccolo istante solo un programma alla volta è realmente in esecuzione.
I sistemi real-time sono quelli che hanno continue interazioni fra utente e macchina con tempi
di risposta della macchina che devono essere molto brevi. Gli aspetti grafici di questi sistemi
non sono normalmente molto curati per non appesantire il sistema e rallentarne i suoi tempi di
risposta.
Struttura gerarchica di un sistema operativo (struttura a cipolla)
Un sistema operativo è un sostare molto complesso e come tale è suddiviso in moduli ognuno
dei quali realizza una differente funzione. Una delle più note strutture dei sistemi operativi è la
cosiddetta “struttura a cipolla”, una struttura gerarchica composta da vari livelli, ognuno dei
quali gestisce una delle risorse hardware dell’elaboratore:
•
•
•
•
•
livello
livello
livello
livello
livello
0:
1:
2:
3:
4:
hardware
nucleo (gestione del processore)
gestione della memoria centrale
gestione delle periferiche
file system (gestione delle memorie di massa e delle informazioni ivi contenute)
Il livello 0 non fa parte del sistema operativo e serve solo ad indicare che il sistema operativo
si appoggia sull’hardware per gestirne le risorse.
Ognuno di questi livelli gestisce una risorsa hardware virtualizzandola, cioè mettendo a
disposizione di ogni programma una risorsa dedicata. Il livello 1 mette a disposizione del
programma un processore virtuale dedicato al programma; il livello 2 una memoria virtuale
dedicata al programma e così via per i livelli successivi.
Concetto di processo e processore
Un processo è un programma in esecuzione. Mentre il programma è un’entità statica (è un
codice scritto e salvato in un file), che descrive la sequenza di esecuzione delle istruzioni, il
processo è un’entità dinamica rappresentante il programma lanciato in esecuzione che evolve
attraverso degli stati.
Il processore (o CPU) è quel dispositivo che permette l’evoluzione dei processi. In sistemi a
singolo processore solo un processo per volta può essere in esecuzione in un singolo istante.
Cicli di vita di un processo (diagramma degli stati)
Dal momento in cui un programma viene mandato in esecuzione, il processo evolve attraverso
diversi stati fino alla sua conclusione. Il processo quindi subisce varie transizioni di stato in
base agli eventi che si verificano. In seguito alle transizioni di stato il processo può trovarsi in
vari stati di avanzamento passando per un certo stato anche diverse volte.
Gli stati in cui si può trovare un processo sono:
•
•
•
•
pronto
esecuzione
attesa
terminazione
Lo stato di pronto è lo stato in cui un processo di trova quando è stato caricato in memoria
centrale e quindi è pronto per essere eseguito appena ad esso verrà assegnata la CPU. In
questo stato si può quindi trovare una coda di processi (ready-list)
Lo stato di esecuzione è lo stato in cui un processo si trova quando ad esso viene assegnata la
CPU e quindi sta effettivamente evolvendo. Solo un processo per volta si può trovare in questo
stato nei sistemi a singolo processore.
Lo stato di attesa è lo stato in cui un processo si trova quando esso sta richiedendo una risorsa
o un servizio al sistema operativo (generalmente l’esecuzione di una operazione di I/O) e
aspetta che la risorsa gli sia assegnata o il servizio richiesto sia completato.
Lo stato di terminazione è lo stato in cui il processo si trova quando termina e il sistema
operativo può rilasciare le risorse ad esso assegnate.
Il diagramma degli stati illustra le transizioni di stato che un processo può avere.
•
da pronto ad esecuzione: quando al processo (già caricato in memoria centrale) viene
assegnata la CPU
•
da esecuzione a pronto: quando il tempo di CPU assegnato al processo scade senza che il
processo abbia terminato. Il processo viene riaccodato fra i processi in stato di pronto che
aspettano la CPU per poter continuare l’elaborazione
•
da esecuzione a terminazione: quando il processo termina durante il tempo di CPU che il
sistema operativo gli ha assegnato
•
da esecuzione ad attesa: quando il processo in esecuzione richiede al sistema operativo
una risorsa o un servizio (generalmente l’esecuzione di un’operazione di I/O).
da attesa a pronto: quando la risorsa richiesta dal processo è disponibile o il sistema
operativo è in grado di fornire il servizio richiesto dal processo (per esempio è pronto
l’input richiesto dal processo). In questo caso il processo non va subito in esecuzione ma
nello stato di pronto nel quale aspetta che gli venga assegnata di nuovo la CPU per
evolvere.
•
Compiti del nucleo
Il nucleo è il primo livello del sistema operativo e quindi quello più vicino all’hardware e le cui
funzioni sono di supporto ai livelli superiori. Il compito principale del nucleo è quello di
generare un processore virtuale per ognuno dei processi attivi del sistema, in modo tale che
l’assegnazione del processore fisico ad un processo piuttosto che ad un altro non influenzino il
comportamento logico del sistema. Le funzioni principali del nucleo sono:
•
scelta del processo cha passa dallo stato di pronto a quello di esecuzione (schedulazione
dei processi)
•
gestione delle interruzioni
•
salvataggio e ripristino dello stato dei processi
•
sincronizzazione dei processi
•
gestione del deadlock
Gestione delle interruzioni (interrupt)
Una interruzione è un meccanismo attraverso cui un processo interrompe la sua esecuzione. Si
distinguono interruzioni hardware e software.
Le prime sono meccanismi hardware attraverso i quali una periferica avverte la CPU che la
periferica è pronta per scambiare dei dati nelle operazioni di I/O di un programma. Per
esempio in una operazioni di input, l’utente immette un valore nella tastiera in modo asincrono
rispetto al funzionamento di un programma. Il programma rimane fermo fino alla ricezione del
valore inserito in input dall’utente che lo esegue. Per quanto l’utente possa essere veloce
nell’inserimento del dato, (per esempio svolge l’operazione in un secondo) in quel frangente la
CPU potrebbe eseguire circa un miliardo di istruzioni. Per questo l’esecuzione del processo
viene interrotta e la CPU viene assegnata ad un altro processo.
Quando il dato è stato inserito dall’utente attraverso la tastiera, la periferica avverte la CPU di
questo fatto e il processo può così essere in grado di proseguire. L’arrivo di un interrupt
hardware causa il passaggio di stato da attesa a pronto del processo.
Ci sono due tipi di interrupt: mascherabili e non mascherabili. La gestione dei primi può essere
differita nel tempo, quando ad esempio il sistema sta gestendo un altro interrupt di priorità
maggiore. I secondi tipi di interruzione hanno priorità massima e devono essere
immediatamente gestiti. Vengono generati quindi in situazioni di emergenza del sistema.
Per quanto riguarda le interruzioni software ci si riferisce a chiamate del processo a routine
(primitive) del sistema operativo. Si distinguono due tipi di interruzioni software: da
programma e da eccezione. Queste ultime (chiamate anche trap) sono scatenate quando
avviene un errore durante l’esecuzione di un programma (divisione per zero, indice di un
vettore al di fuori dell’intervallo ecc.).
Le interruzioni da programma vengono generate dal programma stesso quando ad esempio il
processo esegue una operazioni di I/O, proprio per evitare che al processo resti assegnata la
CPU nel periodo in cui la periferica, molto più lenta del processore, esegue l’operazione di I/O.
Nell’esempio precedente di un processo che esegue un’operazione di input da tastiera, il
processo richiama il sistema operativo attraverso una interruzione per interrompersi, per poi
essere riattivato dopo che l’input verrà inserito dall’utente. Questa tipo di interruzione software
causa il passaggio di stato del processo da esecuzione ad attesa.
Salvataggio e ripristino dello stato di un processo
Quando un processo viene interrotto a causa di una interruzione – ma anche quando per
esempio è finito il tempo di CPU assegnatogli dal sistema operativo – il suo stato deve essere
memorizzato per poter essere poi ripristinato e far ripartire il processo dal punto in cui era
stato interrotto esattamente nella stessa situazione in cui era il processo al momento della
interruzione. Lo stato del processo viene chiamato contesto.
Il contesto del processo deve innanzitutto contenere il valore del registro PC (indicante
l’indirizzo di memoria della prossima istruzione da eseguire) in modo da far riprendere
l’esecuzione del programma proprio a partire dall’istruzione successiva all’ultima eseguita.
Interrompendo l’esecuzione del processo e attivando l’esecuzione di un altro processo in coda
nello stato di pronto, il valore del PC viene cambiato in modo che contenga l’indirizzo della
prossima istruzione del nuovo processo in esecuzione.
Non è necessario invece salvare il valore del registro IR in quanto l’istruzione in corso viene
portata sempre a termine (cioè fino al termine della fase di execute) prima che avvenga
l’interruzione del processo.
Inoltre il processore ha al suo interno degli altri registri che servono a contenere valori
temporanei ottenuti durante l’esecuzione del processo (per esempio il risultato di una
operazione aritmetico-logica) appena eseguita dalla ALU. Anche questi valori fanno parte del
contesto del processo e devono essere salvati in modo che possano essere rimessi nei registri
nel momento in cui riprenderà l’esecuzione del processo precedentemente interrotto.
Il contesto del processo, formato dal valore del PC e degli altri registri della CPU vengono
salvati in un’area della memoria centrale chiamata stack, una struttura a pila gestita in modo
LIFO (last in first out).
Schedulazione dei processi
Questa funzione del nucleo, la parte del sistema operativo più frequentemente richiamata,
permette di scegliere quale dei processi in stato di pronto andrà in esecuzione. In particolare
attraverso la schedulazione dei processi viene scelto il processo che a cui verrà assegnata la
CPU e per quanto tempo avrà a disposizione il processore per evolvere.
Esistono varie tecniche di schedulazione dei processi, alcune molto semplici e altre più
complesse. Occorre ricordare che il sistema operativo è un software e come tale concorre con
gli altri software applicativi all’acquisizione delle risorse. Questo vuol dire che spesso tecniche
di schedulazione o algoritmi di risoluzione di problemi, per quanto sofisticati possono essere
meno validi di altri più semplici perché richiedono tempi di esecuzione più lunghi e quindi uso
di risorse per più lungo tempo, togliendole così ai programmi applicativi. Nessun sistema può
essere efficiente se serve per far funzionare solo il sistema operativo e non gli applicativi.
Quindi una parte di sistema operativo così frequentemente richiamata, come la schedulazione
dei processi, deve utilizzare algoritmi non troppo complessi.
Le più note tecniche di schedulazione dei processi sono:
FIFO o FCFS (First Come First Served). Al primo processo nella coda di pronto viene assegnata
la CPU fino al suo termine o finché non effettua un’operazione di I/O (che come abbiamo già
visto fa sospendere immediatamente il processo)
Round Robin: ogni processo in stato di pronto avrà a disposizione un quanto di tempo
(chiamato time slice) per evolvere. Il tempo di CPU cioè viene suddiviso in time slice della
stessa dimensione(dell’ordine dei millisecondi) e in ogni time slice verrà schedulato uno dei
processi in stato di pronto. Allo scadere del time slice il processo viene posto in fondo alla coda
dei processi in stato di pronto e il processore viene allora assegnato al primo processo nella
coda di pronto, a meno che durante il time slice il processo non sia terminato (in questo caso
passa nello stato di terminato) oppure esegue un’operazione di I/O (in questo caso viene
interrotto e passa nello stato di attesa); anche in questi casi il processore viene assegnato al
primo processo nella coda di pronto.
Inverso del time slice: questa tecnica si basa sui concetti di I/O Bound e CPU Bound. I primi
sono i processi che eseguono molte operazioni di I/O e quindi sfruttano meglio le altre risorse
hardware del sistema; i secondi sono quelli che fanno molti calcoli utilizzando quindi molto la
CPU e poco le periferiche. Questa tecnica privilegia i processi del primo tipo, utilizzando il
concetto di time slice della tecnica precedente. I processi che utilizzano meno il time slice
vengono messi in testa alla coda dei processi in pronto e saranno schedulati prima. Se
abbiamo tre processi, che hanno utilizzano rispettivamente 20,10,30 millisecondi prima di
essere interrotti da un’operazione di I/O, nel giro successivo verranno riordinati con il secondo
processo che verrà schedulato per primo, poi il primo e infine il terzo.
Ci sono altre tecniche più complesse che usano più liste (code) nello stato di pronto con
differenti priorità. Una di queste tecniche assegna una priorità predefinita ad ogni processo, in
base al fatto che il processo sia di sistema operativo, oppure di un utente con maggiore priorità
(amministratore) o di utente “normale”. Ogni coda viene gestita attraverso le tecniche FCFS o
Round Robin e finché queste liste sono piene vengono schedulati i processi di queste code.
Quando la coda di priorità maggiore è vuota si passa a gestire la coda di priorità successiva e
così via, ma quando una coda ritorna ad avere qualche processo si riprende a gestire la lista
con priorità maggiore.
Un’altra tecnica multilista, favorisce processi più corti rispetto a quelli più lunghi. Ogni processo
quando parte viene accodato nella lista con la massima priorità, ma ogni volta che viene
interrotto o scade il time slice perde un livello di priorità e viene accodato nella lista con una
priorità minore. Le code vengono gestire come nella tecnica precedente. Entrambe le tecniche
con liste multiple potrebbero portare a situazione in cui alcuni processi con bassa priorità
possano non ottenere mai la CPU: per evitare questo (blocco individuale) periodicamente
vengono gestite comunque le code con bassa priorità dando un tempo di CPU più alto per
portarli a termine.
Sincronizzazione dei processi
Il problema della sincronizzazione dei processi è legata al fatto che i processi (compresi quelli
di sistema operativo) sono in competizione per ottenere le stesse risorse nello stesso istante.
Le risorse possono essere fisiche (quelle HW relative al processore, la memoria centrale e le
periferiche) e quelle logiche (buffer, file).
Esistono diversi tipi di relazione fra processi concorrenti:
•
cooperazione
•
competizione
•
interferenza
Due processi sono in cooperazione se sono legati logicamente fra loro in modo che ognuno
ha bisogno dell’altro per evolvere. Il caso tipico è lo schema dei processi produttore/
consumatore, nel quale un processo produttore P genera dei dati e li inserisce in un buffer
(comune al consumatore) e un processo consumatore C che legge i dati dal buffer e li
manipola. C può leggere i dati dal buffer solo dopo che P l’ha riempito e P può riempire il buffer
solo dopo che C l’ha svuotato. Non è possibile avere una sequenza non ordinata P-C-P-C…,
perché se avessimo una sequenza P-P-C-C si perderebbe il primo blocco di dati inseriti nel
buffer e con una sequenza P-C-C-P il consumatore leggerebbe due volte i dati del primo blocco.
Due processi sono in competizione se sono indipendenti dal punto di vista logico ma hanno in
comune la necessità di lavorare sulla stessa risorsa. L’acquisizione di tale risorsa e i tempi di
completamento del lavoro possono dipendere sensibilmente da questa “competizione”. La
questione diventa stringente quando è associata al problema della mutua esclusione, per cui
l’acquisizione di una risorsa è esclusiva di un processo con conseguente attesa di tutti gli altri
processi che necessitano della stessa risorsa.
Si ha una interferenza fra processi quando il risultato degli stessi dipende dal momento in cui
vengono eseguiti, cioè dalle sequenze temporali di utilizzo delle risorse del sistema da parte di
tutti i processi che le richiedono. Un caso tipico è quello di due processi che richiedono una
stampa: non si possono intervallare i blocchi mandati in stampa da un processo con quelli
dell’altro processo. Alcune risorse infatti non possono essere sottratte ad un processo che le ha
acquisite ed assegnarle ad un altro, altrimenti si possono avere risultati che variano da
esecuzione ad esecuzione, cioè causare errori dipendenti dal tempo, difficili da trovare perché
dipendenti dalla sequenza di esecuzione e non facilmente ripetibili.
Il meccanismo più semplice di sincronizzazione sarebbe quello di assegnare una risorsa ad un
processo per tutto il tempo necessario per farlo terminare, ma questo penalizzerebbe troppo
gli altri processi.
In realtà nelle situazioni sopra introdotte spesso il problema è legato ad alcuni passi limitati
dell’esecuzione dei programmi, nei quali l’esecuzione del processo che sta utilizzando una
determinata risorsa non deve essere interrotto. Queste parti del programma sono chiamate
“zone critiche” (per esempio operazioni di stampa o di scrittura su un file). Queste operazioni
devono essere rese indivisibili, cioè non interrompibili. Quando un processo acquisisce una
risorsa non deve essere interrotto finché non ha terminato l’operazione a rischio, cioè ha
eseguito la zona critica.
Un meccanismo tipico per gestire queste zone critiche e la sincronizzazione dei processi è
quello del semaforo. Quando un processo acquisisce una risorsa e sta eseguendo una zona
critica il semaforo diventa “rosso” e gli altri processi rimangono bloccati in attesa che il
semaforo diventi “verde”. Quando il primo processo ha finito di eseguire la zona critica e quindi
di lavorare su quella risorsa sblocca il semaforo e un altro processo precedentemente bloccato
può acquisirla.
Lo schema dei processi produttore/consumatore precedentemente descritto si risolve con due
semafori, il primo che serva a bloccare il consumatore quando il produttore sta scrivendo i dati
sul buffer e il secondo per bloccare il produttore quando il consumatore sta leggendo i dati dal
buffer. Questo meccanismo permette ai due processi di alternarsi nell’accesso del buffer.
Deadlock
Un sistema è in stato di deadlock (o stallo) quando nessuno dei processi attivi riesce a
terminare.
Un tipico caso di deadlock è quello di due processi e di due risorse: un processo ha acquisito
una risorsa che è richiesta dall’altro processo che ha acquisito la seconda risorsa, richiesta dal
primo processo (attesa circolare).
Seguono esempi.
Il deadlock viene generato quando si hanno contemporaneamente le seguenti situazioni:
o
o
o
allocazione parziale delle risorse: le risorse sono allocate ad un processo non all’inizio
dell’esecuzione di una zona critica ma man mano che le utilizza
negazione del prerilascio forzato: il sistema non può sottrarre forzatamente le risorse
già allocate ai processi
attesa circolare: lista di processi, ognuno dei quali è in attesa di una risorsa già allocata
al processo successivo.
Il deadlock può essere gestito o attraverso meccanismi di prevenzione o attraverso meccanismi
di riconoscimento e recovery.
Nella progettazione di un sistema operativo occorre tenere in considerazione sia che il deadlock
è una situazione grave che porta ad un blocco totale del sistema e quindi alla necessità di farlo
ripartire abortendo tutti i processi che erano in esecuzione in quel momento, sia che è
comunque un fenomeno che accade raramente e quindi non sempre la scelta di adottare
meccanismi di prevenzione è quella migliore. Infatti l’allocazione delle risorse è l’attività che
viene eseguita continuamente da un sistema operativo e il dover controllare ogni volta se una
allocazione di una risorsa porti ad uno stato di deadlock può essere molto gravosa.
Le principali tecniche di prevenzione sono:
•
•
•
allocazione globale delle risorse
allocazione gerarchica delle risorse
algoritmo del banchiere
L’allocazione globale delle risorse prevede che ad un processo vengono assegnate le risorse
che richiede solo se sono tutte disponibile, altrimenti rimane in attesa. La controindicazione di
questa tecnica è che ad un processo possono venire allocate delle risorse che utilizzerà solo
dopo molto tempo e che verranno quindi rese indisponibili ad altri processi che ne hanno
bisogno, peggiorando le prestazioni del sistema.
L’allocazione gerarchica delle risorse prevede che alle risorse vengano assegnati dei livelli di
priorità e che un processo deve ottenere le risorse partendo da quelle di priorità più bassa fino
a quelle di priorità a più alta. In questo modo si impedisce che un processo possa avere
acquisito risorse più rare - e di tenerle quindi bloccate ad altri processi - in attesa che gli
vengano assegnate risorse più comuni.
L’algoritmo del banchiere è applicabile solo se è noto a priori il numero massimo di risorse
che ogni processo necessita, informazione non sempre conosciuta. Il sistema operativo tramite
questo algoritmo effettua sempre una scelta sicura: ogni qualvolta i processi richiedono
l’allocazione di risorse l’algoritmo sceglie la richiesta per cui almeno un processo può
terminare.
Seguono esempi
Per quanto riguarda il riconoscimento del deadlock in realtà il sistema operativo non può mai
essere sicuro che il deadlock si sia effettivamente verificato. Ci si può basare solo su metodi
statistici basati sul superamento di alcune soglie temporali: tempo massimo di attesa di una
risorsa da parte di un processo o tempo massimo di possesso di una risorsa da parte di un
processo. Se vengono superate tali soglie il sistema considera che si sia verificata una
situazione di deadlock e cerca di effettuare il recovery, ovvero recuperare lo stato del sistema
precedente al deadlock. Ovviamente non sempre si può essere certi che il superamento di
queste soglie sia realmente causato da un deadlock.
Le principali tecniche di recovery sono:
o
o
o
interruzione di tutti i processi
interruzione di un processo alla volta fino a quando il deadlock non sia rimosso
rilascio forzato di una risorsa per volta ad un processo prescelto fino a quando il
deadlock non sia rimosso
Il primo metodo è sicuro ma è anche il meno efficiente perché prevede che i processi siano
tutti abortiti e occorre farli ripartire tutti quanti. Inoltre dovendo far ripartire tutti i processo di
nuovo, si potrebbero ripetere le condizioni che avevano portato al deadlock.
Il secondo metodo prevede che si scelga un processo da eliminare in modo da riallocare le sue
risorse agli altri processi bloccati. Se il deadlock è eliminato il procedimento termina, altrimenti
si sceglie un altro processo da eliminare e così via.
Il terzo metodo prevede che si selezioni un processo al quale viene tolta una risorsa già
assegnata per allocarla ad un altro processo in attesa della stessa. Se il deadlock è eliminato il
procedimento termina, altrimenti la stessa risorsa viene tolta ad un altro processo e
rassegnata e così via.
I criteri per scegliere i processi da eliminare si basano o su una priorità più bassa, piuttosto
che su quanto il processo ha già lavorato e su quanto manca alla sua terminazione, scegliendo
i processi che hanno lavorato di meno e su quelli a cui manca più tempo per terminare.
La situazione di blocco individuale si ha quando solo alcuni processi sono bloccati mentre il
sistema nella sostanza lavora. Può accadere quando alcuni processi hanno bassa priorità e
sono molto penalizzati nella competizione con altri processi sull’assegnazione delle risorse. E’ il
caso ad esempio delle tecniche di schedulazione dei processi basate su liste multiple, alcune
delle quali con priorità molto bassa. I processi delle liste con bassa priorità potrebbero non
essere mai schedulati e quindi essere di fatto bloccati.
Livello 2: gestione della memoria centrale
Un programma per essere eseguito deve prima essere caricato in memoria centrale. Il compito
del secondo livello del sistema operativo è quello di gestire la memoria centrale in modo da
rendere massimo il numero di processi allocati e aumentare quindi il grado di
multiprogrammazione. Ovviamente per un sistema operativo monoprogrammato la gestione
della memoria è molto semplice ed è dedicata all’unico processo in esecuzione in quel
momento (a parte l’area riservata al sistema operativo stesso).
Prima di vedere le varie tecniche utilizzate per gestire la memoria da parte del sistema
operativo occorre fare alcune premesse. In un programma (scritto in un linguaggio di
programmazione ad alto livello) i riferimenti e gli indirizzi di memoria vengono effettuati
tramite nomi simbolici assegnati a delle variabili. Per poter eseguire il programma occorre che
venga stabilita una corrispondenza fra lo spazio logico (nomi simbolici delle variabili) e lo
spazio fisico (indirizzi di locazioni di memoria). Questa corrispondenza può essere svolta in vari
modi. Quando il traduttore genera il codice binario del programma tradotto, gli indirizzi sono
relativi ad un indirizzo base (offset o piazzamento rispetto ad un indirizzo base). Per quanto
riguarda gli indirizzi delle istruzioni del programma l’indirizzo base è quello a partire dal quale
verrà poi caricato il programma; per quanto riguarda le variabili, l’indirizzo base è quello a
partire dal quale verrà caricato poi la parte dati del programma.
Per mandare il programma in esecuzione dovranno intervenire altri due software – linker e
loader – col compito rispettivamente di risolvere i riferimenti esterni presenti nel programma
(per esempio le chiamate alle funzioni di I/O) e di caricare il programma effettivamente in
memoria centrale. Il loader ha anche il compito di risolvere i riferimenti agli indirizzi in termini
di indirizzi fisici assoluti, operazione che prende il nome di rilocazione.
Un modo più efficiente per risolvere la corrispondenza fra spazio logico e spazio fisico della
memoria può essere risolta attraverso una rilocazione dinamica effettuata a livello hardware da
un dispositivo intelligente chiamato MMU (memory management unit) come vedremo più
avanti nella trattazione delle tecniche di gestione della memoria virtuale.
E’ importante sottolineare e rammentare che se si hanno a disposizione N bit per indirizzare
una locazione di memoria (per esempio il registro PC per indirizzare la locazione di memoria
che contiene la prossima istruzione da eseguire), lo spazio logico di indirizzamento del
processore sarà di 2N locazioni di memoria. Lo spazio fisico della memoria è invece composto
dalle locazioni fisiche realmente presenti nella memoria fisica.
Di seguito si descriveranno le tecniche di gestione della memoria che si sono susseguite a
partire dalle più antiche a quelle più recenti.
Partizionamento statico
Il partizionamento statico prevede che la memoria centrale fisica sia suddivisa in aree
(partizioni) di dimensione fissate a priori, in ognuna delle quali può essere caricato un
programma di dimensioni minore o uguale alla partizione.
Per gestire il partizionamento statico il sistema operativo necessita di una tabella delle
partizioni contenente il numero della partizione, la sua dimensione, l’indirizzo iniziale della
partizione e un bit di stato per indicare se la partizione in quel momento contiene o meno un
programma.
Ad esempio per una memoria fisica di 1024KB
numero partizione
dimensione
indirizzo iniziale
bit di stato
1
8K
312K
1
2
32K
320K
1
3
32K
352K
0
4
120K
384K
0
5
520K
504K
1
dove il valore 1 nel bit di stato indica che la partizione è occupata, 0 che è libera.
Nell’esempio precedente se c’è da caricare un programma di 30KB può essere caricato sia nella
partizione 3 che nella 4. Un programma di 50KB verrà caricato nella partizione 4, mentre un
programma di 150KB non può essere caricato finché non si libera la partizione 5. Se nella
partizione 5 è stato caricato un programma di 200KB gli altri 320KB non utilizzati dal
programma non verranno utilizzati, creando quelli che vengono chiamati frammenti, cioè aree
di memoria non utilizzate.
Partizionamento dinamico o variabile
Il partizionamento dinamico prevede che la memoria centrale fisica sia suddivisa in partizioni la
cui dimensione coincide esattamente con quella del programma che vi sarà caricato. Per
gestire il partizionamento dinamico il sistema operativo dovrà gestire due tabelle, una per le
partizioni libere, una per quelle occupate. Ad esempio
Tabelle delle partizioni occupate
numero partizione
dimensione
indirizzo iniziale
1
8K
312K
2
32K
320K
3
24K
352K
4
120K
384K
Tabelle delle partizioni libere
numero partizione
dimensione
indirizzo iniziale
1
8K
376K
2
520K
504K
Se a questo punto dovrà essere caricato un programma di 120K la partizione 2 delle partizioni
libere verrà spezzata in due, formando una partizione occupata di 120K in cui verrà caricato il
programma, e una seconda partizione libera di 400K.
Tabelle delle partizioni occupate
numero partizione
dimensione
indirizzo iniziale
1
8K
312K
2
32K
320K
3
24K
352K
4
120K
384K
5
120K
504K
Tabelle delle partizioni libere
numero partizione
dimensione
indirizzo iniziale
1
8K
376K
2
400K
624K
Questa tecnica migliora la situazione della frammentazione della memoria rispetto al
partizionamento statico, nel quale le parti non utilizzate dal programma allocato in una
partizione non erano disponibili per nessun altro programma, ma a lungo andare nella
memoria fisica si potranno venire a creare dei piccoli frammenti non sufficienti a caricare
ulteriori programmi, cioè la memoria fisica sarà piena di “buchi” non sfruttabili per ulteriori
allocazioni di programmi.
La scelta da parte del sistema operativo delle partizioni da utilizzare per caricare un
programma si basano su tre distinte tecniche:
•
•
•
first fit: si sceglie la prima partizione libera in grado di contenere il programma da caricare
best fit: si sceglie la partizione che meglio si adatta al programma da caricare. In pratica
viene scelta la partizione più piccola fra quelle in grado di contenere il programma. Più
efficiente della tecnica first fit ma alla lunga crea frammenti piccoli non più riutilizzabili
worst fit: si sceglie la partizione che peggio si adatta al programma da caricare. In pratica
viene scelta la partizione libera più grande. Questa tecnica apparentemente poco efficiente
invece permette di creare nuove partizioni libere grandi ed adatte a contenere altri
programmi.
Attività del S.O. in connessione con la gestione della memoria.
Rilocazione
• Il S.O. deve associare agli indirizzi logici di un programma gli indirizzi fisici.
Allocazione
• Il S.O. deve tener traccia di quali parti della memoria sono correntemente usate e da quali
processi.
Protezione e Condivisione
• Il S.O. deve proteggere ogni processo da interferenze (accesso da parte di un processo allo
spazio di indirizzamento di un altro processo) indesiderate da parte di altri processi e deve
permettere a diversi processi di accedere alla stessa porzione di memoria.
Gestione degli scambi
• Il S.O. deve gestire gli scambi di informazioni tra memoria centrale e disco quando la prima
non è abbastanza grande per contenere i processi (swapping).
Rilocazione
Per rilocazione si intende l’ associazione(Mapping) tra lo spazio di indirizzi logici del programma
e lo spazio di indirizzi fisici della memoria.
L’ associazione degli indirizzi nella fase di esecuzione è svolta da un dispositivo detto unità di
gestione della memoria (MMU-Memory Management Unit) attraverso il registro di
rilocazione o registro base
La possibilità di caricare un programma a partire da una posizione arbitraria della memoria
presuppone che sul codice venga effettuata una rilocazione di tutti gli indirizzi logici presenti
nel programma in indirizzi fisici.
Rilocazione effettuata mediante un registro base, registro di rilocazione, che contiene l’indirizzo
di caricamento del programma.
Quando un processo utente genera un indirizzo, si somma a tale indirizzo, chiamato OFFSET il
valore contenuto nel registro di rilocazione.
Partizionamento rilocabile
Per risolvere il problema della frammentazione creato dalla tecnica del partizionamento
dinamico, la soluzione più evidente è quella di ricompattare tutti i frammenti di memoria in
un'unica partizione, di solito in fondo alla memoria fisica. Questo è ciò che fa la tecnica del
partizionamento rilocabile.
Questa soluzione ovvia da un punto di vista concettuale comporta però problemi pratici, ad
iniziare dal cosa fare quando occorre spostare i programmi già allocati. E’ inaccettabile pensare
di far ripartire daccapo il programma stesso, sia perché si perderebbe tutto il lavoro svolto fino
a quel momento, sia perché i programmi potrebbero avere svolto delle operazioni irreversibili
(come la scrittura su un file).
Per evitare questi problemi occorre prevedere che il sistema operativo gestisca una coppia di
registri per ogni partizione: un registro base di rilocazione e un registro limite.
Quando un processo viene schedulato per l’esecuzione, il registro base viene caricato con
l’indirizzo di partenza della partizione e quello limite con la lunghezza della partizione. Ogni
indirizzo di memoria viene sommato al registro base prima di essere spedito alla memoria.
Ad esempio se prima del ricompattamento la situazione della memoria fisica era la seguente:
SO
0K
libero
100K
prog. 1
150K
libero
200K
prog. 2
220K
libero
250K
prog. 3
300K
libero
400K
500K
dopo il ricompattamento la nuova situazione sarà:
SO
0K
prog. 1
100K
prog. 2
150K
prog. 3
180K
libero
280K
500K
Nell’esempio il registro base del progr 1 ad esempio verrà cambiato e caricato con l’indirizzo di
partenza dopo il ricompattamento, cioè 100k invece che 150k, come era prima del
ricompattamento.
Il registro limite per ogni partizione ha la funzione di protezione degli accessi non consentiti in
memoria anche dopo il ricompattamento per evitare di accedere ad un indirizzo non facente
parte del programma in questione. Per sapere se l’indirizzo ricalcolato è un indirizzo consentito
per quel programma basterà che non superi la somma dei valori del registro base e del registro
limite.
Il ricompattamento può avvenire ogni qualvolta si libererà una partizione alla fine
dell’esecuzione di un processo (efficiente per avere sempre un’unica area libera ma con grandi
costi di gestione) oppure periodicamente o quando non c’è spazio sufficiente in memoria per
caricare un nuovo programma.
Memoria virtuale
Il partizionamento rilocabile supera il problema della frammentazione. Non risolve il problema
di poter caricare in memoria un programma più grande dell’area libera a disposizione o
addirittura più grande della memoria fisica stessa.
Il concetto di memoria virtuale permette di superare il limite della dimensione fisica della
memoria, mettendo a disposizione dell’elaboratore una memoria illimitata, aumentando di gran
lunga il grado di multiprogrammazione.
Il concetto di memoria virtuale si basa su due principi statistici, chiamati principi di località
spaziale e temporale.
Il principio di località spaziale afferma che quando è stato utilizzato un indirizzo di memoria è
probabile che vengano utilizzati presto quelli adiacenti ad esso (sequenzialità dei programmi e
di strutture dati quali i vettori)
Il principio di località temporale afferma che se è appena stato utilizzato un indirizzo è
probabile che possa essere riutilizzato di nuovo (istruzioni cicliche, sottoprogrammi, dati
strutturati).
Dai principi di località si deduce che non è necessario caricare tutto il programma in memoria
nello stesso istante per eseguirlo, ma che per un certo periodo si utilizzeranno le stesse
locazioni o locazioni adiacenti. Per cui ad ogni istante saranno necessarie in memoria solo
porzioni del programma.
Inoltre le porzioni di programma caricate in memoria non dovranno necessariamente caricate
in area fisiche contigue: basterà garantire la contiguità logica del programma.
Le parti non caricate dei programmi saranno comunque presenti in un’area del disco fisso
chiamata area di swap.
Memoria virtuale paginata
Nella gestione della memoria virtuale paginata la memoria fisica sarà suddivisa in blocchi
(pagine fisiche) tutte della stessa dimensione; il programma è suddiviso in pagine logiche tutte
della stessa dimensione. La dimensione delle pagine logiche e fisiche è la stessa, per cui una
pagina logica è esattamente contenuta in una pagina fisica, eliminando completamente il
problema della frammentazione. Le pagine logiche e fisiche sono normalmente di dimensioni
limitate e la dimensione delle pagine è una potenza di 2 (2K, 4K).
Di ogni programma verranno caricate in memoria solo alcune pagine, quelle che serviranno in
un determinato momento, le altre si trovano nell’area di swap. Quando si fa riferimento ad un
indirizzo logico di una pagina questo sarà composto da un numero di pagina e da uno
spiazzamento (offset). La conversione dell’indirizzo logico in indirizzo fisico è fatta dalla MMU
attraverso la tabella delle pagine, una tabella che associa ad ogni pagina logica di un processo
la corrispondente pagina fisica (se allocata in memoria). All’indirizzo della pagina fisica verrà
sommato lo spiazzamento. Esiste quindi per ogni processo una tabella delle pagine per quel
processo.
Nel caso in cui si fa riferimento ad una locazione di una pagina logica che non sia caricata in
memoria, la pagina dovrà essere copiata dall’area di swap alla memoria centrale. Se non ci
fossero pagine fisiche libere in memoria occorre scaricare una delle pagine caricate. Nel caso in
cui la pagina da scaricare fosse stata modificata (per esempio una pagina contenente l’area
dati di un processo) dovrà essere salvata nell’area di swap. Questa informazione è
memorizzata per ogni pagina in un bit della tabella delle pagine, chiamato change bit, che
indica se la pagina è stata modificata dall’ultima volta che è stata caricata in memoria. Un altro
bit, chiamato reference bit, indica se la pagina è stata utilizzata dall’ultima volta che è stata
caricata in memoria centrale e servirà per decidere l’eventuale pagina fisica da scaricare (vedi
in seguito).
L’algoritmo può essere schematizzato così:
calcolo del numero di pagina logica da utilizzare
SE la pagina logica è in memoria
ALLORA
eseguire l’istruzione
ALTRIMENTI
SE c’è una pagina fisica libera
ALLORA
carica la pagina logica nella pagina fisica libera
ALTRIMENTI
scegliere la pagina fisica da scaricare
SE change bit=1
ALLORA
salvala nell’area di swap
FINE SE
carica la pagina logica nella pagina fisica libera
FINE SE
aggiorna la tabella delle pagine
eseguire l’istruzione
FINE SE
Quando una pagina logica non si trova nella memoria centrale viene generato un page fault
che indica un fallimento nella ricerca della pagina. Se non ci sono pagine fisiche libere occorre
scegliere una pagina da scaricare. Le tecniche utilizzate dal sistema operativo per scegliere la
pagina da scaricare sono le seguenti:
o FIFO: viene eliminata dalla memoria la pagina fisica che sta da più tempo in memoria.
Tecnica semplice ma poco efficiente e genera molti page fault, perché se una pagina è
presente in memoria da molto tempo è probabile che sia una pagina usata molto
frequentemente
o LRU (last recently used): viene scaricata la pagina fisica che non è stata utilizzata da
più tempo. Occorre salvare per ogni pagina l’ultimo tempo di accesso. Tecnica molto
efficiente ma che comporta uno spreco di memoria
o NUR (not used recently): viene scaricata una delle pagine fisiche non usate di recente.
Periodicamente il reference bit viene resettato. Quando occorre scaricare una pagina si
sceglie una di quelle con il reference bit a 0, cioè una di quelle che non è stata utilizzata
dopo l’azzeramento periodico. Infatti se una pagina viene utilizzata dopo l’azzeramento
il reference bit verrà posto a 1. Un po’ meno efficiente della tecnica LRU ma che non
comporta spreco di memoria e di tempo.
In ogni caso quando occorre scaricare una pagina fisica si evita di scegliere se possibile una di
quelle che hanno il change bit a 1 (pagina modificata) perché per queste occorre salvarle
nell’area di swap, operazione questa che richiede del tempo.
Memoria virtuale segmentata
Nella gestione della memoria virtuale segmentazione la memoria fisica sarà suddivisa in
segmenti di differenti dimensioni; il programma è suddiviso in segmenti logici di differenti
dimensioni in base alla struttura logica del programma (segmento codice, segmento dati,
segmento stack ecc.) La dimensione dei segmenti logici e fisici sono del tutto indipendenti fra
loro.
Di ogni programma verranno caricate in memoria solo alcuni segmenti, quelli che serviranno in
un determinato momento, gli altri si trovano nell’area di swap. Quando si fa riferimento ad un
indirizzo logico di un segmento questo sarà composto da un indirizzo base del segmento e da
uno spiazzamento (offset). La conversione dell’indirizzo logico in indirizzo fisico è fatta dalla
MMU attraverso la tabella dei segmenti, una tabella che associa al numero del segmento logico
di un processo l’indirizzo iniziale del segmento fisico in cui è allocato (se allocato in memoria).
All’indirizzo iniziale del segmento fisico verrà sommato lo spiazzamento. Esiste quindi per ogni
processo una tabella dei segmenti per quel processo.
Nel caso in cui si fa riferimento ad una locazione di un segmento logico non sia caricata in
memoria, il segmento dovrà essere copiata dall’area di swap alla memoria centrale. Se non ci
fossero segmenti fisici liberi in memoria occorre scaricare una dei segmenti già caricati. Nel
caso in cui il segmento da scaricare fosse stata modificato (per esempio un segmento
contenente l’area dati di un processo) dovrà essere salvato nell’area di swap. Questa
informazione è memorizzata per ogni segmento in un bit della tabella dei segmenti, chiamato
change bit, che indica se il segmento è stata modificato dall’ultima volta che è stata caricato in
memoria. Un altro bit, chiamato reference bit, indica se il segmento è stata utilizzato
dall’ultima volta che è stata caricato in memoria centrale e servirà per decidere l’eventuale
segmento fisico da scaricare.
L’algoritmo è analogo a quello già descritto per la memoria virtuale paginata
Quando un segmento logico non si trova nella memoria centrale viene generato un segment
fault che indica un fallimento nella ricerca del segmento. Se non ci sono segmenti fisici liberi
occorre scegliere un segmento da scaricare. Le tecniche di rimozione dei segmenti fisici da
rimuovere sono simili a quelle utilizzati per le pagine con una ulteriore avvertenza che il
segmento da scaricare deve essere di dimensioni maggiori o uguali a quelle del segmento
logico da caricare.
Questo è uno degli svantaggi della gestione della memoria virtuale segmentata rispetto a
quella paginata. Un altro aspetto più grave della memoria virtuale segmentata è il problema
della frammentazione, eliminabile però con un ricompattamento periodico dei frammenti, a
fronte comunque di un costo di gestione che non esiste nel caso della memoria virtuale
paginata.
Un chiaro vantaggio della gestione della memoria virtuale paginata segmentata rispetto a
quella paginata è il fatto che ad ogni segmento logico si può associare un tipo (codice, dati
ecc.) e che quindi si possono definire chiaramente le operazioni che si possono effettuare su
quel segmento: lettura, scrittura per i dati, esecuzione per il codice. Inoltre il compilatore può
creare segmenti ulteriori (per ogni sottoprogramma, per ogni variabile strutturata) rendendo
più alti i controlli di accesso ad ogni parte della memoria. Nella memoria virtuale paginata non
è invece possibile effettuare alcun controllo sugli accessi perché ogni programma è suddiviso a
“fettine” della stessa dimensione, senza alcuna accortezza che si tratti di parti di codice, di dati
o altro.
Memoria virtuale segmentata-paginata
Nella gestione della memoria virtuale segmentata-paginata la memoria fisica sarà suddivisa in
blocchi (pagine fisiche) tutte della stessa dimensione; il programma è suddiviso in segmenti
logici a loro volta suddivisi in pagine logiche tutte della stessa dimensione. La dimensione delle
pagine logiche e fisiche è la stessa, per cui una pagina logica è esattamente contenuta in una
pagina fisica, eliminando completamente il problema della frammentazione presente nella
gestione della memoria virtuale segmentata. Anche il problema della gestione della memoria
virtuale paginata di non aver alcun controllo sugli accessi ad ogni pagina logica viene eliminato
perché ogni processo è suddiviso in segmento a cui è associato il tipo (codice, dati ecc.). E’
una tecnica che elimina quindi i difetti di entrambe le tecniche da cui derivano ma è più
complessa da realizzare. L’indirizzo logico è composto da tre parti: numero del segmento,
numero della pagina e spiazzamento e si dovranno gestire sia la tabella delle pagine che quella
dei segmenti.