Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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”.
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.
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.
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:
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Indicando, nella posizione del parametro mailbox l’identificativo univoco della mailbox o della porta utilizzata
nella comunicazione indiretta.
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).
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.
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.
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.
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.
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.
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
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.
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.
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.