5. Relazioni tra processi
Nonostante esistano processi indipendenti (ovvero che non comunicano con altri processi, nè condividono dati), la maggior parte dei processi risulta cooperante, ovvero scambia in qualche modo informazioni con altri processi durante l'esecuzione.
Ma come avviene lo scambio di informazioni?
I processi possono comunicare tra loro con 2 tecniche, garantendo così l'IPC (Inter-Process Communication):
- Memoria condivisa: viene definita un'area di memoria in cui entrambi i processi posso leggere e scrivere dati. Generamente più veloce, in quanto non richiede l'intervento del kernel, tuttavia è necessario un meccanismo di sincronizzazione per gli accessi per evitare conflitti ed inconsistenze
- Scambio di messaggi: il kernel crea una coda di messaggi alla quale i processi accedono, che quindi comunicano senza condividere variabili. Sebbene richieda l'intervento del kernel, oggi sta diventando popolare e conveniente grazie alla tecnologia multicore. Può essere facilmente esteso ai sistemi distribuiti.
Generalmente i SO implementano entrambe queste tecniche, per poter poi usare l'una o l'altra a seconda delle scelte implementative. Vediamole ora entrambe nel dettaglio.
Scambio di messaggi (Message Passing)
Come detto in precedenza, è un meccanismo utilizzato dai processi per comunicare e sincronizzare le loro azioni, utilizzando lo scambio di messaggi (i processi comunicano tra loro senza condividere variabili).
Sono fornite due operazioni:
- send(message): la lunghezza del messaggio può essere fissa o variabile
- receive(message)
Quindi se i due processi
- Stabilire un canale di comunicazione
- Scambiarsi messaggi via send/receive
In ogni caso si ha la necessità di stabilire un link logico tra i processi che può avvenire mediante comunicazione diretta o indiretta.
Comunicazione diretta
Se ogni processo invia direttamente il messaggio all'altro. La comunicazione può essere simmetrica se sia il mittente che il destinatario conoscono l'uno il nome dell'altro, o asimmetrica se il mittente conosce in anticipo il nome. In entrambi i casi lo svantaggio principale sta nel fatto che un cambiamento nel nome del processo implica un cambiamento del codice.
Esempio:
- Simmetrica
- send (P1, message)
invia un messaggio a - receive (P2, message)
Riceve in message un messaggio da
- send (P1, message)
- Asimmetrica
- send (P1, message)
- receive (id, message)
Riceve messaggi da tutti e in id si trova il nome del processo che ha eseguito la send
Comunicazione indiretta
i processi non comunicano direttamente, ma utilizzano una mailbox condivisa su cui leggono e scrivono messaggi. A seconda dell'implementazione possono esistere più mailbox condivise tra due processi, oppure la stessa mailbox può essere usata da più di due processi. In questo caso è necessario, in caso di receive() multiple, decidere a chi vada il messaggio. Inoltre, la mailbox può avere un processo server, nel qual caso solo il server può ricevere e controllare la mailbox, mentre gli altri possono solo scrivere. Un'alternativa è che la mailbox sia gestita direttamente dal SO.
Esempio:
- send(A, message)
spedire un messaggio alla mailbox - receive(A, message)
ricevere un messaggio dalla mailbox
Ovviamente questo introduce un problema:
Come sempre, sono disponibili diverse soluzioni:
- Permettere ad un canale che sia associato con al massimo due processi
- Permettere a solo un processo alla volta di eseguire l’operazione receive()
- Permettere al sistema di selezionare in modo arbitrario il ricevente. Il mittente è notificato di chi ha ricevuto il messaggio
Sincronizzazione
Oltre che per l'utilizzo di mailbox o meno, l'implementazione può differenziarsi nelle politiche di sincronizzazione di ricevitore e mittente. Lo scambio di messaggi può essere bloccante o non-bloccante:
- Bloccante (sincrono):
- Send bloccante: dopo la send() aspetta conferma dalla mailbox o dal receiver prima di continuare
- Receive bloccante: il ricevente, una volta invocata la receive(), è bloccato finchè non riceve conferma dalla mailbox o dal mittente
- Non-bloccante (asincrono):
- Send non-bloccante: il mittente spedisce il messaggio e continua, non aspetta alcun ACK
- Receive non-bloccante: il ricevente continua la sua esecuzione anche dopo la receive()
Memoria condivisa
Generalmente, in un sistema a memoria condivisa, uno dei processi crea un segmento condiviso, con un nome, al quale può accedere l'altro processo. La sincronizzazione di lettura e scrittura è comunque gestita dai processi e non dal SO. Ovviamente questo metodo è più veloce dello scambio di messaggi ma meno sicuro, ed è necessaria la sincronizzazione per gli accessi per evitare conflitti.
Vediamo di seguito un esempio di questo in POSIX.
Il processo prima crea il segmento di memoria condivisa
segment id = shmget(IPC_PRIVATE, size, S_IRUSR | S_IWUSR);
Il processo che vuole accedere alla memoria condivisa deve attaccarsi ad essa
shared memory = (char *) shmat(id, NULL, 0);
Ora il processo puo scrivere nel segmento condiviso
sprintf(shared memory, "Writing to shared memory");
Quando il processo ha finito, stacca il segmento di memoria dal proprio spazio di indirizzi
shmdt(shared memory);
Infine, per eliminare il segmento di memoria
shmctl(shm_id, IPC_RMID, NULL);
Pipe
È un metodo di comunicazione in cui un processo scrive ad un'estremità della pipe e l'altro legge all'altra estremità.
Le pipe sono bloccanti.
Esse si dividono in 2 tipologie.
Pipe Ordinarie
Permettono la comunicazione in un stile standard produttore-consumatore
- Il produttore scrive ad un estremitá (la write-end della pipe)
- Il consumatore legge all’altra estremitá (la read-end della pipe)
Le pipe ordinarie sono quindi unidirezionali e funzionano solo tra processi padre-figlio
Pipe con nome
Le pipe con nome sono piú potenti delle pipe ordinarie. Inoltre le comunicazioni sono bidirezionali e non è richiesta la relazione parent-child tra i processi comunicanti. Infine piú processi possono usare la stessa pipe per comunicare.
Sono disponibili sia in sistemi UNIX sia in sistemi MS Windows.