Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

domenica 7 febbraio 2016

Irrational File
come creare un Memory Mapped File in C - pt.1

Un Memory Mapped File è un file irrazionale, proprio come il nostro amico: sembra un file normale, ma in realtà vive in memoria e ci fa credere di essere (anche) sul disco.

I nostri amati UNIX e Linux ci forniscono varie forme di crearne uno (siamo nella categoria IPC, che è abbastanza vasta), e, visto che le interfacce disponibili non sono proprio immediatissime ho pensato di scrivere una piccola libreria ad-hoc, la libmmap, così spostiamo la complicazione nella libreria (ecco a cosa servono le librerie!) e possiamo, a livello applicativo, fare e disfare i nostri Memory Mapped File come, quando e quanto vogliamo, con grande facilità.

Da qui in avanti seguirò lo stesso approccio che ho usato in un mio vecchio post, così approfitto anche per riciclare un po' di frasi (si, lo so, è un auto-plagio, ma non credo che mi arrabbierò per questo...).

Vai con la descrizione!
Ma cosa ci facciamo in un blog di programmazione?
Sia perché non voglio fare un post troppo lungo (diventerebbe noioso), sia per seguire una specie di approccio specifica+implementazione (adattato a un C-Blog), ho diviso il post in due puntate: nella prima descriverò, a mo' di specifica funzionale, l'header file (libmmap.h) e un doppio esempio di uso (msgreader.c e msgwriter.c). Nella seconda puntata descriverò la vera e propria implementazione.

Vediamo l'header file, libmmap.h:
#ifndef LIBMMAP_H
#define LIBMMAP_H
#include <sys/mman.h>

// definizione struttura messaggio
typedef struct {
    int  type;              // tipo di messaggio
    int  data_a;            // un dato (esempio)
    int  data_b;            // un altro dato (esempio)
    char text[1024];        // testo del messaggio
} Message;

// prototipi globali
Message *memMapOpen(const char *memname);
int     memMapClose(Message *ptr, const char *memname);
int     memMapFlush(Message *ptr);
#endif /* LIBMMAP_H */
Semplice e auto-esplicativo, no? La nostra libreria è formata da due funzioni canoniche (apri, chiudi), che ci permettono di aprire un Memory Mapped File dandogli un nome e un size, e di chiuderlo usando gli stessi dati più un pointer che ci ha restituito la funzione di apertura. Per mappare il file definiamo un tipo Message che contiene dei dati e un campo testo: ho usato la forma messaggio perché la libreria ci serve per comunicare tra processi, ma, in realtà, possiamo definire il tipo come vogliamo. La terza funzione (flush) ci permette di aggiornare il file sul disco (ma di questo parleremo meglio nella prossima puntata...).

Notare l'uso degli include guard per il nostro header file: questa è una libreria che possiamo usare anche in un grosso progetto, e bisogna premunirsi contro eventuali/erronee inclusioni multiple.

Vediamo ora la prima parte del doppio esempio d'uso, msgwriter.c:
#include <stdio.h>
#include <string.h>
#include "libmmap.h"

// main del programma di test
int main(int argc, char *argv[])
{
    // apre memory mapped file
    const char *memname = "message";
    Message *msg = memMapOpen(memname);

    // loop infinito di scrittura
    for (;;) {
        // compone messaggio per il reader
        printf("Scrivi un messaggio per il reader: ");
        scanf("%s", msg->text);

        // flush dati nella memoria condivisa. N.B.: OPZIONALE
        //memMapFlush(msg);

        // loop sleep
        mySleep(100);
    }

    // chiude memory mapped file
    memMapClose(msg, memname);
}
Semplice, no? Apro il file condiviso in memoria e lo uso per inviare messaggi (in loop infinito) all'altra applicazione di test , msgreader.c:
#include <stdio.h>
#include <string.h>
#include "libmmap.h"

// main del programma di test
int main(int argc, char *argv[])
{
    // apre memory mapped file
    const char *memname = "message";
    Message *msg = memMapOpen(memname);

    // loop infinito di lettura
    for (;;) {
        // test presenza messaggio in memoria condivisa
        if (strlen(msg->text)) {
            // legge e reset msg dalla memoria condivisa
            printf("mi hai scritto: %s\n", msg->text);
            msg->text[0] = 0;
        }

        // loop sleep
        mySleep(100);
    }

    // chiude memory mapped file
    memMapClose(msg, memname);
}
Il reader è, come si vede, una applicazione speculare del writer, la prima legge e la seconda scrive. Il meccanismo di sincronizzazione scelto è semplice: il reader legge solo quando si accorge (con una strlen())che sono cambiati i dati nel file. Per un uso semplice questo metodo va più che bene, ma, per applicazioni pìu complesse (magari multithread) bisognerebbe usare un sistema più evoluto, e, solitamente si inserisce nel tipo Message stesso un mutex o un semaforo (ma questa è un altra storia, magari in futuro farò un post sull'argomento).

Usando le due applicazioni in due terminali differenti cosa vedremo? in quella del writer avremo:
Scrivi un messaggio per il reader: pippo
Scrivi un messaggio per il reader: paperino
Scrivi un messaggio per il reader:
e in quelle del reader vedremo:
mi hai scritto: pippo
mi hai scritto: paperino
Per 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. Ah, dimenticavo: i loop sono infiniti, quindi il file, in realtà, non viene mai chiuso: ricordarsi che, in una applicazione reale, la funzione di chiusura si deve usare quando si smette di usare il file.

Per oggi abbiamo finito. I più volenterosi potranno, nell'attesa della seconda parte, scrivere una propria implementazione, e poi confrontarla con la mia, ma la mia sarà sicuramente meglio... (si allontana, un altra volta, sghignazzando).

Ciao e al prossimo post!