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, 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.
...una imagen de The Thing para el remake de Irrational Man: empezamos bien...
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:
#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 3
Para 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.
Rocco Errno y sus hermanos
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:
    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!

sábado, 11 de noviembre de 2017

Strerror y sus hermanas
qué strerror escoger en C

Esta es una historia de encuentros extraños y relaciones equivocadas, como en la obra maestra (otra más) del gran Woody, "Hannah y sus hermanas". La vida a menudo nos lleva a tomar decisiones importantes, como le sucedió a Hannah, y otras un poco menos importantes como las que veremos en breve... sin embargo, siempre de decisiones se trata.
Hannah Strerror y sus hermanas
(Abro un paréntesis: en este post hablaremos de la strerror() y sus variantes. La strerror() es una función de la libc que, al pasarle un número de error, te devuelve la cadena descriptiva correspondiente. Muchas funciones de librería y system calls en el caso de un error actualizan el valor de una variable global, errno, que contiene, en cualquier momento, el valor del último error de ejecución. Existe otra variable global, _sys_errlist, que contiene las cadenas correspondientes a cada errno, de modo que antes de que otra parte del programa en ejecución modifique el valor de errno, deberíamos ubicar en _sys_errlist la cadena de error que queremos tratar. Como adelantado, esta última operación se puede realizar usando la strerror(), de la que ya hemos hablado aquí de manera indirecta. Cierro el paréntesis)

Se dijo: decisiones. La strerror() tiene muchas personalidades, entonces, ¿cual elijo? ¿la strerror() o la strerror_r()? Y si utilizo esta última, ¿elijo la versión XSI-compliant o la versión GNU-specific? (sin mencionar, luego, las otras variantes, la strerror_l(), la strerror_s(), etc., pero estas son variantes secundarias).

Comencemos con la primera pregunta: ¿strerror() o strerror_r()? Antiguamente existía solo la primera, pero luego aparecieron los thread y comenzaron los problemas, porque en el código multi-thread hay algunas partes críticas donde solo deberían usarse las funciones thread-safe. La strerror() no está declarada thread-safe en el estándar, y para entender por qué es suficiente analizar una implementación simplificada (aunque muy similar a las implementaciones reales que podemos encontrar en las diversas libc disponibles). ¡Vamos con el código!
#include <stdio.h> // stdio.h incluye sys_errlist.h que declara las variables
                   // globales _sys_errlist (array errores) y _sys_nerr (num.errores)
static char buf[256]; // buffer global estático para la string a retornar

char *strerror(int errnum)
{
    // test si errnum es un valor valido
    if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
        // error desconocido: copio en buf un mensaje de error genérico
        snprintf(buf, sizeof(buf), "Unknown error %d", errnum);
    }
    else {
        // error conocido: copio en buf el mensaje correspondiente
        snprintf(buf, sizeof(buf), "%s", _sys_errlist[errnum]);
    }

    // retorno buf que ahora contiene el mensaje de error
    return buf;
}
es obvio por el código (bien comentado, como siempre, así que no tengo que explicarlo línea por línea) que la strerror() no devuelve directamente _sys_errlist [errnum] (y si fuera así sería thread-safe) sino crea un mensaje de error (para tratar eventuales errnum no válidos) usando un buffer global estático buf: entonces si dos thread de una aplicación usan (casi) al mismo tiempo, la strerror() el contenido de buf no será fiable (prevalece el thread que ha escrito por ultimo).

(Otro paréntesis: no es imposible escribir una strerror() que sea thread-safe, y en algunos sistemas sí lo es: pero dado que según el estándar no lo es, no podemos estar seguros de que en el sistema que estamos usando (o en el sistema en que un día se ejecutará la aplicación que estamos escribiendo) no haya una implementación como la que acabamos de describir, así que...)

Entonces, para el software multi-thread, ha nacido la strerror_r() que es thread-safe. ¿Cómo funciona? ¡Vamos con el código!
#include <stdio.h> // stdio.h incluye sys_errlist.h que declara las variables
                   // globales _sys_errlist (array errores) y _sys_nerr (num.errores)

char *strerror_r(int errnum, char *buf, size_t buflen);
{
    // test si errnum es un valor valido
    if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
        // error desconocido: copio en buf un mensaje de error genérico
        snprintf(buf, buflen, "Unknown error %d", errnum);
    }
    else {
        // error conocido: copio en buf el mensaje correspondiente
        snprintf(buf, buflen, "%s", _sys_errlist[errnum]);
    }

    // retorno buf que ahora contiene el mensaje de error
    return buf;
}
también en este caso se trata de un código simplificado, pero muy cercano a la realidad: el truco es simple, en lugar de usar un buffer global estático (que es el origen de los problemas de la strerror()) el que llama la función debe preocuparse de asignar y pasar un buffer (y su longitud) a la strerror_r(). De esta forma, el buffer que usa strerror_r() es local para el thread que lo llama, y no puede ser sobrescrito por otro thread concurrente. Hemos sacrificado un poco de simplicidad de uso ¡pero hemos conseguido el deseado comportamiento thread-safe!

Y ahora añadimos una pequeña complicación: la versión de strerror_r() que se acaba de mostrar es la GNU-specific. Pero, desafortunadamente, existe también la  XSI-compliant, que es la siguiente:
int strerror_r(int errnum, char *buf, size_t buflen);
Como se puede ver, esta segunda versión no devuelve el buffer con la error-string, y devuelve, en vez, un código de error, y la cadena encontrada hay que buscarla directamente en el buffer que hemos pasado. Con respecto al código de error, es 0 en caso de éxito, y dependiendo de la versión de libc en uso, puede devolver -1 si hay un error (seteando errno con el valor de error específico) o un valor positivo correspondiente a errno (bah, este comportamiento dual no es realmente el máximo en simplicidad de uso...). Para usar esta versión o la GNU-specific, hay que jugar correctamente con los flag _GNU_SOURCE, _POSIX_C_SOURCE y _XOPEN_SOURCE del preprocesador (como se describe en el manual de la strerror()).

Y ahora estamos listos para la segunda decisión: ¿qué usamos, la GNU-specific o la XSI-compliant? Bueno, yo diría que cuando escribimos código para tratar los códigos de error probablemente no nos interesa tratar también los errores generados en esta fase (y en la siguiente fase, etc., etc., un loop infinito de búsqueda de errores); estamos interesados, en vez, en escribir código lineal y simple... para quitarnos la duda podemos analizar dos pequeños ejemplos de uso:
GNU-specific
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    // error socket()
    char errbuf[MAX_ERROR_LEN];    // buffer para strerror_r()
    printf("socket() error (%s)\n", strerror_r(errno, errbuf, sizeof(errbuf)));
    return EXIT_FAILURE;
}
XSI-compliant
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    // error socket()
    char errbuf[MAX_ERROR_LEN];    // buffer para strerror_r()
    int my_error = strerror_r(errno, errbuf, sizeof(errbuf)));
    if (! my_error)
        printf("socket() error (%s)\n", errbuf);
    else {
        // proceso el error (¿quizás usando otra vez strerror_r()?)
        ...
    }

    return EXIT_FAILURE;
}
No sé lo que pensáis vosotros, ¡pero yo siempre uso la versión GNU-specific! A vosotros la eleccion...

¡Hasta el próximo post!

sábado, 7 de octubre de 2017

Thread Ringers
cómo usar los thread en C - pt.3

Con este post cerramos (a lo grande, espero) la mini-serie sobre los thread (Dead Ringers para los amigos).
...exactamente la imagen que te esperas en un blog de programación...
Después de los ejemplos básicos de las dos primeras partes de la serie (¿que acabáis de leer otra vez, verdad? aquí y aquí), es una buena idea enseñar un ejemplo real de una de las muchas aplicaciones que pueden tener los thread. Y entre las muchas, he elegido una que me parece interesante, es decir, un Socket Server multithread, donde cada conexión con un Client remoto se maneja con un thread separado. Una recomendación: antes de seguir, deberíais leer uno de mis antiguos post, o sea: El Server oscuro: la leyenda renace, que es una introducción ideal al tema actual, ya que describe (y bien, espero) la funcionalidad y el código de un Socket Server single-thread. Por cierto, como se verá en breve, el nuevo código que voy a mostrar está estrechamente relacionado con el que se muestraba en el antiguo post.

Y ahora vamos al grano, ¡vamos con el código!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>

#define BACKLOG   10      // para listen()
#define MYBUFSIZE 1024

// prototipos locales
void *connHandler(void *conn_sock);

int main(int argc, char *argv[])
{
    // test argumentos
    if (argc != 2) {
        // error args
        printf("%s: numero argumentos equivocado\n", argv[0]);
        printf("uso: %s port [i.e.: %s 9999]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // crea un socket
    int my_socket;
    if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        // errore socket()
        printf("%s: could not create socket (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // prepara la struct sockaddr_in para este server
    struct sockaddr_in server;          // (local) server socket info
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(atoi(argv[1]));

    // bind informaciones del server al socket
    if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) == -1) {
        // error bind()
        printf("%s: bind failed (%s)", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // start escucha con una cola de max BACKLOG conexiones
    if (listen(my_socket, BACKLOG) == -1) {
        // error listen()
        printf("%s: listen failed (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // acepta conexiones de un client entrante
    printf("%s: espera conexiones entrantes...\n", argv[0]);
    pthread_t thread_id;
    socklen_t socksize = sizeof(struct sockaddr_in);
    struct sockaddr_in client;          // (remote) client socket info
    int client_sock;
    while ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) != -1) {
        printf("%s: conexion aceptada\n", argv[0]);
        if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) {
            // error pthread_create()
            printf("%s: pthread_create failed (%s)\n", argv[0], strerror(errno));
            return EXIT_FAILURE;
        }
    }

    // error accept()
    printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
    return EXIT_FAILURE;
}

// thread function para gestión conexiones
void *connHandler(void *conn_sock)
{
    // extrae el client socket del argumento
    int client_sock = *(int*)conn_sock;

    // loop di recepción mensajes del client
    int read_size;
    char client_msg[MYBUFSIZE];
    while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) {
        // send mensaje de retorno al client
        printf("%s: recibido mensaje del sock %d: %s\n", __func__, client_sock, client_msg);
        char server_msg[MYBUFSIZE];
        sprintf(server_msg, "me has escrito: %s", client_msg);
        send(client_sock, server_msg, strlen(server_msg), 0);

        // clear buffer
        memset(client_msg, 0, MYBUFSIZE);
    }

    // loop terminado: test motivo
    if (read_size == -1) {
        // error recv()
        printf("%s: recv failed\n", __func__);
    }
    else {
        // read_size == 0: el client se ha desconectado
        printf("%s: client disconnected\n", __func__);
    }

    return NULL;
}

Bueno, no vamos a contar otra vez cómo funciona un Socket Server (ya hecho en el antiguo post, releerlo, please), pero vamos a centrarnos en las diferencias entre el codigo single-thread y el multithread: seguramente habeis notado que son prácticamente idénticos hasta la fase de listen(), e, incluso después, las diferencias son mínimas: la fase de accept() está ahora en un loop y se crea un nuevo thread para cada conexión aceptada (de un Client remoto). ¿Y qué hace el thread? Ejecuta la función connHandler() que contiene, por cierto, el loop de recv() que en el código antiguo se ejecutava inmediatamente después de la fase de accept(). tambien el siguiente test del motivo de salida (antes de tiempo) del loop está contenido en connHandler(), y enseña la señal de error correcta (recv() error o client disconnected, dependiendo del código devuelto por la recv()).

¿Qué añadir? Simple y super-funcional: ¡un Socket Server multithread con cuatro líneas de código! Por supuesto, la sintaxis de creación de los thread  y la ejecución de la start_routine del mismo son idénticas a las descritas aquí. Para probar el nuestro Socket Server, también es necesario compilar un Socket Client (por supuesto el que se describe en mi antiguo post El Client oscuro: la leyenda renace) y ejecutar, por ejemplo, una instancia del Socket Server y dos instancias del Socket Client (en tres terminales diferentes de la misma máquina, o en tres máquinas diferentes). Ejecutando en mi máquina (Linux, por supuesto) en tres terminales, el resultado es el siguiente:
 
En la terminal 1:
aldo@ao-linux-nb:~/blogtest$ ./sockserver-mt 9999
./sockserver-mt: espera conexiones entrantes...
./sockserver-mt: conexión aceptada
./sockserver-mt: conexión aceptada
connHandler: recibido mensaje del sock 4: pippo
connHandler: recibido mensaje del sock 5: pluto
connHandler: client disconnected
connHandler: client disconnected

En la terminal 2:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Escribe un mensaje para el Server remoto: pippo
./sockclient: Server reply: me has escrito: pippo
Escribe un mensaje para el Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

En la terminal 3:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999
Escribe un mensaje para el Server remoto: pluto
./sockclient: Server reply: me has escrito: pluto
Escribe un mensaje para el Server remoto: ^C
aldo@ao-linux-nb:~/blogtest$

notar que cuando uno de los Client sale (con un CTRL-C, por ejemplo) el Server se da cuenta y enseña, como era de esperar, client disconnected... perfecto.

Ok, hemos acabado con los
thread. Ahora intentaré pensar en algún nuevo tema interesante para el próximo post. Como siempre os recomiendo de no aguantar la respiración en la espera...

¡Hasta el próximo post!

sábado, 16 de septiembre de 2017

Thread Ringers
cómo usar los thread en C - pt.2

¿Donde lo dejamos? Ah, sí: en la primera parte de Dead Ringers (oops... Thread Ringers) hemos introducido el argumento thread hablando de la base, es decir, los POSIX Threads. Ahora, como prometido, vamos a tratar de escribir el mismo ejemplo de el último post utilizando una interfaz alternativa, o sea los C11 Threads.
...te lo explicaré: yo soy un POSIX thread y tu un C11 thread...
Una premisa que parte del lado oscuro de la fuerza (bueno, el C++): el committee ISO del C++ decidió introducir en la versión C++11, los thread dentro del lenguaje. Por lo tanto, no más uso directo de los POSIX Threads a través de (por ejemplo) la librería libpthread, sino el uso directo de constructos del lenguaje mismo. El resultado final ha sido (en mi opinión) brillante, y los C++11 Threads son una de las pocas cosas que uso frecuentemente del C++11 (y ya sabéis lo que pienso del mal camino tomado por el C++ por culpa del committee ISO, y si no ir a leer ese post). El committee ISO del C no ha podido quedarse atrás, por lo que han pensado de hacer lo mismo con el C11, por lo que los thread son ahora directamente parte del C... o no? Os adelanto una consideración: aprecio el committee ISO del C muchísimo más que el committee ISO del C++ (esto era evidente...), pero en este caso me parece que algo no ha funcionado bien: a seguir veremos por qué .

¿Cómo se han realizado los nuevos C11 Threads? Vale, han tomado todas las funciones y variables que componen los POSIX Threads y le han cambiado el nombre (y tengo que admitir que los nuevos son más simples); además, en algunos casos (pocos, afortunadamente), han cambiado los tipos de los códigos de retorno y de los argumentos de las funciones. Punto. ¿Brillante? No exactamente, diría, y nada que ver con la brillante solución utilizada en C++11. ¿Razones para usar esta nueva versión? Cero, diría, y todavía no he expuesto el problema principal...

De todos modos, he reescrito el ejemplo del último post usando los C11 Threads. ¡Vamos con el código!


#include <stdio.h>
#include <threads.h>
#include <string.h>
#include <unistd.h>

// creo un nuevo tipo parar pasar datos a los thread
typedef struct _tdata {
    int   index;      // thread index
    int   *comdata;   // dato común a los thread
    mtx_t *lock;      // mutex común a los thread
} tdata;

// prototipos locales
int tMyThread(void *arg);

// función main()
int main(int argc, char* argv[])
{
    int error;

    // init mutex
    mtx_t lock;
    if ((error = mtx_init(&lock, mtx_plain)) != thrd_success) {
        printf("%s: no puedo crear el mutex (error=%d)\n", argv[0],  error);
        return 1;
    }

    // init threads
    thrd_t tid[2];
    tdata  data[2];
    int    comdata = 0;
    for (int i = 0; i < 2; i++) {
        // set data del thread y crea el thread
        data[i].index   = i;
        data[i].comdata = &comdata;
        data[i].lock    = &lock;
        if ((error = thrd_create(&tid[i], tMyThread, &data[i])) != thrd_success)
            printf("%s: no puedo crear el thread %d (error=%d)\n", argv[0], i, error);
    }

    // join threads y borra mutex
    thrd_join(tid[0], NULL);
    thrd_join(tid[1], NULL);
    mtx_destroy(&lock);

    // exit
    printf("%s: thread acabados: comdata=%d\n", argv[0], comdata);
    return 0;
}

// thread routine
int tMyThread(void *arg)
{
    // obtengo los datos del thread con un cast (tdata*) de (void*) arg
    tdata *data = (tdata *)arg;

    // thread loop
    printf("thread %d comenzado\n", data->index);
    int i = 0;
    for (;;) {
        // lock mutex
        mtx_lock(data->lock);

        // incrementa comdata
        (*data->comdata)++;

        // unlock mutex
        mtx_unlock(data->lock);

        // test counter parar eventual salida del loop
        if (++i >= 100) {
            // sale del loop
            break;
        }

        // thread sleep (10 ms)
        usleep(10000);
    }

    // el thread sale
    printf("thread %d acabado\n", data->index);
    return 0;
}

Como se puede ver el código es prácticamente lo mismo, me he limitado a utilizar las nuevas funciones en lugar de las viejas (por ejemplo, thrd_create() en lugar de pthread_create()), he utilizado los nuevos tipos (por ejemplo, mtx_t en lugar de pthread_mutex_t) y he ligeramente modificado el test de los valores de retorno: pocas diferencias, tengo que decir, y, en algunos casos, a peor: por ejemplo, ha desaparecido el parámetro attr de pthread_create(), que (por simplicidad) en el último ejemplo había dejado a NULL, pero que a veces puede ser útil (leer el manual de pthread_create() para darse cuenta). Sin embargo, se podría decir (sin ser demasiados exigentes) que la nueva interfaz no nos ofrece ninguna ventaja sustancial, pero ni siquiera un deterioro decisivo, por lo que también se podría utiliza (de gustibus).

Pero hay un problema: parece que los C11 Threads no son considerados una prioridad para los que escriben los compiladores y las libc, por lo que actualmente es difícil compilar/ejecutar un programa como el que he enseñado. Incluso nuestro amado GCC (que suele ser el primero en dar soporte a las últimas novedades) no soporta los nuevos thread (en realidad debido a la falta de integración en la glibc). Por lo tanto, si realmente deseáis usarlos a toda costa, tendréis que esperar a que algún compilador/librería proporcione el soporte completo o bien, por ejemplo, utilizar la librería c11threads, que no es más que un wrapper que simula los C11 Threads utilizando los POSIX Threads.

Yo, al final, he compilado el ejemplo usando lo que (creo) es la solución más interesante actualmente disponible: he instalado en mi sistema la musl libc que es una libc alternativa a la glibc, y tiene un wrapper para GCC (musl-gcc): musl proporciona (en Linux) el soporte completo a C11, thread incluidos. Una vez compilado, el programa se comporta correctamente, como se puede ver a continuación:

aldo@ao-linux-nb:~/blogtest$ musl-gcc c11thread.c -o c11thread
aldo@ao-linux-nb:~/blogtest$ ./c11thread 
thread 0 comenzado
thread 1 comenzado
thread 1 acabado
thread 0 acabado
./c11thread: thread acabados: comdata=200

¿Pero vale la pena hacer este cambio? No, por mi parte seguiré utilizando los POSIX Threads, que utilizo desde hace años y siguen siendo la referencia de excelencia. Y una última consideración: independientemente de lo que estamos utilizando (C11/C++11 threads) es muy probable que, por debajo, haya los POSIX Threads (esto es cierto en muchas implementaciones). Y si cuando compiláis tenéis que añadir el flag -pthread entonces la duda se convierte en una certeza, ya que con este flag vais a usar libpthread o sea la librería POSIX Threads. Sorpresa, sorpresa...

¡Hasta el próximo post!