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, 7 de febrero de 2016

Irrational File
cómo crear un Memory Mapped File en C - pt.1

Un Memory Mapped File es un file irracional, al igual que nuestro amigo: parece un archivo normal, pero en realidad vive en la memoria y nos hace pensar que está (también) en el disco.

Nuestros queridos UNIX y Linux nos proporcionan diversas formas para crear uno (estamos en la categoría de IPC, que es bastante extensa), y ya que las interfaces disponibles no son clarísimas he pensado en escribir una pequeña librería ad-hoc, la libmmap, así movemos las complicaciones en la librería (¡a eso sirven las librerias!) y podemos, a nivel de aplicación, hacer y deshacer nuestro Memory Mapped File cómo, cuándo y cómo queremos, con facilidad.

A partir de aquí, seguiré el mismo enfoque que utilicé en un mio antiguo post, así aprovecho también para reciclar un poco de frases (sí, lo sé, es un auto-plagio, pero no creo que voy a enfadarme por esto...).

¡Vamos con la descripción!
¿Pero que hacemos en un blog de programación?
Sea porque no quiero hacer un post muy largo (se convertiría en aburrido), que por seguir una especie de enfoque específica+implementación (adaptado a un C -Blog ), he dividido el post en dos partes: en la primera describiré, a modo de especifica funcional, el header file (libmmap.h) y un doble ejemplo de uso (msgreader.c y msgwriter.c). En la segunda entrega describiré la implementación real .

Vemos el header file, libmmap.h:
#ifndef LIBMMAP_H
#define LIBMMAP_H
#include <sys/mman.h>

// definición estructura mensaje
typedef struct {
    int  type;              // tipo de mensaje
    int  data_a;            // un dato (ejemplo)
    int  data_b;            // un otro dato (ejemplo)
    char text[1024];        // texto del mensaje
} Message;

// prototipos globales
Message *memMapOpen(const char *memname);
int     memMapClose(Message *ptr, const char *memname);
int     memMapFlush(Message *ptr);
#endif /* LIBMMAP_H */
Simple y que se explica por sí mismo, ¿no? Nuestro producto se compone de dos funciones canónicas (abre, cierra) que nos permiten abrir Memory Mapped File, dándole un nombre y un size, y de cerrarle con los mismos datos mas un pointer que nos devuelve la función de apertura. Para mapear el file definimos un tipo Message que contiene datos y un campo de texto: he utilizado el formado mensaje porque la librería nos sirve para la comunicación entre procesos, pero, de hecho, podemos definir el tipo que queremos. La tercera función (flush) nos permite poner al día el file en el disco (pero de esto hablaremos mejor en el próximo episodio...).

Observe el uso de los include guard para el nuestro header fileg: esta es una librería que podemos utilizar en un gran proyecto, y debemos evitar eventuales/erróneas inclusiones múltiplas.

Ahora vamos a ver la primera parte del doble ejemplo de uso, msgwriter.c:
#include <stdio.h>
#include <string.h>
#include "libmmap.h"

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

    // loop infinito de escritura
    for (;;) {
        // compone mensaje para el reader
        printf("Escribe un mensaje para el reader: ");
        scanf("%s", msg->text);

        // flush datos en la memoria compartida. NOTA: OPCIONAL
        //memMapFlush(msg);

        // loop sleep
        mySleep(100);
    }

    // cierra memory mapped file
    memMapClose(msg, memname);
}
Sencillo, ¿no? Abro el file mapeado en memoria y lo uso para enviar mensajes (en loop infinito) a la otra aplicación de test , msgreader.c:
#include <stdio.h>
#include <string.h>
#include "libmmap.h"

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

    // loop infinito de lectura
    for (;;) {
        // test presencia mensaje en memoria compartida
        if (strlen(msg->text)) {
            // lee y reset msg de la memoria compartida
            printf("me has escrito: %s\n", msg->text);
            msg->text[0] = 0;
        }

        // loop sleep
        mySleep(100);
    }

    // cierra memory mapped file
    memMapClose(msg, memname);
}
El reader es, como se nota, una aplicación especular del writer, la primera lee y la segunda escribe. El mecanismo de sincronización elegido es simple: el reader lee sólo cuando se da cuenta (con strlen()) que han cambiado los datos en el file. Para un uso sencillo este método está muy bien, pero para aplicaciones complejas (posiblemente multithread) se debería utilizar un sistema más avanzado, y, normalmente, se añade en el mismo tipo Message un mutex o un semáforo (pero eso es otra historia, quizás en el futuro voy a hacer un post sobre el tema).

¿Usando las dos aplicaciones en dos terminales diferentes qué vamos a ver? en la del writer tendremos:
Escribe un mensaje para el reader: hola
Escribe un mensaje para el reader: que tal
Escribe un mensaje para el reader:
y en la del reader veremos:
me has escrito: hola
me has escrito: que tal
Para enviar a dormir los loop he utilizado la función mySleep(), que es una nuestra vieja conocida: se puede añadir en una librería a parte o en la misma librería libmmap. Ah, me olvidaba: los loop son infinitos, entonces el file, de hecho, nunca se cierra: recordar que, en una aplicación real, la función de cierre se tiene que utilizar cuando se deja de usar el file.

Para hoy terminamos. Los mas trabajadores podrán, esperando la segunda parte, escribir su propia implementación, y luego compararla con la mía, pero la mía será seguramente mejor... (se aleja, otra vez, riendo).

¡Hasta el próximo post!