Escribir software es un placer. Un programa no sólo debe funcionar bien y ser eficiente (esto se da por supuesto), sino que también debe ser bello y elegante de leer, comprensible y fácil de mantener, tanto para el autor como para eventuales futuros lectores. Programar bien en C es un arte.
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, 23 de diciembre de 2017
sábado, 9 de diciembre de 2017
Remapped File cómo sincronizar un Memory Mapped File en C - pt.1
Este post es un remake. Es
el remake de mi post (en dos episodios) de hace casi dos años, Irrational File (venga, venga, ir enseguida a releerlo, ¡no nos hagáis rogar!). Normalmente, los remake dejan un
sabor amargo, no añaden nada nuevo y, si el original era una gran
película, difícilmente pueden igualarla. Hay
excepciones, sin embargo, como The Thing de J.Carpenter (uff, también esta película ya la he
usada...) que es una obra maestra superior al (aunque bueno) original de 1951. Bueno, este post pertenece (espero) a los remake buenos, porque, como verán, añade mucho al original.
Vale,el post original describía una (simple) librería que había
escrito para compartir datos entre procesos (IPC) utilizando como medio de comunicación un Memory Mapped File. En
una frase del viejo post (que os propongo parcialmente) ya se anunciaba este remake: "...para un uso sencillo este método está muy bien, pero para aplicaciones complejas [...] se debería utilizar un sistema más avanzado [...] un mutex o un semáforo (pero eso es otra historia, quizás en el futuro voy a hacer un post sobre el tema)...". Entonces ya está: esta es la versión con los accesos sincronizados de
la nuestra antigua libmmap, casi lista para un uso profesional.
Repitiendo la estructura del viejo post (y si no, ¿qué remake sería?) he dividido todo en dos partes: en la primera describiré, a modo de especifica funcional, el header file (libmmap.h) y un ejemplo de uso (data.h, datareader.c y datawriter.c). En la segunda entrega describiré la implementación real de la librería (libmmap.c).
Empezamos: vamos a ver el header-file, libmmap.h:
El flag de data_ready se usa (protegido por el semáforo) para indicar la disponibilidad de nuevos datos a leer. Luego tenemos, dulcis in fundo, los campos len y data que nos llevan al segundo objetivo que había establecido: los datos intercambiados son, ahora, genéricos, con formato y longitud que se deciden a nivel de aplicación. Nótese, de hecho, que el campo data es un array de dimensión 1: esto es una especie de truco (a usar con las debidas precauciones) bastante utilizado en C para tratar datos genéricos de forma y longitud no disponibles a priori. En nuestra struct el campo debe colocarse, obviamente, como el último miembro, transformándola así en una especie de estructura de tamaño variable. Sin embargo, en el próximo post veremos mejor cómo funciona todo.
libmmap.h termina con los prototipos de las funciones que componen la libreria: tenemos dos funciones de apertura, que nos permiten abrir un mapped-file en modo Master o Slave (en el próximo post veremos el motivo de esta doble apertura); luego tenemos una función de cierre, una función de flush y, por supuesto, dos funciones para escribir y leer datos. Estas dos últimas funciones confirman el discurso de generalidad descrito anteriormente: las variables de read/write son de tipo void*, por lo tanto, son adecuadas para aceptar cualquier tipo de datos. Como el formato de los datos a intercambiar se mueve al nivel de la aplicación, he escrito un header-file (como ejemplo), data.h, que está incluido en las dos aplicaciones que se comunican:
¿Usando las dos aplicaciones en dos terminales diferentes qué vamos a ver?
Por hoy hemos terminado. Esperando la segunda parte, podríais intentar imaginar cómo será la implementación de la libreria que os presentaré... pero llega la Navidad e imagino (y espero) que tengais cosas más interesantes en qué pensar en este período ...
¡Hasta el próximo post!
![]() |
...una imagen de The Thing para el remake de Irrational Man: empezamos bien... |
Repitiendo la estructura del viejo post (y si no, ¿qué remake sería?) he dividido todo en dos partes: en la primera describiré, a modo de especifica funcional, el header file (libmmap.h) y un ejemplo de uso (data.h, datareader.c y datawriter.c). En la segunda entrega describiré la implementación real de la librería (libmmap.c).
Empezamos: vamos a ver el header-file, libmmap.h:
#ifndef LIBMMAP_H #define LIBMMAP_H #include <semaphore.h> #include <stdbool.h> #define MAPNAME "/shmdata" // estructura del mapped-file typedef struct { sem_t sem; // semáforo dei sincronización accesos bool data_ready; // flag de data ready (true=ready) size_t len; // longitud del campo data char data[1]; // datos a compartir } ShmData; // prototipos globales ShmData *memMapOpenMast(const char *shmname, size_t len); ShmData *memMapOpenSlav(const char *shmname, size_t len); int memMapClose(const char *shmname, ShmData *shmdata); int memMapFlush(ShmData *shmdata); int memMapRead(void *dest, ShmData *src); void memMapWrite(ShmData *dest, const void *src); #endif /* LIBMMAP_H */Simple y autoexplicativo, ¿no? la nuestra librería utiliza una estructura de datos ShmData para mapear el mapped-file: aquí notamos de inmediato la primera gran mejora obtenida: hay un semáforo (un POSIX unnamed semaphore) para sincronizar los accesos a la memoria, lo que hace que la nuestra librería sea adecuada para un verdadero uso multitask (y multithread), que era el primer objetivo programado. El mecanismo de sincronización lo he elegido cuidadosamente entre los diversos disponibles: tal vez un día escriba una publicación específica sobre el tema, pero, por el momento, me limitaré a decir que el método elegido es simple de implementar, muy funcional y perfectamente adecuado para el propósito que hay que lograr (¿y esto es suficiente, no?).
El flag de data_ready se usa (protegido por el semáforo) para indicar la disponibilidad de nuevos datos a leer. Luego tenemos, dulcis in fundo, los campos len y data que nos llevan al segundo objetivo que había establecido: los datos intercambiados son, ahora, genéricos, con formato y longitud que se deciden a nivel de aplicación. Nótese, de hecho, que el campo data es un array de dimensión 1: esto es una especie de truco (a usar con las debidas precauciones) bastante utilizado en C para tratar datos genéricos de forma y longitud no disponibles a priori. En nuestra struct el campo debe colocarse, obviamente, como el último miembro, transformándola así en una especie de estructura de tamaño variable. Sin embargo, en el próximo post veremos mejor cómo funciona todo.
libmmap.h termina con los prototipos de las funciones que componen la libreria: tenemos dos funciones de apertura, que nos permiten abrir un mapped-file en modo Master o Slave (en el próximo post veremos el motivo de esta doble apertura); luego tenemos una función de cierre, una función de flush y, por supuesto, dos funciones para escribir y leer datos. Estas dos últimas funciones confirman el discurso de generalidad descrito anteriormente: las variables de read/write son de tipo void*, por lo tanto, son adecuadas para aceptar cualquier tipo de datos. Como el formato de los datos a intercambiar se mueve al nivel de la aplicación, he escrito un header-file (como ejemplo), data.h, que está incluido en las dos aplicaciones que se comunican:
#ifndef DATA_H #define DATA_H // definición estructura data para aplicaciones de ejemplo typedef struct { int type; // tipo de datos int data_a; // un dato (ejemplo) int data_b; // un otro dato (ejemplo) char text[1024]; // testo de los datos } Data; #endif /* DATA_H */Como podéis ver he elegido utilizar una estructura de datos que incluye un campo de texto, pero se puede intercambiar cualquier cosa, también solo un simple int, por ejemplo. Ahora vamos a ver la primera aplicación de uso, datawriter.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include "libmmap.h" #include "data.h" #include "mysleep.h" // main del programa de test int main(int argc, char *argv[]) { // abre mapped-file ShmData *shmdata; if ((shmdata = memMapOpenMast(MAPNAME, sizeof(Data)))) { // file abierto: start loop de escritura for (int i = 0; i < 100; i++) { // compone datos para el reader Data data; snprintf(data.text, sizeof(data.text), "nuevos datos %d", i); // escribe datos en el mapped-file memMapWrite(shmdata, &data); printf("he escrito: %s\n", data.text); // loop sleep mySleep(100); } // cierra mapped-file memMapClose(MAPNAME, shmdata); } else { // sale con error printf("no puedo abrir el file %s (%s)\n", MAPNAME, strerror(errno)); return EXIT_FAILURE; } // esce con Ok return EXIT_SUCCESS; }Simple, ¿no? Abre el file compartido en la memoria (en modo Master) y lo usa para escribir datos (en loop) para la otra aplicación de test, datareader.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include "libmmap.h" #include "data.h" #include "mysleep.h" // main del programa de test int main(int argc, char *argv[]) { // abre esperando que un writer abra come master el mapped-file ShmData *shmdata; while ((shmdata = memMapOpenSlav(MAPNAME, sizeof(Data))) == NULL) { // acepta solo el error de file todavía no existente if (errno != ENOENT) { // sale con error printf("no puedo abrir el file %s (%s)\n", MAPNAME, strerror(errno)); return EXIT_FAILURE; } // loop sleep mySleep(100); } // file abierto: start loop de lectura for (int i = 0; i < 100; i++) { // busca datos a leer en el mapped-file Data data; if (memMapRead(&data, shmdata)) { // enseña los datos leidos printf("me has escrito: %s\n", data.text); } // loop sleep mySleep(100); } // cierra mapped-file y sale con Ok memMapClose(MAPNAME, shmdata); return EXIT_SUCCESS; }El reader es, como se nota, una aplicación especular del writer (lee en lugar de escribir). Notar que, en ambas aplicaciones, se testean los errores en las funciones de open y se cierra (si necesario) la ejecución enseñando el error con strerror(): esto es posible porque (como veremos en el proximo post) las funciones de apertura salen en caso de error de las funciones de la libc que usan internamente, y, en ese punto, la descripción del error que ha ocurrido está disponible con errno (pero de esto hemos hablado extensamente en los dos últimos post ¿recuerdan?).
¿Usando las dos aplicaciones en dos terminales diferentes qué vamos a ver?
En la terminal 1: aldo@mylinux:~/blogtest$ ./datawriter he escrito: nuevos datos 1 he escrito: nuevos datos 2 he escrito: nuevos datos 3 ^C En la terminal 2: aldo@mylinux:~/blogtest$ ./datareader me has escrito: nuevos datos 1 me has escrito: nuevos datos 2 me has escrito: nuevos datos 3Para enviar los loop a dormir he utilizado la función mySleep(), que es una nuestra vieja conocida: se puede insertar en una libreria separada o en la misma libreria libmmap (yo he utilizado un file separado mysleep.c con su header-file mysleep.h que solo contiene el prototipo). En este simple ejemplo, las dos aplicaciones que se comunican pueden detenerse usando CTRL-C, y (cuando vais a usar la libreria) podrais verificar que iniciando/detenendo/reiniciando ambas aplicaciones, en cualquier orden, siempre se vuelven a sincronizar sin problemas.
Por hoy hemos terminado. Esperando la segunda parte, podríais intentar imaginar cómo será la implementación de la libreria que os presentaré... pero llega la Navidad e imagino (y espero) que tengais cosas más interesantes en qué pensar en este período ...
¡Hasta el próximo post!
sábado, 25 de noviembre de 2017
Errno y sus hermanos cómo està implementado errno en C
Este post es un necesario addendum al post anterior (que si aún no lo habéis leído deberíais hacerlo en seguida, ya que están estrechamente vinculados). Esta vez hablaremos de errno y sus hermanos, una familia compleja como la de Rocco y sus hermanos, una obra maestra del neorrealismo Italiano.
Vamos al grano: después de leer el último post, los lectores más atentos se habrán preguntado: "Ok, con strerror_r() podemos manejar las cadenas de error de una manera thread-safe, pero ¿de qué nos sirve si, en la base de todo, hay la variable global errno que realmente no tiene el aire de ser thread-safe?" La pregunta es legítima, y para responder tenemos que retroceder un poco en el tiempo... la historia es análoga y paralela a la de strerror() (¡solo faltaria que no!). Antiguamente errno estaba definido en el header errno.h:
Y, para terminar a lo grande, no podemos renunciar a un pequeño extracto del estándar POSIX.1c, que señala todo lo que se ha dicho hasta ahora:
¡Hasta el próximo post!
![]() |
Rocco Errno y sus hermanos |
extern int errno;y hacia referencia a una simple variable global de la libc, exactamente un int llamado errno. Luego vinieron los thread, con el estándar POSIX 1003.1c (también conocido como POSIX.1c, pero hace lo mismo), y con él vino también la strerror_r() y, como no, también errno ha conseguido un hermano thread-safe. Como bien se puede leer en el manual de errno (después de POSIX.1c) ahora errno es:
errno is defined by the ISO C standard to be a modifiable lvalue of type int, and must not be explicitly declared; errno may be a macro. errno is thread-local; setting it in one thread does not affect its value in any other thread.Entonces la nueva definición de errno ahora está en bits/errno.h (que se incluye desde el clásico errno.h). Simplificando un poco (he omitido algunos detalles para que sea más fácil de leer) la nueva impostacion es:
en el header-file errno.h #include <bits/errno.h> /* Declare the `errno' variable, unless it's defined as a macro by bits/errno.h. This is the case in GNU, where it is a per-thread variable. This redeclaration using the macro still works, but it will be a function declaration without a prototype and may trigger a -Wstrict-prototypes warning. */ #ifndef errno extern int errno; #endif en el header-file bits/errno.h /* Function to get address of global `errno' variable. */ extern int *__errno_location (void); /* When using threads, errno is a per-thread value. */ #define errno (*__errno_location ())Entonces, en pocas palabras, ahora errno ya no es un int global, sino que es "el contenido de una dirección devuelta por una función global". Obviamente, la variable int a la que apunta este objeto es el nuevo errno local de un thread (es decir, cada thread tiene su propio errno). Un ejemplo (muuuy simplificado) de cómo se podría implementar la __errno_location() es el siguiente:
// errno local de un thread: es una variable de tipo Thread-local storage (TLS) __thread int th_errno; int *__errno_location(void) { // retorna la dirección de la variable th_errno return &th_errno; }Y, al final de todo, a pesar de los cambios descritos, aún será posible hacer operaciones como estas:
int my_errno = errno; // Ok, equivale a: int my_errno = (* __errno_location()); errno = EADDRINUSE; // Ok, equivale a: (* __errno_location()) = EADDRINUSE;porque, por supuesto, todo ha sido diseñado para ser retro-compatible, y por lo tanto errno, aunque ahora es una macro, todavía tiene que comportarse como si fuera un simple int.
Y, para terminar a lo grande, no podemos renunciar a un pequeño extracto del estándar POSIX.1c, que señala todo lo que se ha dicho hasta ahora:
Redefinition of errno In POSIX.1, errno is defined as an external global variable. But this definition is unacceptable in a multithreaded environment, because its use can result in nondeterministic results. The problem is that two or more threads can encounter errors, all causing the same errno to be set. Under these circumstances, a thread might end up checking errno after it has already been updated by another thread. To circumvent the resulting nondeterminism, POSIX.1c redefines errno as a service that can access the per-thread error number as follows (ISO/IEC 9945:1-1996, n2.4): Some functions may provide the error number in a variable accessed through the symbol errno. The symbol errno is defined by including the header <errno.h>, as specified by the C Standard ... For each thread of a process, the value of errno shall not be affected by function calls or assignments to errno by other threads. In addition, all POSIX.1c functions avoid using errno and, instead, return the error number directly as the function return value, with a return value of zero indicating that no error was detected. This strategy is, in fact, being followed on a POSIX-wide basis for all new functions.Notar que la ultima parte del extracto (de In addition... en adelante) explica el porqué existe la strerror_r() XSI-compliant descrita en el post anterior: ¿Habéis visto? todo se aclara al final... Y con esto también podemos considerar resuelto el misterio del errno thread-safe. ¡Misión cumplida!
¡Hasta el próximo post!
Suscribirse a:
Entradas (Atom)