Sei sulla pagina 1di 25

ANALISI DELLO SCHEDULING DEI PROCESSI

Introduciamo ora tutte le generalità per il Process Management (attività fondamentale del SO).

Per ciò che concerne la nomenclatura, in modo più o meno indistinto, si farà utilizzo dei termini:
-JOB;
-TASK;
-PROCESS.

Questi termini sono sinonimi solo in italiano perché in italiano tutto ciò si traduce con “Processo”.

In realtà, utilizzando una


nomenclatura americana, c’è una
distinzione esatta tra i tre termini.
Process e Job sono “grossomodo”
sinonimi mentre Task è una sotto
attività computazionale di un job.

Dentro un job, abbiamo un


insieme di processi e ciascuna
delle attività computazionale dei
processi è suddivisa in task. Molto
spesso un task viene identificato con un flusso di controllo del processo e quindi con un Thread. Il SO deve
incaricarsi della gestione e della schedulazione a livello minimo del process ma anche del thread control per
i SO multithread di più recente diffusione.

Che cosa contiene un processo e cosa è un processo?


Un programma (codice) mandato in esecuzione (e quindi vitalizzato) diventerà un processo. In quanto tale
un processo si compone di due parti fondamentali:

 Sezione di codice: rappresenta un file salvato su disco contenente un set di istruzioni che
compongono il processo e che quindi verranno nella sequenza di esecuzione, prima o poi, mandate
in “run” (saranno materialmente eseguite). La sezione di codice è STATICA e questa parte resta così
finché il processo non va in conclusione (fissato esclusivamente un PID (Process IDentifier) avremo
fissato una sezione di codice).
 Attività corrente: non è STATICA ma viene valorizzata e vitalizzata DINAMICAMENTE con
l’esecuzione. L’attività corrente per un processo rappresenta un insieme di informazioni che
riguardano, in primis, i registri della CPU (i quali assumono un valore diverso a seconda dei casi, man
mano che il processo va in avanti con la propria computazione). Inoltre, rappresenta anche il program
counter, cioè quell’indice del livello di esecuzione in cui ci troviamo che punta alla successiva
istruzione da eseguire (anche il program counter si aggiorna man mano che il processo evolve).
Un processo include anche il proprio Stack che contiene i dati temporanei ed una sezione dei dati
che mantiene al suo interno le variabili globali del processo in esecuzione. Le variabili globali sono
tali, per tutta la durata del processo, e fanno parte della sezione dati e vanno in un’area statica che
è l’area dati. Può essere anche incluso uno Heap, cioè la memoria allocata dinamicamente in fase di
esecuzione per contenere i dati. L’attività corrente del processo è specifica per un processo in un
dato istante (fissato un PID ed un intervallo temporale avremo fissato un’attività corrente). Quello
che è l’attività corrente viene denominata un “contesto computazionale della macchina” perché
rappresenta quello che è lo stato di esecuzione di un processo eventualmente in utilizzo dalla CPU.
Questo significa che è possibile differenziare due processi semplicemente per l’attività corrente, cioè è
possibile prendere la stessa sezione di codice e avere due diverse istanze di essa (quindi differente attività
corrente) per poter dire che quei processi sono diversi: è il caso dei PROCESSI MULTIISTANZA. È possibile
quindi distinguere ognuno dei processi sulla macchina in base allo stato in cui esso si trova.

STATO DEI PROCESSI


Un processo in esecuzione evolve assumendo stati diversi, esso può trovarsi in uno dei seguenti:

 NEW: il processo è stato creato, cioè significa che un programma viene caricato in memoria centrale
diventa processo e gli verrà associato un PID;
 RUNNING: il processo è in esecuzione ovvero che sta materialmente utilizzando la CPU, quindi le
istruzioni vengono eseguite;
 READY: il processo è pronto all’esecuzione avendo tutte le risorse disponibili ma è in attesa di essere
assegnato alla CPU;
 COMPLETED (TERMINATED): È un processo che è terminato ed ha completato la propria esecuzione
ma non è stato ancora rimosso dalla memoria;
 WAIT O WAITING: Sono tutti quei processi diversi dagli stati precedenti che sono in uno stato di
fermo, detti anche “processi dormienti”.
Le ragioni del fatto che un processo si trovi in stato di WAIT sono tre: 1) è in attesa di una
segnalazione; 2) è in attesa che venga completata un’istruzione di I/O da lui lanciata; 3) è in attesa
che gli venga associato un dispositivo o un insieme di dispositivi.

Riepilogando, appena si sottomette un processo nella macchina, esso viene


classificato come NEW e gli verrà associato un PID, che è un identificativo univoco
che nasce e muore con il processo e che ci permette di citarlo ed identificarlo con
precisione in tutti i momenti della sua vita. Dopo che il processo è stato creato,
esso chiederà l’utilizzo di dispositivi, periferiche e memoria, per poter entrare nella
propria computazione: nell’attesa che il processo ottenga dei device, passerà in
WAIT; appena ottiene i device può mettersi in contesa per l’utilizzo della CPU, e
quindi passa nello stato di READY. In questo stato, ci sono tutti i processi che hanno
a disposizione le periferiche della macchina, ma non hanno ancora a disposizione
la CPU. Appena ottiene l’accesso alla CPU, passa nello stato di RUN, quindi va in
esecuzione; infine può uscire dall’esecuzione o perché gli viene requisita la CPU (avendo finito il time-slice) o
perché va nuovamente in wait (attesa di I/O o signal) oppure perché va in COMPLETED.
L’evoluzione di un processo all’interno di una macchina è un’evoluzione di stato continua: il processo passa
in questi stati un numero indefinito di volte a seconda della propria computazione. L’evoluzione dipende da
processo a processo ma dipendono anche dalla particolare situazione della macchina, intesa come stato SW
(insieme di altri processi) ma anche come HW (situazione specifica HW). Questi tre elementi non sono
correlati tra loro, quindi non possiamo prevedere in modo deterministico quale sarà l’evoluzione dei processi
della macchina; sarebbe infatti fantastico capire a priori quali sono le evoluzioni dei processi, questo
renderebbe l’algoritmo di scheduling molto più semplice, algoritmo che al momento è la cosa più complicata
che esista all’interno di un sistema operativo.

Dal punto di vista della transizione di stato l’evoluzione di un processo, all’interno di una macchina, passa
attraverso un diagramma fisico (DIAGRAMMA DI TRANSIZIONE DEGLI STATI DEI PROCESSI).

Un processo nello stato di NEW finisce nella CODA DI SUBMIT (contiene tutti i processi che devono essere
caricati all’interno della memoria). La coda di submit è una coda FIFO (First In First Out), i processi non hanno
un ordinamento diverso se non il criterio di arrivo.

La coda successiva è la CODA DI HOLD, qui c’è un ordinamento che tiene conto della richiesta dei device, cioè
mette in cima alla coda quei processi che richiedono dispositivi che all’atto della richiesta sono disponibili:
per verificare la disponibilità dei dispositivi il SO interroga la Device Status Table (contiene anche lo stato di
disponibilità dei processi), e quando la richiesta su un processo non va a buon fine, quest’ultima viene
accodata. Ad interrogare la DST è il SO, ed in particolare il MACROSCHEDULATORE (o job scheduler o
macroscheduler). Il macroschedulatore è quel componente del S.O. che interroga i vari gestori delle risorse
(memory manager, file manager, disk manager, I/O manager, etc..) per poter verificare la disponibilità delle
periferiche. Il nome deriva dal fatto che organizza i processi in una fase macroscopica preliminare e dovendo
lavorare con le periferiche ha una frequenza di aggiornamento dell’ordine del minuto (nella migliore delle
ipotesi del secondo). Questo perché interroga la DST che a sua volta è agganciata ai device che hanno
frequenza di aggiornamento dell’ordine del secondo/minuto. Il job scheduler organizza la coda di hold,
attraverso i propri gestori, quindi all’interno della coda di hold avremo una primaria organizzazione
(ALGORITMO DI MACROSCHEDULAZIONE) che metterà in cima i processi che hanno la disponibilità di tutte
le risorse richieste, eccetto la CPU.

Quando entrano nella coda di ready vengono riorganizzati utilizzando un algoritmo di organizzazione
particolare detto ALGORITMO DI MICROSCHEDULING. Il microschedulatore (o process scheduler) è
completamente diverso dal macroschedulatore perché viene invocato e si aggiorna con la frequenza della
CPU (invece che dei device) quindi con tempi nell’ordine dei 10-9 secondi. Un componente SW che deve
lavorare con tempi di 10-9 sec, quindi un miliardesimo di secondo circa, comporta complessità e costi: difatti
il microscheduler è il componente più costoso e più sofisticato del SO. Generalmente un progetto di un buon
SO ruota attorno al microscheduler (sono quasi tutti ancora in linguaggio macchina nonostante il SO sia
orientato agli oggetti) e sono il nucleo fondamentale (che è essendo iper-ottimizzato, viene spesso riciclato
cambiando il progetto del SO) di un progetto di SO attorno al quale andiamo a mettere un’interfaccia grafica
migliore, intelligenza artificiale, ecc.
Dentro la microschedulazione, quello che funziona è l’algoritmo di microscheduling. Il microschedulatore,
cioè, dà priorità, secondo un algoritmo specifico, a quello o quei processi che secondo quell’algoritmo, hanno
maggiore dignità di utilizzo della CPU.

Quando un processo è in cima alla CODA DI READY ha la disponibilità dei device tranne della CPU (però è tra
quelli che hanno più diritto a richiedere l’utilizzo della CPU essendo in cima alla coda) e passando in run, avrà
la CPU. Run non è una coda ma uno stato: è multistato se la macchina è multiprocessore, quindi se un
processo esce da una coda di ready e può finire indistintamente in più run diversi, significa che si è in presenza
di una macchina multicore o multiprocessore. In generale però, run è uno stato singolo. Se un processo è in
run, cioè in esecuzione, sta usando la CPU e a quel punto possono accadere 3 cose fondamentali:

 finisce la computazione complessivamente, quindi da run va a finire in completed (il processo ha


esaurito il proprio ciclo di vita, va in completamento in attesa di essere eliminato dalla memoria);
 fa un’operazione di I/O o una comunicazione interprocesso, quindi attende un segnale di risveglio, o
in generale è in attesa di un altro passaggio in CPU, comunque sia va in wait (ovvero va in uno stadio
che non è di esecuzione bensì di sleeping);
 esaurisce il time-slice d’esecuzione e a quel punto viene messo fuori, cioè si ha il cosiddetto CONTEXT
SWITCHING, scatta la protezione hardware e ritorna di nuovo all’interno della coda di ready (un
processo che entra in run e non ne esce mai, è un processo fraudolento che si è impadronito della
macchina).

Quindi da run posso passare in: completed, wait o di nuovo in coda di ready. Nel momento in cui si fa la
riorganizzazione con il process scheduler, e mi trovo nuovamente in coda di ready posso ripercorrere
nuovamente il ciclo e quindi rifare run, wait ed eventualmente completed. L’insieme degli stati ready, run e
wait si chiama CICLO DI ESECUZIONE di un processo, cioè un ciclo che fa il passaggio da ready a run, poi a
wait e ritorno, è un ciclo complessivo di esecuzione del processo (tutto quello che succede in questi salti è
comunque esecuzione).

Cosa sono materialmente e operativamente queste code?


Esse sono delle strutture dati, in particolare di tipo dinamico, cioè delle liste ordinate: la testa della coda di
ready contiene il puntatore al primo e all’ultimo PCB della lista; ciascun PCB contiene un campo puntatore al
prossimo PCB nella lista.

Le code di READY, di HOLD e di SUBMIT non sono altro che liste unidirezionali di PCB; dal punto di vista
ingegneristico, le vedo rappresentate come delle liste di PCB. Quindi dire che un processo con un dato PID è
in cima alla coda di ready, significa dire che la lista unidirezionale di PCB vede nell’elemento di testa il process
control block di quel processo, e appena questo processo viene mandato nella coda di wait, bisognerà
mettere in BROKEN questo link, in modo tale da avere il processo successivo a quello in questione, come
testa della lista. Mettere in broken un link, cioè sostituire un processo da una coda all’altra è una cosa
rapidissima; se un dato processo passa dalla coda di ready alla coda di wait significa che questo link lo dovrò
valorizzare verso il processo di PCB che sta nella coda di wait; per questa ragione è tutto quanto
estremamente veloce, le procedure di passaggio di coda e quindi di modificazione degli stati sono delle
sostituzioni di indirizzi, cioè sostituzioni di puntatori.

Inoltre, è presenta anche una CODA DELLE PERIFERICHE DI I/O che contiene i processi in attesa di una
particolare periferica di I/O.

Cosa è il Process Control Block (PCB)?


Ogni processo alla sua creazione (new) viene rappresentato nel SO da una struttura dati che si chiama
PROCESS CONTROL BLOCK (PCB), cioè un abstract in C e quindi una struttura dati composta, che all’interno
contiene tutte l’attività corrente del processo. Essa contiene:

 Stato del processo: new, ready, wait, ecc.;


 PID: il numero identificativo del processo;
 Registri della CPU: accumulatori, registri indice, stack pointer, ecc. Sono tutte
informazioni che devono essere salvate quando avviene un’interruzione;
 Informazioni per la gestione della memoria centrale: ad esempio i valori dei
registri base e limite, tabelle delle pagine o dei segmenti;
 Informazioni per l’accounting: ad esempio la quantità di CPU utilizzata, i limiti di
tempo, ecc.;
 Informazioni sullo stato dell’I/O: ad esempio la lista dei dispositivi di I/O allocati al
processo;
 Informazioni per la schedulazione della CPU: ad esempio la priorità del processo o
qualsiasi altro parametro di schedulazione.

Si può interpretare come se fosse la carta d’identità del processo e tutto lo stato che lo stesso processo ha
realizzato, da quando nasce (NEW) fino a quando non verrà smaltito (COMPLETED).
CONTEXT SWITCHING
La prima cosa da vedere è relativa alla sostituzione di un processo all’interno di uno stato di run con
l’operazione chiamata CONTEXT SWITCHING; essa punta a sostituire il processo in CPU da un processo
corrente al processo direttamente successivo in coda di ready. Quello che abbiamo all’interno della CPU è un
processo corrente che ad un certo punto esce perché va in wait o va in complete o torna in ready e quindi dà
spazio al processo immediatamente successivo all’interno di stato di run.

L’operazione per cui si sostituisce il processo nello stato di run è appunto il context switching: esso consiste
nel salvataggio del contesto computazionale del processo corrente (uscente), dove il contesto
computazionale altro non è che lo stato dell’esecuzione ed è rappresentato nel PCB del processo. Quindi
tutto quello che è contenuto nei registri, lo stato del processo e altre informazioni devono essere salvate su
una memoria gerarchicamente più bassa e non volatile, in particolare la cache e poi nella memoria di massa.
Una volta che viene salvato lo stato del contesto del processo corrente allora può essere scaricato dallo stato
di run attraverso uno swap-out, e viene caricato il processo nuovo subentrante con la sovrascrittura, senza
cancellare nulla, semplicemente con uno swap-in. Quindi vengono caricati i registri degli indirizzi presi dal
PCB del processo subentrante e messi a partire dalla RAM in cache e poi dalla cache all’interno dei registri di
CPU. Quando l’informazione del processo va dalla cache ai registri di CPU si sta spostando il contesto
computazionale, cioè sostituendolo.

La sostituzione del contesto, cioè lo swap out del processo uscente e lo swap in del processo subentrante
NON È A COSTO ZERO, ha cioè una durata non nulla, ed è eseguita da un componente che si chiama
DISPATCHER: Il tempo necessario a fare il context switching prende il nome di DISPATCH LATENCY (latenza
del dispatcher). Quindi è evidente che nel momento in cui sostituiamo un processo all’interno dello stato di
run, bisognerà tener conto, all’atto della scelta dell’algoritmo di scheduling, del numero di schedulazioni fatte
a parità di throughput. Cioè, bisognerà stabilire il numero di sostituzioni fatte all’interno di un tempo di
calcolo complessivo, in considerazione del
fatto che troppe sostituzioni comportano
un ingrandirsi della dispatch latency
complessiva e quindi un aumentare del
tempo speso in sostituzioni di contesto
computazionale.

Seguendo l’immagine possiamo capire


cosa significa procedere al cambiamento
di uno stato. Abbiamo tre linee di tempo,
quella del processo P0 che è in esecuzione,
quella del sistema operativo e quella del
processo P1. I processi sono alternativi, in
considerazione del fatto che abbiamo un
sistema monoprocessore.

Incomincia P0 in esecuzione: ipotizziamo


che ad un certo punto venga effettuata una chiamata di sistema I/O od un interrupt come per esempio un
evento imprevisto oppure un evento di protezione HW; in tutti questi casi si deve sostituire il processo in
esecuzione, quindi bisogna cambiare contesto ed il controllo passa per un’istante al sistema operativo, che
salva lo stato dell’esecuzione cioè il contesto computazione nel PCB di P0, quindi salva i contenuti dei registri
nella cache e poi dalla cache nel PCB del processo P0 ed una volta fatto questo il processo in esecuzione viene
swappato fuori. Successivamente, il SO caricherà, a partire dal processo in cima alla coda di ready (cioè il
primo che ha il diritto ad utilizzare la CPU), lo stato; quindi prenderà il PCB del primo processo in coda di
ready e caricherà all’interno della cache memory, e poi dalla cache memory al set di registri della CPU, i valori
fondamentali che servono per partire con la computazione (cioè program counter, instruction register, i
valori dei registri general porpuse, registri accumulatore, gli indirizzi base e limite per i controlli della
protezione della memoria, stack e heap pointer). Al termine di questo passaggio, il processo è
completamente configurato e ripartirà da solo, quindi dal momento in cui verrà caricato lo stato dal PCB del
processo P1, riparte il processo P1 fino ad un nuovo interrupt o chiamata di sistema, cioè fino a quando il
processo P1 avrà pieno diritto ad utilizzare la CPU. Questa operazione è ripetuta per tutti i processi in coda di
ready in sequenza, un numero imprecisato di volte, a seconda di quella che è la situazione dei processi
all’interno della macchina e a seconda di quello che è lo stato dell’algoritmo di scheduling di CPU.

Il tempo per il cambio di contesto computazionale è un tempo, come abbiamo detto, non nullo. Si chiama
DISPATCH LATENCY, e poiché è un tempo di load and store, è un tempo condizionato dalla natura dell’HW.
Si può dire che la dispatch latency è puro tempo di gestione del sistema, poiché durante il cambio non
vengono compiute operazioni utili per la computazione. Esso si può ridurre, ma non azzerare, sia attraverso
miglioramenti HW (ad esempio prendendo cache più ampie e memorie più veloci) sia facendo in modo che il
numero di cambi di contesto deve essere commisurato al TROUGHTPUT (numero dei processi completati
dalla macchina per unità di tempo), in maniera tale che il tempo complessivo per la dispatch latency, che
chiameremo TDispatchLatency, risulti essere molto minore del tempo complessivo di CPU (TDISPATCH LATENCY<<TCPU).
Una buona schedulazione è tale, se il numero dei cambi di contesto è mantenuto sotto controllo.

Vedendo in una time-line, l’esecuzione e l’evoluzione di un certo processo ed il successivo, in linea teorica
prenderà un tempo non nullo, cioè quando si va a calcolare la computazione del processo, avremo del tempo
speso in calcolo e del tempo detto tempo tecnico, che si associa al SO in cui vi è il processo. Quindi, se un
processo inizia in un dato istante tA e termina in un dato istante tB, non sarà possibile far cominciare il
processo successivo da tB ma esso partirà da tB+Δt, con Δt fondamentalmente piccolo rispetto all’intervallo
di tempo di computazione. Questo intervallo di tempo Δt si può schiacciare e ridurre, ma non annullare, e
dipende dall’HW. Esso non è gradito dal punto di vista della computazione, perché in un intervallo lungo di
esecuzione e soprattutto in una serie di cambi di contesto molto frequente, vanno sommati tutti i Δt poiché
tempi di cambio di contesto computazionale. Da questo punto di vista, avere in un intervallo di osservazione
un numero eccessivo di cambi di contesto computazionale, comporta il proliferarsi di questi microtempi Δt;
questo è un indice di una non corretta applicazione dell’algoritmo di scheduling della CPU.

DIAGRAMMA DELLE CODE


Attraverso il diagramma delle code
possiamo rappresentare l’evoluzione di
un processo in esecuzione all’interno di
una macchina. In questo diagramma
utilizziamo una rappresentazione
leggermente diversa rispetto a quella del
diagramma di esecuzione dei processi che
abbiamo visto in precedenza, perché nel
secondo abbiamo fondamentalmente un
diagramma di stato, mentre in questo
caso abbiamo una rappresentazione
basata su code. Principalmente ci sono
due code: la coda dei processi pronti (coda di ready) e la coda dell’I/O (coda dei dispositivi).

In questa rappresentazione dell’evoluzione dei processi, un processo selezionato dalla coda di ready per
l’utilizzo della CPU (dispatched) proseguirà nell’esecuzione fino a quando o ha completato l’esecuzione
oppure perché fa un operazione che non prevede l’utilizzo immediato della CPU ovvero operazioni di I/O,
operazioni di forking (cioè di generazione di un processo figlio) e quindi deve aspettare la terminazione di
esso, operazioni derivanti da interruzioni, tempo scaduto (time slice esaurita), oppure una System Call. In
tutte queste circostanze non c’è utilizzo della CPU e il processo ritorna, all’interno della coda di ready oppure
dopo l’I/O all’interno della coda di I/O. Quella appena descritta è una rappresentazione diversa, di alto livello
e quindi più concettuale, mentre la rappresentazione che abbiamo visto in precedenza è più vicina a come la
macchina realmente funziona.

GLI SCHEDULATORI
Dal punto di vista del funzionamento dell’evoluzione dei processi all’interno della macchina, hanno un ruolo
fondamentale i macroschedulatori e i microschedulatori. Il macroschedulatore si aggiorna con la frequenza
dei secondi (al più dei minuti), il microschedulatore lavora con la CPU con tempi di lavoro di 10-6 ed anche
più bassi. Mentre il macroschedulatore è scritto in un linguaggio di alto livello (fatto in Java) nei SO moderni,
il microschedulatore, poiché deve essere efficiente sui tempi di lavoro, sarà programmato ed implementato
in linguaggio macchina. Dal punto di vista dell’utilizzo complessivo della macchina esiste un terzo
schedulatore, detto schedulatore a medio termine, che si pone in modo intermedio rispetto al
macroschedulatore (o schedulatore a lungo termine) e microschedulatore (o schedulatore a breve termine):
L’aggettivo “lungo” o “breve” associata a “termine” identifica esattamente la frequenza d’invocazione. Dire
che lo schedulatore è a lungo termine significa dire che viene invocato con una bassa frequenza dopo un
intervallo di tempo sufficientemente più ampio rispetto allo schedulatore a breve termine che ha diversi
ordini di grandezza di differenza nei tempi di invocazione.

Che caratterizzazione bisognerà dare allo schedulatore a breve termine dal punto di vista della
funzionalità all’interno della coda di ready?
Mentre lo schedulatore a lungo termine, non fa altro che alimentare la coda di ready (cioè fa arrivare in coda
di ready tutti i processi che hanno la disponibilità dei device che richiedono), lo schedulatore a breve termine
provvede a svuotare la coda di ready, allocando la CPU al primo dei processi all’interno della coda: quello con
maggiore priorità dal punto di vista dell’algoritmo di scheduling.

Da questo punto di vista lo schedulatore a lungo termine è quello che determina materialmente il numero
dei processi che entrano in esecuzione all’interno della macchina. In altri termini, il macroschedulatore
controlla il livello di multitasking (multiprogrammazione) della macchina: più la macchina è performante, più
il macroschedulatore dovrà essere in grado di dare materiale di calcolo alle/a CPU ossia processi; meno la
macchina è performante, più basso dovrà essere tenuto il livello di multiprogrammazione. Per poter
controllare il livello di multiprogrammazione bisogna che il macroschedulatore abbia un conto preciso del
carico della macchina (non possiamo avere una macchina scarica, in attesa cioè dello swap-in dal disco né
tanto meno possiamo avere una macchina sovraccarica cioè una macchina in cui l’alternanza in coda di ready
sia elevatissima e che ci sia sovrappopolazione dei processi in coda di ready).
Il macroschedulatore quindi, deve bilanciare il grado di multiprogrammazione mantenendolo stabile, intorno
cioè ad un valore mediano: sarà un operatore statistico rappresentato da una scala che va da un valore
minimo ad un massimo del livello di multiprogrammazione. Il valore mediano rappresentato dal
macroschedulatore, rispetto al quale è ritenuto statico il livello all’interno di una macchina è riferito al
numero di processi che principalmente vanno in I/O (detti I/O BOUND) rispetto ai processi che utilizzano la
CPU (detti CPU BOUND). Dire che il macro schedulatore mantiene il livello di multiprogrammazione statico
significa dire che viene fissato il valore mediano cioè quel valore per cui: N° processi I/O Bound = N° processi
CPU Bound. In linea di massima, una macchina bilanciata deve fare tanto utilizzo delle periferiche (I/O)
quanto calcolo (CPU).
Un processo può essere I/O-Bound se è un
processo che trascorre il suo tempo
facendo più I/O che computazioni oppure
CPU-Bound se, al contrario, genera
richieste di I/O poco frequentemente,
usando la maggior parte del suo tempo in
computazioni.

Una macchina sbilanciata verso l’I/O Bound


sarebbe una macchina che fa grandissimo
utilizzo delle periferiche ma appena avrà un
processo che richiede un grande calcolo lo
mette in coda e quindi si osserverebbero i processi a impatto di calcolo sulla CPU, estremamente lenti;
viceversa una macchina che da privilegio maggiormente ai processi di CPU Bound rispetto a quelli di I/O
Bound, sarebbe una macchina molto poco interattiva, che nell’utilizzo delle periferiche sarebbe poco reattiva
e molto lontana dall’utente (se per esempio l’utente fa un input od un output e la macchina da privilegio alla
CPU, essa risponderà all’utente dopo molto tempo). I processi CPU Bound entrano in CPU e solitamente
arrivano ad utilizzare tutto quanto il time slice (tutto l’arco di tempo che la protezione dell’HW ti mette a
disposizione) mentre i processi I/O Bound invece la utilizzano un po’ di tempo, poi vanno fuori perché devono
fare input-output; sono quindi processi con molta interattività. Visto in senso esteso, il processo I/O Bound
è un processo (in terminologia Linux si chiama processo “foreground”) che richiede un’interazione fortissima
con l’utente mentre i processi CPU Bound sono processi (in terminologia Linux chiamati processi
“background”) in cui l’utente non c’entra quasi nulla, immette i dati all’inizio ed attende i risultati. Tutti questi
processi hanno in realtà “pari dignità” dal punto di vista della macchina; essa non può preferire processi I/O
Bound rispetto a quelli CPU-Bound né tantomeno il contrario. L’ideale è avere un bilanciamento tra una
categoria di processi e l’altra. Questo bilanciamento è garantito dal valore mediano riportato statisticamente
sulla mediana dal macroschedulatore per cui il numero dei processi I/O Bound dev’essere uguale al numero
dei processi CPU Bound.

Che cosa succede se una macchina perde il bilanciamento del carico? Cioè cosa succede se il
macroschedulatore immette all’interno della macchina un numero di processi eccessivi rispetto a
quelli che la macchina materialmente riesce a smaltire?
Si ha uno sbilanciamento del carico della macchina, il quale non è assolutamente gradevole; potrebbe
succedere che la macchina faccia evolvere verso un senso o verso l’altro ovvero verso processi I/O-Bound o
CPU-Bound la propria computazione e quindi risulterebbe inevitabilmente, agli occhi dell’utente, poco
reattiva o comunque molto lenta. Per correggere questa problematica si utilizza un terzo schedulatore che si
chiama SCHEDULATORE A MEDIO TERMINE o MID-TERM SCHEDULER.
Esso ha la funzione di abbassare forzosamente e per un tempo limitato il livello di multiprogrammazione della
macchina: questo consiste nell’operazione di SWAP-OUT dei PCB di processi anche avviati (quindi processi
anche parzialmente avviati/eseguiti). Questi processi vengono cioè estratti dalla memoria centrale come se
non fossero mai partiti e portati in un’area di memoria di massa temporanea (SWAP OUT), quindi vengono
eliminati addirittura dalla coda di submit come se non fossero mai entrati in quest’ultima. Il livello di
multiprogrammazione viene quindi abbassato ed il numero di processi dentro la memoria complessivamente
si abbassa. Abbassandosi il numero di processi ci si attende che la macchina reagisca meglio andando a
smaltire con più facilità la coda di ready, portando i processi in completed ed alzando il THROUGHPUT,
dopodiché i processi inizialmente messi in memoria di massa vengono reintrodotti sempre dallo schedulatore
a medio termine che li riposiziona all’interno della coda di ready (SWAP IN). Ad un certo istante la coda di
ready con l’intervento del mid-term scheduler, se sovraccarica (quindi è una coda che non si muove), viene
svuotata per un intervallo di tempo necessario ad aumentare e far ricrescere il throughput per poi essere
ripopolata quando quest’ultimo torna al di sopra del livello di guardia.

Partendo dalla coda di ready si ha


un’evoluzione verso la CPU e poi
eventualmente verso il completed. Se nella
coda dei processi pronti, interviene il mid-
term scheduler allora all’uscita dalla CPU i
processi non vengono riposizionati in coda
di ready ma escono e vanno in memoria di
massa, per poi essere ricaricati quando il
mid-term scheduler rileva una situazione
del livello di multiprogrammazione nuovamente sotto controllo (throughput aumenta). I processi non
devono ripartire da zero, dopo che il mid-term scheduler entra in azione, ma vengono salvati con il loro PCB
in base allo stato che hanno raggiunto (non vi è perdita di calcolo) ma vi è inevitabilmente una perdita di
tempo di esecuzione perché il salvataggio avviene su una memoria di massa.

Cos’è il throughput rispetto al mid-term scheduler?


Il throughput è il numero di processi completati in un’unità di tempo. Esso non è nè un valore costante né
deterministico ma varia a seconda delle caratteristiche della macchina e in base al numero e al tipo di processi
che su quella macchina si ha. Ad un certo punto può accadere che il numero di processi completati per unità
di tempo (cioè il throughput) cala; è come se la macchina stesse in sottoproduzione perché non riesce a
completare quanto dovrebbe e quindi si osserva, nella macchina, una produttività computazionale molto più
bassa rispetto allo standard prefissato. Se questo accade, una delle ragioni è attribuibile al sovraccarico della
macchina cioè non riesce a chiudere definitivamente delle attività computazionali; il rischio è che la macchina
comunque rimanga in questo stato di stallo per un lungo periodo quindi non funziona come dovrebbe. In
questi casi, conviene ridurre il carico temporaneamente, cioè eliminare del calcolo che la macchina deve fare
dalla coda di ready, buttar fuori dei processi, far ripartire la macchina (alleggerendola), reimpostare
successivamente il livello di multiprogrammazione riportando i processi lasciati fuori di nuovo nella coda di
ready: tutto questo lo fa il mid term scheduler.

CREAZIONE DEI PROCESSI


Dal punto di vista dell’origine dei processi,
utilizziamo come schematizzazione quella di
UNIX (Linux), per comodità di
rappresentazione ma quanto segue vale per
tutti i SO.

All’interno del SO i processi vengono creati


secondo un albero di dipendenze e
discendenze: a partire dal processo
progenitore ROOT (radice) vengono creati
con un meccanismo di biforcazione chiamato
FORK (primitiva di sistema) tre processi
fondamentali: il demone di pagina (per
gestire la paginazione), il processo scambiatore (per gestire gli swap) ed il processo INIT o initiator
(progenitore di tutti i processi utente e di sistema, all’interno della macchina; in particolare in Unix il processo
init assume PID=1 a far comprendere che è il primo processo dal quale vengono biforcati tutti quanti i processi
della macchina).
L’albero delle discendenze dei processi utente lo ritroviamo a partire da init in giù, creato in modo gerarchico,
cominciando dalle vere e proprie interfacce carattere (per esempio la Shell). I modelli di biforcazione a partire
da init possibili sono diversi.

Dal punto di vista dell’esecuzione può succedere:

1. Il processo padre sia concorrente rispetto al/ai processi figli (significa che i processi
contemporaneamente sono in competizione in coda di ready);
2. Il padre attende la terminazione di uno o più processi figli per poter riprendere l’esecuzione (quindi
il processo padre è in wait in attesa di un segnale di risveglio proveniente dai/dal processo figli/o; in
questo caso il processo padre non è in competizione con i figli perché è fuori dalla coda di ready e
non compete istantaneamente con loro per l’utilizzo della CPU. Quando il processo figlio, o i processi
figli, hanno terminato, risvegliano come ultimo atto il proprio padre che ripassa dalla coda di wait a
quella di ready).

Dal punto di vista dell’utilizzo delle risorse della macchina, invece, i modelli possibili sono 3:

1. I figli condividono un sottoinsieme delle risorse del processo padre (risorse intese come dispositivi,
periferiche, aree di memoria, dati o memoria temporanea);
2. I figli condividono tutte le risorse del processo padre;
3. I figli non condividono nulla rispetto al processo padre (in questo caso c’è una generazione di due
aree di memoria disaccoppiate con due meccanismi di protezione completamente diversi).

Nel caso di processi che condividano tutto, o parte, delle risorse del processo padre, allora nei meccanismi di
protezione della memoria vanno introdotti i valori di registro base e registro limite, tali per cui ai processi
figli deve essere consentito l’accesso in quelle aree dove c’è condivisione rispetto al processo padre. Nel caso
i processi figli non condividono nulla con il processo padre, si hanno due aree di memoria completamente
separate, base e limite non hanno intersezione (il registro base e il registro limite servono per fare, nel calcolo
dell’indirizzamento, la protezione della memoria). Se condividono tutte le risorse, base e limite sono uguali
ovvero gli spazi di indirizzamento sono identici, mentre nel caso in cui condividano solo una parte delle
risorse, i registri base e limite del processo figlio devono rientrare all’interno dell’area di memoria del
processo padre entro cui effettuare la condivisione.
FORK DI UN PROCESSO FIGLIO
Prendiamo ad esempio che cosa significa
dal punto di vista della programmazione
del SO fare una fork di un processo figlio.

Una fork in programmazione è la


modalità attraverso cui un processo crea
in memoria una copia distinta dello
spazio di indirizzamento di sé stesso: la
copia prenderà il nome di processo figlio
e avrà stesso codice del programma e
stessi dati al momento della creazione,
mentre il processo originale verrà
chiamato processo padre. Fonte:
Wikipedia.
Il punto di partenza è la generazione di un PID, il Process IDentifier è un identificativo univoco del processo
perché non è possibile che il processo venga frainteso dal SO, ma è anche un identificativo della salute di un
meccanismo di generazione di fork. Questo perché quando un processo padre genera un processo figlio si
possono avere tre output possibili che sono restituiti in termini di PID. Innanzitutto, il PID è un integer, un
valore intero e quindi maggiore o uguale a zero. Quando si va a fare una fork, essa restituirà come output un
PID che per il padre è l’identificatore del figlio mentre per il figlio sarà proprio 0. Questo PID può avere tre
valori fondamentali: può valere zero, un numero maggiore stretto di zero, ed in questi due casi la fork è
andata a buon fine, oppure assume un valore minore di zero ed in questo caso c’è un errore:

 PID MINORE DI ZERO: In questo caso abbiamo un errore nella fork e quindi exit con codice -1 mi
riporta fuori dal processo di generazione del figlio, cioè si ha un abort. Una possibile ragione per cui
una fork fallisce potrebbe essere una violazione degli indirizzi quindi una violazione dei meccanismi
di condivisione, una violazione dei meccanismi di esecuzione, fino ad arrivare ad una problematica
di codice: per esempio l’impossibilità di generare il processo figlio a seguito di un errore materiale
nella fork;

 PID UGUALE A ZERO: Se invece PID non è minore di zero ma è uguale a zero, sta succedendo che il
processo padre e il processo figlio sono completamente gemelli, ovvero sono identici, quindi non
generiamo un nuovo PID perché il processo padre ed il processo figlio sono indistinguibili e ciò
significa che il processo figlio evolverà con lo stesso PID del processo padre. I due processi sono
comunque distinti ma non distinguibili. La primitiva che mette in esecuzione il processo figlio gemello
rispetto al padre si chiama EXEC. L’exec è una system call, ed essa genera l’interrupt che
materialmente alloca nuovo spazio di memoria per il process control block del nuovo processo e
l’area di memoria per il nuovo processo, in maniera tale che questo possa poi partire. Quindi la exec
è il primo atto di caricamento del processo in coda di submit. Essa materialmente carica un file binario
nella memoria centrale (distruggendo l’immagine in memoria del programma che contiene la
chiamata di sistema exec()) ed inizia la sua esecuzione.

 PID MAGGIORE DI ZERO: Il terzo caso possibile, maggiore di zero, è quello del processo padre distinto
dal processo figlio che presenterà quindi un PID a sé stante, in cui il modello di condivisione adottato
è “padre che attende il completamento del figlio”, infatti il processo padre che ha fatto la fork,
innanzitutto non genera la exec perché non ci aspettiamo che la exec vada sul processo figlio che è
ripartito a seguire la fork, ma il processo padre si ferma su un ciclo definito wait che è un ciclo che
potrà essere risvegliato soltanto da un kill (da un signaling che in Linux o Unix è attuato attraverso il
comando kill) proveniente o dall’utente o dal processo figlio che ha terminato. In questo caso quando
il processo figlio termina, il padre si risveglia ed entra in coda di ready; rispetto alla segnalazione di
attesa il processo padre rimane infatti in coda I/O cioè in coda di wait, fino a quando non arriva un
comando di risveglio, ovvero exit=0.

TERMINAZIONE DEI PROCESSI


Un processo termina, cioè va in complete, quando finisce di eseguire la sua ultima istruzione e chiede al
sistema operativo di rimuoverlo dalla memoria centrale attraverso la chiamata exit(), cioè una chiamata di
sistema eseguita in modalità supervisore.
Quando si ha una exit, il processo che sta terminando manda un segnale di terminazione, al proprio processo
padre (tutti i processi a parte root, hanno un processo padre ed inevitabilmente hanno una dipendenza nei
confronti di un processo rispetto al quale devono inviare indicazione di terminazione). Quando si restituisce
il controllo al processo padre, si invia un segnale del valore di stato del processo (complete). Il processo padre
è fermo in attesa di una segnalazione, rispetto ad una porta, e l’arrivo del complete da parte del processo
figlio rappresenta, nell’ipotesi di un solo processo figlio, un segnale di risveglio. Il processo padre passa dallo
stato di wait allo stato di WAKE UP (salto dalla coda di I/O alla coda di ready). Quando un processo va in
terminazione, le risorse (memoria virtuale e fisica, buffer di I/O, ecc..) dello stesso vengono DEALLOCATE:
significa che nelle strutture di controllo, viene eliminato il flag di associazione a quel processo.
Ad esempio, se vi è un processo che occupa l’area di memoria, e ad un dato istante il processo va in
completed, si ha una struttura dati che gestisce la memoria (MEMORY TABLE) rispetto alla quale, si ha un
identificativo dell’area di memoria, il PID del processo che la occupa ed un flag di stato che indica se l’area di
memoria è occupata oppure no. Quando un processo sta uscendo (complete), deallocare la memoria non
significa ripulire l’area di memoria mettendola a zero e nemmeno ripulire la memory table, ma si resettano i
flag di associazione al processo. Questa appena descritta è la deallocazione di tipo SW. Quando si tratta di
deallocazione di tipo fisico (HW) allora si fa un’operazione fisica (non è il caso del SO).

Oltre alla restituzione di tutte le risorse, quindi al reset all’interno della memory table dei flag, si ha una
restituzione del controllo, cioè il processo figlio fa una segnalazione di ritorno al processo padre. Questa
segnalazione comporta che il processo è, dal punto di vista dell’SO, un processo ZOMBIE. I processi zombie
sono quei processi che sono in attesa di essere completamente cancellati dalla memoria. Esiste una serie di
casistiche, per cui i processi zombie, mantengono materialmente delle dipendenze nella memoria pur non
essendo fisicamente presenti. Questa è la spiegazione del fatto che molti processi occupano, con le proprie
strutture dati, la memoria senza avere materialmente uno scopo all’interno della macchina.

Nei SO progettati con un linguaggio in alto livello, interviene un componente detto GARBAGE COLLECTOR
(tipicamente mutuato dal linguaggio di Java) che controlla le dipendenze all’interno della memoria e verifica
se ci sono strutture dati non collegate ad elementi all’interno della memoria, e ne ripulisce quelli che non
hanno dipendenza. Il Garbage collector ha una funzione utile dal punto di vista della allocazione della
memoria, in quanto elimina le risorse “appese”, ma ha una funzione dispendiosa dal punto di vista delle
risorse di calcolo, infatti viene usato solo in casi estremi in quanto rallenta il sistema. In effetti, il Garbage
collector non è quasi mai implementato nelle macchine embeeded o macchine mobili.

Perché un processo può terminare?


Può terminare naturalmente se va in complete ed ha esaurito la propria computazione, ma possono anche
esserci delle terminazioni forzose provenienti dal processo padre:
 Si ha una terminazione diretta quando il processo padre decide di terminare uno o più processi figli,
ad esempio se il figlio ha superato i limiti di utilizzo di una risorsa (eseguendo degli indirizzamenti
illegali oppure ha violato la protezione dell’HW della risorsa) oppure se il processo figlio ha un task
in esecuzione non più necessario.
 Si ha una terminazione a cascata, che viene attuata in molti sistemi, e si verifica quando un processo
padre termina (in modo normale o anormale) e in sequenza tutti i processi figli devono terminare
forzatamente.
In alcuni sistemi, come in UNIX, i processi figli possono essere adottati dal processo init in modo che
possano continuare con la loro esecuzione.

COMUNICAZIONE TRA PROCESSI


Analizziamo ora, i paradigmi di comunicazione tra i processi. I processi in esecuzione concorrente in un
sistema operativo possono essere:

 PROCESSI COOPERANTI;
 PROCESSI INDIPENDENTI.

Due processi si dicono INDIPENDENTI se non hanno alcun tipo di relazione l’uno con l’altro. Il processo
indipendente ha la propria computazione ed essa non ha, in nessun punto del codice, delle interazioni con
processi differenti, cioè i processi indipendenti non sono influenzati o non influenzano altri processi in
esecuzione nel sistema. I processi indipendenti sono, dal punto di vista del sistema, quelli più semplici da
gestire perché sono processi in cui non si creano commistioni (unione, mescolanza), né tra processi e né tra
processo e SO.
I processi che, invece, prevedono una interazione l’uno con l’altro si chiamano processi COOPERANTI. Un
processo cooperante ha un impatto su un altro processo o su un altro set di processi, oppure può essere egli
stesso influenzato da altri processi (influenza reciproca). Tra due o più processi cooperanti, in genere, si attua
la condivisione delle risorse
Vediamo quali sono i vantaggi della cooperazione:
 Condivisione delle risorse: poiché molti utenti possono essere interessati alle stesse porzioni di
informazioni bisogna fornire un meccanismo che permetta l’accesso concorrente a questi tipi di
risorse.
 Velocizzazione della computazione: si velocizza il calcolo, sfruttando la disponibilità di più
processori, andando a suddividere un job in sotto-attività ed eseguendole in parallelo.
 Modularità: consente di progettare il sistema un aspetto per volta, per poi mettere insieme in modo
opportuno i moduli che implementano tali aspetti.
Una conseguenza della modularità è il vantaggio del debugging perché avere un codice modulare
permette di localizzare bug e problemi concettuali con più facilità, isolando più agevolmente possibili
errori di progetto.
 Scalabilità: permette di ampliare le capacità operative aggiungendo nuove istanze dello stesso
processo.
 Convenienza: un singolo utente, nell’ipotesi di processi/moduli cooperanti, può lavorare
singolarmente su attività differenti contemporanee; viceversa, se così non fosse, la multiutenza ed
il multitasking prevedrebbe che un utente singolo può lavorare su di un pezzo complessivo di codice
e senza poterlo suddividere ed avere impatti differenti sulla propria attività computazionale.

Sebbene vi siano tutti questi vantaggi, nella cooperazione, bisognerà implementare una serie di meccanismi
sicuri per poter consentire al processo di dialogare con un altro (si pensi ad una libreria che esegue il proprio
set di calcolo: essa deve poter agire con la funzione/programma/oggetto chiamante in modo sicuro ossia
dovrà essere richiesto un calcolo e fornito un risultato in maniera sicura).

Paradigma Produttore-Consumatore
Generalmente i SO implementano il meccanismo di scambio di informazioni tra processi cooperanti,
attraverso un paradigma (datato anni 90), il quale è fortemente utilizzato nel mondo informatico. Questo
paradigma, che attualmente è utilizzato nei sistemi a gestione dei messaggi in coda, prende il nome di
paradigma PRODUCER-CONSUMER.

Il paradigma produttore-consumatore è immaginato per processi che, tra di loro, condividono un canale di
comunicazione. Vi è un processo che si chiama PRODUCER che costruisce, elabora e mette a disposizione i
dati per un altro processo che deve utilizzarli, costruendo una comunicazione diretta che si chiama
“produzione”; i dati messi a disposizione in input, dal processo produttore, verranno consumati, quindi
elaborati, dal processo destinatario che è il processo CONSUMER.
In altre parole, il processo produttore è un generatore di dati mentre quello consumatore è un elaboratore
di dati.

L’esempio classico in questo genere di comunicazione, è il caso di produzione-consumo tra il compiler,


assembler e loader: Il compilatore è un produttore per l’assembler che è il consumatore, a sua volta,
l’assembler si comporta da produttore per il loader che sarà il consumatore. L’entità di scambio, in questo
caso sono i moduli che, di volta in volta, il produttore scambierà con il consumatore. Di fatto il compilatore,
l’assemblatore ed il loader sono dei processi cooperanti, in cui ognuno ha un task predeterminato, ciascuno
con input e output, e questo task viene eseguito per poter dar luogo ad un flusso di informazioni complessivo,
che deriva dall’insieme della funzionalità dei tre processi.
Una cosa fondamentale del paradigma produttore-consumatore, ed in generale in tutti quanti i meccanismi
di interscambio tra un set di processi cooperanti, è la disponibilità di un canale di comunicazione, in
particolare, il flusso di informazioni deve avere una sede e le stesse informazioni devono essere localizzate.

Generalmente, nel paradigma produttore-consumatore, il canale di informazione è rappresentato da una o


più memorie tampone, ossia da BUFFER: quindi, i processi in cooperazione condividono un buffer. Nell’ipotesi
di una sincronizzazione esatta tra il producer e il consumer, ad opera del SO, si ha che se due processi sono
in dialogo tra loro, ciò significa dire che il produttore riempie il buffer ed il consumatore lo svuota. Se non c’è
una perfetta sincronizzazione si possono generare gravi danni alle informazioni, ad esempio il consumatore
può voler consumare un oggetto non ancora prodotto

Ci sono due possibili implementazioni del paradigma produttore-consumatore a seconda del tipo di buffer
utilizzato, cioè se esso è BUFFER LIMITATO (bounded buffer) oppure un BUFFER ILLIMITATO (unbounded
buffer):

 Nel caso del paradigma produttore-consumatore con un buffer tampone LIMITATO, si ha una
cooperazione connessa alla dimensione del buffer, cioè la memoria tampone utilizzata come canale di
scambio, ha una dimensione fissata (non variabile) che rappresenta un vincolo per il problema della
comunicazione.
Questo significa che il consumatore attende se il buffer è vuoto, mentre il produttore attende se il buffer
è pieno: questi due controlli (check) devono essere inseriti nel codice del processo producer e del processo
consumer.

 Al contrario, nel caso del paradigma produttore-consumatore con un buffer ILLIMITATO (ovviamente il
buffer illimitato può esistere solo da un punto di vista teorico) l’unico vincolo che si ha è sul consumatore.
Infatti, in questo caso il consumatore può dover aspettare nuovi oggetti, ma il produttore può sempre
produrne: cioè si pone solo il problema sul consumatore che deve attendere se il buffer è vuoto, mentre
viene meno il vincolo sul buffer pieno.

Il paradigma produttore-consumatore e l’esistenza condivisa di un buffer, implica l’adozione di STRATEGIE DI


SINCRONIZZAZIONE evitando così il consumo di oggetti non ancora prodotti; Quindi oltre alla presenza di una
memoria tampone condivisa, devono essere altrettanto presenti ed implementate le strategie di
sincronizzazione.

INTER PROCESS COMUNICATION


Il paradigma produttore-consumatore va inteso, in un’ottica più generale, come una delle strategie di INTER
PROCESS COMUNICATION (IPC), cioè il complesso di tutte quelle strategie di comunicazione tra processi
cooperanti, intendendo in questo senso anche, strategie di gestione delle memorie condivise e strategie di
sincronizzazione.

Nella IPC, a differenza del paradigma produttore-consumatore generale, si definiscono due primitive
fondamentali che si chiamano SEND e RECEIVE, perché nella IPC la comunicazione è basata su scambio-
ricezione di messaggi, senza condividere lo stesso spazio indirizzi che risulta particolarmente vantaggioso in
un sistema distribuito.

Come avviene lo scambio di messaggi?


Le primitive send e receive sono due primitive di sistema e permettono al produttore e al consumatore,
quindi ai due processi che sono in comunicazione, di scambiare un contenuto predeterminato attraverso la
forma send (messaggio) e receive (messaggio).

I messaggi spediti da un processo possono avere una dimensione fissa o variabile. Se i messaggi hanno una
dimensione fissa, l’implementazione nel sistema è semplice, ma questa restrizione rende più difficile il
compito del programmatore dell’applicazione. Al contrario, se la dimensione è variabile, l’implementazione
a livello di sistema sarà più complessa, ma l’attività di programmazione è più semplice.

Quindi dati due processi P e C che vogliono stabilire una comunicazione inter processo, bisognerà definire il
canale di comunicazione, quindi stabilire quale sarà il veicolo del messaggio, e materialmente attuare uno
scambio del flusso informativo attraverso le direttive send-receive.

Il canale di comunicazione può essere sia di tipo fisico che di tipo logico: il primo è tipicamente un buffer,
cioè un’area di memoria generalmente temporanea condivisa dai processi produttore e consumatore,
oppure il bus, od una rete ad alta o bassa velocità (un canale di comunicazione fisicamente localizzabile ed
indirizzabile).

Esiste anche la possibilità di attuare un canale di comunicazione di tipo logico: quando si definisce la
comunicazione tra un processo produttore ed uno consumatore secondo lo schema IPC, si utilizzano dei
paradigmi consolidati che prevedono la classificazione esatta dell’entità produttore e dell’entità
consumatore, delle primitive di scambio dati e la definizione della tipologia di comunicazione. In particolare,
si possono adottare strategie di comunicazione diretta od indiretta.

COMUNICAZIONE DIRETTA
Con la comunicazione diretta, ogni processo che vuole comunicare deve conoscere esplicitamente il nome
del destinatario o del mittente della comunicazione all’atto del send o del receive. Ciò significa che il processo
produttore ed il processo consumatore si conoscono, cioè il sender e il receiver hanno una conoscenza
reciproca.

Nella comunicazione diretta si può adottare un indirizzamento simmetrico oppure uno asimmetrico; In
entrambi i casi sender e receiver si conoscono, cioè uno conosce l’identificativo dell’altro.

 Nell’indirizzamento SIMMETRICO la struttura è:


 SEND (P, messaggio), cioè manda un messaggio al processo P
 RECEIVE (Q, messaggio), cioè ricevi un messaggio dal processo Q

dove P è il processo destinatario e Q è il processo mittente (come si può notare il processo sender
conosce il processo receiver, ed il processo receiver conosce il processo sender), nel parametro
messaggio vi è il contenuto da scambiare.

 Nell’indirizzamento ASIMMETRICO la struttura è:


 SEND (P, messaggio): cioè manda un messaggio al processo P
 RECEIVE (id, messaggio): cioè ricevi un messaggio da un processo qualunque

In questo caso, soltanto il sender conosce il receiver. Infatti, al receiver gli viene passata la variabile id,
cioè un identificativo, che occupa il posto ricoperto prima dal mittente e che, di volta in volta, viene
valorizzato con il nome del processo con cui è stata instaurata una comunicazione.

È una comunicazione diretta perché in ogni caso, per poter stabilire la comunicazione, deve essere
noto all’atto della send o della receive, il nome del mittente e/o del destinatario a prescindere dal fatto
che questo possa essere fissato oppure no. Inoltre, la comunicazione diretta ad indirizzamento
asimmetrico, permette di generare un contenuto che possa poi essere consumato, in modo indistinto,
da parte di processi indistinti.

Fissata la tipologia di comunicazione e fissato il tipo di indirizzamento che si vuole attuare, bisogna stabilire
la caratteristica che deve assumere il canale di comunicazione che si andrà ad operare. Da questo punto di
vista, i vincoli che il SO impone, sono i seguenti:
 Le connessioni vengono fissate automaticamente attraverso l’installazione di un SOCKET, cioè una
coppia di semi-associazione, nella forma di indirizzo più porta. Quindi, il sender ha la propria semi-
associazione e il receiver ha la sua. Queste due semi-associazioni devono essere scambiate
vicendevolmente perché il sender deve conoscere l’indirizzo del receiver e viceversa. Lo scambio
delle semiassociazioni deve essere gestito automaticamente. Il socket disponibile per la IPC,
dev’essere a disposizione cioè, bisogna averlo già pronto, e deve essere costruito automaticamente
e distrutto automaticamente quando la connessione viene meno.
 Una connessione è associata esattamente a due processi.
 Vi deve essere una ed una sola connessione fisica per ogni coppia di processi, sender e receiver, non
possono esistere connessioni multiple.
 La natura della connessione di norma è bidirezionale ma potrebbe essere unidirezionale,
quest’ultimo è un caso limite. Si preferisce la bidirezionalità in modo che un sender può diventare un
receiver e quello che è un receiver può diventare un sender per lo scambio promiscuo delle
informazioni.

Lo svantaggio in entrambi gli schemi è dato dalla limitata modularità della definizione dei processi risultanti,
in quanto cambiare l’identificatore di un processo può rendere necessario l’esame delle definizioni di tutti gli
altri processi. Quindi può portare a trovare tutti i riferimenti al vecchio identificatore, in modo che possano
essere cambiate nel nuovo identificatore.

COMUNICAZIONE INDIRETTA
Nella comunicazione indiretta, si utilizza come strumento di comunicazione una MAILBOX o una PORTA. In
questo caso, non è necessario che il sender conosca il receiver e viceversa, ma è necessario che il sender
sappia qual è la mailbox dove depositare il messaggio, mentre il receiver sappia qual è la mailbox da cui
leggere il messaggio.

Perciò è necessario avere un identificativo univoco per ciascuna delle mailbox all’interno della struttura di
comunicazione interprocesso, e d’altra parte soltanto l’esistenza di una mailbox condivisa permette la
comunicazione tra due processi.

In questo tipo di comunicazione, una mailbox può essere di proprietà del sistema operativo e quindi risulta
essere indipendente e non correlata a nessun processo. Per questo motivo, il SO deve permettere a un
processo di compiere le seguenti azioni sono:
- Creazione e distruzione di una mailbox, e gestione della mailbox quando questa deve essere
mantenuta;
- Gestione dell’invio e della ricezione dei messaggi per mezzo della mailbox.

La struttura delle primitive è:


 SEND (mailbox, messaggio): manda un messaggio alla mailbox, ad esempio A.
 RECEIVE (mailbox, messaggio): ricevi un messaggio dalla mailbox, ad esempio A.

Indicando, nella posizione del parametro mailbox l’identificativo univoco della mailbox o della porta utilizzata
nella comunicazione indiretta.

Inoltre, il canale di comunicazione in questo tipo di comunicazione ha le seguenti proprietà:


 Viene stabilita una connessione fra due processi solo se entrambi hanno una mailbox condivisa.
 Una connessione può essere associata a più di due processi.
 Fra ogni coppia di processi comunicanti possono esserci più connessioni. Ciascuna di esse
corrisponde ad una mailbox.
 La connessione può essere unidirezionale o bidirezionale.
Esempio di condivisone di una mailbox:
Nell’ipotesi di utilizzo di uno scambio di comunicazione interprocesso mediante mailbox, questa mailbox va
creata, mantenuta, gestita e poi eventualmente quando la comunicazione non ha più senso, la mailbox viene
distrutta. Ci sono dei problemi però nella gestione della mailbox di cui si deve occupare il SO.

Ipotizzando, infatti, di avere tre processi che condividano in una comunicazione indiretta una mailbox: i
processi si chiamano P1, P2, P3, e la mailbox si chiama A. Se il processo P1 invia un certo contenuto
informativo nella mailbox A, e contemporaneamente P2 e P3 eseguono una receive sulla stessa mailbox A.
Quindi si ha che: P1 fa SEND (A, messaggio), P2 e P3 fanno RECEIVE (A, messaggio).

Quale tra P2 e P3 riceve materialmente il messaggio spedito da P1?


Non esiste una risposta univoca ma dipende dal particolare schema di gestione della mailbox che il SO adotta.
I modelli possibili sono:

1. Il più utilizzato, è quello di permettere al SO di decidere arbitrariamente quale processo riceverà il


contenuto, comunicando l’avvenuta lettura con un acknowledgement (riscontro) restituito al
mittente. Quindi, il mittente manda un messaggio alla mailbox poi uno qualsiasi, in generale il primo
che arriva tra P2 e P3 riceverà il messaggio. L’importante è che P1 sappia, attraverso un ack di ritorno,
che il messaggio sia stato ricevuto da P2 o da P3;
2. Il secondo schema consiste nel rendere mutuamente esclusivo l’accesso al contenuto della mailbox,
quindi solo un processo per volta può fare la receive (RICEZIONE BLOCCANTE)
3. In alterativa, si può permettere che una connessione sia associata con al più due processi (P3 non
legge).

A chi può appartenere una mailbox?


Una mailbox può appartenere a:
 al sistema operativo: in tal caso è un’area condivisa e sganciata dai singoli processi, Quindi il SO si
occupa della gestione della mailbox che nasce con il bootstrap e muore con lo shut-down della
macchina;
 al processo: ciò significa che nello spazio della memoria del processo viene riservata un’area specifica
per la IPC, cioè l’area specifica della mailbox. In questa situazione, quando il processo termina, la
mailbox termina con il processo e quindi se c’erano delle send in sospeso, queste verranno interrotte
da una comunicazione di interrupt che segnala la “non sussistenza” della mailbox.

Nell’ultimo caso, il processo che genera nella sua area di memoria la mailbox, ne diventa proprietario. Quindi
una mailbox ha come UNICO proprietario il processo che la crea. Il proprietario ha solo la facoltà di ricevere
messaggi direttamente dalla mailbox, mentre i processi UTENTE possono solo inviare messaggi alla mailbox.

Quindi fondamentalmente un processo che crei la mailbox come spazio di memoria condiviso definisce un
meccanismo di protezione dell’hardware, in particolare della memoria, per l’area che va dell’inizio alla fine
della mailbox consentendo ai processi utente di scrivere in quell’area mentre il processo proprietario può
solo leggere soltanto e “controllare”.

SINCRONIZZAZIONE
Se il modello di receive sulla mailbox è un modello non casuale, quindi lettura esclusiva, c’è bisogno di
implementare nella mailbox anche un meccanismo di SINCRONIZZAZIONE. Nella gestione della
sincronizzazione possiamo avere due modelli a diverso livello di rigidità: INVIO/RICEZIONE BLOCCANTE
oppure INVIO/RICEZIONE NON BLOCCANTE, in funzioni delle quali le primitive vengono implementate
diversamente:
 INVIO/RICEZIONE BLOCCANTE: Il send() ed il receive() sono completamente sincroni. Un invio
bloccante implica che il sender invia e viene bloccato, cioè non può inviare nuovi messaggi, finché il
messaggio non viene ricevuto dal processo o dalla mailbox. La ricezione bloccante implica che il
receiver si blocca (non legge) fino a quando un messaggio non viene reso completamente disponibile
all’interno della mailbox.
In alcuni casi, tipo i sistemi real-time, dove si ha la necessità di avere delle limitazioni di tempo si
impone l’invio bloccante, vincolando il processo sender alla completa lettura da parte del receiver.
Diciamo che in linea generale, quello che si fa è bloccare finché la mailbox non completa l’operazione
di deposito. In sintesi, succede che fino a quando il messaggio non viene completamente
materializzato all’interno della mailbox, il sender ed il receiver si fermano quindi in qualche modo
vengono sincronizzati fra loro. Appena il messaggio è stato completamente materializzato nella
mailbox e quindi si è esaurita la fase di trasmissione allora il sender ed il ricever vengono sbloccati e
quindi si può procedere alla lettura.
 INVIO/RICEZIONE NON BLOCCANTE: Il send() e il receive() sono asincroni. Un invio non bloccante
implica che il processo sender deposita il contenuto sul canale di comunicazione, cioè sulla mailbox,
e riprende la propria attività computazionale: quindi non attende la ricezione. D’altra parte, una
ricezione non bloccante implica che il receiver fa la sua attività computazionale, cioè acquisisce un
messaggio, che non è condizionata dalla materializzazione del contenuto all’interno della mailbox:
questo significa che è ammessa la possibilità che arrivi un messaggio non valido o un messaggio nullo.

Sono possibili differenti tipi di combinazioni delle primitive. In particolare, quando il send() e il receive() sono
di tipo bloccanti si ha il meccanismo di comunicazione a RENDEZVOUS (dal francese incontro), proprio a
testimoniare il fatto che la mailbox è il luogo di incontro tra il processo sender ed il processo receiver, i quali
si fermano in attesa della materializzazione del messaggio.

BUFFERIZZAZIONE
Sia nella comunicazione diretta, sia in quella indiretta, i messaggi scambiati dai processi in fase di
comunicazione risiedono in una coda temporanea, che viene implementata come un buffer. Quest’ultimi
possono essere a CAPACITA’ ZERO, a CAPACITA’ LIMITATA od a CAPACITA’ ILLIMITATA.

 BUFFER ZERO CAPACITY: la coda dei messaggi in attesa sul buffer di comunicazione è zero, cioè la
coda ha lunghezza massima zero. Quindi non esisteranno messaggi stazionanti all’interno del buffer
di comunicazione. In altri termini significa che, sulla connessione tra il produttore ed il consumatore,
non esisteranno messaggi scritti e non letti e d’altra parte nell’attesa della scrittura del messaggio
non verrà materializzato nulla all’interno della coda (questa definizione implementa esattamente il
rendez-vous). È chiaro che dal punto di vista fisico ci sarà un istante in cui il buffer non è davvero zero
capacity però dal punto di vista concettuale e logico c’è capacità zero poiché non si ferma nulla
all’interno della memoria tampone.
 BOUNDED CAPACITY: la coda di attesa dei messaggi ha una dimensione finita e fissata N, se questo
valore non è raggiunto, si può accodare un nuovo messaggio (il messaggio viene copiato o viene
mantenuto un puntatore) e il mittente può continuare l’esecuzione. Se la lista ha raggiunto la sua
dimensione massima, quindi è piena, il mittente si deve fermare fino a quando non è disponibile
spazio nella coda.
 UNBOUNDED CAPACITY: la coda di attesa dei messaggi ha teoricamente lunghezza infinita, ciò
significa che né il sender e né il receiver si fermano mai. Perciò, in realtà, si parla di un meccanismo
senza vincoli che ricalca esattamente l’invio-ricezione non bloccanti.

In altre parole, l’invio-ricezione bloccante equivale al buffer a zero capacità, invio-ricezione non bloccante
equivale ad un buffer unbounded, mentre schemi intermedi equivalgono a buffer bounded con dimensione
fissata, dove la dimensione del buffer fissa il vincolo rispetto al quale fare il controllo.
IMPLEMENTAZIONE DEI MECCANISMI INTERPROCESSO NEI SISTEMI DISTRIBUITI
Abbiamo visto come i processi possono comunicare usando la memoria condivisa e lo scambio di messaggi.
Queste tecniche possono essere usate anche per la comunicazione client-server, però ora vediamo altre
tecniche possono essere usate per la comunicazione in sistemi distribuiti client-server, quindi si ha:

 SOCKET;
 REMOTE PROCEDURE CALL (RPC): con la quale l’implementazione della IPC avviene secondo uno
schema procedurale.
 REMOTE METHOD INVOCATION (RMI): con la quale, invece, si ha un’implementazione della IPC
secondo un meccanismo di programmazione ad oggetti.

SOCKET
Un socket è definito come un “estremo di un canale di comunicazione” ed è identificato dalla concatenazione
di un indirizzo IP e un numero di porta (coppia di semi-associazione).

In generale i socket utilizzano un’archittetura client-server: il server resta in ascolto su di una data porta per
l’arrivo di richiesta da un client, una volta ricevuta la richiesta il server accetta la connessione del socket del
client per completare la procedura di connessione.
I server che implementano specifici servizi (come telnet, http, ftp) ascoltano sulle cosiddette well-known
ports , cioè tutte le porte sotto la 1024.

Esempio COMUNICAZIONE SOCKET:


Supponiamo che un processo client inizia una richiesta di connessione allora dal suo computer host gli viene
assegnata una porta con un valore maggiore di 1024.

Ad esempio, un host X con indirizzo IP 146.86.5.20 vuole


stabilire una connessione con un web browser (che è in
ascolto sulla porta 80) all’indirizzo IP 161.25.19.8. Perciò,
per quanto detto prima all’host X gli viene assegnata la
porta 1625. In questo modo, la connessione consisterà in
una coppia di socket: (146.86.5.20:1625) sull’host X e
(161.25.19.8:80) sul server web.
I pacchetti che viaggiano tra i due host sono consegnati al
processo appropriato sulla base del numero di porta di
destinazione.

Tuttavia, tutte le connessioni devono essere uniche. Pertanto, se un altro processo di un host X desidera
stabilire un’altra connessione con lo stesso web server, allora il computer dell’host X assegnerà a quel
processo un numero di porta maggiore di 1024 e diverso da 1625. In questo modo si assicura che tutte le
connessioni siano costituite da una coppia di socket univoca.

REMOTE PROCEDURE CALL


I modelli che si possono attuare sono due, a seconda che si faccia un’implementazione della IPC secondo uno
schema procedurale oppure secondo uno schema ad oggetti. In programmazione procedurale si parla di
REMOTE PROCEDURE CALL (RPC), mentre in un meccanismo di programmazione ad oggetti si parla di
REMOTE METHOD INVOCATION (RMI). In entrambi i casi, avremo una entità chiamante ed una entità
chiamata, ossia un componente che richiede dei servizi (client) ed un componente che offre dei servizi o delle
risorse (server). La cosa importante da osservare nel modello di RPC o di RMI è l’utilizzo della comunicazione
interprocesso nel senso più esteso del termine: in particolare, si trasferiscono dei messaggi in una forma
predeterminata, quindi secondo una struttura ben nota sia al client che al server, che adottano un protocollo
di comunicazione.

Struttura Remote procedure call:


La struttura della comunicazione in una RPC è fondamentale perché il client ed il server devono accordarsi
sul modello del messaggio che viene scambiato, sia con un messaggio di richiesta che con un messaggio di
risposta, prima di poter procedere materialmente allo scambio delle informazioni. Il messaggio che viene
mandato dal sender verso il receiver è in realtà mandato verso un processo DEMONE. Il processo demone è
un processo silente, cioè in running in background, che esegue come funzione solo ed esclusivamente
l’ascolto su una porta nota (http, per esempio, a livello server è gestito con un demone che si chiama http
demon, il quale ascolta sulla porta 80 oppure FTPD è il processo server demone che ascolta sulla porta per
FTP, ovvero la 21). Le WEB KNOW PORT sono porte (fino alla 1024) fissate per l’ascolto di processi demoni
che non fanno altro che attendere l’arrivo di richieste di comunicazione interprocesso.

La semantica di RPC consente ad un client di invocare una procedura su un host remoto come se invocasse
una procedura locale, infatti il sistema RPC nasconde i dettagli che consentono alla comunicazione di aver
luogo fornedo uno stub sul lato client.

Lo stub, elemento fondamentale su tutte le macchine moderne, è un terminale remoto sul lato client della
procedura lato server, cioè è un codice sul lato client che serve per implementare una procedura lato server.

Quando il client invia una richiesta di comunicazione, cioè invoca una procedura remota che contiene un
identificativo della funzione da eseguire e i parametri da passare a quella funzione, essa arriva al demone; il
demone la interpreta, la processa e la restituisce verso l’applicazione che deve materialmente eseguire il
compito per poi riaccorpare l’elaborazione da mandare al destinatario e restituirla al client in una forma ben
nota corrispondente al protocollo di comunicazione scelto. Molto importante è il fatto che, il processo
destinatario, cioè il processo residente sulla macchina server, si avvale di un componente che si chiama STUB.

Quindi quando arriva la richiesta di comunicazione, il sistema RPC chiama lo stub sul lato client appropriato,
passandogli i parametri forniti dalla procedura remota. Lo stub del server sul lato client, gestisce sia
l’agreement della connessione attraverso la porta sul server e sia la traduzione del formato dei parametri
(operazione di MARSHALLING), secondo quelle che sono le caratteristiche del destinatario. Inoltre,
l’operazione prevede anche l’impacchettamento dei parametri in un formato che possa essere trasmesso
sulla rete. Lo stub poi trasmette il messaggio al server, utilizzando il meccanismo dello scambio dei messaggi.
A questo punto, sul lato server è presente un altro stub che riceve il messaggio e si occupa di estrarre i
parametri tradotti ed invoca la procedura sul server. Se necessario, i valori di ritorno sono inviati al client
usando la stessa tecnica.

Problema della rappresentazione dei dati su client e su server:


Quindi in una comunicazione client-server, se non ci fosse, dal lato client, uno stub che fa il marshalling, il
server dovrebbe conoscere esattamente il formato di rappresentazione dati di ognuno dei client che si
collega ad esso, per poter restituire correttamente i dati. Sarebbe, quindi, più conveniente restituire i dati
nella forma in cui sono stati elaborati, e poi fare sì che uno stub, ovvero un microcomponente, sul lato client
si occupi di ricevere questi dati e fare il marshalling, cioè la traduzione in un formato più appropriato a
seconda del client.

Un esempio è la rappresentazione degli interi: alcuni sistemi usano gli indirizzi alti di memoria per salvare i
byte più significativi (big-endian), mentre altri sistemi gli usano per salvare i byte meno significativi (little-
endian).
Per risolvere questo problema, molti sistemi RPC utilizzano una rappresentazione dei dati indipendente dalla
macchina. Questo tipo di rappresentazione dei dati si chiama XDR (EXTERNAL DATA REPRESENTATION), cioè
significa che sul lato client, il marshalling dei parametri implica una conversione dei dati dipendenti
dall’archittetura in XDR, prima che vengono mandati al server, mentre sul lato server si esegue la conversione
nella rappresentazione che dipende dal tipo di server.

Esecuzione di una RPC:


Bisogna trovare un modo che se le chiamate alle procedure locali falliscono allora le chiamate RPC non
devono fallire o essere duplicate ed essere eseguite più di una volta. Per fare questo il sistema può assicurare
che i messaggi siano eseguiti esattamente una sola volta, piuttosto che al più di una volta.

Quindi si applica una marca temporale a tutti i messaggi, e il server deve mantenere uno storico di tutte le
marche di tempo che ha già elaborato: in questo modo i messaggi ripetuti possono essere riconosciuti ed
ignorati (at most one).

Mentre per risolvere il problema di “exactly one”, un ack viene rispedito al client come conferma di ricezione
della RPC. Trascorso un timeout senza che il client riceva l’ack di una trasmissione, si procede ad una nuova
RPC.

Problema della comunicazione fra il server e il client:


Le informazioni di connessioni in
sistemi remoti possono essere:

1. Predeterminate: cioè sotto


forma di indirizzo di porta prefissato.
In fase di compilazione, una
chiamata RPC ha un numero di porta
prefissato associato a essa. Quindi
dopo la compilazione, il numero di
porta non può essere cambiato.

2. Dinamico: cioè possono


essere ricavate dinamicamente
secondo un rendezvous realizzato
mediante un apposito demone
chiamato matchmaker o
accoppiatore su una porta RPC
prefissata.
Un client, dunque, manda un
messaggio contenente il nome della
RPC al demone richiedendo
l’indirizzo della porta della RPC da
eseguire; gli viene restituito il
numero della porta che può essere
spedita a quella porta fino al termine
del processo.

REMOTE METHOD INVOCATION


La RMI è basata sugli oggetti: supporta l’invocazione di metodi su oggetti remoti e inoltre, i parametri
possono essere anche oggetti.
La RMI implementa i metodi remoti usando:

 Stub: è un rappresentante degli oggetti remoti residenti nel client. Inoltre, è responsabile della
creazione di un parcel (pacco) contenente nome del metodo da invocare e parametri sui è eseguito
il marshalling.
 Skeleton: è un componente lato server, il quale è responsabile della traduzione dei parametri e
dell’invocazione del metodo desiderato sul server

Come avviene l’esecuzione di una RMI?


Quando il client invoca un metodo remoto, e quindi viene chiamato lo stub per quell’oggetto remoto. Lo stub
crea il pacco e lo invia al server. A questo punto, lo skeleton sul server restituisce il valore di ritorno (o
un’eccezione) al client attraverso un pacco. Lo stub esegue la traduzione del valore di ritorno nel formato
interno e lo passa al processo client.

Se i parametri su cui è stato effettuato il marshalling sono oggetti locali allora il il passaggio avviene per
valore utilizzando una tecnica nota come “serializzazione degli oggetti”. Altrimenti esso avviene per
riferimento.

Ad esempio, un client invoca un metodo su un oggetto remoto sul server:

La chiamata a someMethod() con i parametri A e B invoca lo stub per l’oggetto remoto. Lo stub esegue il
marshalling in un parcel dei parametri A e B e del nome del metodo. Lo skeleton sul server esegue la
traduzione dei parametri e invoca il metodo someMethod() e rimanda questo valore al client. Lo stub
effettua la traduzione di questo valore di ritorno e lo passa al client.
NOTE AGGIUNTIVE SULLA IPC
A titolo di informazione, la IPC si
colloca al livello 7, cioè al livello
applicativo della pila di ISO/OSI di
comunicazione tra due qualsiasi
elaboratori o all’interno di un
elaboratore nell’ipotesi di
utilizzare lo stesso meta-
protocollo. Tutto quello che
riguarda invece, la costruzione del
socket e lo scambio dei dati e
delle informazioni riguardanti il
canale di comunicazione si colloca
utilmente al livello 3 e 4, nella pila
di ISO/OSI.

Quindi per mettere in comunicazione due processi, presuppone l’installazione di un’agreement di


connessione di livello 3/4, posto che la IPC è di livello 7. Al primo livello abbiamo il livello fisico, che può
essere una memoria condivisa, può essere un buffer, un canale di comunicazione ad alta velocità, può
essere il bus; poi si ha un grande livello di comunicazione che incorpora tutto quello che sono i livelli 3 e 4 e
che è relativo alla definizione delle procedure di comunicazione su mezzo fisico; poi, a livello 7, si ha tutto
quello che riguarda il SO e le applicazioni. La IPC, che governa lo scambio dei flussi informativi e dei processi
è al livello 7.

Il SO gestisce internamente due processi che possono essere fisicamente anche non residenti sulla stessa
macchina, attraverso delle aree di memoria che siano condivise. Poiché non si ha un’area di memoria
condivisa, si utilizza un NETWORK come canale di comunicazione. Quando si va a caratterizzare la
comunicazione, essa avverrà a diversi livelli: nella struttura client/server di una comunicazione c’è un’entità
che richiede dei servizi o delle risorse ed un'altra che invece offre servizi e risorse (questo è valido anche nel
caso di una comunicazione inter processo). Se si vuole esaminare la suddetta comunicazione interprocesso
utilizzando ISO/OSI come riferimento teorico della comunicazione (ISO: International Standard
Organization/OSI: Open System Interconnection). Esso è un meta-protocollo ossia un modello/protocollo
(insieme di regole) per costruire protocolli, fatto di 7 layers: dal livello 1 denominato “Livello Fisico” fino al
livello 7 denominato “Livello Applicativo” e di volta in volta, a seconda del protocollo usato, vengono
valorizzati o meno come riferimento teorico della comunicazione. Su ISO/OSI si fonda : “http”, “ftp”, “DNS”
ecc.. ed anche dispositivi bluetooth. Essi si sono basati su tale modello teorico perché garantisce
l’Interoperabilità ossia: se si crea un protocollo e questo protocollo lo si vuole rendere il più possibile
interoperabile tra macchine costruite da produttori diversi che impiantano un chipset per la comunicazione
(basata sul protocollo in questione) si avrà interoperabilità se si costruisce tale protocollo rispettando le
regole offerte da ISO/OSI. L’efficienza di tale modello si ha nel caso, ad esempio, di due entità che
comunicano tra di loro: un’entità Client ed una Server (identica alla prima dal punto di vista della struttura)
e sapendo che il livello 3-4 ISO/OSI, prende dal livello 1 le informazioni, le “impacchetta” e le riproduce per il
livello 7 e poi dall’ altro lato il server riceverà le informazioni dal client al livello 7, le rielabora e le restituisce
al livello 3-4 per poi riportarle al livello fisico allora si può, solo da un punto di vista concettuale, immaginare
che la comunicazione avvenga tra livelli omologhi poiché ogni livello prende le informazioni, le impacchetta,
non le cambia, le restituisce al livello superiore senza toccarle allora è possibile immaginare che l’interazione
avvenga tra livelli omologhi; ecco perché ad esempio Facebook utilizzando HTTP (quindi l’applicazione
dell’utente su broswer e quindi sullo “user agent” che è l’applicazione client di Facebook) dialoga con il server
di Facebook. In realtà vi è uno scambio che prevede dei passaggi di livello per fare il routing, la comunicazione,
la gestione di indirizzamento e poi la memorizzazione delle informazioni dal punto di vista fisico; Però
concettualmente, poiché i livelli non introducono modifiche, è possibile immaginare che la comunicazione
avvenga tra livelli “pari/omologhi”: dal livello applicativo al livello applicativo, dal livello 3-4 al livello 3-4. Per
questo motivo gli sviluppatori di un livello non si interessano del lavoro svolto da altri sviluppatori di altri
livelli ed è questo il punto forte del modello ISO/OSI; è possibile quindi creare una web application, ad
esempio, senza occuparsi delle problematiche di comunicazione perché saranno presenti delle primitive nel
linguaggio di programmazione che andranno a fissare il socket e che in qualche modo andranno a veicolare
le informazione per i livelli 3-4. Infatti dal punto di vista del S.O., che come noto si colloca il più vicino possibile
all’utente, non deve interessare come venga gestita la problematica del canale di comunicazione; ma ci si
aspetta che ci sia una funzione di basso livello (rispetto a quello in cui lo sviluppatore si trova) di generazione
fisica del canale e questa funzione è la creazione di un socket (la creazione di un socket, sarà fatta dalle
primitive di basso livello di instaurazione del socket e viene ereditata dal SO). Se si prova a programmare una
web application in Javascript, il programmatore deve istanziare, attraverso le librerie di sistema, un socket
ed utilizzarlo. La stessa cosa vale per il SO che si occupa di stabilire una comunicazione interprocesso su un
socket già costruito, ovvero ciò significa che nella IPC il canale di comunicazione deve costruirsi
automaticamente e morire automaticamente; Quindi appena serve lo si deve materializzare, appena non
serve più dovrà morire poiché lancio il segnale di creazione del canale di comunicazione o di distruzione e
qualcuno farà quel lavoro. Per scambiare i dati relativi alla comunicazione del canale, il livello 3-4 materializza
ad un dato istante di fronte ad una chiamata di sistema, una semi-associazione: questa è una coppia di valori,
INDIRIZZO e PORTA che bisogna fornire al reicever, che dovrà dare la sua coppia di indirizzo e porta, ed una
volta che questa comunicazione bidirezionale è avvenuta, c’è un “handshake” e quindi un’agreement sulla
connessione, che significa che il canale è completamente settato e posso mandare su quel canale le
informazioni che mi servono.