[Avanti]  [Indietro]  [Su]  

12.2.4 Code di messaggi

Il primo oggetto introdotto dal SysV IPC è quello delle code di messaggi. Le code di messaggi sono oggetti analoghi alle pipe o alle fifo, anche se la loro struttura è diversa, ed il loro scopo principale è appunto quello di permettere a processi diversi di scambiarsi dei dati.

La funzione che permette di richiedere al sistema l'identificatore di una coda di messaggi esistente (o di crearne una se questa non esiste) è msgget; il suo prototipo è:

La funzione restituisce l'identificatore (un intero positivo) o -1 in caso di errore, nel qual caso errno assumerà uno dei valori:

ed inoltre ENOMEM.

Le funzione (come le analoghe che si usano per gli altri oggetti) serve sia a ottenere l'identificatore di una coda di messaggi esistente, che a crearne una nuova. L'argomento key specifica la chiave che è associata all'oggetto, eccetto il caso in cui si specifichi il valore IPC_PRIVATE, nel qual caso la coda è creata ex-novo e non vi è associata alcuna chiave, il processo (ed i suoi eventuali figli) potranno farvi riferimento solo attraverso l'identificatore.

Se invece si specifica un valore diverso da IPC_PRIVATE21 l'effetto della funzione dipende dal valore di flag, se questo è nullo la funzione si limita ad effettuare una ricerca sugli oggetti esistenti, restituendo l'identificatore se trova una corrispondenza, o fallendo con un errore di ENOENT se non esiste o di EACCES se si sono specificati dei permessi non validi.

Se invece si vuole creare una nuova coda di messaggi flag non può essere nullo e deve essere fornito come maschera binaria, impostando il bit corrispondente al valore IPC_CREAT. In questo caso i nove bit meno significativi di flag saranno usati come permessi per il nuovo oggetto, secondo quanto illustrato in sez. 12.2.2. Se si imposta anche il bit corrispondente a IPC_EXCL la funzione avrà successo solo se l'oggetto non esiste già, fallendo con un errore di EEXIST altrimenti.

Si tenga conto che l'uso di IPC_PRIVATE non impedisce ad altri processi di accedere alla coda (se hanno privilegi sufficienti) una volta che questi possano indovinare o ricavare (ad esempio per tentativi) l'identificatore ad essa associato. Per come sono implementati gli oggetti di IPC infatti non esiste una maniera che garantisca l'accesso esclusivo ad una coda di messaggi. Usare IPC_PRIVATE o constIPC_CREAT e IPC_EXCL per flag comporta solo la creazione di una nuova coda.






Costante Valore File in proc Significato








MSGMNI 16 msgmni Numero massimo di code di messaggi.
MSGMAX 8192 msgmax Dimensione massima di un singolo messaggio.
MSGMNB 16384 msgmnb Dimensione massima del contenuto di una coda.





Tabella 12.1: Valori delle costanti associate ai limiti delle code di messaggi.

Le code di messaggi sono caratterizzate da tre limiti fondamentali, definiti negli header e corrispondenti alle prime tre costanti riportate in tab. 12.1, come accennato però in Linux è possibile modificare questi limiti attraverso l'uso di sysctl o scrivendo nei file msgmax, msgmnb e msgmni di /proc/sys/kernel/.


PIC

Figura 12.11: Schema della struttura di una coda messaggi.

Una coda di messaggi è costituita da una linked list;22 i nuovi messaggi vengono inseriti in coda alla lista e vengono letti dalla cima, in fig. 12.11 si è riportato lo schema con cui queste strutture vengono mantenute dal kernel.23


01: struct msqid_ds { 
02:     struct ipc_perm msg_perm;     /* structure for operation permission */ 
03:     time_t msg_stime;             /* time of last msgsnd command */ 
04:     time_t msg_rtime;             /* time of last msgrcv command */ 
05:     time_t msg_ctime;             /* time of last change */ 
06:     msgqnum_t msg_qnum;           /* number of messages currently on queue */ 
07:     msglen_t msg_qbytes;          /* max number of bytes allowed on queue */ 
08:     pid_t msg_lspid;              /* pid of last msgsnd() */ 
09:     pid_t msg_lrpid;              /* pid of last msgrcv() */ 
10:     struct msg *msg_first;        /* first message on queue, unused  */ 
11:     struct msg *msg_last;         /* last message in queue, unused */ 
12:     unsigned long int msg_cbytes; /* current number of bytes on queue */ 
13: }; 
Figura 12.12: La struttura msqid_ds, associata a ciascuna coda di messaggi.

A ciascuna coda è associata una struttura msgid_ds, la cui definizione, è riportata in fig. 12.12. In questa struttura il kernel mantiene le principali informazioni riguardo lo stato corrente della coda.24 In fig. 12.12 sono elencati i campi significativi definiti in sys/msg.h, a cui si sono aggiunti gli ultimi tre campi che sono previsti dalla implementazione originale di System V, ma non dallo standard Unix98.

Quando si crea una nuova coda con msgget questa struttura viene inizializzata, in particolare il campo msg_perm viene inizializzato come illustrato in sez. 12.2.2, per quanto riguarda gli altri campi invece:

Una volta creata una coda di messaggi le operazioni di controllo vengono effettuate con la funzione msgctl, che (come le analoghe semctl e shmctl) fa le veci di quello che ioctl è per i file; il suo prototipo è:

La funzione restituisce 0 in caso di successo o -1 in caso di errore, nel qual caso errno assumerà uno dei valori:

ed inoltre EFAULT ed EINVAL.

La funzione permette di accedere ai valori della struttura msqid_ds, mantenuta all'indirizzo buf, per la coda specificata dall'identificatore msqid. Il comportamento della funzione dipende dal valore dell'argomento cmd, che specifica il tipo di azione da eseguire; i valori possibili sono:

Una volta che si abbia a disposizione l'identificatore, per inviare un messaggio su una coda si utilizza la funzione msgsnd; il suo prototipo è:

La funzione restituisce 0, e -1 in caso di errore, nel qual caso errno assumerà uno dei valori:

ed inoltre EFAULT ed ENOMEM.

La funzione inserisce il messaggio sulla coda specificata da msqid; il messaggio ha lunghezza specificata da msgsz ed è passato attraverso il l'argomento msgp. Quest'ultimo deve venire passato sempre come puntatore ad una struttura msgbuf analoga a quella riportata in fig. 12.13 che è quella che deve contenere effettivamente il messaggio. La dimensione massima per il testo di un messaggio non può comunque superare il limite MSGMAX.

La struttura di fig. 12.13 è comunque solo un modello, tanto che la definizione contenuta in sys/msg.h usa esplicitamente per il secondo campo il valore mtext[1], che non è di nessuna utilità ai fini pratici. La sola cosa che conta è che la struttura abbia come primo membro un campo mtype come nell'esempio; esso infatti serve ad identificare il tipo di messaggio e deve essere sempre specificato come intero positivo di tipo long. Il campo mtext invece può essere di qualsiasi tipo e dimensione, e serve a contenere il testo del messaggio.

In generale pertanto per inviare un messaggio con msgsnd si usa ridefinire una struttura simile a quella di fig. 12.13, adattando alle proprie esigenze il campo mtype, (o ridefinendo come si vuole il corpo del messaggio, anche con più campi o con strutture più complesse) avendo però la cura di mantenere nel primo campo un valore di tipo long che ne indica il tipo.

Si tenga presente che la lunghezza che deve essere indicata in questo argomento è solo quella del messaggio, non quella di tutta la struttura, se cioè message è una propria struttura che si passa alla funzione, msgsz dovrà essere uguale a sizeof(message)-sizeof(long), (se consideriamo il caso dell'esempio in fig. 12.13, msgsz dovrà essere pari a LENGTH).


1: struct msgbuf { 
2:      long mtype;          /* message type, must be > 0 */ 
3:      char mtext[LENGTH];  /* message data */ 
4: }; 
Figura 12.13: Schema della struttura msgbuf, da utilizzare come argomento per inviare/ricevere messaggi.

Per capire meglio il funzionamento della funzione riprendiamo in considerazione la struttura della coda illustrata in fig. 12.11. Alla chiamata di msgsnd il nuovo messaggio sarà aggiunto in fondo alla lista inserendo una nuova struttura msg, il puntatore msg_last di msqid_ds verrà aggiornato, come pure il puntatore al messaggio successivo per quello che era il precedente ultimo messaggio; il valore di mtype verrà mantenuto in msg_type ed il valore di msgsz in msg_ts; il testo del messaggio sarà copiato all'indirizzo specificato da msg_spot.

Il valore dell'argomento flag permette di specificare il comportamento della funzione. Di norma, quando si specifica un valore nullo, la funzione ritorna immediatamente a meno che si sia ecceduto il valore di msg_qbytes, o il limite di sistema sul numero di messaggi, nel qual caso si blocca mandando il processo in stato di sleep. Se si specifica per flag il valore IPC_NOWAIT la funzione opera in modalità non bloccante, ed in questi casi ritorna immediatamente con un errore di EAGAIN.

Se non si specifica IPC_NOWAIT la funzione resterà bloccata fintanto che non si liberano risorse sufficienti per poter inserire nella coda il messaggio, nel qual caso ritornerà normalmente. La funzione può ritornare, con una condizione di errore anche in due altri casi: quando la coda viene rimossa (nel qual caso si ha un errore di EIDRM) o quando la funzione viene interrotta da un segnale (nel qual caso si ha un errore di EINTR).

Una volta completato con successo l'invio del messaggio sulla coda, la funzione aggiorna i dati mantenuti in msqid_ds, in particolare vengono modificati:

La funzione che viene utilizzata per estrarre un messaggio da una coda è msgrcv; il suo prototipo è:

La funzione restituisce il numero di byte letti in caso di successo, e -1 in caso di errore, nel qual caso errno assumerà uno dei valori:

ed inoltre EFAULT.

La funzione legge un messaggio dalla coda specificata, scrivendolo sulla struttura puntata da msgp, che dovrà avere un formato analogo a quello di fig. 12.13. Una volta estratto, il messaggio sarà rimosso dalla coda. L'argomento msgsz indica la lunghezza massima del testo del messaggio (equivalente al valore del parametro LENGTH nell'esempio di fig. 12.13).

Se il testo del messaggio ha lunghezza inferiore a msgsz esso viene rimosso dalla coda; in caso contrario, se msgflg è impostato a MSG_NOERROR, il messaggio viene troncato e la parte in eccesso viene perduta, altrimenti il messaggio non viene estratto e la funzione ritorna con un errore di E2BIG.

L'argomento msgtyp permette di restringere la ricerca ad un sottoinsieme dei messaggi presenti sulla coda; la ricerca infatti è fatta con una scansione della struttura mostrata in fig. 12.11, restituendo il primo messaggio incontrato che corrisponde ai criteri specificati (che quindi, visto come i messaggi vengono sempre inseriti dalla coda, è quello meno recente); in particolare:

Il valore di msgflg permette di controllare il comportamento della funzione, esso può essere nullo o una maschera binaria composta da uno o più valori. Oltre al precedente MSG_NOERROR, sono possibili altri due valori: MSG_EXCEPT, che permette, quando msgtyp è positivo, di leggere il primo messaggio nella coda con tipo diverso da msgtyp, e IPC_NOWAIT che causa il ritorno immediato della funzione quando non ci sono messaggi sulla coda.

Il comportamento usuale della funzione infatti, se non ci sono messaggi disponibili per la lettura, è di bloccare il processo in stato di sleep. Nel caso però si sia specificato IPC_NOWAIT la funzione ritorna immediatamente con un errore ENOMSG. Altrimenti la funzione ritorna normalmente non appena viene inserito un messaggio del tipo desiderato, oppure ritorna con errore qualora la coda sia rimossa (con errno impostata a EIDRM) o se il processo viene interrotto da un segnale (con errno impostata a EINTR).

Una volta completata con successo l'estrazione del messaggio dalla coda, la funzione aggiorna i dati mantenuti in msqid_ds, in particolare vengono modificati:

Le code di messaggi presentano il solito problema di tutti gli oggetti del SysV IPC; essendo questi permanenti restano nel sistema occupando risorse anche quando un processo è terminato, al contrario delle pipe per le quali tutte le risorse occupate vengono rilasciate quanto l'ultimo processo che le utilizzava termina. Questo comporta che in caso di errori si può saturare il sistema, e che devono comunque essere esplicitamente previste delle funzioni di rimozione in caso di interruzioni o uscite dal programma (come vedremo in fig. 12.14).

L'altro problema è non facendo uso di file descriptor le tecniche di I/O multiplexing descritte in sez. 11.1 non possono essere utilizzate, e non si ha a disposizione niente di analogo alle funzioni select e poll. Questo rende molto scomodo usare più di una di queste strutture alla volta; ad esempio non si può scrivere un server che aspetti un messaggio su più di una coda senza fare ricorso ad una tecnica di polling che esegua un ciclo di attesa su ciascuna di esse.

Come esempio dell'uso delle code di messaggi possiamo riscrivere il nostro server di fortunes usando queste al posto delle fifo. In questo caso useremo una sola coda di messaggi, usando il tipo di messaggio per comunicare in maniera indipendente con client diversi.


01: int msgid;          /* Message queue identifier */ 
02: int main(int argc, char *argv[]) 
03: { 
04: /* Variables definition */ 
05:     int i, n = 0; 
06:     char **fortune;       /* array of fortune message string */ 
07:     char *fortunefilename = "/usr/share/games/fortunes/linux"; /* file name */ 
08:     struct msgbuf_read {  /* message struct to read request from clients */ 
09:         long mtype;       /* message type, must be 1 */ 
10:         long pid;         /* message data, must be the pid of the client */ 
11:     } msg_read; 
12:     struct msgbuf_write { /* message struct to write result to clients */ 
13:         long mtype;       /* message type, will be the pid of the client*/ 
14:         char mtext[MSGMAX]; /* message data, will be the fortune */ 
15:     } msg_write; 
16:     key_t key;            /* Message queue key */ 
17:     int size;             /* message size */ 
18:     ... 
19:     Signal(SIGTERM, HandSIGTERM); /* set handlers for termination */ 
20:     Signal(SIGINT, HandSIGTERM); 
21:     Signal(SIGQUIT, HandSIGTERM); 
22:     if (n==0) usage();    /* if no pool depth exit printing usage info */ 
23:     i = FortuneParse(fortunefilename, fortune, n); /* parse phrases */ 
24:     /* Create the queue */ 
25:     key = ftok("./MQFortuneServer.c", 1);  
26:     msgid = msgget(key, IPC_CREAT|0666); 
27:     if (msgid < 0) { 
28:         perror("Cannot create message queue"); 
29:         exit(1); 
30:     } 
31:     /* Main body: loop over requests */ 
32:     daemon(0, 0); 
33:     while (1) { 
34:         msgrcv(msgid, &msg_read, sizeof(int), 1, MSG_NOERROR); 
35:         n = random() % i;             /* select random value */ 
36:         strncpy(msg_write.mtext, fortune[n], MSGMAX); 
37:         size = min(strlen(fortune[n])+1, MSGMAX);   
38:         msg_write.mtype=msg_read.pid; /* use request pid as type */ 
39:         msgsnd(msgid, &msg_write, size, 0); 
40:     } 
41: } 
42: /* 
43:  * Signal Handler to manage termination 
44:  */ 
45: void HandSIGTERM(int signo) { 
46:     msgctl(msgid, IPC_RMID, NULL);    /* remove message queue */ 
47:     exit(0); 
48: } 
Figura 12.14: Sezione principale del codice del server di fortunes basato sulle message queue.

In fig. 12.14 si è riportato un estratto delle parti principali del codice del nuovo server (il codice completo è nel file MQFortuneServer.c nei sorgenti allegati). Il programma è basato su un uso accorto della caratteristica di poter associate un “tipo” ai messaggi per permettere una comunicazione indipendente fra il server ed i vari client, usando il pid di questi ultimi come identificativo. Questo è possibile in quanto, al contrario di una fifo, la lettura di una coda di messaggi può non essere sequenziale, proprio grazie alla classificazione dei messaggi sulla base del loro tipo.

Il programma, oltre alle solite variabili per il nome del file da cui leggere le fortunes e per il vettore di stringhe che contiene le frasi, definisce due strutture appositamente per la comunicazione; con msgbuf_read (8–11) vengono passate le richieste mentre con msgbuf_write (12–15) vengono restituite le frasi.

La gestione delle opzioni si è al solito omessa, essa si curerà di impostare in n il numero di frasi da leggere specificato a linea di comando ed in fortunefilename il file da cui leggerle; dopo aver installato (19–21) i gestori dei segnali per trattare l'uscita dal server, viene prima controllato (22) il numero di frasi richieste abbia senso (cioè sia maggiore di zero), le quali poi (23) vengono lette nel vettore in memoria con la stessa funzione FortuneParse usata anche per il server basato sulle fifo.

Una volta inizializzato il vettore di stringhe coi messaggi presi dal file delle fortune si procede (25) con la generazione di una chiave per identificare la coda di messaggi (si usa il nome del file dei sorgenti del server) con la quale poi si esegue (26) la creazione della stessa (si noti come si sia chiamata msgget con un valore opportuno per l'argomento flag), avendo cura di abortire il programma (27–29) in caso di errore.

Finita la fase di inizializzazione il server prima (32) chiama la funzione daemon per andare in background e poi esegue in permanenza il ciclo principale (33–40). Questo inizia (34) con il porsi in attesa di un messaggio di richiesta da parte di un client; si noti infatti come msgrcv richieda un messaggio con mtype uguale a 1: questo è il valore usato per le richieste dato che corrisponde al pid di init, che non può essere un client. L'uso del flag MSG_NOERROR è solo per sicurezza, dato che i messaggi di richiesta sono di dimensione fissa (e contengono solo il pid del client).

Se non sono presenti messaggi di richiesta msgrcv si bloccherà, ritornando soltanto in corrispondenza dell'arrivo sulla coda di un messaggio di richiesta da parte di un client, in tal caso il ciclo prosegue (35) selezionando una frase a caso, copiandola (36) nella struttura msgbuf_write usata per la risposta e calcolandone (37) la dimensione.

Per poter permettere a ciascun client di ricevere solo la risposta indirizzata a lui il tipo del messaggio in uscita viene inizializzato (38) al valore del pid del client ricevuto nel messaggio di richiesta. L'ultimo passo del ciclo (39) è inviare sulla coda il messaggio di risposta. Si tenga conto che se la coda è piena anche questa funzione potrà bloccarsi fintanto che non venga liberato dello spazio.

Si noti che il programma può terminare solo grazie ad una interruzione da parte di un segnale; in tal caso verrà eseguito (45–48) il gestore HandSIGTERM, che semplicemente si limita a cancellare la coda (46) ed ad uscire (47).


01: int main(int argc, char *argv[]) 
02: { 
03:     ... 
04:     key = ftok("./MQFortuneServer.c", 1);  
05:     msgid = msgget(key, 0);  
06:     if (msgid < 0) { 
07:         perror("Cannot find message queue"); 
08:         exit(1); 
09:     } 
10:     /* Main body: do request and write result */ 
11:     msg_read.mtype = 1;                /* type for request is always 1 */ 
12:     msg_read.pid = getpid();           /* use pid for communications */ 
13:     size = sizeof(msg_read.pid);   
14:     msgsnd(msgid, &msg_read, size, 0); /* send request message */ 
15:     msgrcv(msgid, &msg_write, MSGMAX, msg_read.pid, MSG_NOERROR); 
16:     printf("%s", msg_write.mtext); 
17: } 
Figura 12.15: Sezione principale del codice del client di fortunes basato sulle message queue.

In fig. 12.15 si è riportato un estratto il codice del programma client. Al solito il codice completo è con i sorgenti allegati, nel file MQFortuneClient.c. Come sempre si sono rimosse le parti relative alla gestione delle opzioni, ed in questo caso, anche la dichiarazione delle variabili, che, per la parte relative alle strutture usate per la comunicazione tramite le code, sono le stesse viste in fig. 12.14.

Il client in questo caso è molto semplice; la prima parte del programma (4–9) si occupa di accedere alla coda di messaggi, ed è identica a quanto visto per il server, solo che in questo caso msgget non viene chiamata con il flag di creazione in quanto la coda deve essere preesistente. In caso di errore (ad esempio se il server non è stato avviato) il programma termina immediatamente.

Una volta acquisito l'identificatore della coda il client compone il messaggio di richiesta (12–13) in msg_read, usando 1 per il tipo ed inserendo il proprio pid come dato da passare al server. Calcolata (14) la dimensione, provvede (15) ad immettere la richiesta sulla coda.

A questo punto non resta che (16) rileggere dalla coda la risposta del server richiedendo a msgrcv di selezionare i messaggi di tipo corrispondente al valore del pid inviato nella richiesta. L'ultimo passo (17) prima di uscire è quello di stampare a video il messaggio ricevuto.

Proviamo allora il nostro nuovo sistema, al solito occorre definire LD_LIBRAY_PATH per accedere alla libreria libgapil.so, dopo di che, in maniera del tutto analoga a quanto fatto con il programma che usa le fifo, potremo far partire il server con:

[piccardi@gont sources]$ ./mqfortuned -n10
come nel caso precedente, avendo eseguito il server in background, il comando ritornerà immediatamente; potremo però verificare con ps che il programma è effettivamente in esecuzione, e che ha creato una coda di messaggi:
[piccardi@gont sources]$ ipcs
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
------ Semaphore Arrays --------
key        semid      owner      perms      nsems
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x0102dc6a 0          piccardi   666        0            0
a questo punto potremo usare il client per ottenere le nostre frasi:
[piccardi@gont sources]$ ./mqfortune
Linux ext2fs has been stable for a long time, now it's time to break it
         -- Linuxkongreß '95 in Berlin
[piccardi@gont sources]$ ./mqfortune
Let's call it an accidental feature.
         --Larry Wall
                                                                                       
                                                                                       
con un risultato del tutto equivalente al precedente. Infine potremo chiudere il server inviando il segnale di terminazione con il comando killall mqfortuned verificando che effettivamente la coda di messaggi viene rimossa.

Benché funzionante questa architettura risente dello stesso inconveniente visto anche nel caso del precedente server basato sulle fifo; se il client viene interrotto dopo l'invio del messaggio di richiesta e prima della lettura della risposta, quest'ultima resta nella coda (così come per le fifo si aveva il problema delle fifo che restavano nel filesystem). In questo caso però il problemi sono maggiori, sia perché è molto più facile esaurire la memoria dedicata ad una coda di messaggi che gli inode di un filesystem, sia perché, con il riutilizzo dei pid da parte dei processi, un client eseguito in un momento successivo potrebbe ricevere un messaggio non indirizzato a lui.


[Avanti]  [Indietro]  [Su]  
© 2000-2003 Simone Piccardi
Pubblicazione web curata da Mirko Maischberger