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

sábado, 16 de abril de 2016

The (Duel)lists
cómo usar las Linked Lists en C

Si alguien habla mal de las Linked Lists lo reto a un duelo. Sí, lo haré igual que Harvey Keitel en una de las obras maestras de Ridley Scott. Tener cuidado.
¿Qué decías de las Linked Lists?
Por aquí ya habíamos abordado (indirectamente) el tema de las Linked Lists, en un post prehistórico sobre la malloc(): tendríais que ir a releerlo, pero, si realmente no tenéis ganas de hacerlo, os resumo una parte:

"Para aquellos que nunca la han utilizado, las linked list no son una frivolidad: son una de las herramientas más poderosas de la programación en general (y del C en particular). Los que trabajan intensamente en proyectos grandes y profesional, más pronto o más tarde, acabaran por usarlas: una razón más, por lo tanto, para familiarizarse con la asignación de memoria dinámica."

Premisa: si alguien no tiene la más mínima idea de lo que una lista enlazada le aconsejo la lectura (antes de continuar) de una de las muchas descripciones (algunas realmente buenas) que se encuentran en la red. Esta, por ejemplo. Para este nuevo post he decidido retomar el código que había escrito para el viejo y simplificarlo/mejorarlo de manera de obtener:
  1. condensar en una sola función las dos que en el viejo código creaban un nuevo nodo en la lista (¡simplificar!).
  2. añadir una función que permite colgar un nuevo nodo en lugar de ponerlo en la cabezas de la lista (¡mejorar!).
¡Vamos con el codigo!
#include<stdlib.h>
#include<stdio.h>

// nodo de una linked list con campo datos
typedef struct snode {
    int data;
    struct snode *next;
} node_t;

// alloc en la cabeza de una lista un node con datos y un pointer al próximo elemento
void addNode(
    node_t **head,
    int    data)
{
    // alloc de un nuevo node
    node_t *node = malloc(sizeof(node_t));
    node->data = data;
    node->next = *head;

    // asigna head lista al nuevo node
    *head = node;
}

// alloc al final de una lista un node con datos y un pointer al próximo elemento
void appendNode(
    node_t **head,
    int    data)
{
    // alloc un nuevo node
    node_t *node = malloc(sizeof(node_t));
    node->data = data;
    node->next = NULL;

    // append el nuevo node
    node_t *current = *head;
    if (current == NULL) {
        // caso especial para lista vacia: asigna head lista al nuevo node
        *head = node;
    }
    else {
        // recurre la lista para encontrar el ultimo node
        while (current->next != NULL)
            current = current->next;

        // asigna al ultimo node el nuevo node
        current->next = node;
    }
}

// main() para test
void main()
{
    int i;

    // init lista vacia 1 y inserta con addNode() 3 nodes con data = índice loop
    node_t *head1 = NULL;
    for (i = 1; i <= 3; i++)
        addNode(&head1, i);

    // recurre la lista y enseña los valores
    node_t *myhead1 = head1;
    printf("myhead1=%p - metodo ADD\n", myhead1);
    while (myhead1) {
        printf("data=%d (myhead1=%p next=%p)\n", myhead1->data, myhead1, myhead1->next);
        myhead1 = myhead1->next ;
    }

    // init lista vacia 2 y inserta con addNode() 3 nodes con data = índice loop
    node_t *head2 = NULL;
    for (i = 1; i <= 3; i++)
        appendNode(&head2, i);

    // recurre la lista y enseña los valores
    node_t *myhead2 = head2;
    printf("myhead2=%p - metodo APPEND\n", myhead2);
    while (myhead2) {
        printf("data=%d (myhead2=%p next=%p)\n", myhead2->data, myhead2, myhead2->next);
        myhead2 = myhead2->next ;
    }
}
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.

En el main() he añadido un poco de trazas de log, que nos ayudan, mientras se ejecuta, a entender lo que ocurre cuando añadimos un nodo y cuando, en alternativa, lo colgamos. El resultado final es el mismo (en el ejemplo se muestra una lista con tres nodos), pero el diseño cambia: en el caso append tenemos una disposicion más lógica de nodos: la lista crece hacia adelante, y cada nuevo nodo es el último. En el caso insert, sin embargo, es la cabeza de la lista que cambia en cada inserción, y enyonces, cuando leemos los datos, los encontramos invertidos respeto  la orden de inserción.

Ambas listas tienen usos válidos (por ejemplo una es mejor para crear colas FIFO y la otra es mejor para crear colas LIFO), y, para aplicaciones más sofisticadas, donde se necesita añadir/leer/borrar nodos en posiciones determinadas se puede decidir (casi) sin distinción por una estructura o la otra. Normalmente yo uso el método de append, que me parece más lógico, aunque, lo admito, la función de append es un poco más complicada y menos inmediata. Pero la lista ordenada hacia adelante me parece más consistente y fácil de manejar.

Después de todo el programa nos enseña este log:
myhead1=0x11b5050 - metodo ADD
data=3 (myhead1=0x11b5050 next=0x11b5030)
data=2 (myhead1=0x11b5030 next=0x11b5010)
data=1 (myhead1=0x11b5010 next=(nil))
myhead2=0x11b5070 - metodo APPEND
data=1 (myhead2=0x11b5070 next=0x11b5090)
data=2 (myhead2=0x11b5090 next=0x11b50b0)
data=3 (myhead2=0x11b50b0 next=(nil))
que creo que es inútil explicar (¡habla por sí mismo!). Acabo con una ultima consideración: evidentemente lo que se ha enseñado es solo un programa de test: en una aplicación real habrá que añadir unas cuantas funciones accesorias para buscar y borrar nodos, etc. Y, visto che usamos unas malloc(), habrá que acordarse de usar las correspondientes free()...

Bueno, os deseo un buen trabajo con las Linked Lists, las cuales, sin duda, os darán unas satisfacciones. Y si no las dan, paciencia: si estos fuesen los problemas de la vida...

¡Hasta el próximo post!

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!