Rick Dalton: Gli attori devono fare un sacco di cose pericolose. Cliff qui ha la parte più dura.Nel grande C'era una volta a... Hollywood dell'altrettanto grande Quentin Tarantino (grande appassionato di Cinema Italiano, per chi non lo sapesse), Rick"DiCaprio"Dalton (attore) descrive la durezza del ruolo di Cliff"Pitt"Booth (stuntman): ecco, anche noi programmatori abbiamo (senza doppi sensi) parti dure del lavoro, tipo trattare problemi inaspettati che possono sorgere durante l'esecuzione di un programma. E cosa dovrebbe fare una applicazione robusta e industriale quando c'è un problema in corso? Ma alzare un allarme! Elementare, mio caro Watson!. Beh, visto che ho analizzato molte applicazioni che si fregiano dell'aggettivo "industriale" senza avere un Gestore di Allarmi (Alarm Manager per gli amici) ho pensato che era il caso di scriverci sopra e un articolo, perché diciamolo chiaramente: una applicazione industriale senza un Alarm Manager "è come un giardino senza fiori" [semicit.].
Intervistatore: E' così che descrivi il tuo lavoro, Cliff?
Cliff Booth: Cioè che ho la parte più dura? Sì, e me ne vanto.
...ero molto allarmato, capisci... |
programmatore: Ma allora cosa fa un Alarm Manager?Ok, è giunto il momento di mostrare il codice, che è, al solito, così ben commentato che mi permetterà di evitare di discutere tutti i micro-dettagli. Vai con l'header!
mio cuggino: Vediamo, un po'... serve a mantenere una lista di allarmi attivi e, in parallelo, a mantenere una lista di allarmi storici, gli allarmi non più attivi.
programmatore: Ma quindi a cosa serve?
mio cuggino: Beh, io direi che serve a darci, in tempo reale, una immagine dei problemi attivi del programma che stiamo eseguendo. Ovviamente i problemi devono essere ben descritti e facilmente identificabili rispondendo alle domande: "cosa è successo" e "quando è successo". Poi serve a darci, in tempo reale, una immagine dei problemi che si sono verificati ma che sono stati risolti con un intervento di auto-correzione, o con una azione esterna, o con svariati altri metodi. Rispetto agli allarmi attivi deve essere evidenziata anche la risposta alla domanda "quando è terminato".
programmatore: A, vabbè, ma io queste cose le faccio con un Logger!
mio cuggino: No! Il Logger ti serve a priori, ad esempio se il programma si è schiantato e vuoi analizzare quello che è successo, mentre l'Alarm Manager ti offre un immagine filtrata, in tempo reale, di quello che succede e che è successo recentemente: è utile anche e soprattutto per l'utente finale dell'applicazione, tipo un tecnico che controlla una macchina di produzione. Il Logger, che convive sempre con l'Alarm Manager in una buona Applicazione Industriale, serve più ai programmatori che agli utilizzatori.
programmatore: Continuo a pensare che è meglio il Logger...
mio cuggino: Continuo a pensare che non capisci una mazza... detto con tutto il rispetto, però!
#ifndef ALARMER_H #define ALARMER_H #include <pthread.h> #include <stdbool.h> #include <time.h> #define TEXT_SIZE 80 // size del testo #define MAX_ALARMS 256 // numero max di allarmi #define MAX_HIST_ALARMS 8 // numero max di allarmi storici // structure Alarm definition typedef struct { int id; // ID allarme int owner; // owner allarme char fmt[TEXT_SIZE]; // testo base allarme (formato printf) char text[TEXT_SIZE]; // testo allarme bool active; // flag di attività (true=attivo) time_t on_time; // istante di attivazione (secondi da Epoch) time_t off_time; // istante di disatttivazione (secondi da Epoch) } Alarm; // structure Alarmer typedef struct { // tabelle allarmi attivi/storici Alarm *alarms_table[MAX_ALARMS]; Alarm hist_alarms_table[MAX_HIST_ALARMS]; // indici per find first/next allarmi attivi/storici int find_active_index; int find_hist_index; // dati per gestione circolare allarmi storici int hist_index; // indice per gestione circolare allarmi storici int num_hist_found; bool restart_hist; // mutex di sincronizzazione (per uso thread-safe) pthread_mutex_t mount_mutex; // mutex per operazioni mount pthread_mutex_t add_mutex; // mutex per operazioni add pthread_mutex_t onoff_mutex; // mutex per operazioni on/off } Alarmer; // prototipi globali int setAlarmer(Alarmer *alarmer); //void delAlarmer(Alarmer *alarmer); void mountAlarms(Alarmer *alarmer, const Alarm *alarms/*, int offset, int instance*/); //void umountAlarms(Alarmer *alarmer, int owner, int offset, int instance); void addAlarm(Alarmer *alarmer, int id, int owner, const char *fmt); //void delAlarm(Alarmer *alarmer, int id, int owner); void onAlarm(Alarmer *alarmer, int id, int owner, ...); void offAlarm(Alarmer *alarmer, int id, int owner); char* getAlarms(Alarmer *alarmer, char *dest, size_t size); char* getHistAlarms(Alarmer *alarmer, char *dest, size_t size); #endif /* ALARMER_H */Come avrete notato, abbiamo due strutture, una definisce "un allarme" e l'altra definisce l'Alarm Manager stesso (che ho chiamato Alarmer per essere un po' più sintetico). L'allarme contiene tutti i dati che ne consentono l'identificazione: un identificatore unico, un proprietario, un testo, un flag di attività e gli istanti di inizio e fine. L'Alarmer contiene invece tutti i dati necessari a trattare gli allarmi attivi e storici, tra l'altro in modo thread-safe (le cose bisogna farle bene fin dall'inizio). E poi ci sono i prototipi delle funzioni globali, quelle che permettono l'uso dell'Alarmer.
Avrete anche notato che alcuni prototipi sono commentati: infatti quello che sto per offrirvi è una versione ridotta e semplificata di un Alarm Manager, perché quello completo usa molto codice e non voglio annoiare nessuno: vi assicuro però che, una volta letto l'articolo e compilato il demo (funzionante), vi sarà tutto così chiaro che potrete scrivere quello che manca senza problemi (e comunque anche questa versione light è utilizzabile quasi così com'è).
E adesso andiamo avanti con il codice del demo, mentre l'implementazione vera e propria sarà nella seconda parte dell'articolo: è una scelta non casuale, perché grazie all'esempio d'uso saranno più chiare alcune scelte di implementazione, fidatevi.
Il nostro demo è composto da tre sorgenti: un main() e due utilizzatori del Alarm Manager: è una scelta reale che rispecchia la filosofia del progetto: un utilizzatore (che può essere un client, un server, un processore di dati, ecc.) si registra al Alarm Manager montando i propri allarmi (questo meccanismo l'avevamo già visto col Watchdog, ricordate?), e può anche smontarli prima di uscire (usando la funzione di umount, che è una di quelle commentate). L'operazione di mount inserisce gli allarmi nelle posizioni opportune della tabella interna, e può gestire anche "gli stessi allarmi" più volte: ad esempio una applicazione che contiene due istanze dello stesso tipo di utilizzatore, avrà due gruppi identici di allarmi montati che saranno differenziati usando i campi instance e offset (che in questa versione light sono commentati, occhio!).
Bando alle ciance, vai col main()!
#include "alarmer.h" #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> // prototipi esterni void *tcpServer(void *arg); void *udpServer(void *arg); // funzione main() int main(int argc, char* argv[]) { int error; // init del alarmer Alarmer alarmer; if (setAlarmer(&alarmer) != 0) { printf("%s: non posso usare l'alarmer\n", argv[0]); return EXIT_FAILURE; } // avvio tcpServer pthread_t tid_A; if ((error = pthread_create(&tid_A, NULL, &tcpServer, (void *)&alarmer)) != 0) { printf("%s: non posso creare il tcpServer (%s)\n", argv[0], strerror(error)); return EXIT_FAILURE; } // avvio udpServer pthread_t tid_B; if ((error = pthread_create(&tid_B, NULL, &udpServer, (void *)&alarmer)) != 0) { printf("%s: non posso creare il tcpServer (%s)\n", argv[0], strerror(error)); return EXIT_FAILURE; } // loop infinito del main for (;;) { char buf[4096]; // mostro gli allarmi attivi getAlarms(&alarmer, buf, sizeof(buf)); if (strlen(buf)) { printf("ALLARMI\n"); printf("%s %2s %-80s %-20s\n", "ID", "Pr.", "Allarme", "Attivato il"); printf("%s\n", buf); } // mostro gli allarmi storici getHistAlarms(&alarmer, buf, sizeof(buf)); if (strlen(buf)) { printf("ALLARMI STORICI\n"); printf("%s %2s %-80s %-20s %s\n", "ID", "Pr.", "Allarme", "Attivato il", "Disattivato il"); printf("%s\n", buf); } // sleep del loop sleep(5); } // attesa terminazione tcpServer if ((error = pthread_join(tid_A, NULL)) != 0) { printf("%s: non posso unire il tcpServer (%s)\n", argv[0], strerror(error)); return EXIT_FAILURE; } // attesa terminazione udpServer if ((error = pthread_join(tid_B, NULL)) != 0) { printf("%s: non posso unire il udpServer (%s)\n", argv[0], strerror(error)); return EXIT_FAILURE; } // esce con Ok return EXIT_SUCCESS; }Come avrete notato il main() è un semplice programma multithread, simile a quello visto nell'articolo sul Watchdog. La struttura della nostra applicazione è modulare, ed ogni modulo è un thread del main(). In questo demo abbiamo due moduli, che ho chiamato, tanto per fare un esempio, tcpServer e udpServer. Riepilogando: abbiamo un Alarm Manager, instanziato dal main(), che viene passato ai moduli, che possono essere anche molti, e ogni modulo monterà i propri allarmi nell'Alarm Manager.
E adesso e`il momento di mostrare il codice del tcpServer che, essendo un esempio, di server TCP ha solo il nome, mentre è, in realtà, solo un loop di attivazione/disattivazione allarmi per mostrare visualmente il funzionamento. Vi aggiungo che il codice dell'udpServer è inutile mostrarlo: é praticamente identico all'altro modulo, con qualche nome diverso e con i testi degli allarmi cambiati per differenziarsi dal tcpServer (ricordarsi, però, di usare un nuovo owner: se il tcpServer ha owner=0 l'udpServer deve avere owner=1). Vai col codice!
#include "alarmer.h" #include <unistd.h> // owner degli allarmi #define O_TCPSERVER 0 // ID degli allarmi #define A_TCPSERVER_INDEX_ERR 0 #define A_TCPSERVER_WATCH_ERR 1 // alarm offset per addAlarm() //#define OFFS_A_TCPSERVER 2 // tabella allarmi tcpserver static const Alarm alarms_tcpsrv[] = { // id owner fmt { A_TCPSERVER_INDEX_ERR, O_TCPSERVER, "%s: indice non valido (%d)" }, { A_TCPSERVER_WATCH_ERR, O_TCPSERVER, "%s: non posso usare il watch (%d)" }, { -1, 0, "" }, }; // tcpServer() - un (finto) tcpServer da lanciare come thread del main() void *tcpServer(void *arg) { // ottengo i dati del thread con un cast (Alarmer *) di void *arg Alarmer *alarmer = (Alarmer *)arg; // monto gli allarmi mountAlarms(alarmer, alarms_tcpsrv); // lancio i due allarmi per il test onAlarm(alarmer, A_TCPSERVER_INDEX_ERR, O_TCPSERVER, __func__, 10); onAlarm(alarmer, A_TCPSERVER_WATCH_ERR, O_TCPSERVER, __func__, 20); // loop del thread for (int i = 0; ; i++) { // per il test il thread attiva (on) e disattiva (off) gli allarmi periodicamente if (i == 7) // ogni 7 secondi offAlarm(alarmer, A_TCPSERVER_WATCH_ERR, O_TCPSERVER); if (i == 14) { // ogni 14 secondi onAlarm(alarmer, A_TCPSERVER_WATCH_ERR, O_TCPSERVER, __func__, 30); i = 0; } // sleep del thread (1 secondo) sleep(1); } // il thread esce return NULL; }Ricetta finale: prendere il main e il tcpServer, aggiungere il modulo udpServer (ispirato al tcpServer), compilare bene (uhm... quando disponibile il codice dell'implementazione), aggiungere un pizzico di sale ed eseguire. Vedrete che ogni 7 secondi si mostreranno gli allarmi attivi e storici, in maniera dinamica. Tra l'altro gli allarmi storici (che hanno, nel demo, un buffer di 8) quando raggiungono il limite cominciano a "circolare", cioè i più vecchi vengono cancellati per far posto ai nuovi. Provare per credere.
E per oggi è tutto, nella seconda parte vi mostreró l'implementazione che è, secondo me, abbastanza interessante. Non trattenete il respiro nell'attesa, mi raccomando...
Ciao, e al prossimo post!
Nessun commento:
Posta un commento