Nei titoli e nei testi troverete qualche rimando cinematografico (eh 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 mando avanti anche una versione in Spagnolo (e, normalmente, i post sono disponibili un po' in ritardo rispetto alla versione in Italiano...). Potete trovarlo su El arte de la programación en C. Buona lettura.

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!

venerdì 16 febbraio 2018

The Test Connection
come testare la connettività in C

La Test Connection di questo post è un po' meno pericolosa della French Connection di cui parla il capolavoro di W.Friedkin. Ma è comunque una attività molto importante in alcuni tipi di applicazioni, quindi lungi da noi sottovalutarla.
...facce da connettività avanzata...
Quando usiamo interattivamente un computer e vogliamo verificare lo stato della rete abbiamo molti semplici modi per farlo: ad esempio con un bel ping verso google.com sappiamo se siamo connessi a Internet, e, se non funziona, possiamo usare qualche utility del sistema per scoprire cosa c'è che non va. Supponiamo, però, di dover sviluppare una applicazione per un sistema embedded (siamo o non siamo programmatori C?) e questa applicazione è collegata in rete e, nel caso che non ci sia comunicazione, deve segnalarlo in qualche maniera (chessoio, accendendo un led di errore, certamente non inviando un messaggio in rete!). Ok, e... come facciamo? Vediamolo, vai col codice!
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    int sock;

    // test interfaces
    //

    // ottiene tutti i network devices configurati
    struct ifaddrs *addrs;
    getifaddrs(&addrs);

    // loop sui network devices configurati
    int i_alr = 0;
    struct ifaddrs *tmpaddrs = addrs;
    while (tmpaddrs) {
        // test interface
        if (tmpaddrs->ifa_addr && tmpaddrs->ifa_addr->sa_family == AF_PACKET) {
            // apre un socket per il test
            if ((sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
                // mostra errore e continua
                printf("test interfaces: errore socket per la interface %s: %s\n", 
                       tmpaddrs->ifa_name, strerror(errno));
                continue;
            }

            // prepara i dati per ioctl()
            struct ethtool_value edata;
            edata.cmd = ETHTOOL_GLINK;
            struct ifreq ifr;
            strncpy(ifr.ifr_name, tmpaddrs->ifa_name, sizeof(ifr.ifr_name) - 1);
            ifr.ifr_data = (char *)&edata;

            // esegue ioctl()
            if (ioctl(sock, SIOCETHTOOL, &ifr) == -1) {
                // errore ioctl: chiude il socket e continua
                printf("test interfaces: errore ioctl per la interface %s: %s\n", 
                       tmpaddrs->ifa_name, strerror(errno));
                close(sock);
                continue;
            }

            // mostra i risultati e chiude il socket
            printf("test interfaces: interface %s: %s\n", 
                   tmpaddrs->ifa_name, edata.data ? "OK" : "NOK");
            close(sock);
        }

        // passa al prossimo device
        tmpaddrs = tmpaddrs->ifa_next;
    }

    // libera la devices list
    freeifaddrs(addrs);

    // test connettività
    //

    // apre un socket per il test
    if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        // errore socket
        printf("test di connettività: socket error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }

    // set di un timeout per send() (in questo caso in realtà lo usa connect()) per 
    // evitare un blocco su errore di connessione)
    struct timeval tv;
    tv.tv_sec  = 1;     // set timeout in secondi
    tv.tv_usec = 0;     // set timeout in usecondi
    if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)) < 0) {
        // errore ioctl
        printf("test di connettività: setsockopt error: %s\n", strerror(errno));
        close(sock);
        return EXIT_FAILURE;
    }

    // NOTE: alternativa (non portabile) all'uso di SO_SNDTIMEO:
    //int syn_retries = 1;     // send di un totale di 1 SYN packets => timeout ~2s
    //if (setsockopt(sock, IPPROTO_TCP, TCP_SYNCNT, &syn_retries, sizeof(syn_retries)) < 0) {
    //  ...

    // prepara la struttura sockaddr_in per il server remoto
    struct sockaddr_in server;                  // server (remoto) socket info
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;                // set famiglia indirizzi
    server.sin_addr.s_addr = inet_addr("216.58.214.174");   // set indirizzo server
    server.sin_port = htons(80);                // set numero port server

    // connessione al server remoto
    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
        // errore connect
        printf("test di connettività: errore connect: %s\n", strerror(errno));
        close(sock);
        return EXIT_FAILURE;
    }

    // mostra i risultati ed esce
    printf("test di connettività: connettività OK\n");
    close(sock);
    return EXIT_SUCCESS;
}
una premessa: questo codice è pensato per applicazioni Linux Embedded, quindi tutto ciò che segue si riferisce a un intorno Linux (beh, come sempre, ed è inutile che mi dilunghi sull'argomento...). Il codice è ampiamente commentato, così non devo dilungarmi eccessivamente in spiegazioni. In questo esempio il codice di test è scritto direttamente nel main() ma, in un progetto reale, si dovrebbe trasformarlo in una funzione che si può chiamare, periodicamente, nella posizione più opportuna, magari direttamente nel main() della applicazione. Visto il tipo di test che si esegue si dovrebbe chiamare ogni tanto, in base a quanto deve essere immediata la segnalazione di allarme che necessitiamo. Normalmente l'uso è a bassa frequenza (parliamo di secondi, non di millisecondi), se no la nostra funzione si mangerebbe molto tempo di CPU solo per controllare la connettività (e non mi sembra il caso).

Il test avviene in due fasi: test delle interfacce di rete e test di connettività. La prima verifica eventuali problemi, diciamo, Hardware: se ho sulla stessa macchina una connessione WiFi e una Ethernet potrei avere connessione Internet anche se, per esempio, il cavo di rete è staccato, e a me interessa segnalare questa situazione, quindi il solo test di connessione non sarebbe sufficiente. La seconda fase verifica la connettività vera e propria e, nel caso che questa manchi, possiamo sapere se non c'è connettività nonostante che le interfacce di rete siano ben collegate, così isoliamo meglio le possibili cause (insomma, è già un test abbastanza completo, ma si può sofisticare a piacere).

La fase di test delle interfacce si esegue con una funzione (relativamente nuova) della sempre indispensabile (in casi come questo) ioctl(). Grazie alla funzione SIOCETHTOOL possiamo verificare il funzionamento a basso livello di ogni interfaccia, visto che il nostro codice include un loop con cui si analizzano tutte le interfacce che, normalmente, sono due (loopback e Ethernet) e a volte sono di più (il WiFi, un altra scheda di rete, ecc.). L'interfaccia loopback c'è sempre e, nel caso che non ci interessi testarla, si può saltare facendo un semplice test sul nome (che è sempre "lo", ma comunque il nome si può verificare previamente, a scanso di equivoci, nel file /etc/network/interfaces).

Il test di connettività si basa, invece, su una semplice connessione tipo Client verso una direzione "sicura": nell'esempio ho usato IP e Port di google.com, ma si può usare quella che ci sembra più opportuna, ad esempio per un dispositivo collegato solo in rete locale si può usare la connessione a un server della rete, oppure ci si può collegare all'indirizzo del gateway locale, ecc. Dipende anche se quello che necessitiamo testare è la connettività Internet o una semplice connettività locale. Notare che tutto ruota sulla system call connect(), che attua inviando dati e ricevendo una risposta, quindi è un test più che sufficiente (senza ricorrere a un ben più complicato ping). Visto che la connect() si blocca per molto tempo quando non riceve subito una risposta ho inserito un opportuno timeout (leggere il commento nel codice) usando setcsockopt() + SO_SNDTIMEO, ma (leggere l'altro commento) si poteva usare anche il flag TCP_SYNCNT, che però è una soluzione meno ortodossa e meno portabile.

Su un normale PC (invece che su un sistema embedded) con connessione Internet via Ethernet, il risultato in condizioni normali è il seguente:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: OK
test di connettività: connettivitá OK
e, se forziamo la disconnessione Software (usando, ad esempio, il NetworkManager di Linux) il risultato è:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: OK
test di connettività: errore connect: Network is unreachable
mentre, se forziamo la disconnessione Hardware (staccando il cavo di rete) il risultato è:
aldo@mylinux:~/blogtest$ ./conntest 
test interfaces: interface lo: OK
test interfaces: interface enp3s0: NOK
test di connettività: errore connect: Network is unreachable
Mica male, no? Una funzione semplice semplice però molto utile. E per oggi può bastare, missione compiuta!

Ciao, e al prossimo post!

domenica 14 gennaio 2018

Remapped File
come sincronizzare un Memory Mapped File in C - pt.2

Beh, credo che sia ora di pubblicare la seconda parte di Remapped File. Spero che vi siate ricaricate bene durante le feste, magari guardando un gran film come Primer, che contiene internamente vari remake di se stesso: potete rivederlo quante volte volete e ogni volta scoprirete dettagli nuovi e, inevitabilmente, perderete dettagli vecchi, entrando in un loop temporale senza fine come i protagonisti stessi del film. Vi assicuro che il post di questo mese non è complicato da capire come Primer, di cui si trovano in rete addirittura pagine wiki dedicate alle timeline con tanto di descrizioni grafiche...
...è nato prima l'uovo o la gallina?...
Allora, andiamo avanti con la nostra libreria per IPC. Dopo aver descritto l'header file (libmmap.h) e un doppio esempio di uso (datareader.c e datawriter.c) è venuto il momento di descrivere la vera e propria implementazione. Ovviamente chi non ha letto la prima parte deve vergognarsi e andare subito a leggerla, e poi tornare qui.

Tornati? Ok, bando alle ciance... vai col codice!
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include "libmmap.h"

// memMapOpenMast() - apre un mapped-file come master
ShmData *memMapOpenMast(
    const char *shmname,    // nome del mapped-file
    size_t     len)         // size del campo data da condividere
{
    // apre un mapped-file (il file "shmname" é creato in /dev/shm)
    int fd;
    if ((fd = shm_open(shmname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR)) == -1)
        return NULL;    // esce con errore

    // tronca un mapped-file
    if (ftruncate(fd, sizeof(ShmData) + len) == -1)
        return NULL;    // esce con errore

    // mappa un mapped-file
    ShmData *shmdata;
    if ((shmdata = mmap(NULL, sizeof(ShmData) + len,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
        return NULL;    // esce con errore

    // init semaforo
    if (sem_init(&shmdata->sem, 1, 1) == -1)
        return NULL;    // esce con errore

    // init flag di data_ready e lunghezza
    shmdata->data_ready = false;
    shmdata->len = len;

    // ritorna il descrittore
    return shmdata;
}

// memMapOpenMast() - apre un mapped-file come slave
ShmData *memMapOpenSlav(
    const char *shmname,    // nome del mapped-file
    size_t     len)         // size del campo data da condividere
{
    // apre un mapped-file (il file "shmname" é creato in /dev/shm)
    int fd;
    if ((fd = shm_open(shmname, O_RDWR, S_IRUSR|S_IWUSR)) == -1)
        return NULL;    // esce con errore

    // mappa un mapped-file
    ShmData *shmdata;
    if ((shmdata = mmap(NULL, sizeof(ShmData) + len,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
        return NULL;    // esce con errore

    // init semaforo
    if (sem_init(&shmdata->sem, 1, 1) == -1)
        return NULL;    // esce con errore

    // ritorna il descrittore
    return shmdata;
}

// memMapClose() - chiude un mapped-file
int memMapClose(
    const char *shmname,    // nome del mapped-file
    ShmData    *shmdata)    // pointer al mapped-file
{
    // elimina semaforo
    if (sem_destroy(&shmdata->sem) < 0)
        return -1;      // esce con errore

    // un-mappa un mapped-file
    if (munmap(shmdata, sizeof(ShmData)) < 0)
        return -1;      // esce con errore

    // cancella un mapped-file
    if (shm_unlink(shmname) < 0)
        return -1;      // esce con errore

    // esce con Ok
    return 0;
}

// memMapFlush() - flush di un mapped-file
int memMapFlush(
    ShmData *shmdata)       // pointer al mapped-file
{
    // sync su disco di un mapped-file
    return msync(shmdata, sizeof(ShmData) + shmdata->len, MS_SYNC);
}

// memMapRead() - legge dati dal mapped-file
int memMapRead(
    void    *dest,
    ShmData *src)
{
    // lock memoria
    sem_wait(&src->sem);

    // test presenza dati nel mapped-file
    if (src->data_ready) {
        // legge dati dal mapped-file
        memcpy(dest, src->data, src->len);
        src->data_ready = false;

        // unlock memoria ed esce
        sem_post(&src->sem);
        return 1;
    }
    else {
        // unlock memoria ed esce
        sem_post(&src->sem);
        return 0;
    }
}

// memMapWrite() - scrive dati nel mapped-file
void memMapWrite(
    ShmData    *dest,
    const void *src)
{
    // lock memoria
    sem_wait(&dest->sem);

    // scrive dati nel mapped-file
    memcpy(dest->data, src, dest->len);
    dest->data_ready = true;

    // unlock memoria ed esce
    sem_post(&dest->sem);
}
Come si nota è abbastanza semplice e conciso, e, come sempre, il codice è auto-esplicativo, ampiamente commentato e con commenti che parlano da soli.

Tutte le funzioni usano, internamente, le opportune system call Linux/POSIX per trattare il nostro Memory Mapped File. In caso di errore su una system call si ritorna immediatamente -1, e questo ci permette, a livello applicativo, di usare direttamente strerror() per verificare l'errore (e questo è un argomento che i miei lettori più affezionati dovrebbero conoscere bene...). Come si nota ci sono due funzioni di open, una master e una slave: perché? Perché, il tipo di comunicazione scelto è (leggermente) asimmetrico, quindi è necessario che uno dei due estremi (il writer) apra il canale, mentre l'altro (il reader) accede al canale sono quando lo trova creato. Quindi adesso si comprende meglio (spero) come funzionano le due funzioni descritte nella prima puntata del post. Questo meccanismo asimmetrico ricorda molto il meccanismo Client/Server che si usa coi socket (altro argomento già affrontato qui), e, infatti, questa libreria è una alternativa al classico IPC coi socket.

Le funzioni di read e write si limitano a usare memcpy() per copiare i dati dal/sul mapped-file. E, come anticipato nel post precedente, le letture e scritture usano un meccanismo di sincronizzazione (un POSIX unnamed semaphore) che viene inizializzato nelle funzioni di open: semplicemente chi accede ai dati mette in rosso (lock) il semaforo (quindi chi arriva dopo si ferma) e quando ha finito lo rimette in verde (unlock).

La funzione di flush è solo un wrapper per la chiamata msync() e, normalmente, non è necessario usarla: con questa libreria noi vogliamo trattare file mappati in memoria per condividere dati tra processi, per cui, non solo ci interessa poco che il file abbia una immagine reale sul disco, ma, per una semplice questione di prestazioni dovremmo evitare di scaricare realmente sul disco tutti i cambi effettuati in memoria, se no tanto varrebbe far comunicare i processi con dei file veri. Quindi a cosa serve il flush? Serve solo ad avere una eventuale versione reale e aggiornata del file condiviso, nel caso volessimo trattarlo anche con le classiche funzioni open(), close(), read(), ecc. Per questo nel file msgwriter.c descritto nel post precedente la chiamata memMapFlush() non viene usata.

E veniamo all'altra novità introdotta in questa nuova versione della libmmap: i dati trattati sono, ora, generici, quindi le funzioni di read e write usano dei void* come argomenti: questo è un vantaggio notevole, perché permette di scambiare dati in IPC usando qualsiasi formato; una struttura complessa oppure una singola variabile (chessoio? un int). Ad esempio nell'ultimo post ho definito un tipo Data (una struct con un campo text) da usare a livello applicativo e che si può passare come argomento alle read e write senza neppure fare un cast. Una grande flessibilità, simile a quella delle funzioni della libc come la memcpy(), ma con un qualcosa in più: la dimensione (e, indirettamente, il tipo) dei dati che si scambiano viene passata, una volta per tutte, durante la fase di open (attraverso il parametro len), quindi le funzioni di read e write non hanno il classico campo "size_t len" che uno si aspetterebbe.

Ci manca solo da descrivere una cosa: il trucco del "char data[1]" usato per rendere generici i dati da condividere. Questo campo è (non a caso) l'ultimo della struttura dati che descrive il mapped-file, e funziona così: quando si crea il file si passa, alla memMapOpenMast(), il size dei dati da scambiare (con un operatore sizeof, vedi l'esempio dell'ultimo post), e, come si nota nel codice della memMapOpenMast(), il mapped-file viene mappato usando la system call mmap() passandogli un argomento length che indica la dimensione del mapped-file in oggetto: nel nostro caso si passa "sizeof(ShmData) + len", quindi il mapped-file è impostato per scambiare dati nella sua parte variabile "char data[1]", che di base è lunga un char, ma che, in realtà, è lunga len char una volta mappato il file. Un trucchetto da niente

Spero che la nuova versione della libreria vi sia piaciuta. Vi assicuro che, con solo qualche miglioria (tipo la gestione di tutti gli errori interni possibili, lo sdoppiamento del semaforo per gestire lock read/write, aggiungere meccanismi di accesso blocking/nonblocking... va beh, forse un è un po' più di qualche miglioria!), si potrebbe usare anche in progetti professionali... e scusate se è poco!

Ciao, e al prossimo post!