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!