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...

lunes, 14 de marzo de 2016

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

Estamos de vuelta con nuestro Irrational Man (oops... Irrational File), o sea, un file conpartido en memoria. Después de describir el header file (libmmap.h) y un doble ejemplo del uso (msgreader.c y msgwriter.c) es hora de describir la implementación real. Obviamente, los que no han leído la primera parte, deberían avergonzarse y ir en seguida a leerla, y luego volver aquí.
te explico: supongamos que este es un plato compartido...
Los lectores más atentos se habrán dado cuenta, releyéndolo, que he modificado ligeramente el post anterior (magia del blogger ...) añadiendo (y no-usando) una función de flush que no estaba allí antes, y quitando el parámetro size de las funciones (veremos más adelante por qué).

Vamos a ver lo que contiene la librería (libmmap.c)... ¡vamos con el codigo!
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include "libmmap.h"

// memMapOpen() - abre un memory mapped file
Message *memMapOpen(
    const char *memname)
{
    // abre un memory mapped file (el file "memname" se crea en /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;
    }

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

    // mapea 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() - cierra un memory mapped file
int memMapClose(
    Message    *ptr,
    const char *memname)
{
    // un-mapea un memory mapped file
    if (munmap(ptr, sizeof(Message)) < 0) {
        fprintf(stderr, "munmap error: %s\n", strerror(errno));
        return -1;
    }

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

// memMapFlush() - flush de un memory mapped file
int memMapFlush(
    Message *ptr)
{
    // sync en disco de un memory mapped file
    return msync(ptr, sizeof(Message), MS_SYNC);
}
Como se puede ver, no es mucha cosa pero densa. Como siempre, el código es auto-explicativo, ampliamente comentado y los comentarios hablan por sí mismos.

Las funciones de open, close y flush usan las oportunas system call Linux/POSIX para tratar el nuestro Memory Mapped File. 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() está comentada y descrita como opcional.

¿Y el clásico parámetro size donde está? Bueno, esta es una librería especializada para un tipo específico que hemos definido (el tipo Message), entonces dentro de la librería podemos usar sizeof(Message) como y cuando queremos. El size, sin embargo, se utiliza para funciones de librerías más genéricas (que tratan tipos void*), en las cuales el campo de datos puede contener varias cosas y la función puede disponer del size de los datos sólo a través de un parámetro apósito.

Deberes para las vacaciones de la Semana Santa: hacer una versión genérica de la librería utilizando void *ptr en lugar de Mensaje *ptr en todas las funciones (¡acordarse de añadir también el size!). Os daréis cuenta que hay que hacer muy pocos cambios, una cosa muy simple. ¡Confiar en el vuestro C-Blogger favorito!

¡Hasta el próximo post!

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!

lunes, 4 de enero de 2016

Mad Sleep
cómo escribir un wrapper para la nanosleep() en C

La sleep() tiene personalidades múltiples, tal vez está un poco loca, al igual que nuestro amigo Max. Si queremos enviar a dormir por un tiempo la nuestra aplicación (normalmente un thread de una aplicación, pero vamos a tratar este tema en otra ocasión...) tenemos muchas (¿demasiadas?) opciones: sleep(), usleep(), nanosleep(), ¡y más todavia!
...¿a quién has llamado loco?
Vamos a centrarnos en las tres mencionadas anteriormente: cada una tiene sus pros y sus contras:
  • sleep(): está bien, pero tiene una resolución demasiado baja: un segundo es una eternidad para algunas aplicaciones.
  • usleep(): es obsoleta: fue eliminada por el estándar Posix, ya que tiene una respuesta a las señales indefinida.
  • nanosleep(): es OK (alta resolución, trata las señales), pero tiene una interfaz un poco complicada...
Para el uso normal (se podría decir  usos no hard-realtime, pero vamos a tratar este tema en otra ocasión...) nos necesitaría una función con el interfaz de la sleep() y una resolución media, yo diría en milisegundos. Pues, entonces, ¡hacemos un wrapper para la usleep()!

¡Vamos con el código!
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// prototipos locales
static void mySleep(long msec);
static char *getDateUsec(char *date);

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

    // enseña la hora, sleep y enseña otra vez la hora
    char date[32];
    printf("hora antes de la sleep:  %s\n", getDateUsec(date));
    mySleep(1500);
    printf("hora después de la sleep:%s\n", getDateUsec(date));

    // esce con Ok
    return EXIT_SUCCESS;
}

// mySleep - sleep para un numero especifico de milisegundos
void mySleep(
    long msec)              // sleep time en milisegundos
{
    // split argumento en segundos y milisegundos (msec está en milisegundos pero podría ser > 1000)
    unsigned int seconds      = msec / 1000;
    unsigned int milliseconds = msec % 1000;

    // set datos para nanosleeep() y los usa
    struct timespec t;
    t.tv_sec  = seconds;
    t.tv_nsec = milliseconds * 1000000; // i.e.: tv_nsec=500000000; 500 millones de ns es 1/2 sec
    nanosleep(&t, NULL);
}

// getDateUsec - obtiene date-time con los microsegundos
char *getDateUsec(
    char* date)             // string destinación
{
    ...
}
Y, como siempre:
  1. Codigo que se auto-explica, ampliamente comentado y... los comentarios hablan por sí mismos.
  2. La función main() no hace más que arrancar la mySleep() y a enseñar (con los datos de tiempo antes y después) que la función trabaja.
    La mySleep() es el nuestro wrapper, y se puede integrar fácilmente en cualquier aplicación o en una librería. Internamente utiliza la nanosleep(), formateando oportunamente los datos a pasar con el único argumento (en milisegundos) que le ofrecemos. El main() muestra (con una precisión de microsegundos) el tiempo antes y después de la mySleep(), utilizando otra función escrita ad-hoc, la getDateUsec(): esta es una nuestra vieja conocida descrita aqui, y por eso no he reportado su código (bueno, tal vez asi vais a leer el antiguo puesto donde fue descrita).

    Una última nota para (eventuales) súper-perfeccionistas que habrán pensado, al leer el código: "pero el split de los argumentos se podría hacer directamente en las asignaciones a struct timespec t, ahorrando las dos instrucciones de asignación precedentes...". Bueno, no voy a perder tiempo discutiendo y los invito sólo en reflexionar sobre los siguientes tres puntos:
    1. mySleep() sirve para retrasar: sabéis lo que nos importa la eficiencia en este caso...
    2. Leer esto.
    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).
    Os recuerdo una vez más que, para probar el programa en Linux (lo cual hice, por supuesto...) es suficiente compilar el programa con:
    gcc mysleep.c -o mysleep
    ...uhmmm... tengo que admitir que la mención del mítico D.Knuth es la parte más interesante de todo el post. Meditar gente, meditar...

    ¡Hasta el próximo post!