Sei sulla pagina 1di 45

1.

Introduzione ai Sistemi Operativi


I Sistemi Operativi hanno lo scopo di gestire le risorse di un sistema di calcolo (quale pu
essere un computer, un server, o una lavatrice ). Conoscere la struttura interna di un
sistema operativo molto importante in quanto permette la scrittura di programmi in
maniera pi efficiente. Poich il SO un gestore di risorse, il suo compito quello di, ad
esempio, quale risorsa pu avere accesso alla memoria prima di unaltra; tale scelta
segue delle regole specificate in opportuni algoritmi che il sistema user per ottimizzare
al meglio la potenza dellhardware. In tale ambito si pu introdurre il concetto di
programmazione concorrente, con cui sintendono le diverse tecniche che permettono di
scrivere programmi costituiti da flussi operativi di esecuzione concorrenti. In sostanza,
allora, un sistema operativo lo possiamo vedere come un insieme di programmi, che
girano dietro la shell, che hanno il compito di gestire le risorse del sistema (CPU,
Memoria, ). Limportanza del sistema operativo si esplicita nellillusione che esso da
allutente utilizzatore della macchina: esso infatti ha come la sensazione di lavorare su un
calcolatore con un processore velocissimo e con una memoria infinta. Si dice allora che il
SO si pone come uninterfaccia tra il software e lhardware fornendone unastrazione. Un
esempio di tale astrazione il fornire un accesso uniforme (virtualizzato) a risorse
differenti, ossia fare in modo che se, ad esempio, collego una stampante, o un monitor
agli opportuni slot, lutilizzatore non si accorga che siano cose differenti; il
mascheramento avviene attraverso lutilizzo di driver, che rendono, dunque compatibili
periferiche esterne con lambiente di esecuzione. In definitiva, allora, un sistema
operativo definisce una macchina virtuale (o estesa) cio un insieme di astrazioni che
semplificano lo sviluppo di applicazioni e definiscono criteri con cui i programmi possono
accedere alle risorse del sistema.

Nella figura accanto si


differenzia il modo utente,
che rappresenta il modo di
esecuzione di applicazioni
senza particolari privilegi
(ad es. non posso
modificare registri di
sistema) al fine di fornire
una certa sicurezza e
protezione allHardware. Il
modo Kernel, allopposto,
ha i privilegi di gestione
hardware. In particolare la
figura accanto pu essere
espressa in maniera pi
completa aggiungendo una
interfaccia tra il SO e
Figura 1 Architettura di un Sistema di Calcolo
lhardware, in modo tale da
rendere effettuabile la portabilit dello stesso e di una interfaccia con lutente del sistema
operativo, in modo tale da mascherare quelle che sono operazioni predisposte dal SO
(quali una stampa a video, ad esempio) con una semplice chiamata a funzione. La figura
in basso rende esplicito quanto.

1.1.

Storia

I primi calcolatori che si sono sviluppati,


risalenti agli anni 50/60 non erano
dotati di sistema operativi, e le
operazioni di carico del programma in
memoria, esecuzione, erano
demandate a persone fisiche; tale
approccio era relativo anche al fatto
che i sistemi allepoca non erano molto
potenti quindi i problemi da risolvere
erano confinati allutente che intendeva
utilizzare il calcolatore. Con levoluzione
Figura 2 Architettura Dettagliata
tecnologica e lo sgravio economico che
vigeva sui calcolatori, si sono iniziati a sviluppare dei primi SO molto semplici che
permettevano di automatizzare operazioni che in precedenza erano svolte in manuale.
Comunque tali sistemi prevedevano lutilizzo da parte di un utente esperto e i calcolatori
erano ancora confinati in una elite ristretta. Grazie poi alla diminuzione del costo
dellhardware i computer divennero alla portata di tutti, e nasceva lesigenza di creare un
sistema operativo che permettesse di semplificare quanto pi i compiti dellutilizzatore. A
questo proposito nascono i primi sistemi Time-sharing (che facevano uso di terminali che
si collegavano ad un unico PC-Server). Levoluzione della tecnologia ha poi permesso la
diffusione dei calcolatori anche ad una utenza ben poca esperta, conducendo alla nascita
dei primi Personal Computer (PC) che utilizzavano un sistema operativo semplice,
monoprogrammato (soluzione dovuta anche al fatto che lhardware di cui erano dotati
non era adatto a sistemi operativi pesanti) che trovava terreno fertile nel MS-DOS. Ad

oggi si parla anche di Distributed Computing, a sottolineare il fatto che si passati da


una situazione in cui un computer era utilizzato da pi persone alla situazione in cui una
persona pu avere pi sistemi di calcolo, passando per la fase in cui ad ogni persona era
associato un calcolatore. Lultima fase di evoluzione dei sistemi operativi si presenta con i
OS Real-Time e Transazionali.
Nei primi sistemi dotati, abbiamo detto che il Sistema Operativo era blando; in sostanza
possiamo dire che esso era un loader, che caricava, quindi, il codice macchina sulla
memoria centrale, pi un insieme di librerie di procedure comune. Si passava quindi da
una fase in cui il programma era manualmente trasferito in memoria ad una fase in cui
tale operazione diventava automatica. Lo svantaggio in una situazione simile che luso
dellhardware era molto basso (lelaborazione della CPU occupava il 10%
dellelaborazione totale). Una soluzione a tale problematica la si trovava nei sistemi a
batch (a lotti) che si basavano sul principio di coda. I lavori venivano organizzati in lotti
che venivano quindi memorizzati in supporti magnetici (nastri) che venivano
sequenzialmente, caricati sul calcolatore. Tale operazione avveniva in parallelo
allesecuzione di qualche programma del calcolatore, sfruttando i punti di inattivit della
CPU, garantendo pi efficienza. Il SO, allora, andava a gestire il caricamento dei lavori e
le operazioni di Output con tecniche di Spooling (ad esempio nella stampa: il programma
non aspetta che la stampante finisca di stampare ma salva il documento in un buffer che
si svuota man mano che si stampa) che erano operazioni lente rispetto alla capacit di
elaborazione del processore. Nei sistemi a lotti, quindi, il sistema operativo era pi
complesso e risultava composito di un loader, un sequenziatore e un processore di
Output. In tal modo lutente non gestiva manualmente il caricamento del programma che
risultava quindi automatizzato. Il problema risiedeva ancora nel fatto che lesecuzione di
un programma portava via molto tempo e che durante lesecuzione di uno, il successivo
doveva attendere il suo completamento (sistemi monoprogrammati). Lapproccio del
MultiTasking si pone proprio come obiettivo quello di sfruttare i punti morti di lavoro della
CPU (ad esempio, durante lattesa di un Input la CPU pu effettuare altre operazioni).
Sistemi a lotti che fanno uso di MultiTasking vengono anche detti Sistemi Batch
Multiprogrammati.
Allora in un sistema multiprogrammato i lavori vengono caricati simultaneamente e il
sistema operativo li gestisce in base allutilizzo della CPU in maniera tale da garantire pi
efficienza e velocit.

Figura 3 Multitasking

Parallelamente a tali sistemi si sviluppano i Time-Sharing, con lo scopo di far utilizzare un


unico pc da pi in maniera interattiva. Notiamo che i sistemi a lotti non erano interattivi,
in virt del fatto che eseguivano lavori che passavano ad un sequenziatore, elaborati e
quindi fornivano output.
Il problema che sorge nei sistemi T-S che se un utente utilizza in maniera continua la
CPU non rende possibile ad un altro di utilizzare il calcolatore. Per tale motivo, in questi
ambienti, lutilizzo di un sistema operativo si rende necessario allo scopo di sospendere
un lavoro (che magari utilizza da troppo tempo la CPU) al fine di eseguirne un altro. Lo
svantaggio che, se il SO deve continuamente interrompere lavori, pu generare
overhead e quindi rallentamenti del sistema. Il meccanismo di condivisione della CPU in
tali sistemi fa uso del timer interrupt. Quindi se un lavoro particolarmente dispendioso
per la CPU o, richiede una operazione, ad esempio di I/O, che la rende inattiva, il sistema
sospende il lavoro e con la schedule decide di eseguire un secondo lavoro. La particolarit
che il programma sospeso non deve accorgersi di essere stato sospeso (effettuando
magari degli shoots del contesto). Tale particolarit garantita dallo scheduling.
Sintetizzando, lobiettivo del batch Multiprogrammato massimizzare lutilizzo della CPU
mentre quello del Time-Sharing minimizzare i tempi di risposta.
Il multitasking, anche se di vecchia data, apparso sui dispositivi moderni da poco. Ci in
relazione al fatto che i primi PC che si sono sviluppati erano di hardware non capace di
supportare un sistema operativo con un certo peso sulle risorse, e quindi necessitavano
di un OS semplice che era rappresentato al tempo dal DOS; in seguito, labbassamento
del costo dellhardware e la necessit di soddisfare le esigenze dellutente sono state
cause scatenanti della nascita di SO pi complessi e migliorati.

2. Richiami sulla struttura dei sistemi di calcolo


La CPU la possiamo vedere come una scatola composita di diversi registri tra cui i pi
importanti sono il PC (Program Counter, o registro Prossima Istruzione), il PS (Program
Status, che serve a mantenere informazioni, tra cui la maschera delle interruzioni) e lo SP
(Stack Pointer, che contiene il puntatore allo stack, ove vengono allocate le variabili
automatiche). Tutti questi registri definiscono lo stato del processore. Il processore
esegue programmi elaborando le singole istruzioni atomiche in un ciclo (fetch Operand
Assembly Execute). Il fetch consiste nel prelievo in memoria dellistruzione da
elaborare. Laccesso alla memoria avviene attraverso il bus. In una istruzione la CPU pu
richiedere un input o un output o ancora, comunicare con unaltra periferica: tale
comunicazione avviene sempre attraverso il bus, ad esempio con un approccio memorymapped se ogni dispositivo di I/O ha un controller dotato di un buffer locale, in cui scrive
o legge il dato. Il normale ciclo del processore pu essere interrotto mediante un segnale
di interruzione. Linterrupt pu essere sollevata in ogni punto del ciclo del processore, ma
al fine di preservare lintegrit del programma essa viene servita solo al termine
dellesecuzione dellistruzione. Linterruzione quindi, gestita dallISR (Interrupt Service
Routine) che ritorner il controllo al processo interrotto. Linterruzione serve per andare a
sfruttare meglio lutilizzo con le periferiche esterne, pi lente rispetto la CPU, in modo tale
da rendere il tutto pi efficiente. Abbiamo detto prima che ogni dispositivo di I/O ha un
controller dotato di un buffer locale, in cui in caso di input la CPU legge e viceversa in
caso di output. Quindi la CPU pu spostare i dati da e verso la memoria centrale verso e
da il controller del dispositivo; dopodich pu continuare le sue operazioni. La CPU viene
informata dal controller del termine di una operazione con un interrupt di fine I/O. Ad
esempio, il programma richiede un input da tastiera, mentre la CPU aspetta linput,
poich in attesa pu fare altro; quando linput pronto la periferica genera un interrupt
che informa la CPU della fine delloperazione e la CPU restituisce il controllo al processo.
Pu accadere anche che, la CPU, informata di una interruzione possa momentaneamente
disabilitarla, magari per la sovrapposizione di una interruzione pi importante, oppure in
presenza di operazioni che non possono essere interrotte. Quindi in definitiva, il
meccanismo delle interrupt permette un pi efficiente utilizzo della CPU andando a
sfruttare la lentezza delle periferiche per implementare il multitasking.
Possiamo definire sia Interrupt Software (Trap) che Interrupt Hardware (o interruzioni
esterne). Le prime sono causati da un programma in esecuzione (eccezioni nel
processore) provando ad accedere ad aree di memoria riservate, divisione per zero []; il
sistema, quindi, interviene generando una eccezione e quindi viene sollevata
linterruzione. Anche le richieste di servizi al SO (quali le supervisor call) sollevano una
interruzione (per permettere di lavorare in modo supervisore (o kernel)). Le interruzioni
software sono eventi sincroni, ossia sono generati dallelaborazione. Le interruzioni
Hardware invece possono essere dovute a eventi hardware, non causati dal programma
in esecuzione (fine di I/O, clock (nei sistemi time-sharing se un processo occupa ad
esempio, troppo la CPU)). Sono eventi asincroni rispetto allelaborazione in quanto
possono avvenire in ogni momento.
Abbiamo detto che il meccanismo delle interruzioni pu essere sfruttato per accedere in
modo supervisore per effettuare le svc. Quindi il processore pu lavorare in modo utente
o in modo supervisore. Quando lavora in modo utente, esso ha un parziale controllo della
macchina, in quanto non pu accedere a particolari aree di memoria, non pu eseguire
determinate istruzioni (disabilitazione interruzione); vantaggi che invece possiede se
lavora in modo supervisore. Lutilizzo in modo utente o kernel caratterizzato da un bit

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

Il SO ha continuamente a che fare con la memoria, che pu essere, sostanzialmente,


centrale, con cui sintende linsieme dei registri, ossia il solo spazio di memorizzazione
che pu essere acceduto direttamente dalla CPU e la memoria secondaria, non volatile
con un alta capacit di memorizzazione (ad esempio i dischi magnetici). Le memorie
possono essere organizzate in una gerarchia: man mano che sono dimensionalmente pi
grandi, diventano meno veloci e meno costose. Esistono, tuttavia, anche memorie che
non possono essere gestite dal SO, le cache, che sono di piccole dimensioni ma
estremamente veloci, che possono essere integrate allinterno della CPU, e servono per
conservare dati acceduti di recente. Il SO gestisce, quindi, la memoria centrale facendo in
modo che i dati di un programma siano isolati da altri, e gestisce lorganizzazione dei file
in memoria secondaria attraverso il filesystem. La cache quindi gestita direttamente
dallhardware. Essa pu essere presente in diversi livelli, come ad esempio allinterno
stesso della CPU. La cache allinterno della CPU una memoria estremamente piccola e
veloce rispetto alla RAM poich ha accesso immediato dovuto alla posizione della stessa.
Laccesso di tipo associativo. Inoltre per mantenere lo stato fa uso di transistor (circa 6
per ogni cella). In questo modo, quando la CPU deve prelevare un dato in memoria,
consulta prima la cache; se il dato esiste nella cache loperazione di lettura in memoria
diventa inutile (cache_hit); altrimenti (cache_miss), se il dato non presente lo pesco in
memoria e lo trasferisco nella cache (eliminando qualche altro dato). Un elemento della
cache caratterizzato da un bit di validit, da un tag, che identifica il contenuto con la
parte comune a tutti gli indirizzi delle locazioni del contenuto, e dal contenuto. Laccesso
avviene in maniera associativa attraverso il tag. La cache inoltre sfrutta il principio di
localit del programma, ossia, se sto eseguendo un programma posso caricare in cache
dati e informazioni relative al programma stesso. Tutti questi meccanismi avvengono in
HW. Lunico problema si verifica quando viene definita un Interrupt, che necessita di
copiare i dati della ISR nella cache perdendo i dati precedentemente caricati.

2.2.

Protezione delle risorse hardware

I processori moderni possono lavorare, come gi detto, in modo utente o in modo


supervisore. Tale distinzione dovuta al problema di proteggere le risorse del sistema al
fine di non permettere allutente delle modifiche ad aree di memoria o operazioni che
possano danneggiare lo stato del sistema operativo. Si parla quindi di istruzioni
privilegiate che devono essere eseguite in modalit supervisore. Ad esempio le
supervisor call non possono essere chiamate in modalit utente in quanto non ho la
facolt di accedere allo PS per modificare il bit supervisor. Devo allora sfruttare
linterruzione che automaticamente porta il sistema in modalit supervisore. Ovviamente
anche le istruzioni di modifiche di registri, e quindi, di gestione della memoria sono
riservate. Nei sistemi moderni la gestione della memoria demandata alla MMU che si
occupa della traduzione degli indirizzi logici forniti dalla CPU in indirizzi fisici e verificare
se tali operazioni siano possibili. Ad esempio, supponiamo che abbia un programma che
esegua dallindirizzo 1000. Per far in modo che il programma non acceda ad aree di
memoria riservate (come quelle del SO) definisco i registri base e limite (della MMU) non

modificabili in modo utente, in modo tale che se il programma accede ad un indirizzo


esterno a quelli compresi tra il base e il limite, la MMU genera una eccezione. Un
problema che sincorre nellaccesso in memoria quello relativo agli indirizzi assoluti.
Supponiamo che un programma sa gli indirizzi assoluti della sua localizzazione in
memoria. Se viene effettuata una rilocazione, gli indirizzi cambieranno. Allora per evitare
tale problema possiamo definire degli indirizzi virtuali , calcolati dalla CPU, con la MMU
che avr il compito di sommare gli indirizzi virtuali al registro di base per ottenere
lindirizzo assoluto del programma, al fine di non sovraccaricare di operazioni la CPU.

3. Introduzione a Linux (Esercitazione)


Unix un sistema operativo multiutente e multitasking (time-sharing) che nasce dallidee
della Bell Labs di AT&T come successore del progetto MultiCS (prima idea di sistema
operativo time sharing). Gli sviluppatori di Unix, Thompson e Ritchie, colgono alcuni
importanti aspetti di MultiCS, allo scopo di sviluppare un sistema meno complesso e pi
completo, definito interamente in C e con specifica del sistema aperta. Parallelamente,
nel mondo universitario, a Berkeley si sviluppa una versione di questo sistema, il BSD.
BSD e Unix evolvono parallelamente e differentemente. Con la nascita dellARPANET
entrambi i sistemi aderiscono allo standard TCP/IP come protocollo di rete, e allinizio
degli anni 90 aderiscono allo standard definito dalla IEEE, il POSIX, che stabilisce una
interfaccia standard tra S.O. e programmi. Con lavvento dei primi personal computer,
nasce XENIX, il primo Unix per processori inferiori al 386 e successivamente per i
processori superiori SCO, Linux, FreeBSD. Il MIT implementa poi una interfaccia grafica a
finestre per Unix (X-Window). Dalla famiglia BSD nasce anche il Mac OS, o Solaris (SUN).
Alla met degli anni 80 nasce lidea di software libero di Stallman con la fondazione del
progetto GNU. Lidea quella di creare un sistema operativo libero, gratuito e
distribuibile. proprio dal GNU che discende Linux.
Lidea di software libero non coincide con lidea di non protetto da diritti dautore. Un
programma software libero se gli utenti hanno la libert di eseguire il programma,
libert di studiare come funziona il programma, libert di ridistribuire copie, libert di
migliorare pubblicamente il programma. Linux, viene distribuito attraverso le distro
(distribuzioni) che possono essere free o a pagamento. Ogni distro include il kernel, un
insieme di applicazioni e strumenti per linstallazione e lamministrazione del sistema.
Come Unix, Linux un SO multiprogrammato, multiutente e interattivo. Linterazione
avviene tramite la shell, che non nientaltro che un programma utente ordinario, in
grado di leggere e scrivere da un terminale ed eseguire programmi.
Esempi di comandi:

pwd , Print Work Directory, stampa a video il path della cartella di lavoro;
mkdir [nome] , crea una nuova cartella, seguito dal nome;

touch[nome ] , crea file;


cat [nome] , visualizza file;

chmod [ permessi ] [ nomefile ] , cambia i permessi: rwx per i proprietario, il gruppo,

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:

chmod 764 miofile . txt .

cp , copia file;
mv , sposta il file;

, salva loutput in un file, es.:

, pipe, concatena loutput di un comando con linput del successivo;


sort , ordina;

esempio
cat esempio> dump ;

, esegue il programma in background;


ps , riporta lo stato dei processi attivi sul sistema;

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. Architettura di un Sistema Operativo


Un sistema operativo ha il compito di virtualizzare le risorse hardware, offrendo servizi di
gestione della memoria, dei dispositivi e periferiche e meccanismi di gestione e
coordinamento, quali possono essere di protezione dellhardware e della memoria, di
comunicazione tra dispositivi, o, pi in generale di gestione delle risorse. Il SO lo
possiamo immaginare in un modello a cipolla con una serie di strati successivi che
avvolgono lhardware. Il punto pi a contato con lhardware il kernel. Esso la parte che
interfaccia direttamente con lhardware e contiene le funzionalit principali del sistema
operativo. pertanto chiamato anche nucleo. La virtualizzazione effettuata dal kernel
consiste nel realizzare tante CPU cos quanti sono i processi attivi, in modo tale che ogni
processo abbia lillusione di avere un processore solo per s. La condivisione della CPU
gestita dal sistema operativo, quindi. Questa macchina virtuale non possiede meccanismi
di interruzione, poich tale gestione non compito del processo, e possiede meccanismi
per la sincronizzazione e scambio di messaggi tra processi. Questi sono indipendenti luno
dallaltro, ma ci non toglie la possibilit che essi possano cooperare e interagire. Il livello
immediatamente successivo al kernel quello di gestione della memoria. Lastrazione
realizzata a tale livello dal SO permette di far riferimento alla memoria con indirizzi
virtuali (o logici, non assoluti, che consentono lo spostamento automatico del

programma: ad esempio se un programma sa che la prossima istruzione sta allindirizzo


$1100, se sposto il programma in unaltra area la prossima istruzione non sar pi la
stessa). Tale livello, inoltre, definisce delle misure di sicurezza per laccesso ad aree di
memoria sensibili e gestisce la memoria virtuale, che la parte del sistema operativo ce
si occupa della gestione della memoria. La memoria centrale alquanto limitata e non
pu contenere tutti i dati di un programma in esecuzione; ci comporta che si debba
allocare solo parzialmente un programma su disco, in modo tale da far avere lillusione ad
ogni processo di avere memoria infinita. questo il perch possibile avere pi task
attivi in contemporanea. Ad esempio, supponiamo di eseguire unapplicazione; il SO
carica in memoria le funzionalit del programma pi comuni ed essenziali. Se
nellapplicazione eseguiamo una funzionalit particolare, notiamo che essa ci mette del
tempo: ci perch deve essere caricata in memoria perch non presente.
Sottostante al livello di gestione della memoria vi il livello di gestione delle periferiche,
con il compito di virtualizzare le periferiche del sistema. Il Sistema Operativo fa in modo
che le periferiche abbiano lillusione di avere il controllo completo della CPU, e definisce
le periferiche uniformemente, mascherando le caratteristiche fisiche. A questo livello
sono gestiti anche, almeno, parzialmente, anche i malfunzionamenti, in modo tale che
essi non influiscano sulle prestazioni del sistema. Al livello sovrastante risiede il File
System, che gestisce la virtualizzazione dei file, ossia la mappatura fisica, il controllo
degli accessi e la loro organizzazione. Infine abbiamo i programmi di utilit e la shell
(interprete di comandi) che rappresenta il guscio dellarchitettura.

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:

Controllo di processo (load, execute, allocate mem);


Manipolazione dei file (open, create, delete...);
Gestione dei dispositivi (request device, read, write, );
Gestione delle informazioni di sistema (get time);
Comunicazione tra processi e nella rete.

Le system-call sono implementate come interruzioni, in modo tale da rendere possibile il


passaggio tra modo utente e modo supervisore. Esse sono quindi gestite da routine
dedicate. Anche le system-call lente possono essere spostate nelle code. Ovviamente,
anche se sono implementate come delle interruzioni, lutente non se ne accorge poich
esse vengono chiamate con opportune funzioni presenti in librerie che astraggono il
processo come una semplice chiamata a funzione (tuttavia, osservando il codice
macchina della SYSCALL si pu osservare la chiamata di una interruzione).
Esempio SystemCall getpid():
tale funzione ha il compito di restituire lid del processo. una SVC poich tale
informazione contenuta in una tabella dei processi presente nellarea di memoria del
kernel. Disassemblando la getpid(), nel codice macchina otteniamo la syscall, preceduta
da una move di un codice particolare nel registro eax, che definisce la syscall da
chiamare; il codice in esadecimale.

4.2.

Esecuzione di un Sistema Operativo

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

Il Sistema Operativo un programma molto complesso e di notevole dimensioni; per tale


motivo necessario, nellambito della sua realizzazione lutilizzo delle principali tecniche
dellingegneria del software al fine di garantire correttezza, modularit, manutenibilit e
cos via. I primi sistemi operativi erano piuttosto semplici ed erano caratterizzati da un
unico programma, senza suddivisioni varie. Si parla di architetture monolitiche. In
sostanza vi era un unico Handler dinterruzione che era peraltro scritto in Assembly, e
quindi molto poco adatto al cambiamento e alla revisione. Ad esempio lOS 360, famoso
per la sua facilit di crash. Le architetture modulari sono caratterizzate dalla
strutturazione del SO in moduli, ognuno destinato ad offrire una delle funzionalit del
sistema. I moduli sono caratterizzati da Interfaccia, la specifica delle funzionalit offerte
dal modulo, e Corpo, che contiene limplementazione della specifica. La suddivisione tra il
cosa dal come permette di effettuare modifiche al corpo senza compromettere lutilizzo
del modulo. Un esempio di architettura modulare Linux: la modularit in Linux permette
la sua adattabilit anche a sistemi poco prestanti in quanto permette il linking dei moduli
a runtime, permettendo il caricamento di un range piccolo di moduli essenziali. Tuttavia,
alcuni moduli sono compilati staticamente e non possono essere rimossi. Nascono anche
le architetture a livelli, che sono unevoluzione strutturata gerarchicamente delle
architetture modulari. In questo caso le interazioni possono avvenire solo tra livelli
adiacenti, come ad esempio avviene nel SO THE. Di recente implementazione sono le
architetture a microkernel. In queste il kernel implementa solo i meccanismi essenziali.
Tutto ci che pu essere eseguito in modalit utente viene tolto dal kernel. Ci deriva dal
fatto che se ho un bug in un programma utente posso cercare di recuperarlo; se invece
ho un errore di un programma superuser, ci comporta il fallimento del sistema. In
sostanza, quindi, questarchitettura garantisce maggiore robustezza rispetto alle
precedenti. Un processo applicativo che deve usare una risorsa interagisce con un server,
che sono particolari processi di sistema, con meccanismi di comunicazione forniti dal
microkernel (es. message passing): per esempio se un programma utente richiede
lapertura di un file, esso interagir con il file server. I vantaggi in questo caso di
traducono in maggiore affidabilit, estendibilit; tuttavia si hanno svantaggi in termini di
overhead dovuto alle continue comunicazioni inter-processo. Ecco perch tale approccio
utilizzato in sistemi come Windows, MacOS. In realt Windows basata su unarchitettura
a microkernel ibrida.

5. Makefile e Librerie (Esercitazione)


Ricordiamo che il ciclo di sviluppo di un programma comprende diverse fasi: la prima la
fase di editing che porta alla definizione di un file di estensione .cpp (nel caso di un
programma in C++) o .c (nel caso di un programma in C). Il file sorgente passa, quindi,
nel preprocessore, che effettua pulizia (elimina commenti), e sostituisce le direttive al
preprocessore con le librerie vere e proprie nel caso di include e il valore alla stringa nel
caso del define. Il file sorgente modificato passa quindi al compilatore che lo traduce in
un linguaggio che assimilabile dalla macchina di esecuzione, producendo un file
oggetto (.o). Ovviamente, specie se utilizziamo un approccio di programmazione
modulare dobbiamo tenere conto che il programma composito di diversi moduli,
dunque abbiamo bisogno di un collegatore (linker) che si occupi di collegare i vari moduli,
nonch linkare le librerie utilizzate allinterno di essi. Il linker produce un file eseguibile, e
sar compito del loader il caricamento del programma in memoria (RAM) per
lesecuzione.

Quindi, il codice oggetto la traduzione del sorgente in linguaggio macchina, specifico


del calcolatore su cui verr poi eseguito. Infatti il codice oggetto strettamente legato
allarchitettura della macchina nonch anche dal compilatore: due compilatori diversi
possono produrre codice oggetto differente per una stessa macchina dato lo stesso
sorgente. Ci implica la non portabilit del programma in altri ambienti, ossia lo stesso
programma, per essere eseguito dovr essere ricompilato su ogni macchina cui vogliamo
venga eseguito. Il codice oggetto composito, in genere, da codice eseguibile pi una
serie di informazioni che consentono al linker di unirlo con altri codici oggetto e librerie
per definire un eseguibile. Le librerie sono un insieme di funzioni strettamente
imparentate tra loro. Quando compiliamo un eseguibile che contiene una libreria
staticamente, verr compilato sia il nostro file che la libreria e i due codici oggetto
saranno uniti dal linker a formare un unico eseguibile. Poich una libreria pu essere
molto corposa, spesso si preferisce effettuare il collegamento a run-time. Per essere
eseguito il programma deve essere caricato in memoria; il compilatore genera degli
indirizzi rilocabili, in modo tale che il SO pu spostare il programma in maniera semplice
qualora necessario. I moduli che sono stati compilati separatamente saranno fusi e il
linker trasformer gli indirizzi simbolici in indirizzi rilocabili. Il Loader pu decidere di
adottare politiche di rilocazione statica, nel momento di carica viene sommato leffettivo
blocco fisico al registro di base e limite, o dinamica, effettuata dalla MMU a run-time.
Abbiamo allora parlato dei moduli. La programmazione in moduli importante in quanto
assicura diverse qualit al software, quali migliorie in termini di ritrovo di bug, facilit di
cambiamento, possibilit di lavoro parallelo e quindi in generale maggiore efficienza. Un
modulo lo possiamo vedere come un componente software dotato di una distinzione tra
interfaccia e corpo; linterfaccia rappresenta la specifica del modulo, ossia le funzionalit
offerte dal modulo, mentre il corpo limplementazione delle specifiche offerte. Ci pu
essere realizzato modularizzando un file in uno di intestazione (header .h) e uno di
implementazione. In tal modo se cambiamo un algoritmo nel corpo, lasciando inalterata
la funzione, non creiamo alcun disagio a chi sta utilizzando il modulo.
Quindi, come abbiamo detto prima, un modulo pu includere al suo interno un altro
modulo. compito allora del linking unire tutti i moduli oggetto in un unico modulo
caricabile. I riferimenti simbolici vengono trasformati in salti verso i corrispondenti
indirizzi di load complessivi. Il loader carica ad un indirizzo di memoria il load module.
Tale operazione pu essere assoluta, se le chiamate relative vengono sostituiti da indirizzi
assoluti (JUMP X -> JUMP $1200), o rilocabile se ad ogni indirizzo sommato il valore
allinterno del registro base.
A tal proposito Unix ha definito un formato file standard per eseguibili, codice oggetto,
librerie condivise e core dumps, lELF (Executable and Linkable Format). Ogni file ELF
costituito da una intestazione e da zero o pi segmenti e zero o pi sezioni. I segmenti
contengono informazioni importanti per lesecuzione mentre le sezioni informazioni
importanti per il linking e la relocation.

5.1.

Makefile

Un makefile utile quando abbiamo un programma sparso su diversi moduli,


semplificando le operazioni di compilazione separata. Un makefile eseguito con il
comando make, che cerca nella directory corrente un file di nome Makefile e esegue le
regole in esso contenute. Ovviamente se il nostro makefile non si chiama in questo modo
dobbiamo utilizzare un comando differente (make f nomefile). Il makefile composto di
regole. Una regola segue la sintassi:

target :dipendenze

[ tab ] comando di sistema


Dato un target make esegue i comandi presenti sulla riga successiva solo se sono
soddisfatte le dipendenze. Le dipendenze sono i file da cui il target dipende. Tali possono
essere gi presenti o essere creati da altri target.
Esempio (compilazione di un programma distribuito su un header e un file di
implementazione):
file.o: file.cpp file.h
g++ -c file.cpp
Esempio (programma che utilizza la compilazione separata e librerie esterne):
start: main.o lib.o
g++ -o start main.o lib.o
lib.o: lib.cpp lib.h
g++ -c lib.cpp
main.o: main.cpp lib.h
g++ -c main.cpp
E possibile far eseguire anche solo una regola con il comando: make lib.o che compiler
solo la libreria. Spesso il makefile specifica una regola di clean per ripulire i risultati di una
compilazione;
clean:
rm f *.o
rm f nomeFileEseguibile
Anche in un makefile possiamo dichiarare delle variabili, chiamate macro con la sintassi:
MACRO = valore
e verr utilizzata nel makefile con la sintassi $(MACRO).

5.2.

Librerie

Lapproccio modulare ha portato notevoli miglioramenti anche in termini di riusabilit con


la definizione di librerie che sono moduli software dotati di una intestazione che specifica
il cosa, e di moduli in formato oggetto, gi compilati, pronti per essere linkati allinterno di
un programma. Possiamo avere tre tipi di librerie, statiche, se sono collegate
alleseguibile insieme agli altri moduli software dal linker, dinamiche con caricamento a
run time, se vengono caricate dal loader insieme al load module, oppure dinamiche con

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

6. Gestione dei processi


Un processo lo possiamo descrivere come un programma in esecuzione. Quindi, mentre
un programma una entit statica che corrisponde alla codifica, e quindi alla descrizione,
di un algoritmo, il processo una entit dinamica. In sintesi possiamo definire un
processo in termini di un programma e di uno stato di esecuzione, che identifica le
attivit dellelaboratore specifiche di una determinata esecuzione di un programma.
Ovviamente poich, in generale, un programma pu accettare diversi input, ad ogni
programma possiamo associare processi differenti. Definiamo immagine del processo
linsieme dellarea codice, dellarea dati e delle aree di memorie necessarie alla gestione
del processo stesso. Nella sua vita un processo pu transitare in diversi stati; ad esempio,

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

contiene un insieme di informazioni che permettono al SO di sapere circa il processo. I


PCB sono organizzati in tabelle, le Process Table.
Un PCB ha informazioni circa, il nome del processo, lo stato del processo, la modalit di
servizio, informazioni sulla gestione della memoria, contesto del processo (serve per
riprendere lesecuzione del processo dove terminata dopo una interruzione, ad esempio),
utilizzo risorse, id del processo successivo. I descrittori sono memorizzati nellarea di
memoria del sistema operativo (si giustifica perch il getpid una svc).

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.

Gestione dei processi in Unix

Sappiamo gi che Unix un SO time sharing multiprogrammato. In esso i processi sono


detti pesanti: essi non possono condividere dati ma tuttavia possono condividere larea
codice (che di sola lettura). Si parla allora di codice rientrante a sottolineare come pi
processi possano appoggiarsi allo stesso codice contemporaneamente. Ovviamente, un
processo pu eseguire in modo kernel o utente.
[Immagine modello a stati Unix semplice]
Notiamo che nasce lo stato zombie. In esso vanno i processi terminati che non possono
essere eliminati perch la loro immagine ancora necessaria. Viene conservato il PCB del
processo mentre vengono eliminate le aree dati e codice. Tale motivazione dovuta alla
natura gerarchica dei processi in Unix.
[Immagine modello a stati Unix Completo]
Il PCB Unix suddiviso in due parti:
-

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.

Scheduling di breve-termine (Dispatcher)

In sostanza allora lalgoritmo di scheduling di breve termine deve essere tale da


ottimizzare differenti aspetti del comportamento del sistema. I criteri utilizzati in tale
ambito sono di due tipi: user-oriented, che si riferiscono al comportamento del sistema
cos come percepito dallutente o da un processo, system-oriented, il cui obiettivo
quello di utilizzare in modo efficiente il processore.
I parametri user-oriented sono:
-

Tempo di turnaround: ossia lintervallo di tempo che trascorre da quando il


processo sottomesso al sistema fino alla sua terminazione. Nei sistemi batch tale
parametro importante, poich indica il tempo atteso dallutente per ottenere, ad
esempio, il risultato del task.
Tempo di risposta: tipico di processi interattivi, indica il tempo che trascorre dalla
sottomissione di una richiesta fino a quando la richiesta viene ricevuta.

Ovviamente, dopo la fornitura di tale richiesta il processo continua la sua


esecuzione.
Deadlines: serve per capire se un task in grado di terminare entro una scadenza
(parametro importante per sistemi RT).

I parametri system-oriented sono:


-

Throughput: misura la produttivit di un sistema in termini di processi terminati in


un intervallo di tempo stabilito (in genere ci si riferisce allunit).
Utilizzo della CPU: misura lutilizzo della risorsa in percentuale (da massimizzare se
preferiamo un criterio system-oriented).
Fairness: i processi devono essere trattati allo stesso modo e nessuno di essi
dovrebbe trovarsi in situazioni di attesa indefinita (salvo indicazioni specifiche).

Ovviamente, banale constatare che se preferiamo il tempo di risposta non possiamo


pretendere un throughput alto, quindi impossibile costruire un algoritmo di scheduling
che soddisfi tutti i criteri elencati. Quindi la scelta dellalgoritmo relativa al contesto
duso: ad esempio, per sistemi interattivi preferisco parametri user oriented dando cos
lillusione allutente di un sistema veloce mentre per sistemi server preferisco parametri
system-oriented.
Lo scheduler pu scegliere i processi in base alla priorit degli stessi. Le priorit possono
essere statiche, se determinate a priori e valgono per tutto il ciclo di vita del processo,
dinamiche se invece possono essere modificate durante il loro ciclo di vita (ad esempio se
un task utilizza la CPU da troppo tempo posso abbassarne la priorit). Introducendo la
priorit introduco anche la starvation, ossia situazione (relativa ad un processo) di attesa
indefinita (perch di priorit bassa). Infatti se ho processi a priorit alta essi
sovrasteranno quelli a bassa. Per eliminare tale problema posso pensare di utilizzare
priorit dinamiche introducendo lAging (se un processo sta invecchiando -> aumento
priorit).
Lo scheduler Dispatcher pu essere preemptive se permette di rilasciare (interrompere)
un processo in esecuzione, o non-preemptive, ove il processo in esecuzione fin quando
non si sospender per una interruzione.

7.2.

Algoritmi di scheduling

Vedi Esempi Slide.


Lalgoritmo pi semplice che uno schedulatore pu definire il FCFS (First-Come-FirstServed) in cui viene scelto il processo che attende da pi tempo. un algoritmo semplice
adatto dai primi sistemi batch ma che tuttavia poco si presta ai moderni sistemi
(interattivi) perch se un processo interrotto deve rifare la coda con conseguenti danni
sul tempo di risposta. un algoritmo non-preemptive, il che significa che c rischio che
un processo monopolizzi la CPU. In tal senso quindi favorisce i processi CPU-bound, con
gli I/O Bound che devono attenderne il termine, fenomeno chiamato effetto convoglio.
Il Round-Robin si pone come trasformazione del FCFS, introducendo un timer (diventa
preemptive). Tale definisce il massimo tempo che un processo pu sfruttare nellutilizzo
della CPU. In questo modo tutti i task vengono trattati allo stesso modo. Il vantaggio si
traduce nel fatto che se scelgo un quanto di tempo piccolo, posso alternare pi processi
(favorendo il tempo di risposta); ci significa anche per maggiore overhead per continui
cambi di contesto. La scelta del quanto di tempo potrebbe essere effettuata tenendo

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

esimo come media della durata dei burst di esecuzione passati:

1
1
n1
S n+1 = T i = T n+
S
n i=1
n
n n
Con:

Ti

tempo di esecuzione del burst e

Sn

tempo di esecuzione del burst n-esimo.

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):

S n+1 =aT n+ (1a ) S n con 0<a< 1coefficiente di stima


Tuttavia anche in questo caso abbiamo problemi per tempi di esecuzione di burst non
stabili. Quindi, poich difficile basare la stima sulle esecuzioni passate possiamo tentare
di basarci sul passato. Introduciamo lalgoritmo di feedback. Ci perch un sistema
general-purpose, il pi delle volte esegue programmi distinti (che non conosce). Il
feedback va a penalizzare i processi che spendono pi tempo nellutilizzo della CPU,
abbassandone la priorit. In realt tale algoritmo viene implementato su pi livelli, a cui
associato un concetto di priorit. Il meccanismo di priorit utilizzato dinamico e dipende
dal tempo di utilizzo della CPU. Ad esempio, un processo che entra dallo stato nuovo ha
priorit alta e va nella coda a priorit pi alta. Quando lo scheduler lo mette in
esecuzione, se esso termina la sua esecuzione nel quanto di tempo non succede nulla,
altrimenti viene abbassata la priorit e spostato in una coda di priorit pi bassa.
Intrinsecamente, tale algoritmo preferisce i processi di breve durata ossia adatto per
sistemi interattivi. I processi CPU-bound possono essere dimenticati nella coda a priorit
pi bassa. Pertanto, poich tale algoritmo prevede diverse code, ciascuna di esse
governata con il round-robin, mentre lultima FCFS. Per evitare starvation dei processi
CPU bound, tali possono essere trasportati in una coda a priorit pi alta. In generale, poi
ogni coda ha

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

misura dellutilizzo del processo

del processore

i .

8. Introduzione alla programmazione concorrente


Per programmazione concorrente sintendono linsieme delle tecniche, metodologie e
strumenti necessari a fornire il supporto dellesecuzione di software come un insieme di
attivit svolte simultaneamente. Ad esempio, un server gestisce pi richieste dei client
simultaneamente. La multiprogrammazione una forma elementare di programmazione
concorrente in quanto i vari processi si alternano alluso della CPU, ma non collaborano.
Definiamo elaborazione concorrente la scomposizione di una attivit che pu essere
portata avanti come attivit simultanee. bene distinguere la differenza che esiste tra
attivit concorrenti e parallele. Le prime sono caratterizzate dallalternanza allutilizzo
della CPU, mentre le seconde prevedono che i processi siano eseguiti in parallelo.
[Immagine esempio]
I sistemi operativi moderni definiscono funzioni per creare processi che avanzano
simultaneamente e permette le interazioni tra essi. I processi sono concorrenti se hanno
esecuzioni sovrapposte. Servono allora definire primitive per la creazione di attivit
indipendenti e primitive per la coordinazione, comunicazione e sincronizzazione.
La fork serve per creare un processo che sar concorrente con quello che ha chiamato la
fork. Biforca quindi il flusso di esecuzione in due flussi concorrenti. La complementare la
join che serve per ricongiungere flussi di esecuzioni divisi dalla fork. Il principio base la
sincronizzazione: deve aspettare tutti i flussi di esecuzione, e dopo ci pu continuare
lesecuzione. Per tale motivo anche detta bloccante (a differenza della fork).
La condizione standard per la creazione di processi concorrenti la comunicazione tra
essi. Per tale motivo dobbiamo definire delle metodologie che permettono ci.
Innanzitutto dobbiamo porci un problema: se due processi accedono simultaneamente ad
una risorsa (un file ad es.) possono lasciarlo in uno stato inconsistente. Questo problema

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:

Competizione: due o pi processi competono allutilizzo di una risorsa condivisa,


che non possono accedere contemporaneamente (mutua esclusione).
Cooperazione: i processi cooperano per un fine comune; per tale motivo serve
scambio di informazione (comunicazione).
Interferenza: si verifica quando pi processi interagenti interferiscono per uso
errato di primitive di comunicazione e di competizione. Tale situazione si manifesta
in modo non deterministico, in quanto relativo a particolari situazioni non
facilmente riproducibili.

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.

Modello ad ambiente globale

Per iniziare a parlare di ci, ricordiamo il concetto di risorsa.


Una risorsa un qualsiasi oggetto fisico o logico di cui il processo ha bisogno per
terminare la sua esecuzione. In altro modo, poich essa una entit astratta la si pu
rappresentare come delle procedure (istruzioni che agiscono sulla risorsa) e una struttura
dati in area condivisa. Una risorsa pu essere privata (o locale) se dedicata ad un solo
processo (esso lunico che pu agire sulla risorsa), oppure comune o globale, se pu
essere condivisa tra pi processi, o, infine, ad uso esclusivo, se pu essere utilizzata da
un processo alla volta.
A capo di ci vi il gestore delle risorse che si occupa di definire le regole di utilizzo della
risorsa, e della sua corretta allocazione. Per file e periferiche il gestore il S.O., mentre se
la risorsa di tipo software (creata dallutente) deve essere creato anche un suo gestore,
in modo tale da non avere problemi come quelli elencati sopra.
[Immagine Modello ad Ambiente Globale]

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.

Modello ad ambiente locale

Nel modello ad ambiente locale non c la presenza di memoria condivisa, il sistema


visto come un insieme di processi, ognuno dei quali opera in un proprio ambiente locale
che inaccessibile da altri processi. Ogni forma di sincronizzazione avviene mediante lo
scambio di messaggi. Ovviamente, anche in questo caso esiste il concetto di risorsa
comune, ma qui privata al processo gestore. Un client che vuole utilizzare la risorsa,
deve interagire tramite scambio di messaggi con il processo gestore.
Entrambi i modelli visti sono implementati nei sistemi general-purpose, perch offrono
vantaggi in differenti situazioni. Nel modello ad ambiente globale si utilizzano le primitive
wait e signal per limplementazione dei semafori, mentre nel modello ad ambiente locale
si utilizzano le primitive send e receive.

9. System Calls per la gestione dei processi


Abbiamo visto come le system calls siano un modo in cui i processi possano ottenere
servizi dal kernel (poich implementate attraverso interruzioni che permettono lo switch
utente->supervisor). Un kernel di Unix possiede circa 60-200 system calls. Tali, attraverso
opportune librerie, dispongono per ognuna di esse una interfaccia che astrae la systemcall come una chiamata a funzione.
I processi in Unix sono le uniche entit attive, e lunico modo per creare un processo in
Unix attraverso la primitiva Fork. In questo S.O. si fa riferimento ad una struttura
gerarchica dei processi (una fork, si dice, infatti, che genera un processo figlio dal
processo padre). Pi precisamente, in Unix in seguito al bootstrap il kernel crea un
processo init, che legge il file /etc/inittab per capire il numero di terminali da attivare. Per
ogni terminale crea un figlio che esegue il programma /bin/login, passando il nome di
login come argomento. Il programma login cerca il nome di login nel file /etc/password ed
eventualmente visualizza un prompt per richiedere la password. Il Login, quindi, gestisce
laccesso degli utenti al sistema, chiedendo username e password. La password inserita,
crittata, viene verificata che coincida con quella presente nel file /etc/password e di
seguito, in caso affermativo, esegue la shell.
La shell legge la linea di comando assumendo che la prima parola sia il nome di un
programma; cercato il programma lo lancia in esecuzione mediante fork e exec,

passandogli come argomento le rimanenti parole presenti sulla linea di comando;


attende, quindi, il termine del figlio per ripresentare nuovamente il prompt.
Gi abbiamo visto che ogni processo ha un unico identificatore (il PID), che un numero
intero tra 0 e 30000, assegnatogli dal kernel durante la sua creazione. Per ottenere lid di
un processo avviene attraverso la sys-call getpid(); poich abbiamo detto che in Unix i
processi sono organizzati in maniera gerarchica, ogni processo ha un processo padre
(eccetto init): per conoscere il pid del padre un processo pu invocare la getppid(). Alcuni
processi hanno dei PID speciali: lo scheduler ha PID=0; linit ha PID=1; il PageDaemon ha
PID=2.
Definiamo la funzione sleep(int sec) che sospende il processo (transita nello stato
blocked) per un numero finito di secondi. A questo punto possiamo definire la sys-call
Fork.
La creazione di nuovi processi avviene tramite la fork, che una funzione (sys-call) che
crea una copia esatta del processo originale. Per la propriet di Unix di codice rientrante,
padre e figlio condivideranno larea codice, mentre la U-area, stack, heap, area dati
globali, saranno copiate dal padre al figlio. In questo modo il processo figlio eredita gli
stessi valori delle variabili, i descrittori dei file aperti, il program counter, ma tuttavia le
modifiche che essi opereranno da questo punto in poi saranno non visibili allaltro.
Siccome il processo figlio eredita il PC, entrambi i processi ripartono dallistruzione dopo
la fork. A questo punto come si pu fare per differenziare il padre e il figlio? La funzione
fork ritorna un numero intero. Se pari a -1, significa che la fork non andata a buon fine,
se uguale a 0, significa che sono il figlio, se maggiore di zero sono il padre. Vediamo che
per i passi necessari allimplementazione della fork essa assai dispendiosa.
La sys-call wait permette al padre di raccogliere leventuale stato di terminazione del
figlio:
int wait(int* stato)
Restituisce lID del processo figlio che terminato. Se non esistono figli restituisce -1. Se i
figlio non sono ancora terminati, il kernel sospende il processo padre finch uno dei figlio
non termina. La variabile stato contiene il valore passato dal processo figlio alla sys-call
exit. In Unix se un processo terminato ma il suo genitore non ha ancora atteso la fine
(con wait) esso diventa zombie (in tal caso il kernel elimina tutto del processo tranne lo
stato di terminazione). Se, invece, il processo genitore termina prima dei suoi figli, essi
diventano orfani: in tal caso Unix pone lID del processo padre pari ad 1 (ossia figli di init).
La system-call exit serve per far terminare un processo. Quando essa viene invocata,
viene passato uno stato di uscita numerico intero dal processo al kernel. Tale valore
disponibile al processo padre attraverso la chiamata di sistema wait. Un processo che
termina normalmente, restituisce uno 0.
La system-call exec permette di eseguire un programma in un processo esistente. Tale
viene, quindi, eseguito, nel contesto del chiamante e non cambia pid. Il ritorno al
chiamante si ha solo se si genera un errore. Il processo, dopo lexec mantiene la stessa
process structure, ha codice, dati globali, stack e heap nuovi, si riferisce ad una nuova
area testo e mantiene solo informazioni circa la user-area (tranne PC e codice) e larea
stack del kernel. Nella programmazione moderna, nella maggior parte dei casi, dopo la
fork si chiama una exec, per cui loperazione di copia della memoria tra padre e figlio

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.

10. Sincronizzazione nel modello ad ambiente globale


Abbiamo visto che due processi, per sincronizzarsi possono usare un modello ad
ambiente locale e un modello ad ambiente globale. Nel modello ad ambiente globale
esiste il problema della competizione, ossia processi competono per lutilizzo di una
risorsa comune, e necessita un gestore per assicurare la mutua esclusione, deadlock e
starvation. La risorsa ad uso esclusivo che vuole essere utilizzata (ad esempio una
stampante) definita Risorsa Critica. La porzione del codice che utilizza la risorsa
denominata Sezione Critica. Il gestore delle risorse si occupa di risolvere problemi di:
deadlock, ossia una situazione di stallo, di attesa indefinita che pu avvenire quando si
creano processi in attesa a catena; starvation, dovuta alla priorit di un processo che pu
produrre in certe situazioni la non esecuzione di tale processo.
Per la mutua esclusione, necessita che un solo processo possa accedere alla risorsa
critica; se il processo sta lavorando nella sezione critica e si arresta, non pu lasciare la
risorsa occupata (non deve iterferire con altri processi) evitando starvation e deadlock. Se
la risorsa libera, il processo che la vuole usare non deve essere ritardato; un processo
che utilizza la risorsa critica deve utilizzarla per un tempo limitato; non bisogna fare
assunzioni su velocit di esecuzione dei processi (non dobbiamo considerare la Race
Condition). Il problema della mutua esclusione pu essere risolto via hardware,
disabilitando le interruzioni; in questo modo, un processo mentre utilizza la risorsa critica
non deve essere interrotto. Tuttavia un metodo pericoloso, e funziona solo con sistemi
monoprocessore: nei sistemi multiprocessore dovrei disabilitarli su tutti i core altrimenti
potrei avere un processo su un altro core che mi frega e accede. Il problema che, se un
processo crasha, le interruzioni restano disabilitate. Il meccanismo di interruzione non
utilizzato tranne dal kernel nel caso in cui debba effettuare specifiche operazioni non
bloccanti sul processore. Un altro meccanismo al supporto della mutua esclusione
attraverso le istruzioni di test & set lock. Tale istruzione si basa sul concetto di lucchetto
(se aperto->la risorsa critica libera; se chiuso-> la risorsa non libera). A tal proposito
possiamo allora definire due stati: 0, ad indicare la risorsa libera e che quindi il processo
pu accedervi; 1, ad indicare la risorsa occupata, e che quindi il processo deve attendere.
In sostanza allora il processo deve fare un check sul valore del lock. Il problema in questo
caso che se un processo vede il lock a 0, e successivamente viene schedulato, un altro
processo che vuole accedere alla risorsa vede il lock a 0 ed entra posizionandolo a 1;
quando il primo processo torna in esecuzione ricorda ancora del lock a 0 ed entra insieme
allaltro processo. Ci dovuto al fatto che la lettura e la scrittura del lock sono
operazioni distinte. Per ovviare a questo problema, i processori hanno introdotto la
istruzione TSL che fa le due operazioni allo stesso momento. Ossia:
TSL RX,LOCK va a leggere il contenuto di lock e lo scrive in RX; dopodich con una
compare si verifica il valore di RX; se 0, allora entra nella sezione critica, altrimenti
continua il ciclo. Queste operazioni sono eseguite in un unico ciclo, e sono quindi
indivisibili. Inoltre tale soluzione risolve anche il problema del multicore, bloccando il bus.
Poich la variabile lock appartiene allarea di memoria kernel, tale istruzione necessita di
una syscall. Utilizzando questa tecnica, si risolve il problema della mutua esclusione,

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.

11. Interprocess communication e Shared Memory


Il kernel Unix mette a disposizione tre diverse strutture dati che permettono la
comunicazione interprocesso, quali sono la memoria condivisa (SHM), i semafori (SEM) e
infine le code di messaggi (MSG). Ogni risorsa, allinterno del sistema identificata da
una chiave univoca nel sistema stesso, la IPC Key. Tale astrazione necessario in quanto
pu processi, nellaccesso ad aree condivise possono fare riferimento ad uno stesso
identificatore per una determinata risorsa (in questo modo sono certi di comunicare tra di
loro). Una IPC key pu essere cablata nel codice (definita dallutente), oppure pu essere
generata dal sistema mediante la primitiva ftok:
key_t ftok(char* path, char id)

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).

La creazione di un oggetto IPC provoca anche linizializzazione di una struttura dati,


diversa per i vari tipi di oggetto, che contiene una struttura di permessi ipc_perm, il PID
dellultimo processo che lha modificata, tempi dellultimo accesso o modifica.

11.1.2.

Control
Int ctl(int desc,.,int cmd,)

Dove:

Desc indica il descrittore della risorsa;


Cmd specifica il comando da eseguire, tra cui possiamo avere IPC_RMID che
rimuove la risorsa, IPC_STAT che visualizza statistiche informative della risorsa
indicata e IPC_SET che serve per modificare un sottoinsieme di attributi della
risorsa.

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:

Creazione della SHM;


Attach alla SHM;
Uso della SHM;
De-Attach della SHM;
Eliminazione della risorsa.

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

Per creare una memoria condivisa abbiamo bisogno della seguente:


int shmget(key_t key, int size, int flag)
che restituisce un identificatore per la memoria, il descrittore, nel caso sia tutto ok, -1 se
listruzione fallisce. Ovviamente la key rappresenta IPC della SHM (ottenuta tramite ftok o
esplicita o privata), size la dimensione della memoria in byte e infine, flag specifica la
modalit di creazione e dei permessi di accesso (IPC_CREAT, IPC_EXCL, permessi cos
come visto nella get in generale).

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.

Gestione dei semafori

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

Si parla di parallelismo quando possediamo un hardware che fa operazioni in parallelo


(ossia due core elaborano due processi differenti o threads), mentre si parla di
concorrenza quando si sfruttano i tempi morti del processore. Il parallelismo pu essere
implicito (ILP Istruction level) sfrutta il parallelismo del codice nelle istruzioni; esplicito
se si utilizzano architetture multiprocessore. Per concorrenza si fa riferimento al
multitasking (esecuzione di pi processi contemporaneamente) e multithreading (pi
flussi di controllo leggeri per ogni processo). Nelle architetture parallele, pi thread
possono eseguire su processori differenti, ognuno dei quali gestiti in concorrenza. Le
tecniche di parallelismo e concorrenza sono introdotte al fine di ottenere una
velocizzazione (speed-up) nellesecuzione dei programmi.

S=Ts/ Tp
Con

Ts

tempo di esecuzione sequenziale del processo e

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

che gira in un tempo

su un processore; sia

la

frazione di tempo dovuta allelaborazione di codice sequenziale e (1f ) la frazione di


tempo dovuta a codice parallelizzabile. Allora, ricordando la legge dello speed-up nella
sua definizione:

S=

fT + ( 1f ) T
n
=
( 1f ) T 1+ ( n1 ) f
fT +
n

Dove

il numero di core, in quanto le operazioni parallelizzabili sono distribuibili

sugli

core del processore. Notiamo quindi che lunico fattore che influenza lo speed-

up la quantit di codice non parallelizzabile: infatti se

f =0,

allora

coincide con

n (speed-up massimo).

13. Architetture Multiprocessore


Le architetture di elaborazione le possiamo suddividere in SISD (Single Istruction Single
Data Stream), ove un singolo processore elabora in sequenza informazioni che operano
su dati esistenti in una singola memoria, i SIMD (Single Instruction Multiple Data Stream)
ove ogni istruzione eseguita su un insieme di dati di differenti processore (ho una
singola istruzione che ha effetto su un vettore di dati), i MISD (Multiple Instruction Single
Data Stream) in cui un insieme di dati inviato a differenti processori che elaborano in
sequenza le istruzioni e infine i MIMD (Multiple Instruction Multiple Data Stream), che
eseguono pi istruzioni su dati differenti simultaneamente. I MIMD si possono classificare
in shared-memory (memoria fisica condivisa tra processi: valido per processi molto
accoppiati) e distributed-Memory (valido per processi poco accoppiati), il cui esempio
sono i cluster. Per i Sistemi MIMD Shared Memory possiamo indicare i Master-Slave, in cui
identifichiamo un processore master che decide il lavoro per gli slave e gli SMP
(Simmetric MultiProcessor) in cui pi core accedono alla stessa area di memoria e il
multicore trattato in maniera simmetrica (non esiste gerarchia tra i core). Negli SMP, il
kernel pu essere eseguito su ogni core, e quindi eseguire differenti task del kernel in
parallelo. Vedremo come gestire le problematiche affrontate nel single core anche nel
SMP, analizzando quindi lo scheduling, la sincronizzazione, la gestione della memoria.
Negli SMP ho il parallelismo di esecuzione dei processi: essi sono smistati su pi core; per
rendere tale propriet possibile per i task del kernel devo far in modo che il codice kernel
sia rientrante. In questo modo posso servire, ad esempio, due syscall parallelamente.

13.1.

Scheduling nelle architetture multiprocessore

Le problematiche da affrontare sono:


-

Come assegno i processi ai core?


Come gestisco il multitasking?
Come gestisco il dispatching (scelta) del processo?

Le scelte di soluzione a queste problematiche dipendono dal grado di sincronizzazione e


dal numero di core. Il grado di sincronizzazione rappresenta la frequenza di
sincronizzazione dei processi in esecuzione allinterno del sistema. Distinguiamo a tal
proposito:
-

Processi Indipendenti, se tali processi non hanno punti di sincronizzazione. Un


esempio di applicazione di tale parallelismo relativo a sistemi time-sharing. In
questo caso il multicore si comporta come un single core multiprogrammato ma il
tempo di risposta tuttavia pi breve.
Processi con livello di granularit fine, pi alto, se il parallelismo inerente ad un
singolo flusso di istruzioni.
Processi con livello di granularit medium, tipico di threads allinterno di un
singolo processo, ove il programmatore ad esplicitare il parallelismo
dellapplicazione. In effetti, ai threads associato tale livello in quanto quando si
creano threads allinterno di singoli processi si vuole velocizzare le interazioni tra
essi, evitando la creazione di nuovi processi e conseguente memoria condivisa per
comunicare.
Processi con livello di granularit coarse e very coarse, con sincronizzazioni
non troppo frequenti.

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

sono caratterizzati da un PID e un gruppo di task correlati identificato da un thread


Group ID (TGIP). Ad ogni task attribuita una priorit che ne determina i criteri di
schedulazione, e quindi, qual lordine di esecuzione, il quanto di tempo associato ad
ogni task. Linux distingue due categorie di task (real-time e convenzionali) con 140 livelli
di priorit: di questi i primi 100 (0->99) sono dedicati a task RT mentre gli altri 40
rimanenti (da -20 a +19 ==100->139) sono dedicati a task convenzionali. Per task
convenzionali e per task RT si adottano diverse politiche di schedulazione. In sostanza
poich i task RT sono prioritari rispetto a quelli normali, finch c un task RT non c
possibilit di schedulare un task convenzionali. Per questi ultimi si parla di normal
scheduling. Lo scheduling effettuato mediante la funzione schedule(), che pu
intervenire nel caso di una interruzione, quando scade il quanto di tempo, quando
termina una syscall o esplicitamente in opportune parti del kernel. Tale funzione allora
effettua la schedulazione del task a priorit maggiore (tra quelli pronti). Effettua anche
altri compiti come aggiornare la priorit o il load balancing tra CPU. Ricordiamo, infatti
che nel multicore, Linux adotta una approccio load balancing (una coda per ogni
processore con il processo che pu saltare da una coda allaltra).
Dal kernel 2.6, Linux adotta lo scheduler O(1), pensato per avere complessit che non
dipende dal numero di thread da schedulare. In questo modo ottengo overhead costante
indipendente dal numero di processi presenti nel sistema. Come gi anticipato, tale
kernel supporta il multicore, adottando una politica di load balancing dotando, quindi,
ogni singolo processore di una coda di processi pronti. I problemi relativi a
questapproccio vengono in parte risolti adottando il concetto di affinit: processi affini
vengono raggruppati su un singolo processore e la migrazione dei processi prevista solo
per motivi di bilanciamento del carico. Tale soluzione risolve, almeno in parte il problema
di invalidazione della cache. Lo scheduler O(1) adotta una politica per cui il tempo
impiegato per scegliere quale processo schedulare costante e non dipende dal numero
di processi nel sistema. Ad ogni processore associata una runqueue, che una lista di
processi in attesa di essere eseguiti sul processore. Ogni runqueue ha due array di
priorit: expired, ossia i task che hanno esaurito interamente il quanto di tempo; active, i
task che non hanno ancora esaurito il quanto di tempo assegnatogli. I processi attivi
vengono serviti prima degli expired. Quando esauriscono i processi expired diventano
active e eseguiscono. Ogni array di priorit ha tante code quante sono le priorit (0>139). Lalgoritmo sceglie, quando viene invocata la schedule(), il primo processo active
a priorit pi bassa. Ad esempio, supponiamo che abbia un processo I/O bound che prima
della fine del quanto di tempo effettua una syscall bloccante (richiesta I/O); esso viene
rimesso nella coda active. Se esaurisce il quanto di tempo prima di effettuare la richiesta
bloccante verr messo nella coda expired.
Per quanto riguarda le priorit, esse possono essere statiche o dinamiche. Ogni processo,
inizialmente ha una priorit statica, che nel corso del tempo pu essere modificata (ad
esempio, se ho un processo che rimasto bloccato per tanto tempo (I/O) al fine di farlo
eseguire subito ne aumento la priorit. Quindi, pi un task bloccato pi aumento la sua
priorit al fine di privilegiare i sistemi I/O bound. Il quanto di tempo (time-slice) dipende
dalla priorit. Pi ho una priorit alta, pi grande il quanto di tempo.
Entrando nel dettaglio dellalgoritmo O(1), ad ogni runqueue, per ogni active e expired,
associata una matrice (bitmap) di 140 bit. Trovare il primo processo pronto (active)
significa trovare il primo bit alto nella bitmap associata ai processi active. Tale approccio
non lideale per sistemi SMP, in quanto non permette gang scheduling e perch il load

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.

15. Sincronizzazione nel modello ad ambiente locale


Abbiano finora visto come, nel modello ad ambiente globale, i processi si
sincronizzassero in unarea di memoria condivisa tra essi, tenendo fede ai principi per
non lasciarla in uno stato inconsistente rispettando quindi lesclusione mutua e cos via.
Talvolta, la soluzione di unarea condivisa tra pi processi pu essere inadatta
allesigenze di comunicazione dei processi (ad esempio, supponiamo un correttore
ortografico di un editor di testo, che dopo ogni parola digitata deve controllare eventuali
errori; se utilizzassimo una shared memory, ad ogni parola dovremmo andare in memoria
a scriverla su un buffer e attendere il correttore che acceda e verifichi e nel caso tornare
per prendere la parola corretta). Lapproccio ad ambiente globale ha dei grossi svantaggi
in alcuni casi per cui si preferisce lapproccio ad ambiente locale. Innanzitutto scompare il
concetto di memoria condivisa: i processi iniziano a comunicare scambiandosi messaggi.
Tale approccio garantisce apriori la mutua esclusione in quanto ogni processo dotato
della propria area locale. Il SO mette a disposizione delle primitive per lo scambio di
messaggi, che sono la send(destinazione, messaggio) e la receive(sorgente,messaggio),
in cui la prima serve per inviare un messaggio ad una destinazione e la seconda serve
per ricevere un messaggio da una sorgente. Per tali primitive possiamo avere delle
versioni bloccanti e non bloccanti (ad esempio, una send bloccante aspetta che il
messaggio sia stato ricevuto). Un altro aspetto da tener in conto durante
limplementazione di queste primitive lindirizzamento. Un messaggio
sostanzialmente formato da un header (origine, destinazione, msgType, msgLenght,
ctlInformation) e dal messaggio vero e proprio.

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.

La send asincrona quella pi utilizzata nella programmazione concorrente, ed anche


quella implementata in UNIX. Il problema consiste nel verificare che il messaggio sia stato
ricevuto: tale demandato ad un controllo a livello applicativo. Un altro problema risiede
nella perdita dei messaggi che deve essere opportunamente gestita.
procedure sendSincrona(dest, mess) {
sendAsincrona (dest, messRTS) /* messRTS un messaggio di pronto
ad inviare
(Request To Send) */ receiveBloccante (dest, messOTS) /* messOTS un messaggio
di pronto
a ricevere (OK To Send) */ sendAsincrona (dest, mess)
}
Abbiamo detto che il secondo problema da risolvere era quello relativo allindirizzamento.
Esso pu essere diretto o indiretto. Lindirizzamento diretto pu essere simmetrico, se il
mittente conosce la destinazione e viceversa (vado a inserire in sorgente e destinazione i
pid dei rispettivi processi) o asimmetrico, se il mittente conosce il destinatario ma NON
viceversa.
Nellindirizzamento indiretto nessuno sa niente, facendo riferimento ad una mailbox in cui
il sender deposita e il receiver preleva. In questo caso allora i messaggi vengono inviati
ad una struttura condivisa, la mailbox, che che implementata come una coda. Tramite
essa possiamo andare a specificare meccanismi di comunicazione one-to-one, one-tomany, many-to-one & many-to-many. Il many-to-one sarebbe larchitettura client-server
in cui il processo servitore incapsula qualche risorsa e accoglie richieste dei client e le
elabora.

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

che mentre possegga questa risorsa ne richieda unaltra Q senza rilasciare R.


Analogamente il secondo processo abbia bisogno della risorsa Q e innestatamente della
risorsa R. Evidentemente lesecuzione concorrente di questi processi PUO causare
deadlock (non si pu affermare il causare poich pu darsi che un processo sia pi veloce
di un altro e che riesca ad effettuare le sue operazioni rilasciando la risorsa disponibile).
Per tal motivo si dice che la situazione di blocco critico dipende dalla velocit relativa dei
processi. In un SO possiamo avere sia risorse riusabili (ad es. le memorie) sia risorse
consumabili (ad es. il messaggio). In generale, per ciascuna risorsa riusabile ne esistono
diverse istanze (es. un buffer con pi celle). In questo caso la situazione di deadlock
meno probabile che si verifichi poich i processi possono accedere alla risorsa utilizzando
diverse istanze, le quali sono accedute in mutua esclusione, supponendo che ogni
processi rispetti la sequenza richiesta-uso-rilascio. Anche se pi difficile il deadlock lo
stesso verificabile: ad esempio, supponiamo di avere a disposizione un buffer di 200KB; e
supponiamo che un processo abbia bisogno in due istanti separati di 80K e 60K mentre
un processo concorrente abbia bisogno di 70K e in un secondo tempo di 80K. Se tali
processi sono eseguiti, possono causare deadlock: infatti, se accolgo la richiesta degli
80K del primo processo e i 70 del secondo, blocco ambo i processi perch non posso pi
allocare memoria (+60=210;+80=230). Le risorse consumabili sono quelle che vengono
create e distrutte, di cui ne possiamo trovare un numero infinito. Il loro utilizzo pu altres
portare a situazioni di deadlock. Infatti supponiamo che due processi inviano e ricevono
un messaggio e supponiamo che ambo adottino una receive bloccante; se il primo si
mette in attesa e il secondo si mette in attesa nessuno dei due potr inviare il messaggio
=> Stallo.
Per evidenziare le situazioni (possibili) di stallo possiamo utilizzare un grafo che detto
Resource Allocation Graph. Tale caratterizzato da un insieme di nodi V e di archi E. I
nodi possono essere processi o risorse, mentre gli archi possono essere di richiesta (da
processo a risorsa) o di assegnazione (da risorsa a processo). In ogni risorsa in numero di
token rappresenta il numero di istanze della stessa. Dallanalisi di un RAG possiamo
condurre le seguenti considerazioni:
-

Se il grafo non contiene cicli, allora non possibile avere stallo;


Se il grafo contiene cicli e ogni risorsa ha una sola istanza (risorsa interessata nel
ciclo) allora c stallo;
Se il grafo contiene cicli ma ogni risorsa ha pi istanze c la possibilit di
deadlock.

Il deadlock un problema complesso che in determinati ambiti (critici) di fondamentale


importanza evitare perch pu provocare malfunzionamenti che possono essere molto
gravi. In sostanza in una possibile situazione di deadlock in unapplicazione, essa pu
manifestarla tanto subito tanto tra anni. Ci dipende dalla velocit relativa dei processi
(race condition). In tale affermazione presente la complessit del deadlock, che quindi
non rilevabile istantaneamente. Ovviamente il problema cresce con lintroduzione di
threads e multi-core perch, avendo pi processi paralleli c possibilit maggiore di
avere blocchi su risorse richieste. I sistemi operativi, in genere, non definiscono alcuna
soluzione al deadlock.
Un deadlock pu avvenire (condizioni necessarie) se sono valide contemporaneamente le
seguenti condizioni:

Le risorse possono essere utilizzate in mutua esclusione;


I processi trattengono le risorse che gi possiedono mentre richiedono altra
(possesso e attesa);
Le risorse gi assegnate ad un processo non possono essere sottratte (impossibilit
di prelazione).

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:
-

Attesa Circolare: esiste un processo P che in attesa di una risorsa posseduta da A


che a sua volta in attesa di una risorsa posseduta da B che a sua volta.che a
sua volta in attesa di una risorsa posseduta da P.

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

R j Cnj + Cij per ogni j


i=1

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

di un blocco critico pu essere ottenuto in differenti modi. La strategia pi comune


quella di abortire i processi che perseverano in uno stato critico. Altrimenti per evitare
una eccessiva perdita di informazioni si pu forzare labort di un processo alla volta con
conseguente rilascio di risorse, oppure vengono da esso sottratte le risorse. In questo
caso si ha il bisogno di portare il processo in uno stato consistente: per tale motivo si
necessita la presenza di checkpoint, ossia dei punti di salvataggio del processo.

17. Gestione della memoria


La memoria principale, cos come la CPU rappresentano una delle risorse necessarie a
supporto del concetto di processo. Infatti, anche se fin ora abbiamo parlato solo di come
la CPU gestisce ed alloca risorse ai processi, abbiamo dato per scontato laspetto
riguardante la memoria. In sostanza abbiamo definito un processo come un programma
in esecuzione. Ma dove risiede questo programma? Evidentemente deve essere allocato
in memoria, quindi possiamo dire che un processo identificato da una unit di
elaborazione e unarea di memoria principale ove esso allocato. Gi dalla definizione di
processo possiamo assumere che esistono analogie e differenze nella gestione di
memoria e CPU. Abbiamo detto che un processo un programma in esecuzione; esso,
pu, alloccorrenza, richiedere il supporto di qualche risorsa (es. periferiche di I/O) tramite
opportune chiamate di sistema, attraverso cui poi queste risorse possono essere allocate
(o il processo viene posto in attesa (blocked) che la risorsa si liberi). Una volta acquisita e
utilizzata la risorsa essa va rilasciata per permetterne lutilizzo da parte di altri processi.
Essenzialmente, un processo esegue su una unit di elaborazione quindi non ha senso
per il processo andare a richiedere la risorsa CPU, in quanto se gi in esecuzione esso gi
la possiede. Analogamente per la memoria; un processo un programma in esecuzione,
che viene allocato in memoria, quindi, pur s necessario avere memoria, evidente che
non il processo a richiederla, perch eseguendo gi la possiede. Quindi affinch un
processo possa essere in esecuzione deve avere allocata ununit di elaborazione e una
memoria. Per quanto riguarda la CPU, abbiamo detto anche che spesso il numero di
processi in esecuzione superiore al numero di core del processore. Poich vale lasserto
che ad ogni processo deve essere associato ununit, per risolvere questo problema
nato il concetto di virtualizzazione, secondo cui, in base allalgoritmo di scheduling, ogni
processo fa uso esclusivo di un processore virtuale; quindi anche se ho pi processi e un
solo core, posso assegnare il processore a tutti i processi in maniera alternata sfruttando i
tempi morti della stessa CPU. Analogamente nel caso della memoria principale. Ad ogni
processo viene associata una memoria virtuale. Anche in questo caso, poich la somma
delle memorie virtuali superiore alla dimensione della memoria fisica, compito del SO
gestire questa situazione allocando dinamicamente memoria ai processi.

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.