En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

domingo, 14 de enero de 2018

Remapped File
cómo sincronizar un Memory Mapped File en C - pt.2

Bueno, creo que es hora de publicar la segunda parte de Remapped File. Espero que hayáis recargado bien las pilas durante las vacaciones, tal vez viendo una gran película como Primer, que internamente contiene varios remakes de sí misma: podéis verla tantas veces como queráis y cada vez descubriréis nuevos detalles e, inevitablemente, os perderéis detalles antiguos, entrando en un loop temporal sin fin como los mismos protagonistas de la película. Os aseguro que el post de este mes no es tan complicado de entender como Primer, de la cual se encuentran en la web hasta páginas wiki dedicadas a las timeline con descripciones gráficas incluidas...
...¿qué apareció antes, el huevo o la gallina?...
Entonces, sigamos con nuestra biblioteca para IPC. Después de describir el header file (libmmap.h) y un doble ejemplo de uso (datareader.c y datawriter.c), es hora de describir la implementación real. Obviamente quien no ha leído la primera parte debe estar avergonzado e ir a leerla de inmediato, y luego volver aquí.

Tornati? Ok, vamos al grano, ¡vamos con el código!
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include "libmmap.h"

// memMapOpenMast() - abre un mapped-file como master
ShmData *memMapOpenMast(
    const char *shmname,    // nombre del mapped file
    size_t     len)         // size del campo data a compartir
{
    // abre un mapped-file (el file "shmname" se crea en /dev/shm)
    int fd;
    if ((fd = shm_open(shmname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR)) == -1)
        return NULL;    // sale con error

    // corta un mapped file
    if (ftruncate(fd, sizeof(ShmData) + len) == -1)
        return NULL;    // sale con error

    // mapea un mapped-file
    ShmData *shmdata;
    if ((shmdata = mmap(NULL, sizeof(ShmData) + len,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
        return NULL;    // sale con error

    // init semaforo
    if (sem_init(&shmdata->sem, 1, 1) == -1)
        return NULL;    // sale con error

    // init flag de data_ready y longitud
    shmdata->data_ready = false;
    shmdata->len = len;

    // retorna el descriptor
    return shmdata;
}

// memMapOpenMast() - abre un mapped-file como slave
ShmData *memMapOpenSlav(
    const char *shmname,    // nombre del mapped-file
    size_t     len)         // size del campo data a compartir
{
    // abre un mapped-file (il file "shmname" se crea en /dev/shm)
    int fd;
    if ((fd = shm_open(shmname, O_RDWR, S_IRUSR|S_IWUSR)) == -1)
        return NULL;    // sale con error

    // mapea un mapped-file
    ShmData *shmdata;
    if ((shmdata = mmap(NULL, sizeof(ShmData) + len,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
        return NULL;    // sale con error

    // init semaforo
    if (sem_init(&shmdata->sem, 1, 1) == -1)
        return NULL;    // sale con error

    // retorna el descriptor
    return shmdata;
}

// memMapClose() - cierra un mapped-file
int memMapClose(
    const char *shmname,    // nombre del mapped-file
    ShmData    *shmdata)    // pointer al mapped-file
{
    // elimina semaforo
    if (sem_destroy(&shmdata->sem) < 0)
        return -1;      // sale con error

    // de-mapea un mapped-file
    if (munmap(shmdata, sizeof(ShmData)) < 0)
        return -1;      // sale con error

    // cancela un mapped-file
    if (shm_unlink(shmname) < 0)
        return -1;      // sale con error

    // esce con Ok
    return 0;
}

// memMapFlush() - flush de un mapped-file
int memMapFlush(
    ShmData *shmdata)       // pointer al mapped-file
{
    // sync en disco de un mapped-file
    return msync(shmdata, sizeof(ShmData) + shmdata->len, MS_SYNC);
}

// memMapRead() - lee datos del mapped-file
int memMapRead(
    void    *dest,
    ShmData *src)
{
    // lock memoria
    sem_wait(&src->sem);

    // test presenciaa datos en el mapped-file
    if (src->data_ready) {
        // lee datos del mapped-file
        memcpy(dest, src->data, src->len);
        src->data_ready = false;

        // unlock memoria y sale
        sem_post(&src->sem);
        return 1;
    }
    else {
        // unlock memoria y sale
        sem_post(&src->sem);
        return 0;
    }
}

// memMapWrite() - escribe datos en el mapped-file
void memMapWrite(
    ShmData    *dest,
    const void *src)
{
    // lock memoria
    sem_wait(&dest->sem);

    // escribe datos en el mapped-file
    memcpy(dest->data, src, dest->len);
    dest->data_ready = true;

    // unlock memoria y sale
    sem_post(&dest->sem);
}
Como se puede ver es bastante simple y conciso, y, como siempre, el código es auto-explicativo, ampliamente comentado y los comentarios hablan por sí mismos. 

Todas las funciones utilizan, internamente, las oportunas system call Linux/POSIX para procesar el nuestro Memory Mapped File. En caso de error en una system call  se devuelve inmediatamente -1, y esto nos permite, a nivel de aplicación, usar directamente strerror() para verificar el error (y este es un tema que mis lectores más leales deberían conocer bien...). Como podeis ver, hay dos funciones de open, una master y una slave: ¿porqué? Porqué, como el tipo de comunicación elegido es (ligeramente) asimétrico, es necesario que uno de los dos extremos (el writer) abra el canal, mientras que el otro (el reader) acceda al canal cuando lo encuentra creado. Entonces ahora se entiende mejor (espero) cómo funcionan las dos funciones descritas en la primera parte del post. Este mecanismo asimétrico recuerda mucho al mecanismo Client/Server que se usa con los socket (otro tema ya discutido aquí) y, de hecho, esta librería es una alternativa al IPC clásico con los socket.

Las funciones de read y write se limitan en usar memcpy() para copiar los datos desde/hacia el mapped-file. Y, como se anticipó en el post anterior, las lecturas y escrituras utilizan un mecanismo de sincronización (un POSIX unnamed semaphore) que se inicializa en las funciones de open: simplemente quién accede a los datos pone en rojo (lock) el semáforo (entonces quién llega despues se detiene) y cuando termina, lo vuelve a poner en verde (unlock).

La función de flush es sólo un wrapper para la llamada msync() y, normalmente, no es necesario utilizarla: con esta librería queremos tratar a los archivos mapeados en memoria para compartir datos entre procesos, por lo que, no sólo nos interesa poco que el archivo tenga una imagen real en el disco, sino que, por una simple cuestión de rendimiento, deberíamos evitar de descargar en el disco todos los cambios realizados en la memoria, si no seria como usar files reales. Entonces, ¿de que sirve el flush? Sirve sólo para mantener una eventual versión real y actualizada del file compartido, en el caso que queremos tratarlo, también, con la clásicas funciones open(), close(), read(), etc. Por esto en el file msgwriter.c descritos en el post anterior la llamada memMapFlush() no está utilizada.

Y llegamos a la otra novedad presentada en esta nueva versión de libmmap: los datos procesados ahora son genéricos, por lo que las funciones de read y write usan void* como argumentos: esto es una gran ventaja, ya que permite intercambiar datos en IPC usando cualquier formato; una estructura compleja o una sola variable (¿yo que se?, ¿un int?). Por ejemplo, en el último post he definido un tipo Data (una struct con un campo text) para usarla en el nivel aplicación y que se puede pasar como un argumento para las read y write sin siquiera hacer un cast. Una gran flexibilidad, similar a la de las funciones de la libc como la memcpy(), pero con algo más: el tamaño (e, indirectamente, el tipo) de los datos intercambiados se pasa, de una vez por todas, durante la fase de open (a través del parámetro len), por lo que las funciones de read y write no tienen el clásico campo "size_t len" que cualquiera se esperaría.

Solo tenemos que describir una cosa: el truco del "char data[1]" utilizado para hacer que los datos compartidos sean genéricos. Este campo es (como era de esperar) el último de la estructura de datos que describe el mapped-file, y funciona así: cuando creas el file se pasa, a la memMapOpenMast(), el size de los datos a intercambiar (con un operador sizeof, ver el ejemplo de el último post), y, como podemos ver en el código de la memMapOpenMast(), el mapped-file se mapea utilizando la system call mmap() pasandole un argumento length que indica el tamaño del mapped-file en cuestión: en nuestro caso pasamos "sizeof(ShmData) + len", por lo que el mapped-file está configurado para intercambiar datos en su parte variable "char data[1]", que, de base, es larga un char, pero es, en realidad, larga len char una vez que el file ha sido mapeado. Un truquito de nada.

Espero que os haya gustado la nueva versión de la librería. Os aseguro que, con solo algunas mejoras (como la gestión de todos los errores internos posibles, añadir otro semáforo para administrar el lock en read/write, agregar mecanismos de acceso blocking/nonblocking... ¡bien, tal vez es un poco más que algunas mejoras!), se podría usar en proyectos profesionales... ¿y os parece poco?

¡Hasta el próximo post!

sábado, 9 de diciembre de 2017

Remapped File
cómo sincronizar un Memory Mapped File en C - pt.1

Este post es un remake. Es el remake de mi post (en dos episodios) de hace casi dos años, Irrational File (venga, venga, ir enseguida a releerlo, ¡no nos hagáis rogar!). Normalmente, los remake dejan un sabor amargo, no añaden nada nuevo y, si el original era una gran película, difícilmente pueden igualarla. Hay excepciones, sin embargo, como The Thing de J.Carpenter (uff, también esta película ya la he usada...) que es una obra maestra superior al (aunque bueno) original de 1951. Bueno, este post pertenece (espero) a los remake buenos, porque, como verán, añade mucho al original.
...una imagen de The Thing para el remake de Irrational Man: empezamos bien...
Vale,el post original describía una (simple) librería que había escrito para compartir datos entre procesos (IPC) utilizando como medio de comunicación un Memory Mapped File. En una frase del viejo post (que os propongo parcialmente) ya se anunciaba este remake: "...para un uso sencillo este método está muy bien, pero para aplicaciones complejas [...] se debería utilizar un sistema más avanzado [...] un mutex o un semáforo (pero eso es otra historia, quizás en el futuro voy a hacer un post sobre el tema)...". Entonces ya está: esta es la versión con los accesos sincronizados de la nuestra antigua libmmap, casi lista para un uso profesional.

Repitiendo la estructura del viejo post (y si no, ¿qué remake sería?) he dividido todo en dos partes: en la primera describiré, a modo de especifica funcional, el header file (libmmap.h) y un ejemplo de uso (data.h, datareader.c y datawriter.c). En la segunda entrega describiré la implementación real de la librería (libmmap.c).

Empezamos: vamos a ver el header-file, libmmap.h:
#ifndef LIBMMAP_H
#define LIBMMAP_H

#include <semaphore.h>
#include <stdbool.h>

#define MAPNAME "/shmdata"

// estructura del mapped-file
typedef struct {
    sem_t  sem;         // semáforo dei sincronización accesos
    bool   data_ready;  // flag de data ready (true=ready)
    size_t len;         // longitud del campo data
    char   data[1];     // datos a compartir
} ShmData;

// prototipos globales
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 */
Simple y autoexplicativo, ¿no? la nuestra librería utiliza una estructura de datos ShmData para mapear el mapped-file: aquí notamos de inmediato la primera gran mejora obtenida: hay un semáforo (un POSIX unnamed semaphore) para sincronizar los accesos a la memoria, lo que hace que la nuestra librería sea adecuada para un verdadero uso multitask (y multithread), que era el primer objetivo programado. El mecanismo de sincronización lo he elegido cuidadosamente entre los diversos disponibles: tal vez un día escriba una publicación específica sobre el tema, pero, por el momento, me limitaré a decir que el método elegido es simple de implementar, muy funcional y perfectamente adecuado para el propósito que hay que lograr (¿y esto es suficiente, no?).

El  flag de data_ready se usa (protegido por el semáforo) para indicar la disponibilidad de nuevos datos a leer. Luego tenemos, dulcis in fundo, los campos len y data que nos llevan al segundo objetivo que había establecido: los datos intercambiados son, ahora, genéricos, con formato y longitud que se deciden a nivel de aplicación. Nótese, de hecho, que el campo data es un array de dimensión 1: esto es una especie de truco (a usar con las debidas precauciones) bastante utilizado en C para tratar datos genéricos de forma y longitud no disponibles a priori. En nuestra struct el campo debe colocarse, obviamente, como el último miembro, transformándola así en una especie de estructura de tamaño variable. Sin embargo, en el próximo post veremos mejor cómo funciona todo.

libmmap.h termina con los prototipos de las funciones que componen la libreria: tenemos dos funciones de apertura, que nos permiten abrir un mapped-file en modo Master o Slave (en el próximo post veremos el motivo de esta doble apertura); luego tenemos una función de cierre, una función de flush y, por supuesto, dos funciones para escribir y leer datos. Estas dos últimas funciones confirman el discurso de generalidad descrito anteriormente: las variables de read/write son de tipo void*, por lo tanto, son adecuadas para aceptar cualquier tipo de datos. Como el formato de los datos a intercambiar se mueve al nivel de la aplicación, he escrito un  header-file (como ejemplo), data.h, que está incluido en las dos aplicaciones que se comunican:
#ifndef DATA_H
#define DATA_H

// definición estructura data para aplicaciones de ejemplo
typedef struct {
    int  type;          // tipo de datos
    int  data_a;        // un dato (ejemplo)
    int  data_b;        // un otro dato (ejemplo)
    char text[1024];    // testo de los datos
} Data;

#endif /* DATA_H */
Como podéis ver he elegido utilizar una estructura de datos que incluye un campo de texto, pero se puede intercambiar cualquier cosa, también solo un simple int, por ejemplo. Ahora vamos a ver la primera aplicación de 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 programa de test
int main(int argc, char *argv[])
{
    // abre mapped-file
    ShmData *shmdata;
    if ((shmdata = memMapOpenMast(MAPNAME, sizeof(Data)))) {
        // file abierto: start loop de escritura
        for (int i = 0; i < 100; i++) {
            // compone datos para el reader
            Data data;
            snprintf(data.text, sizeof(data.text), "nuevos datos %d", i);

            // escribe datos en el mapped-file
            memMapWrite(shmdata, &data);
            printf("he escrito: %s\n", data.text);

            // loop sleep
            mySleep(100);
        }

        // cierra mapped-file
        memMapClose(MAPNAME, shmdata);
    }
    else {
        // sale con error
        printf("no puedo abrir el file %s (%s)\n", MAPNAME, strerror(errno));
        return EXIT_FAILURE;
    }

    // esce con Ok
    return EXIT_SUCCESS;
}
Simple, ¿no? Abre el file compartido en la memoria (en modo Master) y lo usa para escribir datos (en loop) para la otra aplicación de 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 programa de test
int main(int argc, char *argv[])
{
    // abre esperando que un writer abra come master el mapped-file
    ShmData *shmdata;
    while ((shmdata = memMapOpenSlav(MAPNAME, sizeof(Data))) == NULL) {
        // acepta solo el error de file todavía no existente
        if (errno != ENOENT) {
            // sale con error
            printf("no puedo abrir el file %s (%s)\n", MAPNAME, strerror(errno));
            return EXIT_FAILURE;
        }

        // loop sleep
        mySleep(100);
    }

    // file abierto: start loop de lectura
    for (int i = 0; i < 100; i++) {
        // busca datos a leer en el mapped-file
        Data data;
        if (memMapRead(&data, shmdata)) {
            // enseña los datos leidos
            printf("me has escrito: %s\n", data.text);
        }

        // loop sleep
        mySleep(100);
    }

    // cierra mapped-file y sale con Ok
    memMapClose(MAPNAME, shmdata);
    return EXIT_SUCCESS;
}
El reader es, como se nota, una aplicación especular del writer (lee en lugar de escribir). Notar que, en ambas aplicaciones, se testean los errores en las funciones de open y se cierra (si necesario) la ejecución enseñando el error con strerror(): esto es posible porque (como veremos en el proximo post) las funciones de apertura salen en caso de error de las funciones de la libc que usan internamente, y, en ese punto, la descripción del error que ha ocurrido está disponible con errno (pero de esto hemos hablado extensamente en los dos últimos post ¿recuerdan?).

¿Usando las dos aplicaciones en dos terminales diferentes qué vamos a ver?
En la terminal 1:
aldo@mylinux:~/blogtest$ ./datawriter
he escrito: nuevos datos 1
he escrito: nuevos datos 2
he escrito: nuevos datos 3
^C

En la terminal 2:
aldo@mylinux:~/blogtest$ ./datareader
me has escrito: nuevos datos 1
me has escrito: nuevos datos 2
me has escrito: nuevos datos 3
Para enviar los loop a dormir he utilizado la función mySleep(), que es una nuestra vieja conocida: se puede insertar en una libreria separada o en la misma libreria libmmap (yo he utilizado un file separado mysleep.c con su header-file mysleep.h que solo contiene el prototipo). En este simple ejemplo, las dos aplicaciones que se comunican pueden detenerse usando CTRL-C, y (cuando vais a usar la libreria) podrais verificar que iniciando/detenendo/reiniciando ambas aplicaciones, en cualquier orden, siempre se vuelven a sincronizar sin problemas.

Por hoy hemos terminado. Esperando la segunda parte, podríais intentar imaginar cómo será la implementación de la libreria que os presentaré... pero llega la Navidad e imagino (y espero) que tengais cosas más interesantes en qué pensar en este período ...

¡Hasta el próximo post!