Le code di messaggi (in inglese message queues) sono un meccanismo di comunicazione tra processi che permette a più processi di inviare e ricevere messaggi in maniera asincrona: un processo può inserire un messaggio in una coda, e un altro processo (o più processi) può leggerlo in un momento successivo, senza che i processi debbano sincronizzarsi perfettamente.
1 - Caratteristiche principali
- Asincronia: un processo può inviare un messaggio senza aspettare che l’altro processo lo legga immediatamente.
- Ordinamento: i messaggi sono normalmente gestiti secondo la politica FIFO (First In, First Out), cioè il primo messaggio inserito è il primo a essere letto.
- Identificatori: i messaggi possono avere identificatori o tipi per permettere ai processi di filtrare quelli di interesse.
- Persistenza temporanea: i messaggi rimangono nella coda fino a quando non vengono letti o cancellati.
2 - Funzionamento delle code di messaggi
In un sistema operativo come UNIX o Linux, le code di messaggi sono implementate usando una serie di chiamate di sistema:
msgget()
: crea o accede a una coda di messaggi esistente.msgsnd()
: invia un messaggio a una coda.msgrcv()
: riceve un messaggio da una coda.msgctl()
: esegue operazioni di controllo sulla coda, come eliminarla.
2.1 - Esempio di codice in linguaggio C
Ecco un esempio di uso delle code di messaggi in linguaggio C, in cui un processo invia un messaggio a una coda e un altro lo legge.
Ecco il codice per scrivere in una coda:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
// Definizione della struttura del messaggio
struct msg_buffer {
long msg_type;
char msg_text[100];
} message;
int main() {
// Generazione di una chiave univoca
key_t key = ftok("progfile", 65);
// Creazione di una coda di messaggi e ottenimento del suo ID
int msgid = msgget(key, 0666 | IPC_CREAT);
message.msg_type = 1;
printf("Inserisci un messaggio: ");
fgets(message.msg_text, sizeof(message.msg_text), stdin);
// Invio del messaggio nella coda
msgsnd(msgid, &message, sizeof(message), 0);
printf("Messaggio inviato: %s\n", message.msg_text);
return 0;
}
Ecco il codice per leggere dalla coda:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// Definizione della struttura del messaggio
struct msg_buffer {
long msg_type;
char msg_text[100];
} message;
int main() {
// Generazione di una chiave univoca
key_t key = ftok("progfile", 65);
// Accesso alla coda di messaggi esistente
int msgid = msgget(key, 0666 | IPC_CREAT);
// Ricezione del messaggio dalla coda
msgrcv(msgid, &message, sizeof(message), 1, 0);
// Stampa del messaggio ricevuto
printf("Messaggio ricevuto: %s\n", message.msg_text);
// Rimozione della coda di messaggi
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
Spiegazione del codice:
key_t key = ftok("progfile", 65);
: genera una chiave univoca, utilizzata per identificare la coda di messaggi.msgget()
: crea (o accede, se esiste già) a una coda di messaggi associata a quella chiave.msgsnd()
: invia un messaggio a quella coda. Il messaggio è contenuto nella strutturamsg_buffer
, dovemsg_type
indica il tipo di messaggio emsg_text
contiene il testo.msgrcv()
: riceve un messaggio dalla coda. È possibile specificare un tipo di messaggio (msg_type
) per filtrare i messaggi da leggere.msgctl()
: rimuove la coda di messaggi quando non è più necessaria.
3 - Vantaggi e svantaggi delle code di messaggi
I principali vantaggi delle code di messaggi sono:
- Comunicazione asincrona: i processi non devono essere eseguiti contemporaneamente per scambiarsi dati. Un processo può inviare un messaggio e terminare, mentre il destinatario può ricevere il messaggio in un secondo momento.
- Decoupling tra processi: i processi non devono essere direttamente collegati o conoscere l’identità l’uno dell’altro. Questo rende i sistemi più flessibili e modulari, poiché è possibile aggiungere o rimuovere processi senza influenzare gli altri.
- Ordine dei messaggi: i messaggi vengono normalmente gestiti secondo la politica FIFO (First In, First Out). Questo garantisce che i messaggi vengano elaborati nell’ordine in cui sono stati inviati, il che è utile per mantenere la coerenza temporale.
- Supporto per messaggi di diversi tipi e filtri: è possibile associare a ciascun messaggio un “tipo” o identificatore. Questo permette ai processi di filtrare e leggere solo i messaggi di un certo tipo, migliorando la flessibilità nella gestione di diverse categorie di messaggi.
- Persistenza temporanea dei messaggi: i messaggi rimangono nella coda fino a quando non vengono letti, rendendo possibile la comunicazione anche se i processi sono avviati o terminano in momenti diversi.
- Semplicità di implementazione: le code di messaggi offrono un’interfaccia relativamente semplice per inviare e ricevere dati rispetto ad altre tecniche di comunicazione tra processi più complesse come socket o memoria condivisa.
I principali svantaggi delle code di messaggi sono:
- Dimensione limitata della coda: le code di messaggi hanno una dimensione massima limitata. Se la coda si riempie e non viene letta in tempo, i nuovi messaggi non possono essere inviati, causando un blocco o un ritardo nei processi di invio.
- Performance: le operazioni sulle code di messaggi possono introdurre overhead, poiché ogni operazione di invio e ricezione richiede il passaggio attraverso il kernel del sistema operativo. Questo rende le code di messaggi meno efficienti rispetto a metodi come la memoria condivisa, che permette accesso diretto ai dati.
- Possibile perdita di messaggi: se la coda viene eliminata o se un processo termina bruscamente senza leggere i messaggi, questi possono essere persi, a meno che non venga implementato un meccanismo di persistenza.
- Sincronizzazione non garantita: anche se la comunicazione è asincrona, i processi che devono essere strettamente sincronizzati richiederanno meccanismi aggiuntivi (es. semafori) per evitare condizioni di gara (race conditions) o accessi concorrenti ai messaggi.
- Meno adatte per grandi quantità di dati: le code di messaggi sono più adatte per scambiare piccoli messaggi, piuttosto che grandi blocchi di dati. Per grandi quantità di dati, la memoria condivisa è più efficiente.
- Gestione complessa di timeout: la gestione dei timeout (attesa di un messaggio per un certo tempo prima di rinunciare) può richiedere logica aggiuntiva, rendendo la programmazione più complessa in scenari che necessitano di risposte rapide o certe.
Fonti
- Abraham Silberschatz, Peter Baer Galvin, Greg Gagne - Sistemi Operativi (10ᵃ Edizione) - Pearson, 2019 - ISBN:
9788891904560
. - 🏫 Lezioni e slide del Prof. Aldinucci Marco del corso di Sistemi Operativi (canale B), Corso di Laurea in Informatica presso l’Università di Torino, A.A. 2024-25: