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, 18 de marzo de 2018

Dawn of the CPU
cómo testear el uso de CPU y Memoria en C - pt.1

Francine: Pero porque vienen aquí?Stephen: Una especie de instinto primitivo... De memoria, de lo que solían hacer, tal vez este era un lugar importante en sus vidas.
En este post, con la excusa de mencionar al legendario Dawn of the Dead del grande G.A.Romero, hablaremos sobre cuándo nuestra PC se convierte en zombi, volviéndose realmente difícil de usar. Un zombie de supermercado, capaz de hacer solo la actividad mínima de los buenos tiempos en que estaba vivo...
...zombis de supermercado...
Entonces, vayamos al grano: si el vuestro sistema se vuelve no reactivo, lento, las aplicaciones no se abren inmediatamente... las posibilidades son 2:
  1. si estáis usando un sistema de la familia UNIX (Linux, FreeBSD, macOS, etc.) y no estáis pidiendo demasiado al Hardware disponible (como abrir 50 aplicaciones a la vez en un PC Pentium III de 1999) es probable que una aplicación que no funciona bien se está comiendo la CPU y/o la Memoria: intentad descubrir qué aplicación es y matadla (usando el monitor del sistema o, si sois de la vieja escuela, usando top y kill en una terminal). Dicho hecho.
  2. si estáis usando ese sistema innominable (que comienza con W y termina con s, ya sabéis), entonces tenéis que superarlo: es su comportamiento normal, baby... la única solución es pasar a usar algo un poco más serio (ver el punto 1). ¡Masoquistas!
Pero supongamos que (para volver a conectar con el post anterior ... ¿no lo habéis leído? ¿Y qué estáis esperando?) Estáis escribiendo una aplicación para un sistema embedded y la aplicación, que ni siquiera tendrá una interfaz gráfica (¡ni hablar de usar top + kill!), debe, sin embargo, controlar el estado del sistema (CPU y Memoria) para levantar alguna alarma (o encender un LED de error, etc.) en el caso de que algo salga mal... ¿qué hacer? Vale, en este post, propondré un simple monitor de rendimiento que la vuestra  aplicación embedded puede llamar (con baja frecuencia, por supuesto) para informar de potenciales problemas.

(...um, lo siento si insisto pero, volviendo a los dos puntos descritos anteriormente: el tema ahora es: aplicaciones embedded basadas en UNIX (entonces Linux Embedded, QNX, LynxOS, etc..). Si, por otro lado, realmente os queréis lastimar y escribís aplicaciones de este tipo usando la versión CE del sistema innominable (W ... s, ¿recordáis?) Bien, ¿qué puedo deciros? ...si te lo has buscado no te quejes...)

Bueno, dividiremos el post en dos partes: en esta primera parte propondré una función (que llamaremos testSys()) que hace todo el trabajo necesario. En la segunda parte haremos un main(), que simula el de una simple aplicación embedded, en la que introduciremos un thread escrito ad-hoc para sobrecargar el sistema, y nuestro main(), utilizando la testSys(), debería notarlo sin problemas. ¡Vamos con el código!
// struct para los resultados
typedef struct {
    int   total_cpu;    // cpu total (val.porcentual x prec)
    int   proc_cpu;     // cpu proceso (val.porcentual x prec)
    int   mem_system;   // mem total (val.porcentual x prec)
    int   mem_proc;     // mem proceso (val.porcentual x prec)
    float prec;         // precisión (10=1dec)
    int   loads[3];     // cargas avg (val.porcentual x loads_prec)
    float loads_prec;   // precisión para cargas (100=2dec)
} Results;

// función de test del sistema
void testSys(
    Results *results)   // destinación de los resultados
{
    static unsigned long long total_cpu_last; /* system total cpu-time incluido idle-time
                                                 en jiffies (típicamente centésimos de
                                                 segundo)) */
    static unsigned long long idle_cpu_last;  /* system idle cpu-time (in jiffies
                                                 (tipicamente centésimos de segundo)) */
    static unsigned long proc_times_last;     /* cpu-time del proceso corriente
                                                 (calculado sumando usertime y
                                                 systemtime del proceso) */
    FILE *fp1;
    FILE *fp2;

    // set resultados de default (así el que llama puede tratar los errores)
    results->total_cpu  = -1;
    results->proc_cpu   = -1;
    results->mem_system = -1;
    results->mem_proc   = -1;
    results->prec       = 10.;
    results->loads[0]   = -1;
    results->loads[1]   = -1;
    results->loads[2]   = -1;
    results->loads_prec = 100.;

    /* lee /proc/self/stat (estadísticas de proceso di este proceso) y /proc/stat
       (estadísticas de procesos de esta maquina) */
    if ( ((fp1 = fopen("/proc/self/stat", "r")) != NULL) &&
         ((fp2 = fopen("/proc/stat", "r")) != NULL)) {

        // lee user time e system time
        unsigned long utime, stime;
        fscanf(fp1, "%*d %*s %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu",
               &utime, &stime);
        unsigned long proc_times_cur = utime + stime;

        // lee i valori di user, nice, system, idle, iowait, irq e softirq
        unsigned long long user, nice, system, idle, iowait, irq, softirq;
        fscanf(fp2,"%*s %llu %llu %llu %llu %llu %llu %llu", &user, &nice, &system,
               &idle, &iowait, &irq, &softirq);
        unsigned long long total_cpu_cur = user + nice + system + idle + iowait +
                                           irq + softirq;
        unsigned long long idle_cpu_cur  = idle;

        /* calcula uso cpu total (%cpu = work_over_period / total_over_period * 100
           (adonde work_over_period es (total - idle))
           NOTA: el campo iowait di /proc/stat es incluido nel calculo, aunque sea un
           valor poco fiable (pero el error es trascurable) */
        results->total_cpu =
            (float)((total_cpu_cur - total_cpu_last) - (idle_cpu_cur - idle_cpu_last)) /
                    (total_cpu_cur - total_cpu_last) * 100 * results->prec;

        /* calcula uso cpu del proceso ((proc_times2 - proc_times1) * 100 /
           (float) (total_cpu_usage2 - total_cpu_usage1))
           NOTA: nel programa "top" este valor es multiplicado por NPROCESSORS (usar
           el comando interactivo "I" para deshabilitar esta característica) */
        results->proc_cpu =
            (float)(proc_times_cur - proc_times_last) /
                   (total_cpu_cur - total_cpu_last) * 100 * results->prec;

        // salva valores y cierra files
        total_cpu_last  = total_cpu_cur;
        idle_cpu_last   = idle_cpu_cur;
        proc_times_last = proc_times_cur;
        fclose(fp1);
        fclose(fp2);
    }

    // lee /proc/self/statm (estadísticas de memoria de este proceso)
    struct sysinfo si;  // destinazione per dati ottenuti dalla system call sysinfo()
    if (((fp1 = fopen( "/proc/self/statm", "r")) != NULL) && (sysinfo(&si) != -1)) {
        /* lee el resident set size corriente (physical memory use) medido en bytes.
           NOTA: nel programa "top" el valor llamado RES depende del valor del RSS
           (resident set size) de las estructuras internas de Linux */
        long resident;
        fscanf(fp1, "%*s%ld", &resident);
        long res = resident * (size_t)sysconf(_SC_PAGESIZE) / 1024; // code+data

        // calcula valores de referencia
        unsigned long totalram = si.totalram * si.mem_unit;
        unsigned long freeram  = si.freeram  * si.mem_unit;
        results->mem_system =
            (float)(totalram - freeram) / totalram * 100 * results->prec;

        /* calcula %mem: nel programa "top" este valor es calculado como: %mem =
           RES / totphisicalmem (adonde RES es CODE + DATA (adonde DATA es data + stack)) */
        results->mem_proc = (float)res / (totalram / (float)1024) * 100 * results->prec;
        fclose(fp1);
    }

    // lee cpu loadavg
    double loads[3];    // cargas avg de CPU a 1, 5, e 15 minutos
    if (getloadavg(loads, 3) != -1) {
        // copia las cargas en los resultados
        results->loads[0] = loads[0] * results->loads_prec;
        results->loads[1] = loads[1] * results->loads_prec;
        results->loads[2] = loads[2] * results->loads_prec;
    }
}
¿Que pensáis de eso? Puse tantos comentarios, que casi no queda nada por decir. ¿Y qué puedo añadir? De los comentarios se desprende que los datos obtenidos utilizan el mismo sistema de cálculo que usa top (que es una referencia casi obligatoria) y, en particular, nos referimos a top con la opción "I" (Irix mode Off) habilitada: esto evita , simplemente, tener indicaciones (extrañas a primera vista, pero correctas) como "% CPU = 800" que corresponde a un consumo de 100% de CPU en una máquina con 8 núcleos: con la opción "I" estamos seguros de que el el valor máximo indicado será del 100%, independientemente de la cantidad de núcleos disponibles (para que no se pueda malinterpretar).

La función testSys() escribe los resultados en una estructura TestResults pasada como argumento: esto nos permite presentar y/o procesar datos de manera adecuada en el nivel del llamante. En el ejemplo que veremos proximamente, el main() llama testSys() y enseña los resultados, para que se pueda verificar si coinciden con los de top (que podemos ejecutar simultáneamente en otra terminal). En un caso real los datos deberían, en vez, procesarse, por ejemplo comparándolos con valores de referencia para generar alarmas. Precisamente por esta razón los datos se registran como int multiplicados por un factor de precisión apropiado: esta es una forma normal y clásica de procesar datos que nacen float, ya que la comparación (y cualquier otra operación) entre valores enteros es mucho más simple de realizar y permite una mayor flexibilidad de uso.

Bueno, para hoy puede ser suficiente. Como prometido en el próximo post os mostraré un ejemplo de uso, algunos resultados de pruebas reales y un pequeño estudio sobre la interpretación de datos, especialmente los de la Memoria.

¡Hasta el próximo post!

sábado, 17 de febrero de 2018

The Test Connection
cómo testear la conectividad en C

La Test Connection de este post es un poco menos peligrosa que la French Connection de la cual habla la obra maestra de W.Friedkin. Pero sigue siendo una actividad muy importante en algunos tipos de aplicaciones, entonces ni hablar de subestimarla.
...caras de conectividad avanzada...
Cuando utilizamos un computer interactivamente y queremos verificar el estado de la red, tenemos muchas formas sencillas de hacerlo: por ejemplo, con un buen ping a google.com sabemos si estamos conectados a Internet y, si no funciona, podemos utilizar algunas utilidades del sistema para averiguar lo que esta mal. Supongamos, sin embargo, tener que desarrollar una aplicación para un sistema embedded (¿somos o no somos programadores C?) y esta aplicación está conectada a la red y, en el caso de que no haya comunicación, debe informar de alguna manera (yo que se, encender un led error, ¡ciertamente no enviando un mensaje en la red!). Ok, y... ¿cómo lo hacemos? Veamos, ¡vamos con el código!
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    int sock;

    // test interfaces
    //

    // get de todos los network devices configurados
    struct ifaddrs *addrs;
    getifaddrs(&addrs);

    // loop sobre los network devices configurados
    int i_alr = 0;
    struct ifaddrs *tmpaddrs = addrs;
    while (tmpaddrs) {
        // test interface
        if (tmpaddrs->ifa_addr && tmpaddrs->ifa_addr->sa_family == AF_PACKET) {
            // abre un socket para el test
            if ((sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
                // enseña error y continua
                printf("test interfaces: error socket para la interface %s: %s\n",
                       tmpaddrs->ifa_name, strerror(errno));
                continue;
            }

            // prepara los dati para ioctl()
            struct ethtool_value edata;
            edata.cmd = ETHTOOL_GLINK;
            struct ifreq ifr;
            strncpy(ifr.ifr_name, tmpaddrs->ifa_name, sizeof(ifr.ifr_name) - 1);
            ifr.ifr_data = (char *)&edata;

            // esegue ioctl()
            if (ioctl(sock, SIOCETHTOOL, &ifr) == -1) {
                // error ioctl: cierra el socket y continua
                printf("test interfaces: error ioctl para la interface %s: %s\n",
                       tmpaddrs->ifa_name, strerror(errno));
                close(sock);
                continue;
            }

            // enseña los resultados y cierra el socket
            printf("test interfaces: interface %s: %s\n",
                   tmpaddrs->ifa_name, edata.data ? "OK" : "NOK");
            close(sock);
        }

        // pasa al proximo device
        tmpaddrs = tmpaddrs->ifa_next;
    }

    // libera la devices list
    freeifaddrs(addrs);

    // test conectividad
    //

    // abre un socket para el test
    if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        // error socket
        printf("test de conectividad: socket error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }

    // set de un timeout para send() (en este caso en realidad lo usa connect()) para evitar
    // un bloqueo sobre error de conexion)
    struct timeval tv;
    tv.tv_sec  = 1;     // set timeout en segundos
    tv.tv_usec = 0;     // set timeout en usegundos
    if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)) < 0) {
        // error ioctl
        printf("test de conectividad: setsockopt error: %s\n", strerror(errno));
        close(sock);
        return EXIT_FAILURE;
    }

    // NOTE: alternativa (non portable) a el uso de SO_SNDTIMEO:
    //int syn_retries = 1;     // send de un total de 1 SYN packets => timeout ~2s
    //if (setsockopt(sock, IPPROTO_TCP, TCP_SYNCNT, &syn_retries, sizeof(syn_retries)) < 0) {
    //  ...

    // prepara la estructura sockaddr_in para el server remoto
    struct sockaddr_in server;                  // server (remoto) socket info
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;                // set familia direcciones
    server.sin_addr.s_addr = inet_addr("216.58.214.174");   // set direccion server
    server.sin_port = htons(80);                // set numero port server

    // conexion al server remoto
    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
        // error connect
        printf("test de conectividad: error connect: %s\n", strerror(errno));
        close(sock);
        return EXIT_FAILURE;
    }

    // enseña los resultados ed esce
    printf("test de conectividad: conectividad OK\n");
    close(sock);
    return EXIT_SUCCESS;
}
una premisa: este código está destinado a aplicaciones Linux Embedded, por lo que todo lo que sigue se refiere a un entorno Linux (bueno, como siempre, y es inútil explicar otra vez el tema...). El código es ampliamente comentado, así que no tengo que escribir demasiadas explicaciones. En este ejemplo, el código de test se escribe directamente en el main() pero, en un proyecto real, debería transformarse en una función que se puede llamar periódicamente en la posición más adecuada, tal vez directamente en el main() de la aplicación. Dado el tipo de prueba que se realiza, se debe llamar ocasionalmente, en función de cuanto de inmediata debe ser la señal de alarma que necesitamos. Normalmente, el uso es de baja frecuencia (hablamos de segundos, no de milisegundos), de lo contrario, nuestra función consumiría mucho tiempo de CPU solo para verificar la conectividad (y este no parece ser el caso).

el test se lleva a cabo en dos fases: test de las interfaces de red y test de conectividad. La primera comprueba eventuales problemas, digamos, Hardware: si tengo una conexión WiFi y una Ethernet en la misma máquina podría tener una conexión a Internet incluso si, por ejemplo, el cable de red está desconectado, y me interesa informar de esta situación, por lo que un único test de conexión no sería suficiente. La segunda fase verifica la conectividad real y, en caso de que falte, podemos saber si no hay conectividad a pesar de que las interfaces de red están bien conectadas, así de aislar mejor las posibles causas (en resumen, es un test bastante completo, pero se puede sofisticar aun más).

La fase de test de las interfaces se realiza con una función (relativamente nueva) de la siempre indispensable (en casos como este) ioctl(). Gracias a la función SIOCETHTOOL podemos verificar el funcionamiento de bajo nivel de cada interfaz, ya que nuestro código incluye un loop con el que analizamos todas las interfaces que, normalmente, son dos (loopback y Ethernet) y, a veces, más (WiFi, otra tarjeta de red, etc.). La interfaz de loopback está siempre presente y, si no queremos probarla, se puede omitir haciendo un simple test sobre el nombre (que siempre es "lo", pero en cualquier caso, el nombre puede verificarse con antelación, para evitar malentendidos, en el file /etc/network/interfaces).

El test de conectividad se basa, en vez, en una simple conexión de tipo Client a una dirección "segura": En el ejemplo he usado IP y Port de google.com, pero se puede usar la que parece más apropiada, por ejemplo, para un dispositivo conectado solo a la red local, se puede usar la conexión a un servidor en la red, o bien se puede conectar a la dirección del gateway local, etc. También depende de si lo que tenemos que probar es conectividad a Internet o una simple conectividad local. Tener en cuenta que todo funciona usando la system call connect(), que implementa el envío de datos y la recepción de una respuesta, por lo que es un test más que suficiente (sin recurrir a un mucho más complicado ping). Ya que connect() se bloquea durante mucho tiempo cuando no recibe una respuesta inmediata, he añadido un oportuno timeout (leer el comentario en el código) utilizando setcsockopt() + SO_SNDTIMEO, pero (leer el otro comentario) también se puede usar el flag TCP_SYNCNT, que es, pero, una solución menos ortodoxa y menos portátil.

En un normal PC (en lugar de un sistema embedded) con una conexión a Internet a través de Ethernet, el resultado en condiciones normales es el siguiente:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: OK
test de conectividad: conectividad OK
y, si forzamos la desconexión del Software (utilizando, por ejemplo, el NetworkManager de Linux) el resultado es:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: OK
test de conectividad: error connect: Network is unreachable
mientras que si forzamos la desconexión Hardware (desconectando el cable de red) el resultado es:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: NOK
test de conectividad: error connect: Network is unreachable
No está mal, ¿verdad? Una función simple simple pero muy útil. ¡Y por hoy ya està, misión cumplida!

¡Hasta el próximo post!

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!