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 è:
Restituisce l'identificatore di una coda di messaggi.
La funzione restituisce l'identificatore (un intero positivo) o -1 in caso di errore, nel qual caso errno assumerà uno dei valori:
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.
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
|
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:
Esegue l'operazione specificata da cmd sulla coda msqid.
La funzione restituisce 0 in caso di successo o -1 in caso di errore, nel qual caso errno assumerà uno dei valori:
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:
Invia un messaggio sulla coda msqid.
La funzione restituisce 0, e -1 in caso di errore, nel qual caso errno assumerà uno dei valori:
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).
|
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:
Legge un messaggio dalla coda msqid.
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:
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 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:
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.
|
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).
|
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
[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
[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
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.