...è nato prima l'uovo o la gallina?... |
Tornati? Ok, bando alle ciance... vai col codice!
#include <string.h> #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include "libmmap.h" // memMapOpenMast() - apre un mapped-file come master ShmData *memMapOpenMast( const char *shmname, // nome del mapped-file size_t len) // size del campo data da condividere { // apre un mapped-file (il file "shmname" é creato in /dev/shm) int fd; if ((fd = shm_open(shmname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR)) == -1) return NULL; // esce con errore // tronca un mapped-file if (ftruncate(fd, sizeof(ShmData) + len) == -1) return NULL; // esce con errore // mappa un mapped-file ShmData *shmdata; if ((shmdata = mmap(NULL, sizeof(ShmData) + len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) return NULL; // esce con errore // init semaforo if (sem_init(&shmdata->sem, 1, 1) == -1) return NULL; // esce con errore // init flag di data_ready e lunghezza shmdata->data_ready = false; shmdata->len = len; // ritorna il descrittore return shmdata; } // memMapOpenMast() - apre un mapped-file come slave ShmData *memMapOpenSlav( const char *shmname, // nome del mapped-file size_t len) // size del campo data da condividere { // apre un mapped-file (il file "shmname" é creato in /dev/shm) int fd; if ((fd = shm_open(shmname, O_RDWR, S_IRUSR|S_IWUSR)) == -1) return NULL; // esce con errore // mappa un mapped-file ShmData *shmdata; if ((shmdata = mmap(NULL, sizeof(ShmData) + len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) return NULL; // esce con errore // init semaforo if (sem_init(&shmdata->sem, 1, 1) == -1) return NULL; // esce con errore // ritorna il descrittore return shmdata; } // memMapClose() - chiude un mapped-file int memMapClose( const char *shmname, // nome del mapped-file ShmData *shmdata) // pointer al mapped-file { // elimina semaforo if (sem_destroy(&shmdata->sem) < 0) return -1; // esce con errore // un-mappa un mapped-file if (munmap(shmdata, sizeof(ShmData)) < 0) return -1; // esce con errore // cancella un mapped-file if (shm_unlink(shmname) < 0) return -1; // esce con errore // esce con Ok return 0; } // memMapFlush() - flush di un mapped-file int memMapFlush( ShmData *shmdata) // pointer al mapped-file { // sync su disco di un mapped-file return msync(shmdata, sizeof(ShmData) + shmdata->len, MS_SYNC); } // memMapRead() - legge dati dal mapped-file int memMapRead( void *dest, ShmData *src) { // lock memoria sem_wait(&src->sem); // test presenza dati nel mapped-file if (src->data_ready) { // legge dati dal mapped-file memcpy(dest, src->data, src->len); src->data_ready = false; // unlock memoria ed esce sem_post(&src->sem); return 1; } else { // unlock memoria ed esce sem_post(&src->sem); return 0; } } // memMapWrite() - scrive dati nel mapped-file void memMapWrite( ShmData *dest, const void *src) { // lock memoria sem_wait(&dest->sem); // scrive dati nel mapped-file memcpy(dest->data, src, dest->len); dest->data_ready = true; // unlock memoria ed esce sem_post(&dest->sem); }Come si nota è abbastanza semplice e conciso, e, come sempre, il codice è auto-esplicativo, ampiamente commentato e con commenti che parlano da soli.
Tutte le funzioni usano, internamente, le opportune system call Linux/POSIX per trattare il nostro Memory Mapped File. In caso di errore su una system call si ritorna immediatamente -1, e questo ci permette, a livello applicativo, di usare direttamente strerror() per verificare l'errore (e questo è un argomento che i miei lettori più affezionati dovrebbero conoscere bene...). Come si nota ci sono due funzioni di open, una master e una slave: perché? Perché, il tipo di comunicazione scelto è (leggermente) asimmetrico, quindi è necessario che uno dei due estremi (il writer) apra il canale, mentre l'altro (il reader) accede al canale sono quando lo trova creato. Quindi adesso si comprende meglio (spero) come funzionano le due funzioni descritte nella prima puntata del post. Questo meccanismo asimmetrico ricorda molto il meccanismo Client/Server che si usa coi socket (altro argomento già affrontato qui), e, infatti, questa libreria è una alternativa al classico IPC coi socket.
Le funzioni di read e write si limitano a usare memcpy() per copiare i dati dal/sul mapped-file. E, come anticipato nel post precedente, le letture e scritture usano un meccanismo di sincronizzazione (un POSIX unnamed semaphore) che viene inizializzato nelle funzioni di open: semplicemente chi accede ai dati mette in rosso (lock) il semaforo (quindi chi arriva dopo si ferma) e quando ha finito lo rimette in verde (unlock).
La funzione di flush è solo un wrapper per la chiamata msync() e, normalmente, non è necessario usarla: con questa libreria noi vogliamo trattare file mappati in memoria per condividere dati tra processi, per cui, non solo ci interessa poco che il file abbia una immagine reale sul disco, ma, per una semplice questione di prestazioni dovremmo evitare di scaricare realmente sul disco tutti i cambi effettuati in memoria, se no tanto varrebbe far comunicare i processi con dei file veri. Quindi a cosa serve il flush? Serve solo ad avere una eventuale versione reale e aggiornata del file condiviso, nel caso volessimo trattarlo anche con le classiche funzioni open(), close(), read(), ecc. Per questo nel file msgwriter.c descritto nel post precedente la chiamata memMapFlush() non viene usata.
E veniamo all'altra novità introdotta in questa nuova versione della libmmap: i dati trattati sono, ora, generici, quindi le funzioni di read e write usano dei void* come argomenti: questo è un vantaggio notevole, perché permette di scambiare dati in IPC usando qualsiasi formato; una struttura complessa oppure una singola variabile (chessoio? un int). Ad esempio nell'ultimo post ho definito un tipo Data (una struct con un campo text) da usare a livello applicativo e che si può passare come argomento alle read e write senza neppure fare un cast. Una grande flessibilità, simile a quella delle funzioni della libc come la memcpy(), ma con un qualcosa in più: la dimensione (e, indirettamente, il tipo) dei dati che si scambiano viene passata, una volta per tutte, durante la fase di open (attraverso il parametro len), quindi le funzioni di read e write non hanno il classico campo "size_t len" che uno si aspetterebbe.
Ci manca solo da descrivere una cosa: il trucco del "char data[1]" usato per rendere generici i dati da condividere. Questo campo è (non a caso) l'ultimo della struttura dati che descrive il mapped-file, e funziona così: quando si crea il file si passa, alla memMapOpenMast(), il size dei dati da scambiare (con un operatore sizeof, vedi l'esempio dell'ultimo post), e, come si nota nel codice della memMapOpenMast(), il mapped-file viene mappato usando la system call mmap() passandogli un argomento length che indica la dimensione del mapped-file in oggetto: nel nostro caso si passa "sizeof(ShmData) + len", quindi il mapped-file è impostato per scambiare dati nella sua parte variabile "char data[1]", che di base è lunga un char, ma che, in realtà, è lunga len char una volta mappato il file. Un trucchetto da niente.
Spero che la nuova versione della libreria vi sia piaciuta. Vi assicuro che, con solo qualche miglioria (tipo la gestione di tutti gli errori interni possibili, lo sdoppiamento del semaforo per gestire lock read/write, aggiungere meccanismi di accesso blocking/nonblocking... va beh, forse un è un po' più di qualche miglioria!), si potrebbe usare anche in progetti professionali... e scusate se è poco!
Ciao, e al prossimo post!
Nessun commento:
Posta un commento