...un immagine di The Thing per il remake di Irrational Man: poche idee e ben confuse, vedo... |
Ripetendo la struttura del vecchio post (e se no, che remake sarebbe?) ho diviso il tutto in due puntate: nella prima descriverò, a mo' di specifica funzionale, l'header file (libmmap.h) e un esempio di uso (data.h, datareader.c e datawriter.c) basato su due applicazioni comunicanti. Nella seconda puntata descriverò la vera e propria implementazione della libreria (libmmap.c).
Cominciamo: vediamo l'header-file, libmmap.h:
#ifndef LIBMMAP_H #define LIBMMAP_H #include <semaphore.h> #include <stdbool.h> #define MAPNAME "/shmdata" // struttura del mapped-file typedef struct { sem_t sem; // semaforo di sincronizzazione accessi bool data_ready; // flag di data ready (true=ready) size_t len; // lunghezza campo data char data[1]; // dati da condividere } ShmData; // prototipi globali ShmData *memMapOpenMast(const char *shmname, size_t len); ShmData *memMapOpenSlav(const char *shmname, size_t len); int memMapClose(const char *shmname, ShmData *shmdata); int memMapFlush(ShmData *shmdata); int memMapRead(void *dest, ShmData *src); void memMapWrite(ShmData *dest, const void *src); #endif /* LIBMMAP_H */Semplice e auto-esplicativo, no? La nostra libreria usa una struttura dati ShmData per mappare il mapped-file: qui subito notiamo il primo grosso miglioramento ottenuto: c'è un semaforo (un POSIX unnamed semaphore) per sincronizzare gli accessi alla memoria, il che rende la nostra libreria adatta a un vero uso multitask (e multithread), che era il primo obiettivo programmato. Il meccanismo di sincronizzazione l'ho scelto in maniera oculata tra i vari disponibili: magari un giorno scriverò un post specifico sull'argomento, ma, per il momento, mi limiterò a dire che il metodo scelto è semplice da implementare, molto funzionale e si adatta perfettamente allo scopo da raggiungere (e scusate se è poco!).
Il flag di data_ready si usa (protetto dal semaforo) per segnalare la disponibilità di nuovi dati da leggere. Poi abbiamo, dulcis in fundo, i campi len e data che ci conducono al secondo obiettivo che mi ero prefisso: i dati scambiati sono, ora, generici, con formato e lunghezza che vengono decisi a livello applicativo. Notare, per l'appunto, che il campo data è un array di dimensione 1: questo è una sorta di trucco (da usare con le dovute precauzioni) abbastanza usato nel C per trattare dati generici di forma e lunghezza non disponibili a priori. Nella nostra struct il campo deve essere, ovviamente, posto come ultimo membro, rendendola così una specie di struttura con size variabile. Comunque nel prossimo post vedremo meglio come funziona il tutto.
libmmap.h termina con i prototipi delle funzioni che compongono la libreria: abbiamo due funzioni di apertura, che ci permettono di aprire un mapped-file in modo Master o Slave (nel prossimo post vedremo il perché di questa doppia apertura); poi abbiamo una funzione di chiusura, una di flush, e, ovviamente, due funzioni per scrivere e leggere dati. Queste ultime due funzioni ci confermano il discorso di genericità appena descritto: le variabili di read/write sono di tipo void*, quindi adatte ad accettare qualsiasi tipo di dato. Visto che il formato dei dati da scambiare viene spostato a livello applicativo ho scritto un header-file (di esempio), data.h, che viene incluso dalle due applicazioni comunicanti:
#ifndef DATA_H #define DATA_H // definizione struttura data per applicativi di esempio typedef struct { int type; // tipo di dati int data_a; // un dato (esempio) int data_b; // un altro dato (esempio) char text[1024]; // testo dei dati } Data; #endif /* DATA_H */Come vedete ho scelto di usare una struttura dati che include un campo testo, ma si può scambiare qualsiasi cosa, anche solo un semplice int, per esempio. Vediamo ora la prima applicazione d'uso, datawriter.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include "libmmap.h" #include "data.h" #include "mysleep.h" // main del programma di test int main(int argc, char *argv[]) { // apre mapped-file ShmData *shmdata; if ((shmdata = memMapOpenMast(MAPNAME, sizeof(Data)))) { // file aperto: start loop di scrittura for (int i = 0; i < 100; i++) { // compone dati per il reader Data data; snprintf(data.text, sizeof(data.text), "nuovi dati %d", i); // scrive dati nel mapped-file memMapWrite(shmdata, &data); printf("ho scritto: %s\n", data.text); // loop sleep mySleep(100); } // chiude mapped-file memMapClose(MAPNAME, shmdata); } else { // esce con errore printf("non posso aprire il file %s (%s)\n", MAPNAME, strerror(errno)); return EXIT_FAILURE; } // esce con Ok return EXIT_SUCCESS; }Semplice, no? Apre il file condiviso in memoria (in modo Master) e lo usa per scrivere dati (in loop) per l'altra applicazione di test, datareader.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include "libmmap.h" #include "data.h" #include "mysleep.h" // main del programma di test int main(int argc, char *argv[]) { // apre aspettando che un writer apra come master il mapped-file ShmData *shmdata; while ((shmdata = memMapOpenSlav(MAPNAME, sizeof(Data))) == NULL) { // accetta solo l'errore di file non ancora esistente if (errno != ENOENT) { // esce con errore printf("non posso aprire il file %s (%s)\n", MAPNAME, strerror(errno)); return EXIT_FAILURE; } // loop sleep mySleep(100); } // file aperto: start loop di lettura for (int i = 0; i < 100; i++) { // cerca dati da leggere nel mapped-file Data data; if (memMapRead(&data, shmdata)) { // mostra i dati letti printf("mi hai scritto: %s\n", data.text); } // loop sleep mySleep(100); } // chiude mapped-file ed esce con Ok memMapClose(MAPNAME, shmdata); return EXIT_SUCCESS; }Il reader è, come si vede, una applicazione speculare del writer (legge invece di scrivere). Notare che, in entrambe le applicazioni, si testano gli errori sulle funzioni di open e si chiude (se necessario) l'esecuzione mostrando l'errore con strerror(): questo è possibile perché (come vedremo nel prossimo post) le funzioni di apertura escono in caso di errore delle funzioni della libc che usano internamente, e, a quel punto, è disponibile con errno la descrizione dell'errore che si è verificato (ma di questo ne abbiamo parlato ampiamente negli ultimi due post, ricordate?).
Usando le due applicazioni in due terminali differenti cosa vedremo?
Nel terminale 1: aldo@mylinux:~/blogtest$ ./datawriter ho scritto: nuovi dati 1 ho scritto: nuovi dati 2 ho scritto: nuovi dati 3 ^C Nel terminale 2: aldo@mylinux:~/blogtest$ ./datareader mi hai scritto: nuovi dati 1 mi hai scritto: nuovi dati 2 mi hai scritto: nuovi dati 3Per mandare a dormire i loop ho usato la funzione mySleep(), che è una nostra vecchia conoscenza: si pùo inserire in una libreria a parte o nella stessa libreria libmmap (io ho usato un file a parte mysleep.c con un suo header-file mysleep.h che contiene solo il prototipo). In questo semplice esempio le due applicazioni comunicanti si possono fermare usando CTRL-C, e (quando proverete a usare la libreria) potrete verificare che avviando/fermando/riavviando entrambe le applicazioni, in qualsiasi ordine, si ri-sincronizzano sempre senza problemi.
Per oggi abbiamo finito. In attesa della seconda parte potreste provare a immaginare a come sarà l'implementazione della libreria che vi presenterò... ma si avvicina il Natale e immagino (e spero) che abbiate cose più interessanti da pensare in questo periodo...
Ciao, e al prossimo post!
ciao, complimenti per l articolo (finalmente qualcosa in italiano sull argomento ).
RispondiEliminati volevo chiedere alcune cose che vertono sull argomento ma che spaziano oltre (spazio ultima frontiera .... visto che sei anche un filmografo :D ) , sarebber possibile scambiare due parole per mail ?? , non trovo sul blog la tua mail ti lascio la mia , se ti va , grazie e buona serata stfn77@gmail.com