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, 19 de mayo de 2018

El gran lighttpd
cómo escribir un módulo lighttpd en C - pt.4

Maude: ¿Y en que te dedicas en tu tiempo libre?
El Nota: Bueno, ya sabes: jugar a los bolos, conducir por ahí­, un viaje ácido de vez en cuando...
Dando por echo que para escribir un buen Software no hay necesidad de imitar los hábitos del mítico El Nota de El gran Lebowski, volvemos al tema lighttpd, porque en el tercer post de la serie (aquí, aquí y también aquí) prometí expandir la discusión. ¡Bien, ha llegado el momento, las promesas se mantienen!
...Bueno, ya sabes: jugar a los bolos, conducir por ahí­, un módulo lighttpd de vez en cuando...
Pues bien, en "El gran lighttpd - pt.3" (que acabáis de releer, supongo...) escribimos un agradable módulo elemental para lighttpd (era un clásico "Helloworld": escribía solo una presentación en el browser), pero era un buen comienzo para entrar en el mundo de los módulos para Web Servers. Vale, ahora vamos a reanudar ese módulo y les vamos a añadir una función que extrae los datos POST incluidos en una eventual petición HTTP de tipo POST que llega al módulo. ¿Por qué he elegido añadir esa funcionalidad? Bueno, obviamente porque es bastante normal que un módulo cuente con varios tipos de peticiones (GET, POST, etc.), y luego, en particular, recuerdo que la primera vez que añadí esta funcionalidad en un módulo Apache que escribí, me di cuenta de que no estaban disponibles muchas explicaciones sobre este tema (y esto a pesar de la enorme cantidad de documentación de Apache disponible respeto a la de lighttpd)... os dejo imaginar, por lo tanto, cuánta documentación y cuántos ejemplos encontré para hacer lo mismo con lighttpd: prácticamente nada.

Pues bien, sin molestarse en repetir todo el código y la forma de generarlo, sólo reescribiremos la función mod_helloworld_uri_handler() y añadiremos una nueva función getPost(). Añado que, para integrar el código que he escrito, es necesario seguir la guía de instalación indicada en "El gran lighttpd - pt.1" y repetir los 10 pasos indicados en "El gran lighttpd - pt.2", usando esta vez la versión 1.4.45 de lighttpd (ojo: usando una versión anterior, el código que estoy a punto de mostrar no funciona). Hecho? entonces ¡Vamos con el código
URIHANDLER_FUNC(
    mod_helloworld_uri_handler)
{
    plugin_data *p = p_d;

    UNUSED(srv);

    // test modo (return si error)
    if (con->mode != DIRECT)
        return HANDLER_GO_ON;

    // test uri path (return si error)
    size_t s_len = buffer_string_length(con->uri.path);
    if (s_len == 0)
        return HANDLER_GO_ON;

    mod_helloworld_patch_connection(srv, con, p);

    // test handler (return si error)
    if (con->uri.path->ptr && strstr(con->uri.path->ptr, "helloworld")) {
        // prepara el buffer para la respuesta
        buffer *buf = buffer_init();

        // test método http
        if (strstr(con->request.request->ptr, "GET")) {
            // método GET: escribe respuesta
            buffer_append_string(buf, "<big>Hello, world!</big>");
        }
        else if (strstr(con->request.request->ptr, "POST")) {
            // método POST: controla la presencia de post-data
            size_t len = con->request.content_length;
            if (len) {
                // post-data presentes; los lee para escribir la respuesta
                char *data = malloc(len + 1);
                if (readPost(con, data, len)) {
                    // escribe la respuesta
                    buffer_append_string(buf, data);
                }

                // libera la memoria
                free(data);
            }
            else {
                // error: mensaje POST sin post-data
                buffer_append_string(buf, "mod_helloworld: error: POST sin post-data");
            }
        }
        else {
            // error: mensaje con método no tratado
            buffer_append_string(buf, "mod_helloworld error: método no tratado");
        }

        // escribe el buffer y lo libera
        chunkqueue_append_buffer(con->write_queue, buf);
        buffer_free(buf);

        // envía el header
        response_header_overwrite(
                srv, con, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html"));
        con->http_status = 200;
        con->file_finished = 1;

        // handling terminado
        return HANDLER_FINISHED;
    }

    // handler no encontrado
    return HANDLER_GO_ON;
}

static bool readPost(
    connection *con,                // datos conexión
    char       *data,               // buffer destinación para post-data
    size_t     len)                 // longitud data (sin terminador)
{
    // set valor de return de default
    int retval = false;

    // lee post-data (en el stream de chunks)
    size_t rpos = 0;
    chunkqueue *cq = con->read_queue;
    for (chunk *mychunk = cq->first; mychunk; mychunk = cq->first) {
        // calcula el size del buffer correspondiente al chunk de post-data corriente
        size_t n_tocopy = buffer_string_length(mychunk->mem) - mychunk->offset;

        // test si hay datos a copiar
        if (n_tocopy <= (len - rpos)) {
            // copia un chunk y set de la posición de copia del proximo chunk
            memcpy(data + rpos, mychunk->mem->ptr + mychunk->offset, n_tocopy);
            rpos += n_tocopy;

            // ha sido leído (almeno) un chunk de post-data: set retval=true
            retval = true;
        }
        else {
            // buffer overflow: salida forzada del loop
            break;
        }

        // indica como leído el chunk de post-data corriente
        chunkqueue_mark_written(cq, chunkqueue_length(cq));
    }

    // añade el terminadore de string (el buffer es largo len+1)
    data[len] = 0;

    // sale con retval
    return retval;
}
Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciónes y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales.

la función original mod_helloworld_uri_handler() sigue siendo una especie de función "Helloworld", que ahora distingue los tipos de petición y, en el caso POST, llama a la función readPost() y escribe  como respuesta "Hello, world!" mas los datos. En el caso de peticiones GET se limita a escribir "Hello, world!" y, para otros tipos de peticiones o con peticiones POST sin datos, sale con error.

La función readPost() es simple, pero no es inmediata: dada la casi total falta de ejemplos disponibles en la red, he tenido que hacer, prácticamente,   reverse engineering sobre los (excelentes) módulos integrados en la distribución (¡tener el código fuente original disponible siempre es una gran cosa!) de manera de deducir cómo hacer lo que tenia como objetivo. El resultado final (modestia aparte) es bueno, tanto en apariencia (estilo) como en rendimiento (hice algunas pruebas y funciona bien).

¡Ah, el test, se me olvidaba! Testear con peticiones POST no es simple cómo hacerlo con las GET, adonde es suficiente (como dicho en el post anterior) escribir la URI del modulo en la barra de direcciones de Firefox o Chrome (o cualquier otro navegador menos IE, por favor...) y esperar la respuesta. Para probar el nuestro nuevo módulo es mejor usar un buen plugin (como Poster, por ejemplo) que nos facilitará mucho la tarea.

Vale, de momento, creo que podemos parar por un tiempo el tema módulos de lighttpd. Juro que en el próximo post voy a hablar de otra cosa, no me gustaría que piensen que el C se utiliza sólo para Web Servers...

¡Hasta el próximo post!

domingo, 15 de abril de 2018

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

Aquí estamos, después de la presentación en el último post, ha llegado el momento de ver si nuestro sistema de test continuo de CPU y Memoria funciona. Y si funciona bien. Os acordais el tema, ¿no? Zombies de supermercado, Dawn of the Dead, Dawn of the CPU... sí, eso.
...¿pero realmente tenemos que ir armados para comprar algo de RAM?...
Para probar el funcionamiento de la función de test descrita en el último post, he escrito un pequeño programa de test (testsys.c) que crea un thread que puede estresar la CPU y la Memoria de un sistema y luego, directamente en el main(), llama en un loop infinito la nuestra función testSys(), enseñando los resultados del test cada dos segundos. En el siguiente código solo falta la función de test: podéis ir a buscarla en el último post y aprovechar para volver a leer las partes más importantes (¡bravo quien lo hace!). ¡Vamos con el código!
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <linux/kernel.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/sysinfo.h>

// struct para los resultados
typedef struct {
    ...
} Results;

// prototipos locales
void testSys(Results *results);
void *tMyThread(void *arg);

// función main()
int main(int argc, char *argv[])
{
    // init thread
    pthread_t tid;
    int error;
    if ((error = pthread_create(&tid, NULL, &tMyThread, NULL)) != 0)
        printf("%s: no puedo crear el thread (%s)\n", argv[0], strerror(error));

    // llama testSys() para primer set valores estáticos
    Results results;
    testSys(&results);
    sleep(2);

    // testSys() loop para testear repetidamente el sistema
    for (;;) {
        // get valores
        testSys(&results);
        printf("cpu: total usage = %.1f\n", results.total_cpu / results.prec);
        printf("mem: total usage = %.1f\n", results.mem_system / results.prec);
        printf("cpu: proc  usage = %.1f\n", results.proc_cpu / results.prec);
        printf("mem: proc  usage = %.1f\n", results.mem_proc / results.prec);
        printf("load average: %.2f , %.2f , %.2f\n", 
                results.loads[0] / results.loads_prec,
                results.loads[1] / results.loads_prec, 
                results.loads[2] / results.loads_prec);

        // sleep 2 segundos
        sleep(2);
    }

    // exit
    exit(EXIT_SUCCESS);
}

// función de test del sistema
void testSys(
    Results *results)   // destinación de los resultados
{
    ...
}

// thread routine
void *tMyThread(void *arg)
{
    // alloc memoria
    unsigned long mem = 1024 * 1024 * 512;  // 512 mb
    char *ptr = malloc(mem);

    // thread loop infinito
    for (;;) {
        // usa memoria
        memset(ptr, 0, mem);

        // thread sleep
        usleep(10);    // NOTA: sleep muy pequeña para forzar mucha actividad de cpu
    }

    return NULL;
}
Como podeis ver, el programa es de una simplicidad desarmante (y con excelentes comentarios, como de costumbre). Para estresar la CPU y la Memoria he utilizado algunos simples trucos: asigno (con malloc()) un megabúfer de 512MB, y luego en un loop infinito lo uso intensamente (con una memset() completa). El ciclo del thread  una usleep muuuuy pequeña (10 us) que carga mucho la CPU (que nos odiará un poco, pero es el objetivo del nuestro test, ¿no?).

Ahora viene lo bueno: como se ha mencionado varias veces en el post anterior, nuestra referencia es el comando top de la familia UNIX así que tenemos que abrir dos terminales, y en una ejecutamos nuestro programa de test, y en la otra ejecutamos top para poder comparar en tiempo real si los resultados coinciden (recordaros de activar la opción "I" de top, como descrito en el otro post). Veamos qué ha pasado en mi ordenador:
en la terminal con testsys
...
load average: 0.85 , 0.42 , 0.32
cpu: total usage = 12.9
mem: total usage = 47.8
cpu: proc  usage = 12.5
mem: proc  usage = 6.9
...

en la terminal con top
top - 18:45:16 up 39 min,  2 users,  load average: 0,85, 0,42, 0,32
Tasks: 236 total,   1 running, 235 sleeping,   0 stopped,   0 zombie
%Cpu(s): 12,5 us,  0,1 sy,  0,0 ni, 87,2 id,  0,0 wa,  0,0 hi,  0,1 si,  0,0 st
KiB Mem :  7600656 total,  3962420 free,  1705536 used,  1932700 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  5384092 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND            
 5266 aldo      20   0  604548 524744    628 S 12,5  6,9   0:43.01 testpstat          
 1380 root      20   0  559772  94056  82400 S  0,1  1,2   0:43.89 Xorg               
 1012 root      20   0    4396   1276   1196 S  0,0  0,0   0:00.07 acpid              
 2628 aldo      20   0 2542528 358152 128056 S  0,0  4,7   3:18.25 firefox            
 ...
dado que la salida es continua, he seleccionado solo una parte y he agregado las elipsis, en cualquier caso podéis repetir fácilmente el test en el vuestro ordenador para verificar la consistencia de los resultados: yo diría que el resultado es más que satisfactorio, ¿no? ¡Misión cumplida!

¿Qué falta? Ah, sí, prometí algunas notas sobre los resultados que muestra top (y, consecuentemente, también el nuestro testsys): en lo que concierne a la CPU, los comentarios en el codigo de la testSys() son suficientes (descripción de las cargas medias, opción "I", etc.). Además, podemos agregar que la línea %Cpu(s) mostrada arriba confirma las fórmulas contenidas (y comentadas) en testSys(): por ejemplo, la suma de los diversos componentes (user, idle, sys, etc.) vale, como esperado, 100.

Para la memoria, sin embargo, el discurso es un poco más complejo, y habria que dedicar un post dedicado (tal vez lo haré en el futuro): por el momento os paso un enlace interesante: understanding-memory-usage-on-linux, y os añado sólo una explicación muy simple de algo (extraño) que a veces ocurre en sistemas embedded echos con BusyBox: en algunos casos parece que algunos procesos usan más del 100% de la memoria (en la columna %MEM): esto se debe al hecho de que está utilizando una versión antigua de top proporcionada por BusyBox que calcula %MEM como VSZ/MemTotal en lugar de RSS/MemTotal: debemos tener en cuenta que RSS es un valor residente mientras VSZ es un valor virtual, que por lo tanto está influenciado, por ejemplo, de eventuales shared library que se cargan y se comparten entre varias aplicaciones, de páginas de memoria no utilizadas en ese momento, etc., por lo que no es sorprendente que el valor exceda el 100%. Sin embargo, las nuevas versiones de top para BusyBox redefinen la columna %MEM en %VSZ solucionando así cualquier malentendido (oops... el significado de las siglas extrañas anteriores lo vais a encontrar en los comentarios de testSys(), en el manual de top y en el enlace que os he pasado sobre la memoria de Linux).

Bueno, yo diría que para este post es suficiente. Os dejo con una pequeña nota: dado que he utilizado solamente loop infinitos en el programa de test, podéis modificarlo a vuestro gusto añadiendo condiciones de salida. O, en alternativa, no tengáis miedo de detenerlo con CTL-C, no creo que Linux se enfade mucho...

¡Hasta el próximo post!

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!