COMUNICAZIONE TRA PROCESSI
PROCESSI COOPERANTI
un
processo è cooperante se influenza o può essere
influenzato da altri processi in esecuzione nel sistema (un processo
che condivide dati con altri processi)
utile per
ottenere:
parallelizzazione
dell'esecuzione (es. multi-cpu, cluster)
replicazione
(es. connessioni di rete, servizi)
modularità
(diversi thread per funzioni diverse di una stessa applicazione; es.
correttore ortografico word: posso continuare a scrivere mentre
corregge; posso compiere più azioni contemporaneamente)
condivisione
delle informazioni
PROCESSI IN COMPETIZIONE
due
processi sono in competizione se potrebbero evolvere
indipendentemente ma entrano in conflitto sulla ripartizione delle
risorse
es.
scheduling = tutti i processi competono per la CPU; coda di
stampa (competizione per la risorsa stampante)
MODELLO A MEMORIA CONDIVISA
(PRODUTTORE / CONSUMATORE UTILIZZANDO MEMORIA CONDIVISA)
tipologia
di comunicazione produttore/consumatore = interazione tra 2
processi, uno produce un dato e lo scrive in un buffer (quindi non
devo necessariamente sincronizzare ogni singolo dato), l'altro lo
deve utilizzare (es. stampa: invio dei dati al processo che li va a
stampare)

implementiamo
il buffer con un array circolare di dimensione DIM_VETTORE

abbiamo 2
puntatori: inserisci e preleva che si spostano attorno all'array
si tratta
di una coda FIFO con una dimensione massima prefissata (DIM_VETTORE),
altrimenti si ingrandirebbe fino a sfruttare tutta la memoria
codice
del processo produttore:
while (1){
<produce un dato D>
buffer[produci] = D;
produci = produci + 1;
// bisogna stare attenti perché quando arriviamo a
// DIM_VETTORE bisogna tornare a 0, quindi:
produci = (produci + 1) % DIM_VETTORE;
}
codice del processo consumatore:
while(1){
<consuma il dato D>
D = buffer[preleva];
preleva = (preleva + 1) % DIM_VETTORE;
}
problemi:
produttore
deve partire prima del consumatore
consumatore
deve aspettare che sia stato prodotto il dato
produttore
deve aspettare che dato sia stato letto prima di sovrascriverlo
soluzioni:
per
verificare se il buffer è vuoto guardo quando
inserisci==preleva mentre guardo se è pieno quando
(inserisci+1)%DIM_VETTORE=preleva; in questo modo funziona ma
“spreco” una posizione del buffer, altrimenti dovrei
usare un'altra variabile ma ci sono altri problemi
codice
del processo produttore:
while (1){
<produce un dato D>
while((inserisci + 1) % DIM_VETTORE == preleva)
buffer[inserisci] = D;
inserisci = (inserisci + 1) % DIM_VETTORE;
}
codice del processo consumatore:
while(1){
<consuma il dato D>
D = buffer[preleva];
preleva = (preleva + 1) % DIM_VETTORE;
}
questa tecnica detta “busy waiting”, o attesa
attiva, è poco efficiente
un altra
tecnica è quella di utilizzare una variabile globale che
memorizzi il numero degli elementi scritti del buffer
codice
del processo produttore:
while (1){
<produce un dato D>
while(nelementi == DIM_VETTORE - 1)
buffer[inserisci] = D;
inserisci = (inserisci + 1) % DIM_VETTORE;
nelementi = nelementi + 1;
}
codice del processo consumatore:
while(1){
<consuma il dato D>
D = buffer[preleva];
preleva = (preleva + 1) % DIM_VETTORE;
nelementi = nelementi - 1;
}
questa è teoricamente più efficiente ma non funziona
perché non c'è nessuna architettura che esegue
l'istruzione “nelementi = nelementi – 1” in un
unica istruzione, quindi diventa in realtà una cosa di questo
genere:
leggi nelementi in R1
R1 = R1 - 1;
scrivi R1 nelementi
questo provoca errate letture di dati (pensiamo a un sistema con
scheduler time-sharing)
e quindi
uno dei due processi potrebbe leggere dati inconsistenti
ad
esempio:
il
consumatore legge 5
l'esecuzione
passa al produttore che aggiunge elementi
l'esecuzione
torna nuovamente al consumatore che decrementa 5 a 4, ma avrebbe
dovuto decrementare 6 a 5, e quindi si è perso qualcosa per
strada
MODELLO A SCAMBIO DI MESSAGGI
permette
ai processi di comunicare senza ricorrere a dati condivisi; è
il modello più diffuso fino all'utilizzo della memoria
condivisa, e il più utilizzato nelle reti
si basa
su 2 primitive, che servono ad aprire un canale di comunicazione:
classificazione
in base alla nominazione dei processi (modo con cui un processo si
riferisce agli altri processi):
NOMINAZIONE (O NOMINA O
COMUNICAZIONE) DIRETTA

il
problema è che devo sapere l'ID:

questo
sistema va bene se ho un sistema del tipo qui sotto, di solito so
l'ID se sono io il padre:

NOMINAZIONE INDIRETTA
si
utilizza quando invece i processi sono meno correlati, come i
processi di sistema, in cui non so l'ID; si basa su delle porte (o
mailbox) su cui processi inviano messaggi e da cui i processi possono
leggere

l'idea è
quella della posta elettronica, dove noi per inviare un messaggio non
andiamo a indicare l'indirizzo fisico della macchina
classificazione
delle primitive in base alla sincronizzazione:
la send e
la receive possono essere sincrone e asincrone
generalmente
in un buffer la send è asincrona mentre la receive è
sincrona
PRODUTTORE/CONSUMATORE UTILIZZANDO
LO SCAMBIO DI MESSAGGI
un
processo produttore produce informazioni che sono consumate da un
processo consumatore (es. un programma di stampa produce caratteri
che sono consumati dal driver della stampante)
es.
codice
produttore:
while(1){
<Produce
D>
send(ID
del Consumatore, D);
}
se il
processo ricevente non è pronto:
metto
in attesa il processo mittente (quindi la send diventerebbe
sincrona, i concetti in realtà non sono assoluti)
ritorno
un codice di errore
codice
produttore migliorato:
while(1){
<Produce
D>
if
(send(ID del Consumatore, D) < 0){
<errore>
}
}
codice
consumatore:
while(1){
receive(ID del produttore, &D);
<consuma D>
}
scritto
così la receive è implicitamente sincrona
se il
buffer è vuoto il sistema resta già in attesa (per come
funziona la primitiva)
immaginiamo
di gestire un word processor con questo sistema: diventa estremamente
scomodo, per cui sono stati introdotti i thread
|