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.

sabato 9 dicembre 2017

Remapped File
come sincronizzare un Memory Mapped File in C - pt.1

Questo post è un remake. È il remake di un mio post (in due puntate) di quasi due anni fa, Irrational File (forza, forza, andate a rileggerlo, non fatevi pregare!). Normalmente i remake lasciano l'amaro in bocca, non aggiungono nulla di nuovo e, se l'originale era un gran film, difficilmente riescono a eguagliarlo. Ci sono delle eccezioni, però, come The Thing di J.Carpenter (uff, anche questo film l'ho già usato...) che è un capolavoro superiore al (pur buono) originale del 1951. Ecco, questo post appartiene (spero) ai remake buoni, perché, come vedrete, aggiunge molto all'originale.
...un immagine di The Thing per il remake di Irrational Man: poche idee e ben confuse, vedo...
Allora, il post originale descriveva una (semplice) libreria che avevo scritto per condividere dati tra processi (IPC) usando come mezzo di comunicazione un Memory Mapped File. In una frase del vecchio post (che riporto parzialmente) si preannunciava già questo remake: "...per un uso semplice questo metodo va più che bene, ma, per applicazioni più complesse [...] bisognerebbe usare un sistema più evoluto [...] un mutex o un semaforo (ma questa è un altra storia, magari in futuro farò un post sull'argomento)...". Ecco, ora ci siamo: questa è la versione con gli accessi sincronizzati della nostra vecchia libmmap, quasi pronta per un uso professionale.

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 3
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 (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!

sabato 25 novembre 2017

Errno e i suoi fratelli
come è implementato errno in C

Questo post è un doveroso addendum al post precedente (che se non avete già letto dovreste affrettarvi a farlo, sono strettamente collegati). Questa volta parleremo di errno e dei suoi fratelli, una famiglia complessa, come quella di Rocco e i suoi fratelli, un capolavoro del neorealismo Italiano.
Rocco Errno e i suoi fratelli
Veniamo al dunque: dopo aver letto l'ultimo post i lettori più attenti si saranno chiesti: "Ok, con strerror_r() possiamo trattare le stringhe di errore in modo thread-safe, ma cosa ce ne facciamo se poi, alla base di tutto, c'è la variabile globale errno che non ha proprio l'aria di essere thread-safe?". La domanda è lecita, e per rispondere dobbiamo tornare un po' indietro nel tempo... la storia è analoga e parallela a quella di strerror() (e ci mancherebbe solo!). Anticamente errno era definito così nel header errno.h:
    extern int errno;
e si riferiva a una semplice variabile globale della libc, per l'appunto un int chiamato errno. Poi sono arrivati i thread, con lo standard POSIX 1003.1c (detto anche POSIX.1c, ma fa lo stesso), e con lui è arrivata anche la strerror_r() e, come no, anche errno ha avuto un fratello thread-safe. Come si può ben leggere nel manuale di errno (dopo POSIX.1c) ora errno è:
    errno is defined by the ISO C standard to be a modifiable lvalue of
    type int, and must not be explicitly declared; errno may be a macro.
    errno is thread-local; setting it in one thread does not affect its
    value in any other thread.
Quindi la nuova definizione di errno è ora in bits/errno.h (che viene incluso dal errno.h classico). Semplificando un po' (ho omesso alcuni dettagli per facilitare la lettura) il nuovo giro del fumo è:
nel header-file errno.h
#include <bits/errno.h>
/* Declare the `errno' variable, unless it's defined as a macro by
   bits/errno.h.  This is the case in GNU, where it is a per-thread
   variable.  This redeclaration using the macro still works, but it
   will be a function declaration without a prototype and may trigger
   a -Wstrict-prototypes warning.  */
#ifndef errno
extern int errno;
#endif

nel header-file bits/errno.h
/* Function to get address of global `errno' variable. */
extern int *__errno_location (void);

/* When using threads, errno is a per-thread value. */
#define errno (*__errno_location ())
Quindi, in parole povere, ora errno non è più un int globale ma è "il contenuto di un indirizzo ritornato da una funzione globale". Ovviamente la variabile int a cui punta la funzione in oggetto è il nuovo errno locale di un thread (ossia: ogni thread ha il suo errno). Un esempio (mooolto semplificato) di come si potrebbe implementare la __errno_location() è il seguente:
// errno locale di un thread: è una variabile di tipo Thread-local storage (TLS)
__thread int th_errno;

int *__errno_location(void)
{
    // ritorna l'indirizzo della variabile th_errno
    return &th_errno;
}
E, alla fine della fiera, nonostante i cambi descritti, sarà ancora possibile fare operazioni di questo tipo:
int my_errno = errno; // Ok, equivale a: int my_errno = (* __errno_location());
errno = EADDRINUSE;   // Ok, equivale a: (* __errno_location()) = EADDRINUSE;
perché, ovviamente, tutto è stato pensato per essere retro-compatibile, e quindi errno, nonostante ora sia una macro, deve ancora comportarsi come quando era un semplice int.

E, per finire in bellezza, non facciamoci mancare un piccolo estratto dello standard POSIX.1c, che puntualizza tutto quello detto fin'ora:
Redefinition of errno
In POSIX.1, errno is defined as an external global variable. But this definition
is unacceptable in a multithreaded environment, because its use can result in 
nondeterministic results. The problem is that two or more threads can encounter 
errors, all causing the same errno to be set. Under these circumstances, a thread 
might end up checking errno after it has already been updated by another thread. 

To circumvent the resulting nondeterminism, POSIX.1c redefines errno as a service 
that can access the per-thread error number as follows (ISO/IEC 9945:1-1996, n2.4): 

Some functions may provide the error number in a variable accessed through the 
symbol errno. The symbol errno is defined by including the header <errno.h>, as 
specified by the C Standard ... For each thread of a process, the value of errno 
shall not be affected by function calls or assignments to errno by other threads. 

In addition, all POSIX.1c functions avoid using errno and, instead, return the 
error number directly as the function return value, with a return value of zero 
indicating that no error was detected. This strategy is, in fact, being followed 
on a POSIX-wide basis for all new functions.
Notare che l'ultima frase (da In addition... in poi) ci spiega il perchè dell'esistenza della strerror_r() XSI-compliant descritta nel post precedente: visto? tutti i nodi vengono al pettine... E con questo possiamo considerare risolto anche il mistero dell'errno thread-safe. Missione compiuta!

Ciao, e al prossimo post!