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.

martedì 8 marzo 2016

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

Rieccoci col nostro Irrational Man (oops... Irrational File), ossia un file condiviso in memoria. Dopo aver descritto l'header file (libmmap.h) e un doppio esempio di uso (msgreader.c e msgwriter.c) è venuto il momento di descrivere la vera e propria implementazione. Ovviamente, chi non ha letto la prima parte, deve vergognarsi e andare subito a leggerla, e poi tornare qui.
ti spiego: fai finta che questo sia un piatto condiviso...
I lettori più attenti si saranno accorti, rileggendolo, che ho modificato leggermente il post precedente (magia del blogger...) aggiungendo (e non-usando) una funzione di flush che prima non c'era e togliendo il parametro size dalle funzioni (vedremo più avanti il perché).

Vediamo ora cosa contiene la libreria (libmmap.c)... vai col codice!
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include "libmmap.h"

// memMapOpen() - apre un memory mapped file
Message *memMapOpen(
    const char *memname)
{
    // apre un memory mapped file (il file "memname" è creato in /dev/shm)
    int fd;
    if ((fd = shm_open(memname, O_CREAT | O_RDWR, 0666)) < 0) {
        fprintf(stderr, "shm_open error: %s\n", strerror(errno));
        return NULL;
    }

    // tronca un memory mapped file
    if (ftruncate(fd, sizeof(Message)) < 0) {
        fprintf(stderr, "ftruncate error: %s\n", strerror(errno));
        return NULL;
    }

    // mappa un memory mapped file
    Message *ptr;
    if ((ptr = mmap(NULL, sizeof(Message), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) < 0) {
        fprintf(stderr, "mmap error: %s\n", strerror(errno));
        return NULL;
    }

    return ptr;
}

// memMapClose() - chiude un memory mapped file
int memMapClose(
    Message    *ptr,
    const char *memname)
{
    // un-mappa un memory mapped file
    if (munmap(ptr, sizeof(Message)) < 0) {
        fprintf(stderr, "munmap error: %s\n", strerror(errno));
        return -1;
    }

    // cancella un memory mapped file
    if (shm_unlink(memname) < 0) {
        fprintf(stderr, "shm_unlink error: %s\n", strerror(errno));
        return -1;
    }
}

// memMapFlush() - flush di un memory mapped file
int memMapFlush(
    Message *ptr)
{
    // sync su disco di un memory mapped file
    return msync(ptr, sizeof(Message), MS_SYNC);
}
Come potete vedere, poca roba ma densa. Come sempre il codice è auto-esplicativo, ampiamente commentato e con commenti che parlano da soli.

Le funzioni di open, close e flush usano le opportune system call Linux/POSIX per trattare il nostro Memory Mapped File. 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() è commentata e descritta come opzionale.

Ed il classico parametro size che fine ha fatto? Beh, questa è una libreria specializzata per un tipo specifico che abbiamo definito (il tipo Message), quindi internamente alla libreria possiamo usare sizeof(Message) come e quando vogliamo. Il size, invece, serve per funzioni di librerie più generiche (che trattano tipi void*), dove il campo dati può contenere svariate cose e la funzione può disporre del size dei dati solo attraverso un apposito parametro.

Compito delle vacanze di Pasqua: fare una versione generica della libreria usando void *ptr al posto di Message *ptr in tutte le funzioni (ricordatevi di aggiungere anche il size!). Vi accorgerete che sono quattro modifiche in croce, una cosa semplicissima. Fidatevi del vostro C-Blogger favorito!

Ciao e al prossimo post!

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!

domenica 3 gennaio 2016

Mad Sleep
come scrivere un wrapper per la nanosleep() in C

La sleep() ha personalità multiple, forse è un po' pazza, proprio come il nostro amico Max. Se vogliamo mandare a dormire per un po' la nostra applicazione (tipicamente un thread di una applicazione, ma questo argomento lo tratteremo un altra volta...) abbiamo varie (troppe?) opzioni: sleep(), usleep(), nanosleep() e altre ancora!
...a chi hai detto pazzo?
Concentriamoci sulle tre elencate sopra: ognuna ha i suoi pro e contro:
  • sleep(): è Ok ma ha una risoluzione troppo bassa: un secondo per alcune applicazioni è una eternità.
  • usleep(): è obsoleta: è stata bannata dagli standard Posix perché ha una risposta ai segnali indefinita.
  • nanosleep() è Ok (alta risoluzione, tratta i segnali) ma ha una interfaccia un poco ostica...
Per gli usi normali (si potrebbe dire usi non hard-realtime, ma anche questo argomento lo tratteremo un altra volta...) ci servirebbe una funzione con l'interfaccia tipo la sleep() e una risoluzione media, direi in millisecondi. Ma allora facciamo un wrapper per la usleep()!

Vai col codice!
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// prototipi locali
static void mySleep(long msec);
static char *getDateUsec(char *date);

// main del programma di test
int main(int argc, char **argv)
{
    // test argomenti
    if (argc != 1) {
        // error
        printf("Usage: %s\n", argv[0]);
        return EXIT_FAILURE;
    }

    // mostra l'ora, sleep e mostra di nuovo l'ora
    char date[32];
    printf("ora prima della sleep: %s\n", getDateUsec(date));
    mySleep(1500);
    printf("ora dopo la sleep:     %s\n", getDateUsec(date));

    // esce con Ok
    return EXIT_SUCCESS;
}

// mySleep - sleep per un numero specifico di millisecondi
void mySleep(
    long msec)              // sleep time in millisecondi
{
    // split argomento in secondi e millisecondi (msec è in millisecondi ma potrebbe essere > 1000)
    unsigned int seconds      = msec / 1000;
    unsigned int milliseconds = msec % 1000;

    // set dati per nanosleeep() e li usa
    struct timespec t;
    t.tv_sec  = seconds;
    t.tv_nsec = milliseconds * 1000000; // i.e.: tv_nsec=500000000; 500 milioni di ns è 1/2 sec
    nanosleep(&t, NULL);
}

// getDateUsec - ottiene date-time con i microsecondi
char *getDateUsec(
    char* date)             // stringa destinazione
{
    ...
}
E, come sempre:
  1. Codice auto-esplicativo, ampiamente commentato e... i commenti parlano da soli.
  2. Il main() serve solo a lanciare la mySleep() e a mostrarci (con i dati del tempo prima e dopo) che la funzione funziona.
La mySleep() è il nostro wrapper, e può essere facilmente integrato in qualsiasi applicazione o in una libreria. Usa internamente la nanosleep(), formattando opportunamente i dati da passare usando il solo argomento (in millisecondi) che gli forniamo. Il main() ci mostra (con la precisione del microsecondo) il tempo prima e dopo la mySleep(), usando un'altra funzione scritta ad-hoc, la getDateUsec(): questa è una nostra vecchia conoscenza descritta qui, per cui non ho riportato il codice (così, magari vi viene voglia di rileggervi il vecchio post dove era descritta).

Una ultima nota per gli (eventuali) precisini che leggendo il codice avranno pensato "ma lo split degli argomenti si poteva fare direttamente nelle assegnazioni a struct timespec t, risparmiando le (ben) due istruzioni di assegnazione precedenti...". Beh non perdo neanche tempo a discutere e li invito solo a riflettere sui seguenti tre punti:
  1. mySleep() serve a ritardare: sai cosa ce ne importa dell'efficienza in questo caso...
  2. Leggetevi questo.
  3. "The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming" (D.Knuth, "Computer Programming as an Art", 1974).
Vi ricordo nuovamente che, per testare l'esempio su Linux (cosa che ho fatto, ovviamente...) basta compilare il programma con:
gcc mysleep.c -o mysleep
...uhmmm... devo ammettere che la citazione del mitico D.Knuth è la parte più interessante di tutto il post. Meditate gente, meditate...

Ciao e al prossimo post!