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.

venerdì 18 maggio 2018

Il grande lighttpd
come scrivere un modulo lighttpd in C - pt.4

Maude: E di cosa ti occupi nel tempo libero?
Drugo: Mah, le solite cose: Bowling, un giro in macchina, un trip d'acido quando capita...
Sottinteso che per scrivere del buon Software non c'è bisogno di imitare le abitudini del mitico Drugo del Grande Lebowski, ritorniamo sull'argomento lighttpd, perchè nel terzo post della serie (qui, qui e ancora qui) avevo promesso di ampliare il discorso. Ebbene è venuto il momento, le promesse si mantengono!
...Mah, le solite cose: Bowling, un giro in macchina, un modulo lighttpd quando capita...
Bene, ne "Il grande lighttpd - pt.3" (che, avete appena riletto, immagino...) avevamo scritto un bel modulo elementare per lighttpd. Faceva poco (era un classico "Helloworld": scriveva solo una presentazione nel browser) però era un buon inizio per entrare nel mondo dei moduli di questo notevole Web Server. Ecco, adesso riprenderemo la base fatta a suo tempo e gli aggiungeremo una funzione che estrae i dati POST inclusi in una eventuale petizione HTTP di tipo POST che arriva al modulo. Perché ho scelto di aggiungere proprio questa funzionalità? Beh, ovviamente perché è abbastanza normale che un modulo tratti vari tipi di petizioni (GET, POST, ecc.), e poi, in particolare, ricordo che la prima volta che aggiunsi questa funzionalità in un modulo Apache che avevo scritto, notai che non erano disponibili molte indicazioni su questo argomento (e questo nonostante la enorme mole di documentazione su Apache disponibile rispetto a quella su lighttpd)... vi lascio immaginare, quindi, quanta documentazione e quanti esempi ho trovato per fare la stessa cosa per lighttpd: praticamente niente.

Bene, senza stare a ripetere tutto il codice e la maniera di generarlo, riscriveremo solo la funzione mod_helloworld_uri_handler() e aggiungeremo una nuova funzione getPost(). Premetto che, per integrare il codice che ho scritto, è necessario seguire la guida di installazione indicata in "Il grande lighttpd - pt.1" e ripetere tutti i 10 passi indicati in "Il grande lighttpd - pt.2", usando questa volta la versione 1.4.45 di lighttpd (occhio: se usate una versione più vecchia il codice che sto per mostrarvi non funziona). Fatto? Allora vai col codice!
URIHANDLER_FUNC(
    mod_helloworld_uri_handler)
{
    plugin_data *p = p_d;

    UNUSED(srv);

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

    // test uri path (return se errore)
    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 se errore)
    if (con->uri.path->ptr && strstr(con->uri.path->ptr, "helloworld")) {
        // prepara il buffer per la risposta
        buffer *buf = buffer_init();

        // test metodo http
        if (strstr(con->request.request->ptr, "GET")) {
            // metodo GET: scrive risposta
            buffer_append_string(buf, "<big>Hello, world!</big>");
        }
        else if (strstr(con->request.request->ptr, "POST")) {
            // metodo POST: controlla la presenza di post-data
            size_t len = con->request.content_length;
            if (len) {
                // post-data presenti; li legge per scrivere la risposta
                char *data = malloc(len + 1);
                if (readPost(con, data, len)) {
                    // scrive la risposta
                    buffer_append_string(buf, data);
                }

                // libera la memoria
                free(data);
            }
            else {
                // errore: messaggio POST senza post-data
                buffer_append_string(buf, "mod_helloworld: errore: POST senza post-data");
            }
        }
        else {
            // errore: messaggio con metodo non trattato
            buffer_append_string(buf, "mod_helloworld errore: metodo non trattato");
        }

        // scrive il buffer e lo libera
        chunkqueue_append_buffer(con->write_queue, buf);
        buffer_free(buf);

        // spedisce l'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 finito
        return HANDLER_FINISHED;
    }

    // handler non trovato
    return HANDLER_GO_ON;
}

static bool readPost(
    connection *con,                // dati connessione
    char       *data,               // buffer destinazione per post-data
    size_t     len)                 // lunghezza data (senza terminatore)
{
    // set valore di return di default
    int retval = false;

    // legge post-data (nello stream di chunks)
    size_t rpos = 0;
    chunkqueue *cq = con->read_queue;
    for (chunk *mychunk = cq->first; mychunk; mychunk = cq->first) {
        // calcola il size del buffer corrispondente al chunk di post-data corrente
        size_t n_tocopy = buffer_string_length(mychunk->mem) - mychunk->offset;

        // test se ci sono dati da copiare
        if (n_tocopy <= (len - rpos)) {
            // copia un chunk e set della posizione di copia del prossimo chunk
            memcpy(data + rpos, mychunk->mem->ptr + mychunk->offset, n_tocopy);
            rpos += n_tocopy;

            // é stato letto (almeno) un chunk di post-data: set retval=true
            retval = true;
        }
        else {
            // buffer overflow: forzo uscita dal loop
            break;
        }

        // segnalo come letto il chunk di post-data corrente
        chunkqueue_mark_written(cq, chunkqueue_length(cq));
    }

    // aggiungo il terminatore di stringa (il buffer é lungo len+1)
    data[len] = 0;

    // esco con retval
    return retval;
}
Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale.

L'originale funzione mod_helloworld_uri_handler() è ancora una sorta di funzione "Helloworld", ma ora distingue i tipi di petizione e, nel caso POST, chiama la funzione readPost() e scrive come risposta "Hello, world!" più i dati POST. Nel caso di petizioni GET si limita a scrivere "Hello, world!" e, con altri tipi di petizione o con petizioni POST senza dati, scrive un messaggio di un errore.

La funzione readPost() è semplice ma non immediata: vista la scarsezza quasi totale di esempi disponibili in rete ho dovuto, praticamente, fare reverse engineering sugli (ottimi) moduli integrati nella distribuzione (avere il codice sorgente originale a disposizione è sempre una cosa ottima!) in maniera di dedurre come fare quello che mi ero prefisso. Il risultato finale è (modestia a parte) buono, sia come aspetto (stile) sia come prestazioni (ho fatto dei test e funziona bene).

Ah, il test, dimenticavo! Testare con petizioni POST non è semplice come farlo con le GET, dove è sufficiente (come detto nel post precedente) scrivere la URI del modulo nella barra degli indirizzi di Firefox o Chrome (o qualsiasi altro browser meno IE, per favore...) e aspettare la risposta. Per testare il nostro nuovo modulo è meglio usare un bel plugin del browser (tipo Poster) per semplificare il lavoro.

Va bene, per il momento credo che possiamo fermare per un tempo l'argomento moduli lighttpd. Giuro che nel prossimo post parlerò d'altro, non vorrei che pensaste che il C si usa solo per i Web Servers...

Ciao e al prossimo post!

domenica 15 aprile 2018

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

Ci siamo, dopo la presentazione nello scorso post è venuta l'ora di vedere se il nostro sistema di test continuo di CPU e Memoria funziona. E se funziona bene. Ricordate l'argomento, no? Gli zombi da supermercato, Dawn of the Dead, Dawn of the CPU... si, quello.
...ma veramente dobbiamo andare armati per comprare un po' di RAM?...
Per verificare il funzionamento della funzione di test descritta nello scorso post, ho scritto un piccolo programma di test (testsys.c) che crea un thread in grado di stressare CPU e Memoria di un sistema e che poi, direttamente nel main(), chiama in un loop infinito la nostra funzione testSys(), mostrando ogni due secondi i risultati del test. Nel codice che segue manca solo la funzione di test: quella potete andare a ripescarla nell'ultimo post e così potete approfittare per rileggerne le parti salienti (bravi se lo fate!). Vai col codice!
#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 per i risultati
typedef struct {
    ...
} Results;

// prototipi locali
void testSys(Results *results);
void *tMyThread(void *arg);

// funzione 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: non posso creare il thread (%s)\n", argv[0], strerror(error));

    // chiama testSys() per primo set valori statici
    Results results;
    testSys(&results);
    sleep(2);

    // testSys() loop per testare ripetutamente il sistema
    for (;;) {
        // get valori
        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 secondi
        sleep(2);
    }

    // exit
    exit(EXIT_SUCCESS);
}

// funzione di test del sistema
void testSys(
    Results *results)   // destinazione dei risultati
{
    ...
}

// 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 molto piccola per forzare molta attività di cpu
    }

    return NULL;
}
Come potete ben vedere il programma è di una semplicità disarmante (e con  ottimi commenti, al solito). Per stressare CPU e Memoria ho solo usato alcuni semplici trucchetti: alloco (con malloc()) un bufferone di 512MB, e poi in un loop infinito lo uso intensamente (con una memset() completa). Il loop del thread usa una usleep moooolto piccola (10 us) che carica non poco la CPU (che ci odierà un poco, ma è l'obiettivo del nostro test, no?).

Adesso viene il bello: come detto varie volte nel post precedente, il nostro riferimento è il comando top della famiglia UNIX, quindi dobbiamo aprire due terminali, e in uno eseguiamo il nostro programma di test, e nell'altro eseguiamo top e così possiamo confrontare in tempo reale se i risultati corrispondono (ricordatevi di attivare l'opzione "I" di top, come descritto nell'altro post). Vediamo cosa è successo sulla mia macchina:
nel terminale 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
...

nel terminale 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            
 ...
visto che l'output è continuo ne ho selezionato solo una parte e ho aggiunto i puntini di sospensione, comunque potete ripetere facilmente il test sulla vostra macchina per verificare la costanza dei risultati: direi che il risultato è più che soddisfacente, no? Missione compiuta!

Cosa manca? Ah, si, avevo promesso qualche appunto sui risultati che mostra top (e per riflesso anche il nostro testsys): per quanto riguarda la CPU sono sufficienti i commenti nel codice della testSys() (descrizione dei carichi medi, opzione "I", ecc.). In più si può aggiungere che la linea %Cpu(s) mostrata qua sopra conferma le formule contenute (e commentate) nella testSys(): ad esempio la somma dei vari componenti (user, idle, sys, ecc.) vale, guarda caso, 100.

Per la memoria, invece, il discorso è un po' più complesso, e bisognerebbe dedicargli un post apposito (magari lo farò in futuro): per il momento vi passo un link interessante: understanding-memory-usage-on-linux, e vi aggiungo solo una spiegazione semplicissima di una cosa (strana) che a volte succede su sistemi embedded realizzati con BusyBox: in alcuni casi sembra che alcuni processi usino più del 100% della memoria (nella colonna %MEM): questo è dovuto al fatto che si sta usando una vecchia versione di top fornita da BusyBox che calcola %MEM come VSZ/MemTotal invece di RSS/MemTotal: bisogna considerare che RSS è un valore residente mentre VSZ è un valore virtuale, quindi influenzato, ad esempio, da eventuali shared library che vengono caricate e condivise tra varie applicazioni, pagine di memoria non usate al momento, ecc., per cui non c'è da stupirsi se il valore supera il 100%. Comunque le versioni più recenti di top per BusyBox ridefiniscono la colonna %MEM in %VSZ risolvendo così eventuali incomprensioni (oops... si, i significati delle strane sigle qui sopra li trovate nei commenti della testSys(), nel manuale di top e nel link che vi ho passato sulla memoria di Linux).

Beh, direi che per questo post può bastare. Vi lascio con una piccola nota: visto che ho usato solo loop infiniti nel programma di test potete modificarlo a piacere aggiungendo condizioni di uscita. Oppure non abbiate paura a fermarlo con CTL-C, non credo che Linux si arrabbi molto...

Ciao, e al prossimo post!

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!