Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 17 marzo 2018

Dawn of the CPU
come testare l'uso di CPU e Memoria in C - pt.1

Jane: Ma perché ritornano in un grande magazzino?
Stephen: Dev'essere l'istinto... Il ricordo di quello che erano abituati a fare. Era un posto importate quando erano vivi.
In questo post, con la scusa di citare il mitico Dawn of the Dead del grande G.A.Romero, parleremo di quando il nostro PC si trasforma in uno zombi, diventando veramente difficile da usare. Uno zombi da supermercato, in grado solo di fare l'attività minimale dei bei tempi in cui era vivo...
...zombi da supermercato...
Allora, veniamo al dunque: se il vostro sistema diventa poco reattivo, lento, le applicazioni che aprite non si aprono immediatamente... le possibilità sono 2:
  1. se state usando un sistema della famiglia UNIX (Linux, FreeBSD, macOS, ecc.) e non state chiedendo troppo rispetto all'Hardware disponibile (tipo aprire 50 applicazioni alla volta su un recentissimo PC Pentium III del 1999) è probabile che una applicazione malfunzionante vi stia mangiando la CPU e/o la Memoria: cercate di scoprire quale applicazione è e uccidetela (usando il monitor di sistema oppure, se siete della vecchia scuola, usando top e kill in un terminale). Detto fatto.
  2. se state usando quel sistema innominabile (che comincia con W e finisce con s, già sapete), beh allora dovete farvene una ragione: è il suo comportamento normale, baby... l'unica soluzione è passare a usare qualcosa di un po' più serio (vedi al punto 1). Masochisti!
Ma, supponiamo che (per riallacciarci al post precedente... non l'avete letto? E cosa aspettate?) state scrivendo una applicazione per un sistema embedded e la applicazione, che non avrà neanche una interfaccia grafica (altro che usare top + kill !), deve, comunque, sorvegliare lo stato del sistema (CPU e Memoria) per alzare qualche allarme (o accendere un led di errore, ecc) nel caso che qualcosa vada male... che fare? Ecco, in questo post vi proporrò un semplice monitor di prestazioni che la vostra applicazione embedded potrà chiamare (a bassa frequenza, ovviamente) per segnalare possibili problemi.

(...ehm, scusate se insisto ma, tornando ai due punti descritti qua sopra: l'argomento ora è: applicazioni embedded a base UNIX (quindi Linux Embedded, QNX, LynxOS, ecc.). Se invece volete proprio farvi del male e scrivete applicazioni di questo tipo usando la versione CE del sistema innominabile (W...s, ricordate?) va beh, cosa vi devo dire? chi è causa del suo mal pianga se stesso...)

Allora, divideremo il post in due parti: in questa prima parte vi proporrò una funzione (che chiameremo testSys()) che fa tutto il lavoro necessario. Nella seconda parte faremo un main(), che simula quello di una semplice applicazione embedded, in cui introdurremo un thread fatto ad-hoc per appesantire il sistema, e il nostro main(), usando la testSys(), dovrebbe accorgersene senza problemi. Vai col codice!
// struct per i risultati
typedef struct {
    int   total_cpu;    // cpu totale (val.percentuale x prec)
    int   proc_cpu;     // cpu processo (val.percentuale x prec)
    int   mem_system;   // mem totale (val.percentuale x prec)
    int   mem_proc;     // mem processo (val.percentuale x prec)
    float prec;         // precisione (10=1dec)
    int   loads[3];     // carichi medi (val.percentuale x loads_prec)
    float loads_prec;   // precisione per carichi (100=2dec)
} Results;

// funzione di test del sistema
void testSys(
    Results *results)   // destinazione dei risultati
{
    static unsigned long long total_cpu_last; /* system total cpu-time incluso idle-time
                                                 in jiffies (tipicamente centesimi di
                                                 secondo)) */
    static unsigned long long idle_cpu_last;  /* system idle cpu-time (in jiffies
                                                 (tipicamente centesimi di secondo)) */
    static unsigned long proc_times_last;     /* cpu-time del processo corrente
                                                 (calcolato sommando usertime e
                                                 systemtime del processo) */
    FILE *fp1;
    FILE *fp2;

    // set risultati di default (così il chiamante può trattare gli errori)
    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.;

    /* legge /proc/self/stat (statistiche di processo di questo processo) e /proc/stat
       (statistiche dei processi di questa macchina) */
    if ( ((fp1 = fopen("/proc/self/stat", "r")) != NULL) &&
         ((fp2 = fopen("/proc/stat", "r")) != NULL)) {

        // legge 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;

        // legge 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;

        /* calcola uso cpu totale (%cpu = work_over_period / total_over_period * 100
           (dove work_over_period è (total - idle))
           NOTA: il campo iowait di /proc/stat è incluso nel calcolo, anche se è un
           valore inaffidabile (ma l'errore è trascurabile) */
        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;

        /* calcola uso cpu del processo ((proc_times2 - proc_times1) * 100 /
           (float) (total_cpu_usage2 - total_cpu_usage1))
           NOTA: nel programma "top" questo valore è moltiplicato per NPROCESSORS (usare
           il comando interattivo "I" per disabilitare questa caratteristica) */
        results->proc_cpu =
            (float)(proc_times_cur - proc_times_last) /
                   (total_cpu_cur - total_cpu_last) * 100 * results->prec;

        // save valori e chiude files
        total_cpu_last  = total_cpu_cur;
        idle_cpu_last   = idle_cpu_cur;
        proc_times_last = proc_times_cur;
        fclose(fp1);
        fclose(fp2);
    }

    // legge /proc/self/statm (statistiche di memoria di questo processo)
    struct sysinfo si;  // destinazione per dati ottenuti dalla system call sysinfo()
    if (((fp1 = fopen( "/proc/self/statm", "r")) != NULL) && (sysinfo(&si) != -1)) {
        /* legge il resident set size corrente (physical memory use) misurato in bytes.
           NOTA: nel programma "top" il valore chiamato RES dipende dal valore del RSS
           (resident set size) delle strutture interne di Linux */
        long resident;
        fscanf(fp1, "%*s%ld", &resident);
        long res = resident * (size_t)sysconf(_SC_PAGESIZE) / 1024; // code+data

        // calcola valori di riferimento
        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;

        /* calcola %mem: nel programma "top" questo valore è calculato come: %mem =
           RES / totphisicalmem (dove RES è CODE + DATA (dove DATA è data + stack)) */
        results->mem_proc = (float)res / (totalram / (float)1024) * 100 * results->prec;
        fclose(fp1);
    }

    // legge cpu loadavg
    double loads[3];    // lcarichi medi di CPU a 1, 5, e 15 minuti
    if (getloadavg(loads, 3) != -1) {
        // copia i carichi nei risulatati
        results->loads[0] = loads[0] * results->loads_prec;
        results->loads[1] = loads[1] * results->loads_prec;
        results->loads[2] = loads[2] * results->loads_prec;
    }
}
Che ne pensate? Ho messo così tanti commenti che quasi non c'è più nulla da dire. E cosa si può aggiungere? È evidente dai commenti che i dati ricavati usano lo stesso sistema di calcolo che usa top (che è un riferimento quasi obbligato) e, in particolare, ci riferiamo a top con l'opzione "I" (Irix mode Off) abilitata: questo evita, semplicemente, di avere indicazioni (strane a prima vista, ma corrette) come "%CPU=800" che corrisponde a un consumo di CPU del 100% su una macchina con 8 cores: con l'opzione "I" siamo sicuri che il valore massimo indicato sarà 100%, indipendentemente dal numero di cores disponibili (così non si può fraintendere).

La funzione testSys() scrive i risultati in una struttura TestResults passata come argomento: questo ci permette di poter presentare e/o processare opportunamente i dati a livello del chiamante. Nell'esempio che vedremo prossimamente il main() chiama testSys() e mostra i risultati, così si può verificare se corrispondono a quelli di top (che possiamo eseguire in contemporanea in un altro terminale). In un caso reale i dati si dovrebbero, invece, processare, ad esempio confrontandoli con dei valori di riferimento per poter alzare degli allarmi. Proprio per questo motivo i dati vengono registrati come int moltiplicati per un opportuno fattore di precisione: questa è una maniera normale e classica di trattare dati che nascono float, visto che il confronto (e qualunque altra operazione) tra valori interi è molto più semplice da realizzare e permette più flessibilità d'uso.

Va beh, per oggi può bastare. Come promesso nel prossimo post vi mostrerò un esempio d'uso, un po' di risultati di test reali e un piccolo approfondimento sulla interpretazione dei dati, specialmente su quelli della Memoria.  

Ciao, e al prossimo post!