Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
1.1.
Storia
Figura 3 Multitasking
nel registro di stato. Ovviamente se sono in modalit utente non posso scrivere nel
registro di stato, quindi non avrei modo di passare in modalit kernel. Allora posso
sfruttare il meccanismo delle interruzioni per passare in modo supervisore.
Il meccanismo di gestione delle interruzioni segue diversi punti:
Un segnale di Interrupt viene inviato alla CPU;
La CPU termina lesecuzione dellistruzione corrente, controlla il bit INT del registro
di stato e manda un ack alla periferica che lha sollevato;
Vengono salvati i registri critici necessari alla corretta ripresa dellelaborazione (PS,
SP, PC);
Tramite il vettore delle interrupt la CPU sceglie lhandler da eseguire. Le
interruzioni hanno un indice, in base al quale viene selezionata lISR che ha
lindirizzo salvato nel vettore delle interrupt. Viene inizializzato il Program Counter
con lindirizzo di partenza dellISR e il PS con il valore da caricare. Queste fasi sono
realizzate in Hardware;
Viene salvato lo stato del processore sullo stack (informazioni critiche non salvate
in hardware);
Viene gestita lInterruzione, effettuando se necessario comunicazioni con il
dispositivo che lha generata;
Una volta gestita, viene ripristinato lo stato del processore;
Viene ritornato il controllo al processo in esecuzione ripristinando i registri
PC,PS,SP salvati in hardware sullo stack.
Tutti questi passaggi servono quindi per il passaggio in modalit supervisore.
Ritornando alle periferiche di I/O, la CPU pu comunicare con essi in tre diversi modi:
I/O programmato: una tecnica ormai obsoleta, in cui la CPU deve attendere che il
dispositivo di I/O termini la sua operazione interrogando continuamente la
periferica circa la terminazione della sua attivit (polling).
I/O Interrupt Driven: dopo la richiesta di una operazione, ad esempio, di input, la
CPU sospende il processo ed eseguendone un altro permette al dispositivo di
effettuare i propri compiti. Quando questi ha finito invia una Interrupt alla CPU, che
tramita una ISR dedicata copia i dati dal buffer del controllore in memoria centrale.
DMA (Direct Access Memory): negli approcci precedenti, quando devo effettuare ad
esempio input di dimensioni pi grandi di una certa quantit posso incorrere nel
problema di andare a riservare troppo la CPU. Ad esempio supponiamo di dover
scrivere un file di 5MB; loperazione di scrittura avviene attraverso blocchi di 512
byte alla volta, il che significa che devo effettuare migliaia di operazioni di input
per caricare linput singolo. Posso allora avere delloverhead dovuto alle continue
interruzioni cui la CPU deve sopperire, infatti ad ogni singolo blocco di 512 byte la
periferica solleva uninterruzione. Per tale motivo possiamo definire lapproccio
DMA, che un meccanismo che pu essere offerto o direttamente dal controllore o
presente sulla scheda madre o sfruttando il bus di I/O. Ad esempio, supponiamo
che la CPU invia una richiesta di input al DMA. La CPU deve altres inviare lindirizzo
della periferica di I/O e la locazione di partenza della memoria da cui leggere o
scrivere e infine il numero di parole da leggere o scrivere. Quando il dispositivo
finisce la sua singola operazione invia una interruzione non alla CPU ma al DMA.
Una volta finito le singole operazioni, il DMA invia una interruzione al CPU. In questi
termini il DMA si pone quindi come filtro tra il dispositivo e la memoria. Lunico
problema si ha che quando il DMA utilizza la memoria, poich ruba il bus alla
CPU, questultima non pu accedervi.
2.1.
Memorie
2.2.
pwd , Print Work Directory, stampa a video il path della cartella di lavoro;
mkdir [nome] , crea una nuova cartella, seguito dal nome;
altri utenti; ad esempio supponiamo che un file debba avere rwx per il proprietario,
rw per il gruppo e r per altri utenti; allora rwx=111, rw=110, r=100 -> rwx=7,
rw=6, r=4:
cp , copia file;
mv , sposta il file;
esempio
cat esempio> dump ;
Ogni programma in esecuzione accede a tre file speciali: lo standard input, lo standard
output e lo standard error.
Ricordiamo che il filesystem un componente del SO che si occupa di astrarre
logicamente la gestione fisica dei file. In Linux il FS gerarchico, possiede meccanismi di
protezione di accesso, e i file e device sono indipendenti. Abbiamo infatti tre tipi di file:
ordinari, directory ossia contenitori di file che sono visti come file speciale e infine i file
speciali che sono usati per astrarre i dispositivi di I/O (/dev), o interfacciarsi col kernel. I
link sono utilizzati per far riferimento a file di altri utenti o che si trovano in directory
differenti. Possiamo distinguere gli hard link ( un nome alternativo al file) e i soft link (file
che contiene un percorso). Il FS montabile permette di agganciare il filesystem di device
esterni (floppy, HDD Ext) allalbero dei file principale. Abbiamo visto che le periferiche di
I/O sono gestite attraverso dei file speciali. Essi possono essere: file speciali a blocchi o a
caratteri (usati per dispositivi che producono e ricevono flussi di carattere, ad es.
stampanti, terminali).
4.1.
Elementi Fondamentali di un SO
Un Sistema Operativo lo possiamo vedere come una scatola nella quale possiamo entrare
principalmente, secondo due eventi: una richiesta di una supervisor call (o system call) o
per richiesta di interruzione di un processo o di un dispositivo di I/O. Una volta entrati,
viene eseguito del codice (Handler dellinterrupt o della SVC) che sposta il processo in
una coda. Esistono tre tipi di code (long-term, short-term, I/O Queues). Lo scheduler si
occupa di prelevare i processi dalla short-term e metterli in esecuzione, dando il controllo
al processo. In realt lo scheduler gestisce solo i processi della coda di mezzo; tuttavia
tutti i processi, siano essi provenienti dalla long-term, siano essi provenienti da quelle di
I/O, passeranno per la coda short-term (ad esempio, quando un I/O finisce loperazione, il
processo passa nella short-term). Per tali motivi, si dice che il sistema operativo guidato
dalle interruzioni, siano esse sincrone o asincrone e quasi tutto il codice del SO definito
nellInterrupt Handler. Sfrutta qualsiasi evento esterno (fine esecuzione di un processo,
time-sharing, presenza di task pi importanti) per intervenire e prendere decisioni: ogni
volta che viene data uninterruzione, esso ha la facolt di cambiare il processo in
esecuzione.
Abbiamo visto che un modo per entrare nel SO tramite le system call, che sono
nientaltro che servizi richiesti dal processo alla SO, come, ad esempio, lettura di un file,
scrittura a video. Hanno quindi lobiettivo di soddisfare le richieste di un utente per
laccesso ad una determinata risorsa. Possiamo avere differenti categorie di system-call:
4.2.
Durante la fase di boot, la CPU carica il programma di boot dalla ROM, ad esempio dal
BIOS (insieme minimo di procedure, tra cui un caricatore, driver di tastiera e del video,
del disco). Il boot program esamina la configurazione del calcolatore e costruisce una
struttura dati contenente la descrizione della configurazione hardware del sistema.
Dopodich carica il kernel del SO. Linizializzazione del sistema operativo avviene tramite
linizializzazione della struttura dati del kernel, dei dispositivi hardware, e la creazione di
un certo numero di processi fondamentali per lutilizzo del SO. Una volta caricati questi
processi di base, il SO pu eseguire programmi utente o unidle loop, fin quando non
viene eseguito un programma utente. Lidle loop consiste in un ciclo infinito, per sistemi
UNIX, in operazioni di background per la gestione del sistema, o, nel caso di dispositivi
portatili, il SO entra in modalit low-power, sospendendo il processore. Il SO pu allora
svegliarsi a seguito di interruzioni hardware, eccezioni generate da programmi utente o
system calls.
Interruzioni HW. I dispositivi hardware invocano il SO ad una locazione di memoria
predefinita. Il SO salva lo stato del programma utente e identifica il dispositivo che ha
generato linterrupt; serve linterrupt eseguendo lISR e recupera lo stato del programma
utente restituendone il controllo con listruzione RTI. In questo caso lutente non si
accorge di nulla.
Eccezioni. Anche in questo caso la risorsa che ha provocato una eccezione invoca il SO
ad una locazione predefinita. Il SO identifica leccezione e chiama lexception handler
(programma utente se specificato, proprio altrimenti). Viene restituito il controllo al
programma utente. In genere se non definito lhandler utente, il SO ammazza il
processo. In questo caso gli effetti delle eccezioni sono evidenti in quanto causano
unalterazione del flusso di controllo.
System Calls. Il programma utente esegue una istruzione di trap. Leffetto di tale
istruzione quello di invocare il SO operativo ad una predefinita locazione (trap handler).
Il SO identifica il servizio richiesto e lo esegue. Configura un registro atto a contenere i
risultati dellesecuzione e esegue una RTI per ritornare il controllo al programma utente.
In questo caso, allutente la system call appare come una normale chiamata a funzione.
4.3.
Architetture
5.1.
Makefile
target :dipendenze
5.2.
Librerie
collegamento a run time, se vengono collegate durante lesecuzione del programma solo
quando servono. Spesso si preferiscono le librerie dinamiche poich non appesantiscono
il software e permettono maggiori prestazioni (non bisogna ricompilare dopo eventuali
cambiamenti nella libreria).
Una libreria statica ha estensione .a. Per creare una libreria statica usiamo il comando ar
(archiver):
ar rcs libnomeLibreria.a file.o file2.o
con r che inserisce il file con rimpiazzo, c crea la libreria se non esiste e s crea un indice
oggetto-file. In questo caso il comando crea una libreria che conterr i file definiti a
fianco. Una libreria statica pu essere collegata ad altri moduli oggetto per creare un file
eseguibile; in questo modo il file eseguibile sar di dimensioni maggiori perch conterr
anche i moduli oggetto che formano la libreria. Il collegamento effettuata con la
seguente sintassi:
g++ -o eseg obj1.o objN.o L /path/ -l nomelib
Dato loverhead che produce il collegamento di una libreria statica, possiamo introdurre
una libreria dinamica, o anche shared. A differenza delle statiche, queste sono collegate
alleseguibile in fase di caricamento; in tal modo leseguibile conterr solo il codice
oggetto necessario, e se pi eseguibili lavorano sulla stessa libreria il codice della stessa
non replicato. Le librerie dinamiche hanno estensione .so e si creano:
g++ -shared Wl, -soname, libnomebil.so
-o libnomelib.so obj1.o objN.o
dove:
-
Wl, passa informazioni al linker che corrispondono al nome della libreria in questo
caso;
Shared, indica al compilatore che si vuole creare uno shared object.
Nota: affinch i file che compongono la libreria possano essere rilocati necessario
compilarli con il comando
-fpic (PIC: Position Indipendent Code).
Il collegamento viene specificato in maniera analoga al caso delle statiche, tuttavia
affinch il Sistema Operativo possa localizzare la libreria a tempo di caricamento, il suo
path deve essere aggiunto alla variabile dambiente LD_LIBRARY_PATH con il comando:
export LD_LIBRARY_PATH=$LD_LIRARY_PATH:/path
un processo pu essere interrotto per la richiesta di una SVC, che quindi pu portare il
processo in uno stato di non-esecuzione. In sostanza, allora, la life-cycle di un processo,
nella sua visione pi semplice pu illustrarsi in due soli stati: attivo e bloccato. Un
processo attivo pu sospendersi se, ad esempio, richiede una system-call, e passare nello
stato bloccato, dove in attesa che la chiamata a servizio sia soddisfatta, dopodich pu
essere riattivato e riportarsi nello stato bloccato.
[Immagine Modello a 2 Stati]
Ovviamente, tale modello potrebbe valere qualora astraessimo una CPU dedicata ad ogni
processo. Tuttavia, nella tecnologia odierna, non possiamo permetterci di avere una CPU
per ogni processo. Anzi dobbiamo massimizzare il numero di CPU associabili a pi
processi. Introduciamo, allora, un modello pi vicino alla realt, che il modello a tre
stati. In tale modello si aggiunge un nuovo stato, in cui il processo, dopo essere stato
bloccato nellattesa, ad esempio, di una operazione di I/O, risulta pronto a continuare la
sua esecuzione. Tuttavia, poich nel frattempo, lo scheduler della CPU ha selezionato un
altro processo per massimizzare il suo utilizzo, rendiamo necessaria la presenza della
transizione di assegnazione CPU; parallelamente un processo che , ad esempio, in
esecuzione da troppo tempo, pu essere temporaneamente sospeso con la transizione
prerilascio. Altro caso in cui lo scheduler pu decidere di sospendere un processo in
favore di un altro quando nella coda di processi pronti arriva uno con priorit maggiore,
pi importante. Nei sistemi batch-multiprogrammati non ha senso parlare di prerilascio
poich non definito il concetto di tempo partizionato e di priorit.
[Immagine Modello a 3 stati]
Per rendere ancora pi completo il diagramma degli stati possiamo modellare il caso in
cui di un processo terminato possano servire informazioni (terminato con errore ->
messaggio, ad esempio in Unix/Linux i processi sono visti sotto forma di gerarchia in cui
magari il padre pu richiedere perch il figlio ha terminato la sua esecuzione; dalla shell
do un comando, ritorna il controllo alla shell con messaggio di ok/errore). Introduciamo a
tal proposito lo stato Terminato. Ovviamente i processi per entrare nella coda in attesa di
essere eseguiti devono innanzitutto entrare nel ciclo. Tale transizione avviene con lo stato
Nuovo, che mediante un algoritmo di scheduling (a lungo-termine) decide se accettare o
meno il processo (caso critico: pu non accettarlo nel caso in cui non ci sia memoria per
allocare il processo, coda satura).
[Immagine Modello a 5 stati]
Infine, a questi 5 stati ne possiamo aggiungere un ultimo, che modella il caso in cui i
processi possono essere memorizzati temporaneamente su memoria di massa, ad
esempio perch il sistema molto carico (processi bloccati a lungo li posso trasferire in
memoria secondaria), permettendo ad altri processi di inserirsi in coda. Definiamo lo
stato di Swapped. Le transizione associate (Swap-in,Swap-out) sono gestite dalla parte
del SO che gestisce la memoria secondo uno scheduler detto di medio-termine.
[Immagine Modello a 6 Stati]
In sostanza, abbiamo 6 stati, di cui 5 hanno una coda al loro interno. Ogni processo pu
trovarsi in uno stato differente, e quindi in una coda differente. Il SO per effettuare le
operazioni di scheduling deve sapere dove e in che stato si trova il processo. Si introduce
quindi il PCB (Process Control Block) o descrittore del processo che un record che
6.1.
Code di processi
Quando si passa da un processo allaltro si dice che si effettua una operazione di context
switcher, ossia di cambio di contesto. Un cambiamento di contesto pu avvenire per
timeout, interruzioni I/O, eccezioni, trap, system call. Il cambiamento di contesto implica il
salvataggio (in hardware) dei registri fondamentali per il recupero del processo: Program
Counter, Stack Pointer e Status Register. Dopo leffettuazione del salvataggio dello stato
lo scheduler metter in esecuzione un altro processo ripristinando il proprio stato. In
sistemi modulari, come Linux, lo scheduler eseguito in modo kernel. Nei sistemi a
microkernel, invece, pu anche essere un processo.
6.2.
Process Structure, che contiene informazioni del processo che servono sempre;
User-Area (U-Area), che contiene informazioni necessarie al processo solo quando
esso in memoria (pu essere soggetto a swap-out)
Informazioni contenute dal process structure sono il pid, lo stato, il riferimento a aree dati
o stack, pid del processo padre, riferimento indiretto al codice (vedi dopo), , puntatore
alla U-area che contiene a sua volta la copia dei registri di CPU, informazioni sullutente.
Il codice, abbiamo detto, si definisce rientrante, in quanto pu essere usato da pi
processi. Se il collegamento tra il PCB e il codice fosse diretto, alloccorrenza di cambiare
per rilocazione posizione in memoria al codice, nascerebbe il problema di modificare al
pi n puntatori. Allora possiamo identificare una text table nella quale ogni elemento
contiene un puntatore ad una area di codice, in modo tale che, nel caso precedente
occorre solo cambiare un puntatore.
7. Scheduling
Lo scheduler (o schedulatore) una parte del SO preposta allassegnazione delle risorse
ai processi. Tale assegnazione pu essere effettuata secondo criteri, o meglio, algoritmi,
di cui ne vediamo pi avanti, detti algoritmi di scheduling. Quando abbiamo parlato del
modello a stati, con cui avevamo gestito i vari stati e le transizioni che portavano il
processo da uno stato allaltro abbiamo visto come in tutti gli stati, ad eccezione dello
stato di terminazione e di esecuzione erano presenti code. Ovviamente ciascun processo
nella coda gestito da uno schedulatore, in particolare abbiamo visto che lammissione
di un nuovo processo nello stato Ready dallo stato Nuovo era detto di lungo termine, o
anche, di job, mentre quello che permetteva il transito di processi da Ready a Esecuzione
era detto di breve termine o della CPU. Quando abbiamo poi parlato dellultimo modello
che andava ad astrarre il caso in cui bisogna effettuare lo swap di un processo bloccato in
memoria, si introdotto lo schedulatore di medio-termine. In sostanza tale situazione si
pu cos sintetizzare:
[Immagine Schedulatore]
Entrando pi nel particolare, lo schedulatore di lungo termine definisce il grado di
multiprogrammazione del sistema. La scelta del processo da mettere nello stato Ready
dipende da come si generato lalgoritmo; i pi semplici adottano una politica FIFO, altri
una basata su priorit o su tempo di esecuzione previsto, requisiti di I/O o tempo previsto
di CPU. I processi che possono essere eseguiti dal SO sono di diversi tipi che
sostanzialmente lavorano prevalentemente sulla CPU (CPU-bound) o sono interattivi (I/O
Bound). La soluzione dello scheduling di lungo termine pu essere effettuare un mix di
questi al fine di ottenere un buon compromesso tra prestazioni e efficienza. Lo scheduler
di medio termine invece lavora con la memoria gestendo le operazioni di swap-in e swapout per evitare che la RAM diventi affollata di processi bloccati che attendono ancora
levento, promuovendo un maggior numero di processi gestiti liberando memoria. Il
problema che le operazioni di swap-in e swap-out , se frequenti possono causare
overhead, e quindi rallentamenti. Infine, lo scheduler di breve termine, o anche detto
Dispatcher si occupa del coordinamento della coda dei processi pronti: esso viene
invocato quando avviene un evento che richiede lattenzione del SO, sospendendo il task:
ad esempio, una interruzione, una system-call e quindi nei cambi di contesto.
7.1.
7.2.
Algoritmi di scheduling
conto che il timer debba essere di poco superiore alla durata media di una tipica
interazione (altrimenti ogni interazione richiede due quanti di tempo). Ovviamente pi
grande il quanto di tempo tanto pi il R-R si avvicina al FCFS.
Un altro algoritmo di scheduling fornito dal SPN (Short Process Next) che nella sua
versione con preemptive si chiama SRT (Short Remaining Time). Abbiamo visto che per
sistemi operativi devo minimizzare il tempo di risposta e il turnaround. Tale algoritmo
preferisce i processi brevi nella coda, nel senso di processi che abbiano un tempo di
esecuzione minore rispetto agli altri. Introduce quindi un concetto di priorit. Per le
caratteristiche di tale algoritmo facile incorrere in starvation dei processi lunghi. La sua
versione preemptive, permette il rilascio: se ho un processo in esecuzione, lungo, posso
sospenderlo in favore di un processo pi breve alla scadenza del timeout. Per determinare
il processo pi breve, tale algoritmo si basa su stime: nei sistemi batchmultiprogrammato la stima fornita dallutente o dal SO in relazione alle esecuzioni
passate del job; nei sistemi interattivi, invece il SO pu stimare il tempo di esecuzione del
servizio
n+1
1
1
n1
S n+1 = T i = T n+
S
n i=1
n
n n
Con:
Ti
Sn
Tale stima, come si vede, ad pesa ogni burst allo stesso modo. Il problema che se la
stima non corretta, lalgoritmo va in crisi. Per risolvere questa situazione posso
considerare maggiormente i tempi di esecuzione dei burst pi recenti (media
esponenziale):
quanti di tempo.
7.3.
Scheduling in UNIX
Unix nasce come un SO per sistemi interattivi, quindi il suo scheduler dovrebbe migliorare
i tempi di risposta e turnaround. Per tale motivo adotta un algoritmo di feedback
multilivello (20 code e quindi livelli di priorit 0-19); ogni coda gestita con un algoritmo
Round Robin, e il quanto di tempo statico per ogni coda. Il calcolo delle priorit
(dinamiche) si basa sul tipo di processo e sulla storia di esecuzione:
P j =Base+
CPU j ( i1 )
+ nice
2
Con Base che indica la priorit di base e nice un valore stabilito dallutente.
invece, lutilizzo medio della CPU, calcolata esponenzialmente, del processo
nellintervallo
CPU j ( i )=
i :
U j (i) CPU j ( i1 )
+
2
2
nellintervallo
CPU j (i)
Uj
del processore
i .
nasce soprattutto nel caso in cui processi abbiano la stessa area codice e accedono a
variabili condivise. Si parla allora di Race Condition, se pi processi leggono o scrivono
dati condivisi per cui il risultato finale dipende dallordine e dalla velocit di esecuzioni
delle istruzioni del processo. In sostanza, poich c questa dipendenza non posso
prevedere loutput, che diventa non deterministico. Allora bisogna definire delle primitive
per la sincronizzazione dei processi.
Innanzitutto i processi concorrenti possono essere indipendenti, se la loro esecuzione
distinta, nel senso che luna non inficia laltra, o interagenti, se lesecuzione delluno pu
dipendere dallesecuzione dellaltro. In questultimo caso tale effetto dipende dallordine
e velocit del processo: per tale motivo non facile riprodurre il comportamento.
I meccanismi di interazione che si possono avere, sono:
Per ovviare a questi problemi bisogna definire delle regole, dei vincoli nella esecuzione
delle operazioni dei processi; ad esempio, nel caso della competizione devo supporre che
uno o un numero limitato di processi possano accedere alla risorsa (sincronizzazione
indiretta o implicita) e nel caso della cooperazione, le operazioni dei processi devono
richiedere la sincronizzazione (detta, in questo caso, diretta o esplicita). Queste
tecnologie di sincronizzazione si implementano attraverso il modello ad ambiente locale,
oppure il modello ad ambiente locale. Il primo consiste nella realizzazione di aree
condivise da pi processi in cui essi possono accedere, mentre il secondo consiste nel far
sincronizzare i processi attraverso uno scambio di messaggi (non c area di memoria
condivisa).
8.1.
Quindi, nel modello ad ambiente globale, linterazione tra i processi avviene attraverso
opportune aree di memoria comune. Il sistema visto come un insieme di processi e di
oggetti, ossia le risorse. Ad esempio, nella figura, due processi accedono alla stessa area
di memoria. Per evitare problemi legati allinterferenza devo creare un gestore che
permette ai processi di accedere a questarea come se essa fosse ad uso esclusivo
proprio.
Un gestore lo possiamo vedere come una struttura dati di gestione e delle procedure
(duso) che operano su tale struttura. Ovviamente il gestore una risorsa che condivisa
per i processi che intendono utilizzare la risorsa gestita. ad uso non esclusivo, ed
allocata staticamente dal programmatore. Il meccanismo di sincronizzazione abbiamo
detto che pu essere diretto o indiretto. Nel caso in cui il meccanismo di gestione della
risorsa implementato allinterno delle procedure, si parla di sincronizzazione indiretta.
Altrimenti, possibile implementare tale meccanismo anche allesterno (sincronizzazione
diretta).
8.2.
diventa inutile provocando solo overhead (in windows la fork e lexec sono fornite in
un'unica sys-call). Dunque possiamo utilizzare la vfork, che non effettua copia
dellimmagine del padre, o la copy-on-write, che consiste inizialmente di far condividere
al figlio la memoria del padre; al primo tentativo di modifica il kernel effettua una copia.
perch solo un processo alla volta (o un numero finito lavorando sulla variabile lock) pu
accedere alla risorsa critica, lasciando, quando finito, la stessa in condizioni da poter far
accedere altri processi (risettando il lock a 0). Tuttavia, tale istruzione pu dar anche
problemi di attesa attiva, se il codice che un processo esegue su una risorsa bloccante
per gli altri processi. Oltre al problema dellattesa attiva esiste anche il problema della
priority inversion; ossia due processi accedono alla stessa sezione critica, uno con priorit
bassa e laltro con priorit alta. Ovviamente lo scheduler appena il processo a priorit alta
pronto lo schedula. Supponiamo che il processo a bassa priorit sia nellarea critica, e H
entra nello stato pronto. H viene schedulato ma trova la risorsa occupata da L e siccome
L non verr mai schedulato perch di bassa priorit, H continuer un ciclo di TSL
allinfinito. Per risolvere tali problematiche possiamo introdurre due system call: suspend
e wakeup. La prima serve per sospendere (transita in stato bloccato) un processo che
trova il lock a 1, in modo tale da evitare situazioni di attesa attiva. Tale processo viene
quindi riattivato con wake-up.
10.1.
Semafori
Le primitive che abbiamo appena introdotto sono alla base del concetto di semaforo. Tale
entit basata sul concetto di segnale, ossia, i processi condividono una istanza di
semaforo. Per tale motivo essa deve appartenere allarea di memoria del kernel, con la Uarea di ogni processo che contiene il puntatore al descrittore del semaforo. Le primitive
alla base del semaforo sono la wait e la signal. Un processo pu inviare un segnale sul
semaforo tramite la signal, mentre un semaforo per ricevere un segnale utilizza la wait.
Se il segnale non stato ancora ricevuto, il processo si sospende. Sulle procedure di wait
e signal sono sospese le interruzioni.
Un semaforo lo possiamo definire attraverso un TDA, costituito da una variabile di tipo
intero, che rappresenta il valore del semaforo, e da una coda, per tener conto dei
processi bloccati in attesa del segnale. Su tale struttura definiamo una primitiva per
linizializzazione del value ad un valore non negativo; la wait che ha leffetto di
decrementare il valore del semaforo: se il valore diviene negativo il processo viene
bloccato; la signal ha leffetto di incrementare il valore del semaforo: se esso diventa
minore o uguale a zero viene sbloccato un processo nella coda, che si era sospeso
durante la wait. Le signal e le wait sono eseguite in maniera atomica in modo tale da
eseguire i processi in modo mutuamente esclusivo. Esiste anche una variante del
semaforo, il binario, in cui il valore pu essere 0 o 1. In genere la coda gestita dal
semaforo di tipo FIFO.
Tale primitva restituisce una chiave ottenuta combinando linode number e il minor
device number del file indicato e il carattere indicato come id. Infine la IPC key pu essere
anche assente e in questo caso la risorsa accessibile solo dal processo padre (creatore)
e dai figli.
11.1.
Primitive
La primitiva get permette di creare la risorsa condivisa; viene passato ad essa la IPC key,
parametri e restituisce il descrittore della risorsa.
La primitiva ctl permette, dato un descrittore, di effettuare modifiche sulla risorsa, quali
modifiche dello stato, verifiche dello stato e rimozione della risorsa. Ricordiamo che la
struttura condivisa permanente ossia deve essere eliminata mediante una chiamata di
sistema, altrimenti rimane l, appesa.
11.1.1.
Get
Int get(key_t key,.,int flag)
Dove:
Key lidentificatore della risorsa (IPC) che come abbiamo detto pu essere cablato
nel codice oppure autogenerato; nel caso in cui esso non presente occorre
inserire IPC_PRIVATE, che una costante per creare una nuova entry senza chiave.
Flag, indica la modalit di acquisizione della risorsa, in OR (separati da un |); in
particolare IPC_CREAT crea una nuova risorsa se non ne esiste gi una con la
chiave indicata mentre IPC_EXCL, utilizzabile anche con la IPC_CREAT, ritorna un
errore se la risorsa gi esiste, in modo tale da evitare pi inizializzazioni della
stessa risorsa. Tra i flag possiamo anche avere permessi di accesso (del tipo gi
visto: ad esempio nel modo standard, 0664, dando in questo modo permessi di rw
sia al creatore che al gruppo del creatore).
11.1.2.
Control
Int ctl(int desc,.,int cmd,)
Dove:
Ci sono delle funzionalit che possono essere anche ottenute da shell, ad esempio la
rimozione della risorsa oppure la visualizzazione di tutte le strutture allocate.
ipcs <-m|-s|-q>
ipcrm <shm|sem|msg> <IPC_ID>
11.2.
Memoria Condivisa
In generale, i processi Unix non possono condividere memoria, in quanto essi lavorano su
porzioni di memoria totalmente differenti. Infatti, anche quando si crea un nuovo
processo figlio con la fork, viene effettuata una copia intera del padre, ma, tuttavia,
modifiche da questo punto in poi si ripercuoteranno indipendentemente o sullarea di
memoria del padre o su quella del figlio a seconda di chi ha effettuato la modifica. Lunico
modo per permettere a due processi di comunicare attraverso la shared memory, che
una porzione di memoria accessibile da processi distinti. Ovviamente la memoria
condivisa non gestisce problemi di sincronizzazione e concorrenza, che devono essere
implementati a parte.
Lutilizzo della shared memory prevede dei passi fondamentali, che sono:
Un processo pu autorizzare un altro processo ad accedere alla SHM in due diversi modi:
si parla di richiesta esplicita quando il processo che vuole usare una SHM esistente ne
possiede la chiave e quindi pu fare una richiesta esplicita; tramite fork, ossia un
processo che si collega alla SHM, e dopo effettua una fork, i figli avranno
automaticamente una copia del descrittore della SHM.
Letture e scritture in memoria condivisa non necessitano di chiamate di sistema, ossia
pu essere letta o scritta come una qualsiasi variabile, ma la massima quantit di dati
che pu essere letta o scritta da un processo dipende dallarchitettura.
Header da includere: <sys/shm.h>, <sys/ipc.h>.
11.2.1.
Creazione SHM
11.2.2.
Attach SHM
void* shmat(int shmid, const void *shmaddr, int flag)
Restituisce lindirizzo del segment collegato o -1 se fallisce. Tale istruzione serve per
collegare il segmento di memoria allo spazio di indirizzamento del chiamante, ossia fare
in modo che il processo possa accedere allarea di memoria condivisa come se essa fosse
una variabile nello stack del processo.
Nella istruzione shmid lidentificatore della risorsa , ottenuta dalla shmget; shmaddr
rappresenta lindirizzo dellarea di memoria del processo chiamante al quale si vuole
collegare il segmento di memoria condivisa (0, lo fa in automatico); flag, rappresentano le
opzioni, ad esempio IPC_RDONLY serve per collegare la risorsa in sola lettura.
11.2.3.
Controllo SHM
int shmctl(int ds_shm, int cmd, struct shmid_ds * buff)
invoca un comando sulla SHM; in particolare il campo ds_shm il descrittore della SHM su
cui si vuole operare, cmd il commando da eseguire: IPC_STAT, IPC_SET, IPC_RMID
(rimuove solo quando non vi sono pi processi attaccati) SHM_LOCK, impedisce che ili
segmento venga swappato o paginato e infine buff un puntatore aduna struttura dati
shmid_ds, del tipo:
struct shmid_ds {
struct ipc_perm shm_perm;/* permessi */
size_t shm_segsz; /* dim del segm. in bytes */
__time_t shm_atime; /* tempo ultimo shmat() */
__time_t shm_dtime; /* tempo ultimo shmdt() */
__time_t shm_ctime; /* tempo ultimo shmctl() */
__pid_t shm_cpid; /* pid del creatore */
__pid_t shm_lpid; /* pid dellultimo operatore */
shmatt_t shm_nattch; /* # di attach. correnti */ };
11.3.
Ripetendo i passi definiti per la shared memory vediamo come si crea un semaforo:
int semget(key_t key, int nsems, int semflg);
dove, la key la IPC della risorsa, nsems il numero di semafori e semflg pu essere
riempito con gli stessi flag della get che abbiamo visto prima (IPC_CREAT, IPC_EXCL |
permessi). Dopodich il semaforo va inizializzato con la:
int semctl(int semid, int semnum, int cmd, )
dove il semid, rappresenta lID del semaforo fornito dalla get, semnum indica il numero
del semaforo su cui agire e cmd il comando (ad esempio: semctl(sem,0,SETVAL,val1)).
In realt abbiamo visto che una get pu servire per creare un array di semafori. Ogni
semaforo dellarray una struttura dati che comprende i seguenti campi, sui quali ci si
agisce tramite la semop.
unsigned short semval; /* valore semaforo*/
unsigned short semzcnt; /* # proc che aspettano 0 */
unsigned short semncnt; /* # proc che aspettano incremento*/
pid_t sempid; /* # proc dellultima operazione*/
11.3.1.
SemOp
int semop(int semid, struct sembuf *sops, unsigned nsops)
dove semid rappresenta lId del semaforo ottenuto tramite get, sops un puntatore ad
una struttura sembuf che specifica, tramite lnsops, loperazione da compiere sul
semaforo.
struct sembuf {
unsigned short sem_num; /* numero di semaforo */
short sem_op; /* operazione da compiere */
short sem_flg; /* flags */ }
I valori che pu assumere sem_flg sono IPC_NOWAIT e SEM_UNDO, che specifica che
loperazione sar annullata appena il processo che la ha eseguita termina. Ovviamente
affinch la semop non ritorni immediatamente importante che le operazioni specificate
da sops siano eseguite in maniera atomica (simultaneamente). Ogni operazione
eseguita sul semaforo individuato da sem_num. I valori che pu assumere sem _op sono
tre:
Minore di 0, wait;
Uguale a 0, wait for zero;
Maggiore di 0, signal;
Quindi, se Il sem_op positivo, allora si tratta di una signal; loperazione in questo caso
consiste nelladdizionare il valore di sem_op a semval. Nel caso in cui sia specificato il
flag SEM_UNDO, il sistema aggiorner il contatore undo count ( semadj ) del processo
per il semaforo in questione.
---- da continuare---
12. Threads
Abbiamo visto che un processo un programma in esecuzione dove per programma si
intende la descrizione statica di un algoritmo. Lastrazione del processo permette di
incapsulare due concetti, che sono quello di esecuzione, ossia ogni processo ha un flusso
di controllo, cio esegue delle istruzioni, e evolve attraverso degli stati (blocked, sleeped,
running), e quello di possesso di risorse (o protezione) in quanto ad ogni processo
associata unarea di memoria dati (stack, heap) e un area testo (spesso caratterizzata da
codice rientrante e quindi usufruita da diversi processi).
Abbiamo anche visto che la sincronizzazione tra i processi pu avvenire secondo un
modello ad ambiente globale e secondo un modello ad ambiente locale. Nel primo i
processi si sincronizzano attraverso unarea di memoria condivisa: i processi hanno
memoria in comune a cui accedono (secondo il principio di mutua esclusione) secondo
determinate regole, stabilite attraverso ad esempio, luso dei semafori. Tale approccio
diventa difficoltoso da utilizzare quando ho unapplicazione che magari strutturata
come un insieme di attivit che evolvono contemporaneamente (ad esempio, se
considero un editor di testo con correzione automatica) luso di unarea di memoria
condivisa tra pi processi pu essere oneroso. In pi si aggiunge il fatto che la creazione
di un nuovo processo, attraverso la fork, genera una copia del processo padre: tale
operazione pu provocare overhead. Per tali motivi, se devo prevedere che due processi
utilizzino gli stessi dati, possiamo far in modo che allinterno dello stesso processo possa
avere pi flussi di esecuzione. Da questultima assunzione nasce il concetto di thread. Il
concetto chiave del thread la separazione tra esecuzione e possesso di risorse. In
sostanza con il thread, nello stesso processo posso avere pi flussi di esecuzione, che
condividono uno stesso spazio di risorse. Per tale motivo, il thread anche detto processo
leggero. Il processo vero e proprio prende il nome di processo pesante in quanto
incorpora lo spazio di indirizzamento e le risorse condivise tra i threads. Il thread, quindi,
lo definiamo come un flusso di controllo sequenziale allinterno di un processo. Attraverso
luso dei threads posso quindi separare in una applicazione le attivit di background e le
attivit di foreground (come avviene ad esempio in un IDE; compilazione (bg) e a video
posso continuare linserimento di caratteri). Lutilizzo dei thread si pu estendere anche
quando dobbiamo implementare funzioni asincrone, cio che si attivano in base a segnali
dellutente: invece di bloccare lintero processo, sospendo solo il thread dedicato. Quindi
luso di thread ha vantaggi nella modularizzazione e nella velocit di esecuzione.
Un thread quindi ha un suo insieme di registri un suo stack ma non unarea dati e heap.
Quindi se dichiariamo una nuova variabile nel processo essa sar visibile a tutti i threads
del processo. I vantaggi fondamentali del thread sono soprattutto nella sua creazione
(meno onerosa rispetto al processo, non necessita di copia dellintero processo); la
comunicazione tra threads pi semplice perch non necessita di area condivisa, e
quindi non passa per il kernel; il context switching pi semplice perch devo salvare
meno informazioni.
Poich i threads condividono lo spazio di indirizzamento del processo necessario che
esistano criteri di sincronizzazione tra processi, che in tal caso esplicita (se un processo
desidera conoscere il valore di una variabile condivisa deve provvedere a sincronizzarsi).
Quando si fa riferimento a thread in generale si parla anche di multithreading, ossia la
capacit di un sistema operativo di consentire lesecuzione di pi thread allinterno di un
processo. Tale approccio il risultato dellevoluzione a partire da approcci single-thread:
MS-DOS ad esempio consentiva lesecuzione di un singolo processo, costituito da un
unico filo di esecuzione; Unix, in principio era multiprocesses, ma ogni processo
ammetteva solo un thread; si passato poi alla JVM, caratterizzata da un solo processo
allinterno del quale pi threads fino ad arrivare ai SO moderni multithread e
multiprocess.
Un thread abbiamo detto ha quindi un proprio stack. Essenzialmente anche i thread
hanno un proprio PCB (descrittore) ma pi piccolo rispetto a quello del processo. Lunico
problema che offrono i thread il fatto che se uno fallisce comporta il fallimento di tutto il
processo. Analogamente ai processi, anche il thread ha il concetto di stato: [start>Ready->blocked->Running->Done]. In base a come vengono implementati i thread si
pu parlare di user level (ULT) o kernel level (KLT). I primi sono schedulati direttamente in
user space, attraverso una threads library: la gestione del thread avviene a livello
applicativo. Il vantaggio di implementare i thread a livello utente sono il context switch
(non c passaggio per il kernel mode) il che si traduce in minore overhead. Lo scheduler
dei thread indipendente da quello dei processi e quindi ogni applicazione pu far uso di
uno scheduler personalizzato per i suoi thread. Infine, le applicazioni sono portabili in
quanto non dipendono dal SO (kernel). Gli svantaggi sono che se un thread invoca una
syscall bloccante, tutti i thread del processo si bloccano, e inoltre il vantaggio di flussi in
uno stesso processo si perde nei multiprocessori in quanto non si possono associare
diversi thread a differenti core (il processo schedulato, non il thread). Per ovviare a tali
problemi si possono introdurre i thread kernel level. In questo caso il kernel che gestisce
i thread. La gestione dei thread non fatta, quindi, a livello applicativo, ma stesso il
kernel che schedula i thread direttamente. Il vantaggio di questo approccio che se un
thread si blocca esso non condiziona gli altri thread. In sostanza i vantaggi di prima si
traducono in svantaggi e viceversa. Gli svantaggi principali si hanno in termini di context
switch (pi overhead) e in termini di sincronizzazione (con meccanismi che richiedono
lintervento del kernel). Esistono anche approcci ibridi, che possono associare ad un
thread kernel level, pi threads user level o il contrario. Pi threads a livello utente
corrispondono ad un numero inferiore o uguale al numero di thread a livello del nucleo. La
gestione in questo caso avviene a livello applicazione (vantaggi in termini di
sincronizzazione e overhead).
In Linux non si fa distinzione tra processo e threads. Per task sintende ununit
schedulabile. Se essa condivide memoria allora un threads, altrimenti un processo. Per
creare un thread si usa la primitiva clone() che non crea una copia del processo ma un
processo distinto che contiene la stessa area di indirizzi. La fork implementata
attraverso la clone.
12.1.
Parallelismo e concorrenza
S=Ts/ Tp
Con
Ts
Tp
tempo di esecuzione
parallelo o concorrente.
Ovviamente lutilizzo indiscriminato di thread da valutare perch implica notevole
context switch e quindi aumento di overhead. Inoltre il tempo di esecuzione concorrente
relativo anche alla presenza di punti di sincronizzazione tra i thread (accesso in mutua
esclusione a risorse condivise).
Possiamo parlare sia di parallelismo implicito che di parallelismo esplicito. Nel primo caso,
le architetture multiprocessore sfruttano il parallelismo del codice e i tempi morti del
processore, mentre nel secondo caso, lutente programmatore, che sa dellesistenza di
unarchitettura parallela e diventa suo il compito di trovare un algoritmo per la soluzione
del problema. La maggior parte dei programmi ha funzioni che possono essere
parallelizzate, e quindi, nelle architetture multi-core, ci pu essere un vantaggio in
quanto possiamo portare avanti in parallelo pi porzioni di codice, migliorando i tempi di
risposta. Tuttavia, difficile che le parti parallelizzabili di un programma siano
indipendenti; per tale motivo tali potrebbero richiedere sincronizzazione. Allora il
vantaggio pu essere s consistente ma non quanto aspettato: il tempo di risposta
dipender dal processo pi lento. Lo speed-up ottenibile in un programma lo si pu
calcolare con la Legge di Amdahl:
consideriamo un programma
su un processore; sia
la
S=
fT + ( 1f ) T
n
=
( 1f ) T 1+ ( n1 ) f
fT +
n
Dove
sugli
core del processore. Notiamo quindi che lunico fattore che influenza lo speed-
f =0,
allora
coincide con
n (speed-up massimo).
13.1.
Lassegnazione dei processi ai processori pu essere di tipo statico, ove ogni processo
associato permanentemente ad un processore, e in questo caso ogni processore ha la sua
coda dei processi pronti. Tale approccio prevede overhead minimo in quanto
lassegnazione effettuata una volta sola. Il problema da evidenziare che possiamo
avere core pi carichi di altri. A tal proposito sono stati individuate modalit dinamiche. Il
principio base dellassegnazione dinamica che durante la sua vita, il processo pu
passare da una CPU ad un'altra. Tale soluzione applicata per bilanciare il carico; a tal
proposito possiamo individuare di modalit di implementazione: load-sharing, se ho una
sola coda di processi pronti (condivisa tra i processori); dynamic load balancing, se ogni
processore ha una sua coda, ma il processo pu passare da una coda allaltra.
Se utilizziamo unapproccio Master/Slave, il codice kernel pu eseguire solo sul master
mentre i processi user vengono eseguiti dagli slave; in tal proposito, i processori slave
devono inoltrare le syscall al master. Il vantaggio che di facile implementazione,
mentre lo svantaggio consiste nel single point of failure del master che costituisce il collo
di bottiglia per le prestazioni. Se, invece, utilizziamo un approccio peer, ogni processore
gestisce autonomamente lo scheduling e il kernel esegue su tutti i processori. Tale
approccio di pi difficile implementazione in quanto bisogna gestire la sincronizzazione
allaccesso della coda dei processi pronti per permettere il balancing. Mentre abbiamo
visto come nei single core la multiprogrammazione pu essere vantaggiosa in termini di
prestazioni, nel caso di sistemi SMP, non sempre vero: infatti se il livello di granularit
di sincronizzazione indipendent o coarse, pu essere opportuno che i threads eseguano
in concorrenza; se il livello di granularit medium pu, invece convenire che thread
della stessa applicazione siano eseguiti in parallelo in modo tale da ottimizzare il tempo
di risposta (se venissero bloccati in favore di un processo prioritario, potremmo ottenere il
caso in cui un thread di unapplicazione deve attendere questaltro thread finch non
viene rischedulato -> tempo di risposta aumenta). Ad esempio, supponiamo di dover
visualizzare una immagine. Posso pensare di dividerla in quattro pezzi e dare
lelaborazione di ciascun pezzo ad un thread differente. Se considero unarchitettura
multicore con multiprogrammazione devo aspettare che lultimo di essi venga schedulato
come thread nel singolo processore. Se non uso la multiprogrammazione, posso eseguire
contemporaneamente i quattro thread ottenendo pi velocit. In sostanza, in
questultimo caso ho un miglioramento di prestazioni anche perch ho meno context
switch.
Per quanto riguarda il dispatching (algoritmo di scheduling della coda), nei sistemi
multiprocessore la scelta dellalgoritmo non influenza fortemente le prestazioni (intesa
come throughput). Diciamo che, per quanto riguarda i thread, se essi hanno granularit di
sincronizzazione bassa (molto codice parallelizzabile), luso di una architettura multicore
sicuramente un vantaggio in termini di speed-up. Per il dispatching dei thread possiamo
usare due approcci:
-
Load Sharing: approccio su cui basato il MacOS. In questo caso i thread sono
memorizzati su una singola coda condivisa da tutti i processori su cui agisce un
algoritmo che pu essere FCFS,Smallest Number of thread first (schedulo i thread
dei processi che hanno pochi thread non schedulati) e la sua versione preemptive
ove si d priorit ai thread che sono schedulati poco. Ad esempio se vedo che un
thread da tempo nello stato bloccato, gli aumento la priorit, cos quando ritorna
nello stato pronto pu subito eseguire. In questapproccio ho il vantaggio di avere
una singola coda, il carico distribuito equamente su tutti i processori e non
necessario un controllo centralizzato dello scheduling. Tuttavia, il fatto di avere una
coda condivisa pesa nelle operazioni di sincronizzazione per il prelievo del thread,
definendo una perdita di prestazioni. Il secondo problema che, poich la coda dei
processi pronti condivisa, non detto che un thread che ha eseguito su un core
esegua di nuovo sullo stesso core. In questo caso invalido la cache e la devo,
quindi, ricreare in ogni core ove eseguo il thread. Un altro problema sta nel fatto
che i thread sono trattati allo stesso modo: se ho thread di un processo che
possono essere parallelizzati perdo questo vantaggio, in quanto poco probabile
che essi siano schedulati contemporaneamente su core differenti.
Gang Scheduling: uno scheduling di gruppo che si applica a thread che sono
fortemente correlati tra di loro; lidea che se faccio eseguire threads correlati su
core differenti, riduco i blocchi dovuti alla sincronizzazione (effettuo meno context
switch) e in pi, poich schedulo il gruppo ho meno overhead. Questapproccio
quindi efficiente per applicazioni con granularit di sincronizzazione fine o medium.
Lassegnazione dei processi ai processori pu essere uniforme: consideriamo N
processori e M processi in cui ogni processo ha un numero di threads minore del
numero di processori o al pi uguale; ad ogni processo viene associato un tempo
sui processori pari a 1/M. Tale approccio per inefficiente in quanto supponiamo
di avere due processi uno con un thread e laltro con quattro thread su una
architettura multi core a 4. Ad ogni processo associamo del tempo sul
processore multicore, il che significa che per il primo processo ho tutti i core
occupati, mentre per il secondo ho solo un core occupati e gli altri che aspettano.
Per tale motivo ho un notevole spreco prestazionale. Per ovviare a ci possiamo
introdurre lassegnazione pesata al numero di thread del gruppo.
13.2.
Scheduling in Linux
Il kernel di Linux non distingue processi e threads ma li racchiude in un unico termine che
sono i task. Un task una qualunque unit schedulabile. A partire dal task se esso
condivide memoria con altri task si parla di thread altrimenti un processo. I processo
balancing produce overhead. In linux infatti ogni processore pu eseguire qualunque task.
Se un processore ha una runqueue relativamente vuota si attiva il bilanciamento: per
effettuare ci bisogna estrarre i task non cache-hot (cio quelli che non sono legati
fortemente a quella CPU, ossia per i quali non oneroso invalidare la cache) dalle code
degli altri processori. Questa operazione onerosa perch bisogna accedere ad una coda
di unaltra CPU il ch richiede anche opportuni meccanismi di sincronizzazione.
Dal kernel 2.6.23 Linux ha introdotto lo scheduler CFS (Completely Fair Scheduling). Come
dice il nome questo algoritmo fariness, cio assegna in modo equo la CPU ai threads,
Abbiamo visto che lO(1) aumentava la priorit se il time-slice era maggiore. In questo
caso ci non vero. Il principio di base su cui poggia il fatto che se ho un time-slice, e
per, magari, unoperazione di I/O mi blocco prima della fine del time slice, questo lo
recupero. In questo modo ogni thread ha lo stesso tempo (almeno inizialmente) sulla
CPU.
Lidea alla base dellimplementazione quella di immaginare di assegnare agli n processi
correntemente in esecuzione su una CPU, esattamente 1/n-esimo della potenza di calcolo
complessiva, volendo minimizzare il waiting time (il tempo che un processo rimane in
attesa della CPU) e renderlo uniforme il pi possibile. Viene utilizzato un clock virtuale
che scorre ad una certa velocit e che rappresenta una frazione del clock reale, la quale
viene calcolata dividendo il wall time in nanosecondi (tempo che aspetta processo per
arrivare nello stato Running) per il numero totale dei processi in stato ready. In questo
modo viene tenuta traccia di quanto i processi pronti ad eseguire sono in debito di CPU.
Quando vengono eseguiti il loro debito decresce.
Lo scheduler sceglie il processo con il debito pi alto: un processo esegue sulla CPU
finch il valore a lui associato pi alto di tutti quelli dei processi nello stato ready.
Inoltre, per non penalizzare eccessivamente i processi nello stato wait, il CFS incrementa
anche il loro debito (cos come succede nellalgoritmo O(1), quando viene aumentata la
priorit di un processo che attende da tempo).
Unaltra novit del CFS consiste nellutilizzo di un albero Red Black per gestire le varie
runqueue (le quali sono associate a diversi livelli di priorit): esso un albero
autobilanciante, il quale mantiene un costo logaritmico nelle operazioni di ricerca,
inserimento e cancellazione dei nodi.
Il peso dei nodi la differenza tra il clock virtuale e il waiting time del processo:
utilizzando un Red Black tree lalgoritmo sicuro che il nodo foglia a sinistra quello che
ha accumulato pi debito e sar il prossimo a passare nello stato Running.
Le priorit dei processi sono gestite in termini di rapidit con la quale il processo riduce il
proprio debito quando sta eseguendo sulla CPU. Un processo a bassa priorit lo ridurr in
maniera pi rapida.
Unaltra facility introdotta nel CFS il group scheduling: questo permette allalgoritmo
fair del CFS di operare in modo gerarchico: I processi vengono divisi in gruppi, e
allinterno di ogni gruppo i processi vengono schedulati in modo fair. A pi alto livello, ad
ogni gruppo assegnata una condivisione fair del processore. Una feature del group
scheduling il per-user scheduling, il quale consiste nel creare un gruppo separato per
ogni utente in modo che poi essi possano condividere la risorsa processore.
14. Monitor
Il monitor una soluzione alla sincronizzazione dei processi, che in un certo senso va a
semplificare il costrutto dei semafori. Il problema dei semafori evidente nel caso in cui
al posto dei processi che lavorano come consumatore/produttore consideriamo dei
threads. Infatti, se ho un thread consumatore e un thread dello stesso processo
produttore, il blocco delluno blocca anche laltro e, dunque, si va in una situazione di
deadlock. Il monitor un costrutto sintattico introdotto per facilitare la programmazione
concorrente fornendo meccanismi di mutua esclusione e sincronizzazione dei processi.
Come costrutto somiglia ad una classe, le cui variabili membro definiscono lo stato del
monitor e le cui funzioni membro implementano il gestore di esso. Quindi il costrutto
monitor utilizzato per definire tipi di risorse condivise. Sostanzialmente un tipo di dato
astratto. Lassegnazione di una risorsa condivisa tra processi concorrenti segue un
principio di separazione tra meccanismi e politiche: un solo processo alla volta pu
accedere alle variabili locali del monitor e il programmatore deve definire lo scheduling di
accesso, per i processi, alla risorsa. In tale ambito possiamo ricordare lanalogia della
stanza del dottore: supponiamo che il dottore abbia una sala dattesa, una sala di
consulto. I thread inizialmente si trovano in coda in attesa di essere schedulati e poter
entrare per il consulto. In mutua esclusione entrano quindi nella stanza (accedono alla
risorsa). Quando un thread allinterno del monitor, possono scattare delle condizioni
logiche per cui esso deve lasciare la struttura. Se tale condizione si verifica, allora al
thread imposto di attendere in una sala dattesa, in attesa che la condizione ridiventi
vera. Dopodich pu rientrare (a seconda dellimplementazione del monitor si stabilisce
se entra prima o in coda con gli altri in attesa). La sospensione del processo avviene
attraverso una variabile di condizione var_cond x => x.wait (che blocca il task, ci sar un
altro task che lo sbloccher con x.signal). Abbiamo detto che laccesso al monitor in
mutua esclusione; consideriamo che un task in un monitor si blocca, entra un altro task e
lo sblocca. Cosa succede? O blocco ancora il processo gi bloccato o blocco il processo in
esecuzione sul monitor. Diciamo che se il segnalante viene bloccato si parla di monitor
signal&wait; altrimenti se segnala e continua si parla di monitor signal&continue. Nel
primo caso il segnalante viene rimandato nella sala dattesa mentre il segnalato riprende
subito lesecuzione (tale configurazione avvantaggia, quindi, il segnalato e svantaggia
fortemente il segnalante, che deve ricompetere con gli altri processi per laccesso al
monitor). Per evitare che il segnalante sia troppo danneggiato dalla signal&wait si pu
introdurre la soluzione di Hoare (signal & urgent wait) per cui il segnalante non ritorna
nella sala dattesa ma viene inserito in una coda degli urgenti. In questo caso succede
che quando un segnalante sblocca un processo esso viene posto in urgent queue.
Quando poi il processo segnalato lascia il monitor o si blocca per unaltra condizione,
allora si va a guardare prima la urgent queue e poi la sala dattesa. Quindi nella signal &
wait posso immaginare una situazione del genere:
if(!B)
{
cv.wait_cond();}
Quindi, in questo caso se non verificata la condizione, mi blocco. Poich una
signal&wait allora mi basta un controllo sull if in quanto quando un processo mi sblocca,
sar messo in esecuzione, e dunque, non ho necessit di ricontrollare la condizione (
stata appena segnalata).
Diverso, invece, il caso del signal & continue, in cui un processo che si bloccato per una
condizione, magari, sbloccato da un processo, che effettua la signal su quella condition
variable, ma continua la sua esecuzione. In questo caso quando il processo avr finito o si
sar bloccato, quello segnalato sar stato sbloccato nella waiting room, il che significa
che dovr di nuovo competere con altri processi, e pertanto la condizione pu essere
anche, nel frattempo, da un altro processo, cambiata (in realt rimane nella sua sala
dattesa, tuttavia, dovr competere con i thread in attesa anche dallaltro lato). Ci
significa che esso deve ricontrollare la condizione quando schedulato nel monitor:
while(!B)
{
cv.wait_cond();}
Abbiamo infine, anche la signal_all che sblocca tutti i processi in attesa.
Come si realizza un monitor?
Per realizzare un monitor dobbiamo tener conto di due problematiche: la prima riguarda
la mutua esclusione nellutilizzo del monitor, mentre la seconda riguarda le politiche di
sospensione e riattivazione di un processo in base al valore di una variabile di condizione.
Innanzitutto per la mutua esclusione posso definire un semaforo MUTEX in modo tale da
regolare laccesso alla risorsa monitor, inizializzandolo a 1 se supponiamo che allinizio il
monitor libero. Quindi un processo che vuole utilizzare un monitor effettua la wait sul
MUTEX. Quando un processo esce dal monitor libera con una signal. Per la variabile di
condizione utilizziamo invece un semaforo condsem. Nel caso della soluzione di Hoare
dovremmo prevedere anche un semaforo su urgent in modo tale da permettere, se
esistono processi urgenti la loro schedulazione prima di altri in attesa.
Abbiamo detto che una send pu essere bloccante o meno. In realt si parla,
rispettivamente, di send sincrona e asincrona. La prima deve aspettare che il messaggio
sia ricevuto, pertanto blocca il processo inviante. La seconda invece non aspetta la
ricezione del messaggio, ed quindi non bloccante per il processo inviante.
Analogamente la receive pu essere bloccante, se il processo che vuole ricevere il
messaggio rimane in attesa bloccato finch non arriva, o non bloccante se continua
lesecuzione senza attendere il messaggio. Rispetto a queste quattro tipologie di primitive
le combinazioni tipiche sono tre:
-
Send Sincrona, Receive Bloccante, per permettere una stretta sincronizzazione tra i
processi in quanto fa rimanere in attesa sia il sender che il receiver: si parla di
rendezvous (punto dincontro), il primo che arriva aspetta;
Send Asincrona, Receive Bloccante, tipica delle architetture client/server in cui il
server rimane in attesa di servire una richiesta; il problema nel fatto che se un
sender invia un messaggio e esso si perde, possiamo avere situazioni di blocco per
il receiver.
Send Asincrona, Receive Non Bloccante, se vogliamo che nessuno dei due aspetti
laltro.
16. Deadlock
Il deadlock (o stallo) una condizione che indica un blocco permanente che si pu
verificare quando esiste conflitto nellaccesso ad una risorsa. Il problema principale del
deadlock che la sua soluzione non generale, nel senso dellefficienza. Quando si ha lo
stallo allora c competizione nellutilizzo di risorse tra pi processi. Ad esempio,
supponiamo due processi: ambo hanno bisogno di accedere a due risorse che sono le
stesse per entrambe. Supponiamo che il primo processo abbia bisogno di una risorsa R e
Se a queste condizioni aggiungiamo una quarta, che deriva, sostanzialmente dalle tre
precedenti, nel caso in cui ogni risorsa riusabile abbia una sola istanza, tali condizioni
diventano sufficienti al verificarsi del deadlock:
-
Poich il Sistema operativo non gestisce i deadlock, compito del programmatore far in
modo che il sistema non entri mai in deadlock. Le strategie di cui pu usufruire sono la
prevention e la avoidance, oppure permettere che il sistema entri in uno stato di
deadlock per poi risolvere il problema (ripristinando il sistema) oppure agire come i SO
che se ne fregano.
16.1.
Prevenzione
La prevention consiste nel rimuovere nel sistema ogni possibilit di avere deadlock. una
soluzione statica. Lo svantaggio principale consiste nel minore utilizzo di risorse in quanto
si fa in modo che sia approcciata una sequenzialit nel codice. Ovviamente una tecnica
inefficiente in quanto blocca i processi anche quando non necessario e quindi determina
cali prestazioniali (speed-up condannato dalla maggiore sequenzialit). La prevenzione
(tipica di sistemi critici) pu essere:
-
Indiretta: bisogna far in modo che una delle prime tre condizioni necessarie non si
verifichi. Ad esempio, per il possesso attesa di pu imporre che un processo
richieda allinizio tutte le risorse che gli serviranno durante la sua esecuzione. Se
tutte le risorse sono disponibili esso viene eseguito altrimenti viene bloccato.
Lacquisizione delle risorse, in questo caso, avviene contemporaneamente. Oppure
si pu far in modo che un processo che abbia una risorsa non possa chiederne
un'altra senza rilasciare quella posseduta. Tale soluzione presenta numerosi
svantaggi: innanzitutto limita la concorrenza e in secondo luogo obbliga il
programmatore a conoscere quali sono le risorse che utilizzer in contrasto con i
principi di sviluppo software. Inoltre assume che il processo (nella prima soluzione)
abbia occupato per tutto il tempo le risorse, ma non cos (inefficienza). Per
limpossibilit di prerilascio, invece, posso, far in modo che un processo che
possiede una o pi risorse, nel caso richieda una risorsa che al momento non sia
disponibile, rilasci tutte le risorse e venga rieseguito (roll-out) solo quando saranno
tutte disponibili.
Diretta: che mira ad evitare situazioni di attesa circolare, imponendo che le risorse
vengano acquisite dai processi seguendo un ordine prefissato. In particolare le
risorse vengono organizzate in gerarchie di livelli. Se un processo in possesso di
una risorsa di 2 livello potr richiedere solo risorse dei livelli successivi.
16.2.
Avoidance
Anche detta prevenzione dinamica, in questo caso il processo viene bloccato solo se sta
evolvendo verso uno stato critico. Il sistema decide dinamicamente se lallocazione di una
risorsa pu generare deadlock. Tale tecniche richiede che si conoscano le richieste che un
processo possa fare durante la sua vita. La soluzione consiste nel far dichiarare ad ogni
processo quante e quali sono le risorse di cui abbia bisogno. Lalgoritmo di avoidance
consiste nellandare a verificare dinamicamente lo stato di allocazione delle risorse per
prevenire deadlock. Lo stato di allocazione di una risorsa definito dal numero di istanze
disponibili e allocate e dal numero di richieste massimo del processo per quella risorsa.
Per applicare lavoidance possiamo operare secondo due approcci: il primo (Process Initial
Denial) prevede di non iniziare un processo se le sue richieste possano portare ad un
deadlock; il secondo(Resource Allocation Denial) prevede di non accettare richieste di una
o pi risorse ad un processo se tali possano portare ad una situazione critica. Entrambi
questi approcci si basano sulle seguenti strutture:
detto n il numero di processi e m il numero del tipo di risorsa, definiamo il vettore R
(Resource) delle risorse totali del sistema ove il valore di R il numero di istanze
disponibili. Definiamo il vettore V (Avaiable) che tiene conto per le risorse R le istanze non
allocate. Definiamo la matrice delle richieste C (nxm) dove ogni elemento rappresenta la
richiesta del processo riga alla m-esima risorsa (il numero indica la quantit di istanze).
Infine, definiamo la matrice di Allocation A (nxm) in cui ogni elemento definisce
lallocazione della risorsa m-esima al processo n-esimo. La matrice C (claim) contiene il
numero massimo di richieste di risorse (tipo e quantit) che ciascun processo possa fare.
Tale deve essere fornita prima dellesecuzione. Allora sotto tali ipotesi, un processo viene
eseguito se il numero massimo di richieste di tutti i processi pi quelle del nuovo possano
essere soddisfatte:
n
Il resource allocation denial, riferito anche come algoritmo del banchiere, si basa
sullassicurare che il sistema (processi + risorse) si trovi in uno stato sicuro SEMPRE.
Quindi, se il sistema vede, che, allocando una risorsa ad un processo, il sistema transiti in
uno stato NON SICURO, allora non permette lallocazione. Definiamo stato sicuro uno
stato del sistema in cui c almeno una sequenza sicura di esecuzione di tutti i processi.
Una sequenza (P1,.Pn) di processi sicura se per ogni processo Pi le risorse che Pi pu
ancora richiedere possono essere soddisfatte dalle risorse disponibili correntemente e
quelle possedute da tutti i processi Pj con j<i. Quindi quando un processo richiede un
insieme di risorse, il sistema fa finta di allocarle e verifica, con lalgoritmo del banchiere
se il nuovo stato raggiunto sicuro. Se si, allora effettua lallocazione se no il processo
viene sospeso (Ricorda esempio Slide vedi Stallings). Come faccio a capire se uno stato
sicuro? Bisogna capire se ognuno dei quattro processi sia in grado di terminare la
propria esecuzione con le risorse disponibili, ossia per ogni processo dobbiamo verificare
che
Cij A ij V j
per ogni
j .
----Algoritmo del banchiere---I problemi del deadlock avoidance sono che ogni processo deve dichiarare
preventivamente il numero di risorse di cui ha bisogno. I processi che vengono analizzati
devono, inoltre, essere indipendenti e ci deve essere un numero predeterminato e
costante di risorse da allocare. Inoltre nessun processo pu terminare se ha ancora
possesso di una risorsa. Abbiamo visto che, oltre alla prevenzione (statica e dinamica)
esiste un altro metodo per gestire i deadlock: la detection; in questo caso il sistema
permette una situazione di deadlock per poi ripristinarla. Quindi un algoritmo in questo
caso deve andare a verificare la presenza di blocchi critici, ed eventualmente, in caso
affermativo, provvedere al ripristino. Questo approccio molto meno invasivo in quanto
non chiede nulla ai processi, limitandosi periodicamente allesecuzione dellalgoritmo. Ad
esempio, per verificare la presenza di un blocco critico, lalgoritmo pu andare a
verificare la possibilit di esecuzione di un processo che richiede risorse di un altro
processo, assumendo che esso li rilasci al termine della sua esecuzione. In sostanza,
osservando un grafo di attesa, lalgoritmo va a ricercare i cicli. Se il grafo composito di
n nodi allora il numero di operazioni per trovare un ciclo dellordine di
n2 . Il ripristino
A queste analogie si affiancano per anche delle differenze. Ad esempio, la CPU virtuale
la possiamo vedere come una struttura dati allocata nellarea di memoria del sistema che
contiene tutte le informazioni del processo. Ovviamente la memoria virtuale dovr essere
allocata in una area differente dalla memoria principale. Si parler di swap area che
unarea nel disco di sistema atta a contenere le memorie virtuali di processi non allocati
in memoria principale. Un altro problema riguarda il fatto che la CPU utilizzata in
esclusivit, mentre la memoria principale pu contenere contemporaneamente pi
processi, e di conseguenza saranno altri i problemi da tenere in conto quali la protezione
(evitare che un processo acceda a locazioni di un altro processo). La condivisione della
memoria, tuttavia, ha un aspetto interessante. Se ho infatti due processi che eseguono lo
stesso codice ma su dati diversi, essi possono fare lo sharing dellarea di memoria
contenente il codice, senza che esso sia pi volte caricato.