...¿qué apareció antes, el huevo o la gallina?... |
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!