Realizzazione di un framework di monitoring indiretto per l`analisi di
Transcript
Realizzazione di un framework di monitoring indiretto per l`analisi di
Scuola Politecnica e delle Scienze di Base Corso di Laurea Magistrale in Ingegneria Informatica Tesi di Laurea Magistrale in Sistemi Real-Time Realizzazione di un framework di monitoring indiretto per l’analisi di sistemi critici Anno Accademico 2012/2013 relatore Ch.mo prof. Marcello Cinque correlatore Ch.mo ing. Raffaele Della Corte candidato Giuseppe Romeo matr. M63000268 “Where there is desire there is gonna be a flame Where there is a flame someone’s bound to get burned But just because it burns doesn’t mean you’re gonna die You’ve gotta get up and try try try. . . ” [Pink, Try, The Truth About Love, RCA Records, 2012] Indice Ringraziamenti 1 Introduzione 3 1 Il monitoring nei domini critici 6 1.1 L’attività di monitoring . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.1.1 Il monitoraggio diretto . . . . . . . . . . . . . . . . . . . . . . . 8 1.1.2 Il monitoraggio indiretto . . . . . . . . . . . . . . . . . . . . . . 10 1.1.3 Diagnosi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.2 Stato dell’arte dei framework di monitoring . . . . . . . . . . . . . . . . 13 1.3 Introduzione ai sistemi critici . . . . . . . . . . . . . . . . . . . . . . . 16 1.3.1 Classificazione dei sistemi critici . . . . . . . . . . . . . . . . . . 17 1.3.2 Mission-critical computing . . . . . . . . . . . . . . . . . . . . . 18 2 Un caso reale: la piattaforma Miniminds 2.1 2.2 20 Un dominio mission-critical: Air Traffic Control . . . . . . . . . . . . . 20 2.1.1 Storia del controllo del traffico aereo . . . . . . . . . . . . . . . 21 2.1.2 Concetti chiave del controllo del traffico aereo . . . . . . . . . . 23 2.1.3 Categorie del controllo del traffico aereo . . . . . . . . . . . . . 26 La piattaforma Miniminds . . . . . . . . . . . . . . . . . . . . . . . . 28 i Indice 2.2.1 Perché Miniminds . . . . . . . . . . . . . . . . . . . . . . . . . 28 2.2.2 Il ruolo del middleware . . . . . . . . . . . . . . . . . . . . . . . 30 2.2.3 L’architettura della piattaforma . . . . . . . . . . . . . . . . . . 31 3 Il framework di monitoring indiretto 3.1 3.2 37 Analisi dei requisiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.1.1 Requisiti funzionali . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.1.2 Requisiti non funzionali . . . . . . . . . . . . . . . . . . . . . . 39 Architettura del framework . . . . . . . . . . . . . . . . . . . . . . . . 41 3.2.1 Il file di configurazione . . . . . . . . . . . . . . . . . . . . . . . 42 3.2.2 L’applicativo demone . . . . . . . . . . . . . . . . . . . . . . . . 45 4 Panoramica delle risorse utilizzate dal framework 47 4.1 User Space vs Kernel Space . . . . . . . . . . . . . . . . . . . . . . . . 48 4.2 Loadable Kernel Modules (LKM) . . . . . . . . . . . . . . . . . . . . . 52 4.2.1 Compilazione di un LKM 56 4.2.2 Inserimento di un LKM nel kernel 4.3 4.4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Il proc file system (procfs) . . . . . . . . . . . . . . . . . . . . . . . . . 59 4.3.1 Creazione di una entry del procfs tramite LKM . . . . . . . . . 61 4.3.2 La API seq_file . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 La gestione dei processi in Linux . . . . . . . . . . . . . . . . . . . . . 65 4.4.1 La creazione dei processi . . . . . . . . . . . . . . . . . . . . . . 65 4.4.2 La task list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 4.4.3 La task_struct . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 5 Implementazione del framework 5.1 Implementazione del demone . . . . . . . . . . . . . . . . . . . . . . . . 71 71 ii Indice 5.2 5.1.1 Parsing del file di configurazione . . . . . . . . . . . . . . . . . . 72 5.1.2 Parsing delle informazioni generate dal modulo . . . . . . . . . 75 Implementazione del modulo kernel . . . . . . . . . . . . . . . . . . . . 77 5.2.1 Gestione delle strutture dati nel kernel . . . . . . . . . . . . . . 81 5.2.2 Statistiche delle interfacce di rete . . . . . . . . . . . . . . . . . 82 5.2.3 LoadAVG: il carico di sistema . . . . . . . . . . . . . . . . . . . 84 5.2.4 Statistiche con granularità di processo . . . . . . . . . . . . . . 86 5.2.4.1 Percentuale di utilizzo di CPU . . . . . . . . . . . . . 87 5.2.4.2 La memoria virtuale . . . . . . . . . . . . . . . . . . . 93 5.2.4.3 File descriptor aperti e socket . . . . . . . . . . . . . . 96 6 Risultati sperimentali 6.1 6.2 99 Scenario d’uso: il dimostratore Miniminds . . . . . . . . . . . . . . . . 100 6.1.1 Descrizione dei test di iniezione . . . . . . . . . . . . . . . . . . 101 6.1.2 Profilo comportamentale della golden run . . . . . . . . . . . . . 102 6.1.3 Osservazione di un hang attivo . . . . . . . . . . . . . . . . . . 117 6.1.4 Osservazione di crash ed hang passivo . . . . . . . . . . . . . . . 120 Descrizione dei test di benchmark . . . . . . . . . . . . . . . . . . . . . 123 6.2.1 Pianificazione degli esperimenti . . . . . . . . . . . . . . . . . . 124 6.2.2 Benchmark di CPU . . . . . . . . . . . . . . . . . . . . . . . . . 126 6.2.3 Benchmark di memoria . . . . . . . . . . . . . . . . . . . . . . . 136 Conclusioni e sviluppi futuri 145 Bibliografia 148 iii Ringraziamenti I ringraziamenti: la prima cosa che si legge, ma sempre l’ultima che si finisce per scrivere! Per il tempo che sembra non bastare mai, per le cose da fare che sembrano moltiplicarsi davanti agli occhi... e anche un po’ per scaramanzia! Il primo Grazie da lasciare nero su bianco, su un ricordo indelebile come la tesi di laurea, va sicuramente alla mia famiglia. A mia madre Carla ed a mio padre Giovanni che chiaramente ringrazio per avermi dato la possibilità di studiare, ma anche, e soprattutto, per avermi supportato e, ahi loro, sopportato, durante questi 6 anni di università, per tutte le mie ansie pre-esame, per il mio carattere, per tutte le mie fissazioni assurde e chi più ne ha più ne metta! A mio fratello Francesco, che non ha ancora iniziato l’università ma “grazie a me” ne ha già subito gli effetti collaterali. A lui che nemmeno 3 settimane fa si è diplomato... eppure è stato costretto a conciliare il suo stress con il mio! A mia nonna Emilia, che forse è colei che si preoccupa di più delle mie condizioni di viaggio nei mezzi pubblici, e tutte le sere si accerta che sia tornato a casa sano e salvo! Agli altri miei nonni, Alfonso, Giuseppe e Cira che mi guarderanno da lassù. Un secondo Grazie va agli amici. Agli amici al di fuori dell’ambito accademico ed a coloro che hanno fatto veramente parte del mio percorso universitario non limitandosi ad occupare per qualche ora al giorno una sedia nella mia stessa aula. Dicono che gli amici si scelgono, ma spesso per quanta buona volontà si possa mettere nei rapporti, 1 Ringraziamenti le amicizie vanno e vengono, e a tante persone non interessa che tu le voglia nella tua vita. A valle di quanto appena detto, il primo grazie va a Stefano (forse l’unico che capirà perché le parole precedenti siano in corsivo!), una fra la rare amicizie sincere che si riescono a stringere tra i banchi universitari, che nei 6 anni da cui lo conosco non ha mai rifiutato una mia richiesta né una pausa caffè, e non esagero se dico che mi ha fatto da amico tanto quanto da docente aggiunto! A Martina, che fa parte della mia vita fin dal liceo, che sa bene quanto sia stata importante per me, e che ormai è tanto, troppo, lontana! Ad Andy, grande... anzi piccola amica, per essersi sorbita tutte le mie lamentele su ogni cosa e avere sempre una parola di conforto! A Susy, che conosco da poco più di un anno ma che consiglio dopo consiglio ha saputo diventare una grande amica! A Ferdinando, anzi a Eddinando, che forse prima o poi riuscirà a trascinarmi su un go-kart! Ai ragazzi che conosco dai primi giorni ad Agnano, dalle mitiche lezioni di Fusco: Ernesto, Pacioccone, Alessio, Giuliano, Chiara e Dave! Ai compagni di progetto del primo esame della magistrale, Danilo, Laura e Ciccio (honoris causa)! A Fabio e Agostino, che forse più che un ringraziamento preferirebbero una citazione nella bibliografia! A tutti coloro che, fra una pausa e l’altra, fra un caffè e 10 minuti di fumo passivo, hanno contribuito a rendere meno faticoso lo studio! Un Grazie va anche ai miei relatori. Al prof. Marcello Cinque, per il contributo tecnico e formativo ricevuto durante la stesura della tesi, ed all’ing. Raffaele Della Corte per la grande disponibilità mostrata, correzione dopo correzione, in questi ultimi mesi. In ultimo non un ringraziamento ma un pensiero anche per me. Ed al momento quello che si avvicina di più alle mie speranze dovrei inserirlo come citazione, perché voglio usare una considerazione di un amico! “Tu sei in dirittura di arrivo... significa che il bello deve ancora venire. Dovrebbe spaventare ed eccitare allo stesso tempo...”. Bene, adesso che è finita vediamo un po’ dove si nasconde questo bello! 2 Introduzione L’informatizzazione è ormai pervasiva nella società moderna. I sistemi informatici sono alla base della stragrande maggioranza delle attività umane. Essi sono integrati talmente a fondo nella nostra quotidianità che è ormai impensabile, se non impossibile, immaginare di non poter usufruire di alcuni servizi cui siamo abituati grazie ad essi. Un sistema informatico è un prodotto di hardware e software, e va impiegato senza dimenticare la sua natura. Per meglio comprendere il senso di questa affermazione ricordiamo i concetti di hardware e software. Viene classificata come hardware tutta la parte di componentistica elettronica di un computer, che, in quanto tale, è soggetta a tutti i malfunzionamenti di natura elettrica. Pensiamo ad esempio a malfunzionamenti di natura transitoria come un sovraccarico della rete elettrica che provoca sbalzi di tensione, a malfunzionamenti dovuti all’eccessivo stress dei componenti, ma anche ad eventi che rendono i malfunzionamenti permanenti, come ad esempio un corto circuito. Analogamente, anche la parte software non è pienamente affidabile. Specie nel caso di sistemi complessi, infatti, non è nulla la probabilità di avere bug nel codice che possono portare a fallimenti più o meno gravi. Questo perché il processo di testing nel ciclo di sviluppo di un prodotto software, che mira all’individuazione dei difetti nel codice, non è esaustivo. Ciò vuol dire che il non individuare difetti durante la fase di testing non implica la loro assenza. Come afferma la tesi di Dijkstra: “Il testing non può dimostrare 3 Introduzione l’assenza di difetti, ma solo la loro presenza! ”[10]. Richiamare alla memoria questi concetti di base serve a rendersi conto che un sistema informatico non è infallibile. Ciò è ancora più importante quando il sistema informatico si trova a dover operare in domini critici. Si parla di dominio critico quando un fallimento del sistema si ripercuote in maniera più o meno grave sugli obbiettivi del sistema, sulle cose, sull’ambiente e, nel caso più grave, sulla vita umana. In base a questa classificazione, come verrà approfondito nei successivi capitoli, non è raro sentir parlare di domini mission-critical o safety-critical. Tipici esempi di domini critici si hanno in campo medico, energetico, dei trasporti o anche, come nel caso oggetto di questa tesi, nel settore aeronautico. In questi particolari domini, in cui è fondamentale che il sistema si comporti come atteso, è necessario mantenere un monitoraggio continuo del sistema in modo da controllare che non si verifichino situazioni anomale. Assume quindi una certa rilevanza l’affidabilità del sistema. Questa si ottiene in diversi modi, applicati nelle diverse fasi del ciclo di sviluppo e nella messa in esercizio. Nello sviluppo di sistemi critici è quindi di fondamentale importanza una fase di testing quanto più accurata possibile. Come già detto però, è dimostrata la non esaustività di quest’ultima. Si rende quindi necessario anche lo studio di tecniche volte a misurare il grado di affidabilità di un sistema, e, quindi, occorre un meccanismo che consente di risalire alle cause dei fallimenti. A questo scopo è stato introdotto l’uso dei file di log, dei quali fanno largamente uso le tecniche di monitoraggio diretto. Spesso però i log risultano essere poco accurati, e ciò è dovuto sia all’usanza frequente di demandare la scrittura del codice relativo ai log alle fasi finali del ciclo di sviluppo, ma anche all’assenza di un approccio standard per l’implementazione dei meccanismi di logging, il che può portare all’incapacità dei log stessi di riportare situazioni di fallimento in cui il sistema può incorrere. 4 Introduzione Per questo ed altri motivi, come anche la non sempre disponibilità del codice sorgente del software e quindi alla possibilità di instrumentarlo con le istruzioni che occorrono, spesso è necessario ricorrere a tecniche di monitoraggio indiretto. Attraverso il monitoraggio indiretto, infatti, è possibile ottenere dei profili di comportamento del sistema senza conoscerne i dettagli implementativi, ed attraverso questi profili riuscire a riconoscere dei comportamenti anomali del sistema. L’oggetto di questa tesi è la progettazione e lo sviluppo di un framework di monitoraggio indiretto per l’acquisizione in tempo reale di informazioni di natura prestazionale da infrastrutture informatiche critiche. In particolare il presente elaborato di tesi si colloca nell’ambito del progetto Miniminds, e fa riferimento a sistemi appartenenti al dominio del controllo del traffico aereo. La struttura dell’elaborato di tesi sarà articolata in 6 capitoli. Il primo capitolo sarà introduttivo dei concetti di monitoraggio e di sistema critico, con un focus maggiore sui sistemi mission-critical. Il secondo capitolo introdurrà un caso di studio. Nello specifico la prima parte del capitolo introduce i concetti relativi ad un dominio critico, cioè quello del controllo del traffico aereo (ATC). La seconda parte del capitolo sarà invece descrittiva del progetto Miniminds. Nel terzo capitolo verrà fornita una visione ad alto livello del framework realizzato, descrivendo la sua architettura in relazione alle specifiche fornite in fase di progetto. Il capitolo 4 servirà a fornire una panoramica dei servizi, a disposizione su un sistema operativo basato su kernel Linux, sui quali si fonda il funzionamento del framework. Nel quinto capitolo verranno illustrati i dettagli implementativi del framework realizzato. Nel sesto ed ultimo capitolo si verifica l’effettiva capacità del tool di rilevare situazioni di anomalia facendo uso del dimostratore del progetto Miniminds, e, successivamente, si valuta l’impatto che l’introduzione dell’infrastruttura di monitoraggio ha sul sistema attraverso dei benchmark. 5 Capitolo 1 Il monitoring nei domini critici 1.1 L’attività di monitoring Il monitoring di un sistema consiste nel raccogliere informazioni relative al suo funzionamento allo scopo di verificarne la conformità rispetto al comportamento atteso. La necessità di monitorare i sistemi, nello specifico i sistemi informatici, nasce contestualmente al loro impiego in luogo degli operatori umani, con l’ovvia esigenza di verificare il corretto svolgimento delle mansioni loro assegnate. Oggigiorno la verifica del corretto funzionamento di un sistema informatico è ancor più sentita a causa del loro impiego nell’ambito di domini critici, dei quali si parlerà più approfonditamente in una successiva sezione. Monitorare sistemi, software applicativi e processi è di primaria importanza anche per attività di debugging, per una gestione corretta ed efficiente delle risorse e per poter fornire azioni di ripristino. In un sistema i principali responsabili del consumo di risorse sono sostanzialmente i processi, e pertanto buona parte del monitoraggio deve avere granularità di processo. Tipicamente un’infrastruttura di monitoraggio è composta da 4 strati: 6 Il monitoring nei domini critici 1. Sonda per la raccolta dei dati di interesse. L’attività di raccolta può essere scatenata dal verificarsi di un evento o di una condizione, oppure può avvenire on-demand, ossia su richiesta. 2. Trasformazione dei dati raccolti. Compito di questo strato è la manipolazione dei dati grezzi provenienti dal livello sottostante. Rientrano in questa fase la correzione di dati corrotti, il filtraggio e l’aggregazione, al fine di non saturare le risorse dello strato che dovrà poi effettuare la detection vera e propria. L’output prodotto saranno dei dati processati che possono essere gestiti più facilmente dal livello superiore. 3. Comunicazione dei dati processati al livello di rilevamento. Consiste in un insieme di protocolli usati nella trasmissione. Se il componente che implementa la logica di detection si trova su un altro nodo di rete solitamente viene utilizzata una rete dedicata per la trasmissione dei dati di monitoring, in modo da non inficiare le prestazioni delle applicazioni in esecuzione sul sistema controllato, saturando la banda delle connessioni. 4. Detection delle anomalie, comprendente l’implementazione della logica di controllo dei dati processati. A seconda del quando le informazioni raccolte dall’infrastruttura vengano effettivamente utilizzate, il monitoraggio può essere distinto in offline oppure online. Nel primo caso i dati raccolti vengono analizzati successivamente alla raccolta (post processing) e poi sfruttati per un’eventuale correzione del sistema a posteriori. Gli utilizzi del monitoraggio offline non sono limitati soltanto allo scopo correttivo, ma risultano utili per valutare l’impatto di interventi migliorativi sul sistema, permettendo di determinare il 7 Il monitoring nei domini critici rapporto costi/benefici. Possono anche essere usati per ottenere una predizione di tali miglioramenti basandosi su stime storiche. Nel monitoraggio di tipo online, l’attività di verifica deve avvenire durante la fase di esercizio del sistema, in modo tale da poter intervenire tempestivamente con azioni di ripristino, in caso di effettivo riscontro di anomalie. Ciò è fondamentale nell’ottica di evitare, con un’azione preventiva, situazioni di fallimento del sistema monitorato che in alcuni domini applicativi, come il dominio ATC (descritto in sezione 2.1) che rappresenterà un caso reale nell’ambito di questo lavoro di tesi, possono avere conseguenze anche catastrofiche. Uno dei principali problemi nell’ambito del monitoring, sia online che offline, è la necessità di ridurre al minimo l’impatto che il sistema di monitoraggio ha sulle prestazioni del sistema da monitorare, in quanto il sistema monitorato sarà dato da: sistema monitorato = sistema reale + monitor In più il monitoraggio offline presenta l’ulteriore problema della persistenza dei dati collezionati. In base alle modalità di raccolta dei dati, è possibile distinguere il monitoring in: monitoring diretto e monitoring indiretto. 1.1.1 Il monitoraggio diretto Il monitoring diretto prevede che le informazioni siano fornite direttamente dal componente monitorato. Nello specifico questo approccio considera il sistema da monitorare come un sistema white-box, e quindi con accesso al codice sorgente. Tale approccio, infatti, consiste nell’instrumentare il codice sorgente con delle istruzioni dedicate alla raccolta di informazioni che descrivano il comportamento del software che si intende monitorare. 8 Il monitoring nei domini critici Figura 1.1: Approcci push e pull based Come schematizzato in figura 1.1, in base a chi prende l’iniziativa nella comunicazione, il monitoring diretto può essere push o pull based. Nel primo caso è il componente monitorato che, ad intervalli regolari od in base al verificarsi di una condizione, invia messaggi al monitor. Ad esempio gli heartbeat sono un tipico esempio di messaggio che l’applicazione può inviare periodicamente al monitor, viceversa nel caso event-driven un esempio possono essere i messaggi generati dall’applicazione per segnalare l’ingresso e l’uscita da un loop. Viceversa, nel monitoring con protocollo pull based, è il monitor che sollecita il componente a fornirgli informazioni. La scelta di quale strategia adottare dipende dai requisiti di monitoraggio. Ad esempio, se lo stato del componente deve essere noto soltanto in instanti temporali specifici, risulta di maggior efficacia un approccio di tipo pull based, in modo da non dover caricare il sistema con l’overhead dovuto alla produzione di informazioni non richieste dalle specifiche. Tuttavia, se è necessario conoscere lo stato del sistema in momenti particolari 9 Il monitoring nei domini critici della sua esecuzione (non corrispondenti quindi ad istanti precisi), può rivelarsi utile adottare un approccio push based nella sua versione event-driven. Si pensi al caso in cui la specifica richieda soltanto l’accertamento che il componente non rimanga bloccato in una condizione di loop. In uno scenario del genere, utilizzando l’approccio push eventdriven, in luogo di un approccio pull, si avrebbe un vantaggio in termini di messaggi scambiati, che sarebbero ridotti del 50% non dovendo utilizzare messaggi per sollecitare il componente a fornire le informazioni, come richiede l’approccio pull. Risulta preferibile l’approccio push based, proprio per il dimezzamento della mole di messaggi, se il monitoraggio deve essere continuo, come nel caso dei sistemi missioncritical o safety-critical che saranno descritti nella sezione 1.3. Uno svantaggio del monitoring diretto deriva proprio dal suo considerare il sistema come una white-box. Infatti, dovendo instrumentare il codice sorgente con delle istruzioni sonda, l’immissione di queste ultime risulta evidentemente differente da caso a caso, dovendo essere ottimizzate ad hoc per il sistema oggetto del monitoraggio. Inoltre risulta evidente che il problema di fondo è avere a disposizione il codice sorgente, il quale per alcuni sistemi potrebbe essere non disponibile. È il caso ad esempio di molti sistemi critici, che spesso fanno uso di componenti OTS1 . 1.1.2 Il monitoraggio indiretto Il monitoring indiretto prevede che le informazioni siano ottenute in maniera trasparente rispetto al componente monitorato. Diversamente dall’approccio diretto, il sistema da monitorare viene considerato come una black-box e, dunque, non si ha accesso ai dettagli implementativi. Pertanto le informazioni ottenibili sono di natura differente. 1 (Commercial) Off-the-Shelf, si riferisce a componenti realizzati da terze parti ed acquistati da aziende interessate a utilizzarli nei loro progetti. 10 Il monitoring nei domini critici Alcuni framework di monitoraggio indiretto “sniffano” i pacchetti di rete in transito da e verso il sistema, quindi ottengono informazioni attraverso l’analisi della comunicazione del sistema con l’esterno. Altri usano strategie di più basso livello, facendo affidamento su strumenti di tracing a livello di sistema operativo per risalire alle chiamate di sistema (system call ). Quest’ultimo approccio richiede un overhead non trascurabile poiché utilizza funzioni built-in del sistema operativo, e quindi le prestazioni ne saranno naturalmente affette, motivo per cui non si presta molto ad un uso per monitoraggio online. Da quanto detto finora è facile intuire che questo approccio, seppur con le sue limitazioni, è l’unico applicabile quando si ha a che fare con vincoli, parziali o totali, di accesso al codice sorgente. Partendo unicamente dai parametri prestazionali non si possono fare affermazioni certe sull’effettivo comportamento del sistema, tuttavia è possibile derivarne dei profili comportamentali che possono aiutare a dedurre condizioni di funzionamento anomale. Un limite di questa strategia si verifica nell’ambito del monitoraggio online. Le anomalie deducibili dal profilo prestazionale tracciato sono, di fatto, rilevabili solo ad avvenuta propagazione della reale anomalia nel sistema. Pertanto con l’approccio indiretto si può intervenire solo in maniera correttiva piuttosto che preventiva. Inoltre, il tracciamento del profilo prestazionale richiede tempi non trascurabili per raggiungere un buon livello di dettaglio. Tuttavia all’incremento del livello di dettaglio si accompagna anche un aumento della complessità del modello, e quindi delle risorse necessarie alla sua risoluzione. Conseguenza di tutto ciò è che, nei sistemi con requisiti temporali stringenti per la detection, il livello di complessità che è possibile gestire non è molto elevato determinando quindi una bassa accuratezza. Per un utilizzo con monitoraggio offline, il poter disporre di tempo e risorse dedicate permette di adottare anche modelli con complessità superiore. Ovviamente ciò comporta la perdita della possibilità 11 Il monitoring nei domini critici di agire in maniera preventiva in ambito di sistemi critici, derivante dall’applicazione del monitoraggio online. Un’ulteriore tecnica di monitoring è quella che prevede l’utilizzo di monitoring diretto ed indiretto in maniera combinata per incrementare la quantità di informazioni sul sistema. In questo modo si possono incrociare i dati in modo che i due approcci si validino l’uno con l’altro. Nell’ambito dello svolgimento di questa tesi, le informazioni raccolte saranno di natura prestazionale. 1.1.3 Diagnosi L’ultimo strato dell’infrastruttura di monitoraggio, cioè quello adibito all’effettiva rilevazione delle anomalie, per poter funzionare, richiede la conoscenza del comportamento atteso del sistema. Tale conoscenza è acquisibile in diversi modi, ad esempio a partire da un modello che caratterizza il sistema, oppure ricavato in maniera deduttiva a partire dai dati storici collezionati dal monitor, oppure, ancora, ricavato durante la fase di testing del sistema. L’approccio utilizzato più di frequente è quello che fa uso di un modello, che di fatto è una rappresentazione sintetica del sistema. La caratterizzazione sintetica del comportamento del sistema e delle sue funzioni permette di correlare eventi di interesse al fine di poter reagire appropriatamente in caso di failure. La creazione di un modello richiede comunque una certa conoscenza del sistema che si sta andando a modellare, pertanto quando la conoscenza del sistema non è sufficiente conviene usare la strategia data-driven, la quale prevedere di dedurre il comportamento “normale” del sistema dall’analisi di dati storici. 12 Il monitoring nei domini critici 1.2 Stato dell’arte dei framework di monitoring I lavori accademici in ambito di monitoring online per la rilevazione di anomalie sono sicuramente molto numerosi. In questa sezione viene fornita una classificazione degli approcci ed una panoramica di alcuni framework di monitoraggio, allo scopo di identificare pregi e limitazioni delle soluzioni attualmente esistenti. La tipologia del sistema target del monitoraggio, nonché la conoscenza disponibile riguardo il suo funzionamento, determina dei vincoli da considerare quando si progetta un’infrastruttura di monitoraggio. Questi vincoli riguardano principalmente: + Overhead introdotto dall’infrastruttura, dovuto al lavoro extra richiesto al sistema; + Intrusività della soluzione proposta, in termini di modifiche a cui sottoporre i componenti monitorati; + Granularità del monitoraggio, intesa come livello di dettaglio delle informazioni rilevate. Solitamente più fine è la grana più overhead viene introdotto dal framework. Spesso sistemi software complessi vengono equipaggiati con tool di monitoraggio basati sulla conoscenza interna dei componenti monitorati, in modo da offrire caratteristiche di detection avanzate e una granularità di monitoraggio molto fine. A dispetto del loro potenziale, tuttavia, il tuning manuale che spesso richiedono queste infrastrutture influisce negativamente sul loro reale utilizzo. Una tendenza crescente è quindi basata sull’utilizzo di framework di monitoraggio indiretto, che possono quindi offrire informazioni statistiche indipendentemente dal componente monitorato. Esistono infatti molti tool di monitoring indiretto disponibili, alcuni di questi saranno brevemente descritti di seguito. 13 Il monitoring nei domini critici SysStat è un framework di monitoraggio per Linux che colleziona dati di I/O, CPU, memoria e interrupt a livello di sistema. Tutto ciò è realizzato semplicemente integrando le più utilizzate utility per il monitoraggio sotto Linux, come sar o iostat. Linux Trace Toolkit (LTT) è stato il primo tentativo di fornire un monitoraggio a grana fine capace di tener traccia di eventi per singolo processo, come ad esempio scheduling e interrupt. LTT risulta utile per analisi offline e l’overhead introdotto è inferiore al 2%. Una sua limitazione è che il tracing non possiede un meccanismo di probing timer-based, ossia un meccanismo di produzione di messaggi allo scadere di un timeout. Il progetto è open source, ma il meccanismo di detection di anomalie non è ancora stato implementato. Fra le strategie di monitoraggio indiretto per la rilevazione di anomalie, possono essere adottate anche tecniche di tracciamento dei function boundary, ossia di tracciamento degli ingressi e delle uscite dalle funzioni. A tale scopo è necessaria una fase di code instrumentation, ossia l’inserimento automatico di istruzioni che permettono di monitorare uno o più parametri, tipicamente risorse limitate come la CPU o la memoria. Queste tecniche rivelano un’efficacia maggiore quando impiegate al fine di individuare colli di bottiglia o a scopo di reverse engineering, mentre più difficilmente consentono la rilevazione di anomalie più complesse. Tecniche basate sull’instrumentazione possono essere utilizzate anche per il kernel, tramite delle interfacce che esso stesso esporta e che consentono di instrumentare la quasi totalità delle funzioni del kernel. Queste interfacce sono chiamate Kprobes, e consentono di posizionare dei breakpoint all’interno kernel e di eseguire codice in risposta all’esecuzione dei suddetti breakpoint. SystemTap è un tool di instrumentazione dinamico che permette di eseguire script in un linguaggio C-like allo scopo di estrarre, filtrare e sintetizzare dati di interesse senza necessità di ricompilazione del kernel. Ciò facilita la diagnosi di un vasto range 14 Il monitoring nei domini critici di problemi, anche complessi. Rispetto ad LTT può monitorare potenzialmente milioni di eventi. Esso può essere utilizzato quindi per monitoraggio di tipo on-line, tuttavia l’overhead introdotto non è trascurabile. Esso, infatti, prevede la trasformazione dello script in un modulo del kernel che utilizza le Kprobes, il modulo viene poi compilato ed inserito dinamicamente nel kernel. I risultati vengono infine copiati in user space per la visualizzazione. DTrace utilizza un tracing a livello kernel per monitorare eventi come syscall, creazione/terminazione di processi, operazioni su file ed I/O su disco. Esso consente di instrumentare dinamicamente processi sia in user space che in kernel space e non introduce alcun overhead legato ai messaggi di probe quando non è attivo. DTrace può essere utilizzato per avere una vista globale del sistema in esecuzione, per esempio per quanto riguarda la quantità di memoria utilizzata, il filesystem e le risorse di rete utilizzate dai processi attivi. Può fornire anche altre informazioni a grana più fine, come per esempio il log degli argomenti con cui una certa funzione viene chiamata o una lista di processi che hanno accesso ad uno specifico file. Analogamente al caso di LTT, il progetto è open source e non è ancora stato sviluppato un detector automatico basato su DTrace. L’overhead introdotto a causa della fase di instrumentazione non è però trascurabile. Un noto tool di monitoring è IBM Tivoli Monitoring [28], basato su una architettura di monitoraggio centralizzato che fornisce agli operatori un pannello di controllo centrale da cui poter osservare il sistema target e controllare quali dati di monitoraggio collezionare. Tale framework: + Monitora proattivamente le risorse di sistema per rilevare i problemi potenziali e rispondere automaticamente agli eventi. + Fornisce un’interfaccia di tipo browser, comune, flessibile e intuitiva. 15 Il monitoring nei domini critici + Consente una visualizzazione rapida degli incidenti e la vista della cronologia per accelerare la ricerca degli incidenti. + Inoltre questo tool fornisce un set di utilità, come ad esempio per la correlazione di eventi e il settaggio di soglie per i trigger alarm, per automatizzare la fase di detection. Tuttavia IBM Tivoli Monitoring non è open source ed è caratterizzato da costi di licenza abbastanza elevati. In base alle osservazioni sui tool descritti in precedenza, per questo lavoro di tesi si è optato per lo sviluppo ex-novo di un framework che riunisse in sé la maggior parte dei pregi dei tool descritti. Le caratteristiche saranno evidenziate più dettagliatamente nei capitoli a seguire. 1.3 Introduzione ai sistemi critici La vita di tutti i giorni si basa pesantemente sulla gestione intelligente di infrastrutture critiche su larga scala, come ad esempio i sistemi energetici o le reti di telecomunicazioni. La progettazione, il monitoraggio, il controllo e la sicurezza di questi sistemi è motivo di sfida sempre maggiore, grazie alla loro crescente dimensione, complessità ed interattività. In più queste infrastrutture sono soggette a disastri naturali, fallimenti frequenti così come ad attacchi di tipo malizioso. Da qui il bisogno di sviluppare un framework comune per modellare il comportamento di infrastrutture critiche e per progettare algoritmi di monitoraggio intelligente, controllo e sicurezza di questo tipo di sistemi. 16 Il monitoring nei domini critici 1.3.1 Classificazione dei sistemi critici Il termine critico è ormai utilizzato di frequente per descrivere servizi essenziali per le operazioni quotidiane. A titolo di esempio, se un’attività di un’impresa non può essere interrotta in nessuna circostanza senza danneggiare la produzione, allora, data la sua indispensabilità, questa è considerata un’attività critica. Possiamo dividere i sistemi critici in 3 grandi categorie: 1. Sistemi business-critical : un sistema appartiene a questa categoria se un suo fallimento comporta costi molto alti per l’azienda che utilizza tale sistema. Un esempio di sistema di questo tipo è il sistema bancario di customer accounting. 2. Sistemi mission-critical : un sistema appartiene a questa categoria se un suo fallimento provoca il fallimento di un’attività considerata l’obiettivo stesso del sistema. Un esempio sono i sistemi di navigazione dei veicoli spaziali. Un sistema mission-critical è quindi essenziale per la sopravvivenza dell’organizzazione stessa. 3. Sistemi safety-critical : un sistema appartiene a questa categoria se le conseguenze di un suo fallimento possono causare seri danni all’ambiente, danni all’uomo o, nel caso più grave, perdita della vita. Un esempio di sistemi che rientrano in questa categoria è il sistema ABS2 delle automobili. Un fallimento del software che gestisce l’ABS può renderlo inutile proprio quando ne avremmo più bisogno. Per questo motivo avere delle linee guida che definiscono i processi e gli obiettivi per la creazione di software che abbia la qualità come focus principale ha un valore enorme per gli sviluppatori di software di sistemi safety-critical. L’alto costo di un fallimento di un sistema critico implica che il suo sviluppo sia basato su tecniche e metodologie altamente affidabili. Di conseguenza questo tipo di sistemi è 2 Il sistema anti bloccaggio (ABS, Antilock Braking System) è un sistema di sicurezza per i freni che evita il bloccaggio delle ruote dei veicoli garantendone la guidabilità durante le frenate. 17 Il monitoring nei domini critici solitamente sviluppato attraverso tecniche non molto recenti, ma i cui punti di forza e le cui debolezze siano ormai note, e quindi di comprovata affidabilità, piuttosto che tecniche innovative, o comunque più recenti, che però potrebbero portare problemi ignoti a lungo termine perché non sono state oggetto di una esperienza pratica sufficientemente estesa. Si può quindi affermare che i sistemi critici siano, in un certo senso, di natura conservativa. 1.3.2 Mission-critical computing Storicamente il computing mission-critical è stato definito come l’accoppiamento di un computing sicuro, affidabile e scalabile, e dei processi di ambiente che supportano i processi e le attività del front office di un’organizzazione, ossia quelli che supportano direttamente sia gli utenti dell’organizzazione che i clienti [11]. Le attività sono missioncritical perché rappresentano il nucleo stesso dello scopo dell’organizzazione, ed un loro fallimento causa significativi danni sia finanziari che in termini di reputazione. In alcuni casi, come quelli di sistemi militari o governativi, un fallimento del sistema può avere impatto sulla sicurezza nazionale. Il computing mission-critical si evoluto attraverso 3 differenti epoche: l’epoca pre-Web (prima del 1995), l’epoca del Web (fra il 1995 ed il 2010) e l’era dell’IT consumerization 3 (dal 2010 in poi). Nella prima epoca tali sistemi erano sostanzialmente delle applicazioni transazionali (come ad esempio i sistemi di prenotazione aerea) utilizzate da un numero limitato di utenti ed erano accedute da terminali situati in posizioni sicure. I server su cui giravano queste applicazioni erano dei “silos” altamente sicuri, all’interno dei datacenter, dedicati esclusivamente alla loro esecuzione. 3 Consumerizzazione o meglio IT Consumerization, è il fenomeno in base al quale l’uso e lo stile delle tecnologie in ambiente lavorativo viene dettata, in sostanza, dall’evoluzione del profilo privato degli individui e dal loro utilizzo delle tecnologie personali. 18 Il monitoring nei domini critici Nell’era del Web l’insieme dei sistemi mission-critical comprendeva anche le applicazioni web ed il commercio elettronico, ed erano aperte ad un gran numero di utenti, con accesso affidabile e continuato. Oggigiorno il range di ciò che è considerato mission-critical si è notevolmente espanso, includendo applicazioni che vanno dal mobile computing alle applicazioni social, dai portali web ai sistemi finanziari. Un fattore che ridefinisce il concetto di mission-critical è l’aspettativa dei livelli di servizio per questi sistemi che è ormai cresciuta al punto che la tolleranza per un downtime, di qualunque natura, è minima. Basti pensare, anche se come esempio limite, che il costo per il downtime di un social network come Twitter può stimarsi in cifre esorbitanti dell’ordine di milioni di dollari[13]. L’impatto dei costi per mantenere questi livelli di servizio sono dovuti al fatto che oggigiorno un’organizzazione deve fornire alti livelli di sicurezza informatica contro cyber-attacchi, ed il corrispondente relativo monitoraggio su un numero crescente di applicazioni. Alcuni delle caratteristiche essenziali per un moderno sistema mission-critical sono: + Un esperienza d’uso scorrevole ed ininterrotta; + Assicurare agli utenti un accesso continuo attraverso molteplici dispositivi e/o canali; + Datacenter moderni che supportino i livelli di servizio richiesti; + Supporto per un alto volume di transazioni ed applicazioni: + Elevata sicurezza per gli utenti, le applicazioni e i datacenter. 19 Capitolo 2 Un caso reale: la piattaforma Miniminds Questo capitolo serve a contestualizzare il dominio applicativo nel quale andrà ad operare il framework sviluppato nell’ambito del lavoro di tesi. Nella prima sezione viene presentato il dominio mission-critical dell’Air Traffic Control, mentre nella seconda parte vengono illustrate le specifiche concernenti la piattaforma Miniminds. 2.1 Un dominio mission-critical: Air Traffic Control Il controllo del traffico aereo (ATC, Air Traffic Control ) è quell’insieme di regole ed organismi che contribuiscono a rendere sicuro e ordinato il flusso degli aeromobili sia al suolo che nei cieli di tutto il mondo [9]. Affinché un ente ATC possa fornire un corretto servizio di controllo, esso deve poter disporre delle informazioni sul movimento previsto di ogni volo, di ogni sua variazione e dell’aggiornamento in tempo reale della progressione di ciascun volo. Dalle informazioni ricevute l’organismo di controllo può 20 Un caso reale: la piattaforma Miniminds determinare le posizioni relative dei velivoli, ed in tal modo prevenire collisioni e mantenere il flusso del traffico ordinato e scorrevole. I vari enti ATC devono coordinarsi fra loro per poter coordinare le autorizzazioni emesse per le operazioni di pilotaggio. Un sistema ATC è composto da tre sottosistemi interagenti: 1. Sistema di sorveglianza: esso permette di determinare in tempo reale la posizione degli aeromobili sotto controllo. Sistemi di sorveglianza sono il radar primario e quello secondario, che opera solo in caso di aerei cooperanti e dotati di un apposito transponder 1 . Permettere di tracciare la posizione di un velivolo non è l’unica funzione svolta da un radar di controllo, esso deve infatti riportare informazioni anche sui fenomeni atmosferici riscontrati sulla rotta seguita. 2. Sistema di comunicazione: esso è responsabile dell’effettivo scambio di informazioni tra aeromobili e stazioni a terra. Consiste in un canale radiofonico VHF (Very High Frequency), nella banda da 118 a 136 MHz, condiviso fra tutti gli aeromobili. 3. Sistemi di navigazione: essi consentono ad un aeromobile di seguire una determinata traiettoria (rotta). Comprendono sottosistemi di navigazione autonoma, assistita e satellitare. 2.1.1 Storia del controllo del traffico aereo Alla fine del diciannovesimo secolo, agli albori della storia aeronautica, non vi era ancora l’esigenza di controllare e regolamentare il traffico aereo, semplicemente perché non era 1 Il transponder è un apparato elettronico, sviluppato in ambito militare, per consentire di distinguere gli aerei amici da quelli nemici, in grado di ricevere segnali radio da terra e di rispondere a sua volta con un segnale radio opportunamente codificato allo scopo di fornire determinate informazioni. Con l’introduzione dei transponder a bordo degli aerei, è stato affiancato al radar primario il radar secondario che ricevendo le informazioni dai transponder le sovrappone alle tracce dei radar primari identificandole [20]. 21 Un caso reale: la piattaforma Miniminds ancora possibile definirlo traffico. Infatti era possibile volare soltanto in condizioni meteo favorevoli e le velocità erano molto ridotte. Ciò permetteva ai piloti di poter volare "a vista", pertanto la sicurezza in volo era garantita attenendosi alle sole regole dell’aria, che normavano chi avesse la priorità e quali fossero le manovre da intraprendere quando due aerei rischiavano di entrare in collisione. Fu nel 1910, in seguito alla prima collisione aerea (avvenuta a Vienna), che il problema della regolamentazione del traffico aereo cominciò a suscitare interesse. Ma soltanto 9 anni dopo l’incidente, nel 1919, venne creata l’ICAN (Commissione Internazionale per la Navigazione Aerea) che standardizzò le regole dell’aviazione. Negli anni ’30, poi, fu introdotto il pilotaggio senza visibilità (PSV). Per la guida si utilizzava il cosiddetto radiogoniometro ad onde medie, che sfruttava il codice morse. Nonostante ciò il controllo del traffico aereo, inteso come separazione organizzata dei voli, rimase comunque una pratica approssimativa. Fu alla fine della seconda guerra mondiale, che nacque una chiara esigenza di standardizzazione. Nel 1946 quindi si costituì l’ICAO (Organizzazione Internazionale dell’Aviazione Civile), ratificata l’anno seguente dalla maggior parte dei paesi del mondo, che aveva lo scopo di stabilire regole internazionali a cui ogni aeromobile in volo doveva attenersi. Questo evento segna l’inizio dell’epoca moderna del trasporto e del controllo del traffico aereo [8]. Ogni stato deve avere due enti, uno che detta le norme ed uno che fornisce i servizi ATC. In Italia, il primo dei due è l’ENAC (Ente Nazionale per l’Aviazione Civile), mentre a fornire i servizi ATC ci sono l’ENAV (Ente Nazionale per l’Assistenza al Volo) e l’Aeronautica Militare Italiana, che operano in stretto coordinamento fra loro. 22 Un caso reale: la piattaforma Miniminds 2.1.2 Concetti chiave del controllo del traffico aereo In ambito di sistemi per il traffico aereo si distingue fra gestione del traffico aereo (ATM, Air Traffic Management) e controllo del traffico aereo (ATC, Air Traffic Control ). La distinzione sta nel fatto che il primo è un concetto più ampio che comprende anche la gestione degli spazi aerei e dei flussi di traffico. Per motivi di controllo del traffico lo spazio aereo è suddiviso in varie regioni, all’interno di ognuna delle quali viene garantito un servizio per il traffico (ATS, Air Traffic Service). In Italia è suddiviso in 4 regioni (FIR, Flight Information Region) che sono: Roma, Milano, Brindisi e Padova. All’interno della singola area ci possono essere spazi aerei controllati, in cui è la stazione di controllo che assicura il mantenimento delle distanze reciproche fra i velivoli, e spazi aerei non controllati in cui si fornisce solo il servizio di assistenza al volo. I servizi ATS comprendono sia l’ATC, sia il servizio di informazioni volo FIS (Flight Information Service), nonché il servizio di allarme, che consiste nelle operazioni di ricerca e salvataggio in caso di necessità. Le norme internazionali distinguono gli spazi aerei in classi denominate con le lettere dalla A alla G. Ad ogni classe appartengono determinati livelli di servizio: + Gli spazi aerei delle classi da A ad E prevedono il servizio ATC, ossia di spazio aereo controllato; + Gli spazi aerei delle classi F e G non prevedono il servizio ATC, ma solo il servizio FIS; + Negli spazi aerei delle classi da A a D è obbligatorio l’uso del trasponder del radar secondario di emergenza, sia per voli a vista che strumentali. I concetti chiave del controllo del traffico aereo possono essere riassunti nei seguenti: 23 Un caso reale: la piattaforma Miniminds + Condizioni meteorologiche: inizialmente, come già detto, non era possibile pilotare un aereo senza guardare fuori dal velivolo, ed infatti si volava solo in condizioni meteorologiche favorevoli. Ancora oggi per poter volare in condizioni meteo non ottimali è necessario regolarizzare in ogni momento il comportamento dell’aereo in relazione agli elementi esterni. + Regime di volo: un aeromobile può volare secondo due tipi di regole distinte: il volo a vista (VFR, Visual Fly Rules) e il volo strumentale (IFR, Instrumental Fly Rules). La combinazione di queste due regole, unitamente ad altri principi generali, costituiscono le regole dell’aria. Nel volo VFR si seguono quindi regole a vista, analoghe alle rispettive regole seguite a terra e in mare. Il volo a vista può essere effettuato esclusivamente in condizioni meteo di visibilità, in particolare in un range orario che va da mezzora prima dell’alba a mezzora dopo il tramonto. Inoltre non può essere effettuato sopra i 19500 piedi di altitudine, né nello spazio aereo di classe A. Quando un aereo non si trova in condizioni visibilità tali da poter volare a vista deve per forza praticare il volo strumentale. Sono altresì obbligatorie le regole di volo strumentale per qualunque tipo di trasporto pubblico. Il volo IFR deve effettuarsi sulla base di un contratto, detto piano di volo, depositato presso gli organismi di controllo e da essi approvato. Il piano di volo specifica l’itinerario seguito e l’orario stimato per il passaggio su ogni punto significativo. Con il volo strumentale si opera sotto il controllo delle stazioni da terra, le quali sono responsabili di garantire la sicurezza e la fluidità del traffico, evitare collisioni, fornire assistenza e informazioni utili in volo. Per evitare collisioni, fra 24 Un caso reale: la piattaforma Miniminds gli aeromobili si mantengono distanze reciproche, misurate in miglia nautiche 2 , sia orizzontalmente che verticalmente. In un volo IFR sotto copertura radar le separazioni orizzontali variano in funzione della lontananza dell’aeromobile dal radar, dalla presenza di ridondanza di copertura nonché da norme nazionali. In genere le distanze reciproche variano fra le 3 e le 5 miglia nautiche, a seconda che si tratti di un aereo pesante (circa 130 tonnellate) preceduto da uno leggero (fino a 7 tonnellate), o di uno leggero preceduto da uno pesante. Le separazioni verticali si basano invece sulla quota relativa dei velivoli misurata con l’altimetro barometrico. In assenza di copertura radar, che permette di incrementare la densità del volume di traffico perché può gestire distanze relative inferiori, si utilizza il metodo di controllo procedurale che si basa sulla conoscenza dell’orario di attraversamento di punti prefissati delle rotte. + Controllo: la differenza fra volo non controllato e controllato è essenziale per comprendere l’organizzazione dello spazio aereo e il modo in cui viene utilizzato. Nel primo caso la responsabilità di evitare collisioni con ostacoli fissi e con altri velivoli è unicamente del comandante di bordo, nel secondo caso è in parte condivisa con gli organismi di controllo al suolo. Per poter effettuare un volo controllato è necessario che esso abbia luogo in uno spazio aereo controllato, ed il comandante di bordo deve essere in continuo contatto radio-telefonico, su specifiche frequenze, con gli organismi di controllo al suolo che autorizzano la progressione del piano di volo. Le informazioni relative ai voli vengono scambiate tra i vari enti di controllo attraverso l’impiego della rete AFTN (Aeronautical Fixed TLC Network ). + Rotte aeree: una rotta aerea passa su dei punti particolari che corrispondono 2 1 miglio nautico (1 nm) corrisponde a 1,852 km. 25 Un caso reale: la piattaforma Miniminds a dei raccordi di due tronconi di una rotta. In corrispondenza di questi punti vengono installati dei radiofari omnidirezionali che servono alla comunicazione radio-elettrica. Il volo quindi progredisce di radiofaro in radiofaro, il collegamento ideale fra i punti corrispondenti ai radiofari costituisce un troncone di rotta. L’unione di diversi tronconi delinea una rotta di volo. + Ripartizione geografica del controllo: consiste nella ripartizione dello spazio aereo delle regioni terminali in molteplici settori. 2.1.3 Categorie del controllo del traffico aereo In generale è possibile distinguere 3 grandi categorie di controllo nell’ambito del traffico aereo: 1. I controlli d’area (ACC): Un ACC gestisce grandi porzioni di spazio aereo3 e serve a guidare ed assistere la navigazione in alta quota. Per poter gestire spazi aerei vasti, questi ultimi vengono suddivisi in settori operativi, le cui forme e dimensioni dipendono dai flussi di traffico principali. Ogni settore, gestito da uno o più controllori, è determinato quindi da un proprio volume di spazio aereo e da una frequenza radio (nella banda VHF citata precedentemente) condivisa fra gli aeromobili nel settore. Nel percorrere la propria rotta un aereo attraversa diversi settori ed ACC, e di conseguenza cambia più volte frequenza. 2. I controlli di avvicinamento (APP): Nel controllo di avvicinamento sono compresi la gestione del traffico sull’area di manovra di un aeroporto e nelle vicinanze di esso entro un raggio di 30 nm e ad una quota di 10.000 piedi. Il controllo di avvicinamento si avvale dell’utilizzo del radar. 3 Per spazio aereo si intende una zona definita nelle tre dimensioni in cui un aeromobile deve sottostare a particolari condizioni. 26 Un caso reale: la piattaforma Miniminds 3. I controlli di tor re (TWR): I controlli di torre, come dice il nome, avvengono in una struttura detta torre di controllo. Tale struttura è dotata di grandi finestre su tutto il perimetro poiché il controllo di torre, consistente principalmente nella separazione al suolo dei velivoli, viene effettuato prevalentemente a vista, ed in questo modo si ha una visuale a 360°. Gli aeromobili contattano la torre di controllo prima dell’atterraggio e da essa vengono trasferiti al controllo di avvicinamento subito dopo il decollo, operazione che va sotto il nome di handoff. Tecnicamente l’operazione di handoff (o handover ) consiste nel trasferimento delle comunicazioni (e dell’identificazione) di un aeromobile, e quindi del canale radio VHF impiegato, ad un altro controllore. A queste categorie si aggiungono altre 2 categorie “minori” che sono presenti solo in alcuni aeroporti: 1. I controlli di terra (GND): L’area di competenza di questi controlli è tutta l’area di terra ed i compiti svolti sono principalmente la conferma del piano di volo e l’autorizzazione stessa del volo. In mancanza di GND questi compiti sono a carico della torre di controllo. 2. I controlli di autorizzazione o delivery (DEL): Essi non hanno una vera e propria area di competenza, sostanzialmente il loro compito è fornire il nulla osta all’autorizzazione del volo da parte dei controlli di terra. 27 Un caso reale: la piattaforma Miniminds 2.2 La piattaforma Miniminds Come già anticipato nel capitolo introduttivo, questo lavoro di tesi si colloca nell’ambito del progetto Miniminds. L’acronimo Miniminds, reso in italiano, sta per middleware4 per l’interoperabilità e l’integrazione di sistemi critici per tempo e affidabilità. Il progetto nasce dalla collaborazione fra l’azienda SELEX-ES e partner accademici come il CINI (Consorzio Interuniversitario Nazionale per l’Informatica) ed il DIETI (Dipartimento di Ingegneria Elettrica e delle Tecnologie dell’Informazione) dell’Università degli studi di Napoli “Federico II ”. 2.2.1 Perché Miniminds Sistemi complessi funzionanti raramente vengono sviluppati integralmente ex novo, tipicamente essi evolvono a partire da sistemi esistenti già funzionanti. L’integrazione di sistemi informatici sviluppati in momenti, con linguaggi e con tecniche diversi, ed operanti su piattaforme eterogenee, è un problema centrale delle tecnologie software [24]. Si richiede sempre più di: + utilizzare applicazioni di terze parti (sistemi COTS, Commercial Off-The-Shelf ); + riutilizzare applicazioni esistenti (sistemi ereditati o legacy). Il problema dell’integrazione assume un rilievo ancora maggiore in domini critici, in cui sistemi mission-critical, sempre più complessi, si trovano a dover operare in scenari di interazione tra sotto-sistemi con le suddette caratteristiche di eterogeneità e distribuzione. Da ciò segue che l’interoperabilità rappresenterà una proprietà fondamentale dei 4 Con il termine middleware si intende uno strato software interposto tra il sistema operativo e le applicazioni, in grado di fornire le astrazioni ed i servizi utili per mascherare problemi dovuti all’eterogeneità [24]. Tipicamente vengono impiegati in ambito distribuito per mascherare le eterogeneità dei sistemi su rete. Per tale motivo i middleware vengono anche definiti “glue technologies” (tecnologie collante). 28 Un caso reale: la piattaforma Miniminds sistemi del prossimo futuro. Tuttavia, finora, raramente i sistemi sono concepiti e sviluppati con l’obiettivo di interoperare. Di conseguenza, la loro integrazione, specie nel caso in cui comprenda applicazioni legacy 5 , può dar vita a situazioni di errore non previste in fase di sviluppo, ma soprattutto difficili da verificare. L’introduzione di queste situazioni anomale derivanti dall’integrazione rappresenta un problema di particolare importanza, specialmente nel contesto dei sistemi critici. Il progetto Miniminds ha quindi come obiettivo lo sviluppo di una piattaforma middleware domain independent [22], in grado di: + facilitare l’integrazione sicura ed affidabile di sistemi IT complessi, appartenenti a diversi domini applicativi e distribuiti su larga scala; + di ridurre i costi di integrazione e di manutenzione; + basata su standard open source. L’obbiettivo di un’integrazione rapida ed ottimale dei sistemi complessi viene perseguita sia attraverso la definizione di modelli comuni di dati e servizi, implementati da un apposito componente che maschera le eterogeneità esistenti fra i vari applicativi, sia con la progettazione di adapter per l’interfacciamento con i sistemi legacy. La piattaforma utilizza infine strategie di monitoring on-line con l’obiettivo di rilevare e diagnosticare in maniera automatica fallimenti dovuti a guasti di integrazione portando una conseguente diminuzione dei costi relativi alle attività di integrazione e di manutenzione. Nello specifico, il contributo apportato al progetto da questo lavoro di tesi riguarderà il monitoraggio indiretto tramite gli indicatori di performance e le statistiche sulle risorse utilizzate. 5 Un sistema legacy è un sistema informatico, un’applicazione o un componente obsoleto, che continua ad essere usato poiché un’organizzazione non intende o non può rimpiazzarlo. 29 Un caso reale: la piattaforma Miniminds 2.2.2 Il ruolo del middleware Per raggiungere l’obbiettivo dell’interoperabilità è possibile intraprendere diverse strade. Alcune soluzioni prevedono di definire un’interfaccia di comunicazione ad hoc per ogni coppia di nodi applicativi. Questa soluzione, però, porta ad avere un accoppiamento troppo stretto fra i nodi (ed in genere prevede una comunicazione basata su protocolli di natura sincrona). Inoltre, essendo ognuna delle interfacce specifica per la comunicazione fra una particolare coppia di nodi, il loro incremento in sistemi composti da un numero notevole di nodi diventa ingestibile. L’approccio middleware based, invece, definisce uno strato di integrazione che disaccoppia i nodi comunicanti, i quali possono adoperare anche applicazioni scritte con diverse tecnologie. Esistono due varianti di questa strategia, un approccio diretto ed uno indiretto. Nel primo caso allo strato middleware è demandato il solo problema della connettività, ma rimane a carico del nodo applicativo la gestione e la trasformazione di formato dei messaggi. Nel secondo caso, invece, il livello middleware fornisce sia connessione che trasformazione dei messaggi, avvicinandosi maggiormente al concetto di integrazione vero e proprio. Il middleware offre vantaggi legati all’accoppiamento lasco tra le entità, alta efficienza, determinismo nella consegna dei dati, ed una vasta gamma di parametri per la qualità del servizio, che lo rendono particolarmente adatto al supporto di applicazioni missioncritical e safety-critical. Esso riveste quindi un ruolo strategico per ridurre i costi di sviluppo ed il time to market 6 . L’impiego di middleware in sistemi mission-critical, come nel caso del monitoraggio o controllo di infrastrutture critiche, solleva, tuttavia, problemi significativi di affidabili6 Il Time To Market (o TTM) indica il tempo che intercorre dall’ideazione di un prodotto alla sua effettiva commercializzazione. Nel campo delle nuove tecnologie è di fondamentale importanza abbassare il TTM per imporsi sul mercato prima dei concorrenti. 30 Un caso reale: la piattaforma Miniminds tà. Di conseguenza i fallimenti del middleware ed il loro impatto sul sistema nel suo complesso vanno valutati attentamente in scenari critici [19]. Un esempio di conseguenze dovute ad eventi anomali in un sistema che opera in un dominio critico è l’incidente avvenuto di recente negli Stati Uniti, e riportato su The Guardian. Durante una missione di addestramento di routine, l’attraversamento dello spazio aereo da parte di un aereo spia U-2 ha provocato il sovraccarico del sistema ATC di Los Angeles. I controllori del traffico aereo hanno dovuto ricorrere a procedure di emergenza chiamandosi l’un l’altro per tenere traccia delle rotte attive. L’aereo spia U-2 vola ad alta quota, circa 60.000 piedi, ma il computer ha percepito il piano di volo come un volo a bassa quota, ed ha quindi tentato freneticamente di reindirizzare l’aeromobile a tale altitudine. Ciò ha richiesto un elevato numero di correzioni alle rotte degli altri velivoli, le quali, utilizzando una gran quantità della memoria disponibile nel sistema, hanno travolto il software, interrompendo le funzioni di processing per le altre rotte [21]. 2.2.3 L’architettura della piattaforma Il middleware del progetto mira ad essere una piattaforma che faciliti l’integrazione di applicazioni eterogenee e distribuite, eventualmente preesistenti, nel contesto di un’architettura che soddisfi i requisiti di sicurezza ed adattabilità ed in grado di fornire strumenti per la supervisione dei sottosistemi componenti [22]. Essa rappresenta dunque un middleware a livello di dominio, con un’architettura schematizzata in componenti distribuiti, sintetizzata in figura 2.1. Tutti i componenti sono distribuiti, e le comunicazioni tra le applicazioni con i rispettivi domini e quelle intradominio avvengono attraverso l’ausilio del pattern publish/subscribe o request/reply. Tali componenti sono di seguito descritti: 31 Un caso reale: la piattaforma Miniminds Figura 2.1: Architettura logica della piattaforma Miniminds7 + Componente Communication Protocol : questo componente rappresenta il livello più basso della gerarchia ed implementa le primitive di comunicazione per l’accesso al livello rete, abilitando quindi la comunicazione tra le differenti istanze di Miniminds distribuite sui nodi del sistema. Tali primitive di comunicazione consentono la realizzazione di differenti pattern di interazione, come ad esempio il request-reply. + Componente Data Dissemination: questo componente si occupa di fornire l’astrazione necessaria per garantire il disaccoppiamento fra i nodi che devono comunicare. Esso implementa quindi i servizi per la trasmissione e la condivisione 7 Figura tratta dal Capitolato Tecnico del progetto Miniminds, Middleware per l’integrazione e l’interoperabilità di sistemi critici per tempo e affidabilità, finanziato dal Ministero dell’Istruzione Università e Ricerca nell’ambito del Laboratorio Pubblico/Privato COSMIC 32 Un caso reale: la piattaforma Miniminds dei dati, fornendo in questo modo la trasparenza dalla specifica tecnologia offerta dal componente communication protocol. Questa scelta permetterà l’integrazione di tecnologie di prossima generazione senza la necessità di modificare la logica applicativa. + Componente On-line Monitoring : questo componente implementa tecniche per l’osservazione del funzionamento del sistema durante la fase di esercizio, al fine di supportare il rilevamento di malfunzionamenti dovuti ai guasti di integrazione, e di isolare per tempo i componenti responsabili di tali malfunzionamenti. Tale componente racchiude tecniche congiunte di monitoring diretto ed indiretto. + Componente Reliability & Security : questo componente implementa tecniche per stimare l’affidabilità dei componenti del sistema e garantire la continuità di servizio mediante strumenti per la tolleranza ai guasti, come ad esempio la replicazione ed il ripristino. Per assicurare l’attivazione delle contromisure più adatte all’occorrenza di malfunzionamenti, questi meccanismi si serviranno sia delle informazioni raccolte in fase di esercizio dagli strumenti di monitoring e sia delle informazioni storiche immagazzinate nei log. + Componente Supervision & Control : questo componente definisce le interfacce per la gestione delle applicazioni integrate tramite la piattaforma. Nello specifico tale componente presenta un’architettura federata: ogni dominio applicativo sarà gestito da un’entità di supervisione specifica che coopererà con i componenti di supervisione degli altri domini, attraverso la condivisione delle informazioni riguardanti lo stato operativo di questi ultimi. Questo consente la supervisione sia nel caso le applicazioni appartengano ad uno specifico dominio applicativo e sia nel caso le applicazioni appartengano a domini differenti. 33 Un caso reale: la piattaforma Miniminds + Componente Interoperability Layer : questo componente implementa la logica di gestione delle informazioni. Per la realizzazione di tale componente è necessario definire il modello logico dei servizi necessari allo scambio dei dati e le interfacce che i sistemi interoperanti dovranno invocare per utilizzarli. Tali interfacce rappresentano le modalità attraverso cui i dati ed i servizi saranno resi disponibili attraverso l’infrastruttura. Tutte le applicazioni interessate ad un determinato tipo di dati/servizi dovranno supportare una singola interfaccia piuttosto che una moltitudine di interfacce a seconda del numero di sistemi con cui interagire. Le applicazioni che usufruiscono dei servizi del dominio del middleware, possono essere di due tipi: native e legacy. Le applicazioni native sono applicazioni scritte ad hoc, ed utilizzano i servizi legati ad un dominio della piattaforma in piena compatibilità con il middleware. Le applicazioni legacy, invece, sono applicazioni preesistenti, non compatibili con gli standard impiegati nella piattaforma Miniminds. Quest’ultima, allo scopo di garantire comunque la compatibilità con sistemi legacy già disponibili, prevede ulteriori componenti architetturali di adattamento, i componenti Adapter . Ogni adapter si occupa di assicurare la necessaria mediazione tra il particolare sistema legacy da integrare e la piattaforma stessa. Ogni adapter è responsabile della trasformazione dei dati dal formato dello specifico sistema legacy al formato definito dal modello comune dei dati di Miniminds e viceversa, e della necessaria mediazione semantica al fine di rendere compatibili i servizi/funzionalità fornite dal sistema legacy con quelli attesi dalla piattaforma. L’impiego e la struttura degli adapter è mostrata in figura 2.2. Le applicazioni legacy A e C in figura richiedono opportuni strati software adapter, mentre l’applicazione B, essendo sviluppata nativamente, è direttamente interfacciata al middleware. Essendo ogni adapter specifico per il sistema legacy da integrare, sarà necessario lo sviluppo di 34 Un caso reale: la piattaforma Miniminds Figura 2.2: Adapter per l’interfacciamento di applicazioni legacy8 uno specifico adapter per ciascuno dei sistemi legacy che dovranno essere integrati sulla piattaforma. Ciascun adapter prevede una doppia interfaccia, così suddivisa: 1. uno strato software di adattamento, progettato e realizzato ad hoc, bridge legacy , costituente cioè l’interfaccia verso lo specifico sistema legacy. 2. uno strato software di adattamento bridge Miniminds, sviluppato invece sulla base dei modelli di dati e servizi propri della piattaforma. Essendo tali modelli in comune tra tutti i sistemi interfacciati sulla piattaforma, tale componente è un prodotto riutilizzabile per tutti i sistemi legacy. 8 Figura tratta dal Capitolato Tecnico del progetto Miniminds, Middleware per l’integrazione e l’interoperabilità di sistemi critici per tempo e affidabilità, finanziato dal Ministero dell’Istruzione Università e Ricerca nell’ambito del Laboratorio Pubblico/Privato COSMIC 35 Un caso reale: la piattaforma Miniminds La piattaforma rende quindi possibile sia un’integrazione verticale all’interno di uno specifico contesto applicativo, sia un’integrazione orizzontale, ossia consentirà ad applicazioni di domini differenti di scambiarsi informazioni secondo formati eventualmente diversi. 36 Capitolo 3 Il framework di monitoring indiretto Questo capitolo si occupa di fornire una visione schematica dei requisiti di progetto richiesti al framework di monitoraggio indiretto sviluppato. Nella prima parte del capitolo verranno quindi esposti i vari requisiti funzionali e non funzionali, mentre nella seconda parte verrà definita l’architettura del framework ad un alto livello di astrazione. Verrà quindi descritto il comportamento del tool “ai terminali”, ossia in maniera trasparente rispetto alla specifica implementazione. Di quest’ultima tratterà un successivo capitolo. 3.1 Analisi dei requisiti Il processo di raccolta di requisiti occorre per definire cosa il sistema, o nel caso specifico, cosa il framework dovrà realizzare. Collocandosi nell’ambito del progetto Miniminds, il tool realizzato dovrà coesistere con un’infrastruttura di monitoraggio diretto prototipale già esistente, allo scopo di 37 Il framework di monitoring indiretto coadiuvarne l’azione di rilevamento di condizioni di funzionamento anomale. Inoltre, al verificarsi di situazioni in cui risulti impossibile l’accesso al codice sorgente (come già descritto più nel dettaglio nella sezione 1.1.2), esso dovrà sopperire all’impossibilità di utilizzare l’infrastruttura di monitoraggio diretto. 3.1.1 Requisiti funzionali I requisiti funzionali esprimono un’azione che il sistema dovrebbe eseguire, definendone sia lo stimolo (input) che il risultato (output). Il tool di monitoraggio indiretto dovrebbe essere in grado di fornire degli indici prestazionali e delle statistiche sull’utilizzo delle risorse, dai quali ricavare un profilo di comportamento del sistema o applicazione monitorati. Nello specifico si richiede al tool di essere in grado di produrre le seguenti informazioni: + Informazioni di carattere generale come dimensioni della memoria RAM e percentuale di utilizzo, numero di nuclei elaborativi della macchina e numero di processi presenti correntemente sul sistema. + Informazioni statistiche circa l’utilizzo di CPU, sia con focus globale, ossia l’utilizzo percentuale di un particolare nucleo di elaborazione, sia con granularità di processo, ossia l’utilizzo percentuale di cui il singolo processo è responsabile. + Informazioni statistiche circa l’utilizzo delle interfacce di rete del sistema. Nello specifico il monitor deve essere in grado di rilevare la quantità di dati in trasmissione ed in ricezione sia in termini di byte che in termini di pacchetti, registrando le occorrenze di errori, sia in trasmissione che in ricezione, e di collisioni. 38 Il framework di monitoring indiretto + Informazioni di carattere generale inerenti il processo, ossia identificativo dell’utente proprietario del processo, identificativo del processo, e identificativo della CPU sulla quale viene eseguito. + Informazioni riguardanti il numero di thread che il processo genera durante la sua esecuzione. + Informazioni riguardanti il numero di cambi di contesto che coinvolgono il processo. + Informazioni circa la percentuale di memoria utilizzata. + Informazioni circa la memoria virtuale del processo, ossia dimensione dell’area stack, dell’area codice e dell’area dati. + Informazioni riguardo l’occorrenza di page fault. + Informazioni di I/O come quantità di file descriptor aperti gestiti dal processo e operazioni di lettura e scrittura. + Informazioni circa le socket utilizzate dal processo. 3.1.2 Requisiti non funzionali I requisiti non funzionali sono tutti quei requisiti che non rappresentano la traduzione diretta di azioni richieste dall’utente in funzionalità offerte dal tool, bensì rispecchiano delle condizioni di funzionamento che devono verificarsi all’atto della messa in esercizio. Siccome il tool deve effettuare monitoraggio indiretto, l’ipotesi di fondo è che esso consideri il sistema come una black-box. Il risvolto di questa ipotesi è duplice, da un lato il monitor non deve conoscere i dettagli implementativi del sistema, e dall’altro l’azione del monitor deve essere trasparente al sistema. Per cui uno dei requisiti di maggiore 39 Il framework di monitoring indiretto importanza è quello di non intrusività. Grazie a questo, il sistema continuerà ad effettuare le sue normali operazioni sia in assenza del monitor sia in presenza di esso. Un altro requisito richiesto al monitor è la tempestività, infatti, in domini critici è cruciale mantenere un monitoraggio continuo con una certa frequenza per poter essere in grado di riconoscere situazioni anomale e per l’attivazione tempestiva delle azioni di ripristino più appropriate. Per poter mantenere alte frequenze di funzionamento, e quindi bassi periodi di riattivazione, il meccanismo di produzione delle informazioni necessarie al monitoraggio deve fornire i dati in un periodo ancora minore. Sono quindi richiesti i seguenti ulteriori requisiti non funzionali: + Frequenza di monitoraggio: la specifica richiede di poter effettuare fino a 10 rilevazioni al secondo (ossia con periodo di riattivazione del monitor fino a 100 ms); + Indipendenza dall’applicazione: ossia gli indici prestazionali e le informazioni sulle risorse utilizzate non devono essere vincolate alla particolare applicazione monitorata; + Monitoraggio con granularità di processo: ossia il framework deve poter discriminare le prestazioni del singolo processo. Nello specifico deve essere garantito il monitoraggio di almeno un processo applicativo e del relativo processo container; + Overhead : è richiesto di minimizzare il ritardo nel completamento delle operazioni, nello specifico al massimo del 5%. 40 Il framework di monitoring indiretto Figura 3.1: Architettura del tool di monitoraggio indiretto 3.2 Architettura del framework L’architettura del tool di monitoraggio indiretto è schematizzata in figura 3.1, e logicamente può essere scomposta in 4 parti, che sono: 1. Un file attraverso cui poter configurare i processi ed i parametri da monitorare. 2. Un’applicazione demone, ossia un processo sempre attivo che si occupa della logica di raccolta delle informazioni. 3. Un meccanismo a livello di sistema operativo che si occupa della produzione dei dati per il monitoraggio. 4. Un sotto-albero di directory in cui il demone suddivide i dati raccolti in un formato adatto per i successivi test. 41 Il framework di monitoring indiretto 3.2.1 Il file di configurazione Il file di configurazione ha il compito di interfacciare l’utente con il tool. Tramite questo file infatti l’utente può specificare tutti i parametri che serviranno come configurazione di input per il demone. In particolare attraverso il file di configurazione è possibile specificare i seguenti parametri: + Numero iterazioni : rappresenta il numero di cicli di lettura dal procfs che dovranno essere compiuti dal demone. + Refresh time: rappresenta l’intervallo di tempo fra le iterazioni e quindi fra le letture. + Task command : rappresenta la sequenza di processi di cui si vuole effettuare il monitoraggio. In particolare poiché non è prevedibile il PID che verrà assegnato al processo, una volta in esecuzione, esso viene identificato tramite il nome del comando ad esso associato, che è invece conosciuto dall’utente. Sarà poi compito del modulo kernel risalire al PID e da quest’ultimo alle informazioni richieste. + Informazioni di sistema: rappresentano i parametri che il tool può monitorare con granularità di sistema, e sono i seguenti: – Data – Uptime – Numero di task presenti nel sistema – Carico medio del sistema negli ultimi 1, 5 e 15 minuti – Dimensioni e spazio disponibile in RAM – Numero e statistiche di utilizzo delle CPU 42 Il framework di monitoring indiretto – Statistiche di utilizzo delle interfacce di rete + Informazioni di task : rappresentano i parametri che il tool può monitorare invece con granularità di processo, e sono: – UID, ossia l’identificativo numerico dell’utente cui appartiene il processo. – PID, ossia l’identificativo numerico del processo. – On_CPU, ossia l’identificativo numerico della CPU sulla quale esegue il processo. – %CPU, ossia l’utilizzo percentuale di CPU del processo. – Num Thread, ossia il numero di flussi di esecuzione generati dal processo. – CSW Volontari, ossia il numero di cambi di contesto (context switch1 ) provocati dal processo stesso. Ad esempio rientrano in questa categoria le invocazioni di system call. – CSW Involontari, ossia il numero di cambi di contesto non provocati dal processo. Rientrano invece in questa categoria le prelazioni di CPU. – %MEM, ossia la percentuale di memoria che viene occupata dal processo. – VmSize, ossia il numero di pagine che compongono la memoria virtuale. – VmPeak, ossia il valore di picco del numero di pagine di memoria virtuale. – VmRss, ossia il set di pagine di memoria virtuale attualmente residente in RAM. – RssPeak, ossia il valore di picco di memoria residente. – VmLocked, ossia il numero di pagine di memoria riservate e su cui esiste un lock. 1 Il context switch consiste nel salvare il contesto di un processo, ossia il suo stato corrente, e caricare i registri del processore con il contesto di un altro processo. 43 Il framework di monitoring indiretto – VmData, ossia la dimensione in pagine del segmento di memoria virtuale per l’area dati. – VmStack, ossia la dimensione in pagine del segmento di memoria virtuale per l’area stack. – VmExe, ossia la dimensione in pagine del segmento di memoria virtuale per l’area codice. – Minor Page Faults, rappresenta un page fault che non provoca il caricamento di una pagina di memoria dal disco. Un fault del genere si verifica quando la pagina è già caricata in memoria ma la sua entry nella MMU (Memory Management Unit) non è marcata come tale, come avviene nel caso di pagine di memoria condivisa già caricate da un’altra risorsa che le utilizza. – Major Page Faults, rappresenta invece un page fault che provoca la lettura di una pagina di memoria dal disco. – Read Bytes, ossia il numero di byte di cui il processo ha provocato la lettura dallo storage. – Write Bytes, ossia il numero di byte di cui il processo ha provocato la scrittura su disco. – Open Files, ossia il numero di file aperti detenuti dal processo. – Socket, rappresenta il numero di file detenuti dal processo che costituiscono una socket 2 . Insieme al numero totale vengono restituiti anche gli identificatori numerici dei loro file descriptor. 2 Il termine socket indica un’astrazione software progettata per la trasmissione e la ricezione di dati attraverso una rete oppure come meccanismo di inter process comunication. È il punto in cui il codice applicativo di un processo accede al canale di comunicazione per mezzo di una porta, ottenendo una comunicazione tra processi che lavorano su due macchine fisicamente separate. Dal punto di vista di un programmatore un socket è un particolare oggetto sul quale leggere e scrivere i dati da trasmettere o ricevere. 44 Il framework di monitoring indiretto 3.2.2 L’applicativo demone Il processo demone rappresenta il componente del framework che implementa la logica di controllo ed inoltre gestisce sia l’approvvigionamento sia la fruizione delle informazioni. Logicamente può essere scomposto in 2 sequenze operative: 1. Nella prima sequenza operativa il demone effettua il parsing del file di configurazione. Tramite quest’ultimo, infatti, popola le variabili che gestiscono la frequenza di riattivazione del monitor, e rileva quali sono i processi da monitorare e quali parametri abilitare nella raccolta. 2. Nella seconda sequenza operativa il demone si interfaccia con il meccanismo di produzione dei dati interno al sistema operativo, e contestualmente, si occupa della creazione e popolamento del sotto-albero di directory che conterrà lo storico delle informazioni estratte per una successiva analisi. In particolare la struttura di directory sarà così suddivisa: + la directory radice sarà denominata stats. + all’interno di stats sono previste 4 sotto-directory: – ERR: contiene i messaggi di errore generati dal modulo; – TaskStats: contiene le statistiche con granularità di processo; – NetworkInterface: contiene le statistiche di rete globali; – LoadAVG: contiene le statistiche sul carico di sistema. + all’interno di NetworkInterface ci saranno tante sotto-directory quante sono le interfacce di rete attive. + all’interno di TaskStats ci saranno tante directory quanti sono i task da monitorare (nominate con il nome del comando). 45 Il framework di monitoring indiretto Figura 3.2: Esempio di albero di directory creato dal processo + ognuna delle directory al livello più basso conterrà i file relativi ad ogni sessione di monitoraggio, nominati attraverso il timestamp per ottenere un ordinamento temporale. Una schematizzazione esemplificativa di quanto appena detto è riportata in figura 3.2. A partire da tale sotto-albero uno script Octave 3 genera una serie di grafici per l’analisi dei dati. Per i dettagli implementativi inerenti il meccanismo di produzione dei dati, e del demone stesso, si rimanda ai successivi capitoli. 3 GNU Octave è un linguaggio interpretato di alto livello utilizzato in prevalenza per il calcolo numerico. Il linguaggio Octave è simile al più noto Matlab, facilitando la portabilità da e verso quest’ultimo. 46 Capitolo 4 Panoramica delle risorse utilizzate dal framework In questo capitolo verranno descritti i meccanismi su cui poggia il funzionamento del framework realizzato. Nella prima sezione si valuterà, disponendo di un preesistente sistema operativo basato su kernel Linux, se implementare l’architettura proposta nel capitolo precedente in kernel space o in user space, in base ai requisiti richiesti dal progetto. Nelle sezioni successive verranno descritti il meccanismo dei Loadable Kernel Modules, che permette l’inserimento di moduli kernel a runtime, il /proc file system, che costituisce un’interfaccia fra kernel e spazio utente tramite la quale si possono importare/esportare informazioni, ed infine la gestione dei processi con un focus maggiore sulla struttura dati che implementa i descrittori di processo. 47 Panoramica delle risorse utilizzate dal framework 4.1 User Space vs Kernel Space Per raccogliere le informazioni richieste dai requisiti del tool è possibile ricorrere a diverse strategie, ad esempio: + Utilizzo di moduli che lavorano in kernel space e prelevano le informazioni direttamente dalle strutture mantenute dal sistema operativo. Di questo tipo di moduli si parlerà più approfonditamente nella sezione 4.2. + Lettura e parsing dei vari file relativi al processo da monitorare dal file system virtuale procfs. Si parlerà di tale file system con un maggiore livello di dettaglio nella sezione 4.3. + Utilizzo di utility come ps o top. Per utilizzare le utility all’interno di un programma C si è fatto uso della funzione popen, che apre un processo effettuando le operazioni seguenti: – crea una pipe – esegue una fork – invoca la shell L’argomento passato alla popen rappresenta la riga di comando da eseguire nella shell. Un primo parametro di confronto fra le strategie sarà chiaramente la possibilità di riuscire ad ottenere le informazioni necessarie al funzionamento del tool. A parità di informazioni ottenibili, verrà scelto uno dei metodi prima descritti basandosi sulla velocità di reperimento delle informazioni, in quanto maggiore sarà la velocità e più alta potrà essere la frequenza di monitoraggio del tool. 48 Panoramica delle risorse utilizzate dal framework Figura 4.1: Grafico confronto tempi per la ricerca dei PID Come mostrato nei successivi grafici, il metodo del modulo caricabile è stato preferito dal punto di vista del tempo per recuperare le informazioni. La motivazione della sua maggiore efficienza è dovuta al fatto di lavorare direttamente in kernel space ed al fatto che esso non necessita di mantenere numerosi file aperti per il recupero delle informazioni, bensì un’unica entry del procfs creata ad hoc. L’operazione fondamentale che il tool deve essere in grado di compiere è quella di risalire al PID assegnato ad un processo. Poiché l’utente è a conoscenza del solo comando associato allo stesso, se si vuole realizzare un tool automatizzato, bisogna ricorrere ad una funzione che risalga dal comando al PID. Per questo motivo, risalire al PID di un dato processo sarà oggetto del primo confronto temporale fra le metodologie. In figura 4.1 è mostrato il confronto dei tempi, in microsecondi, richiesti per risalire al PID di un dato processo attraverso le 3 strategie elencate in precedenza. La curva in blu rappresenta il metodo del modulo caricabile. La curva in verde rappresenta la tecnica di lettura e parsing delle directory del procfs finché non si verifica un matching 49 Panoramica delle risorse utilizzate dal framework Tabella 4.1: Tabella media, deviazione standard e C.o.V. per la ricerca dei PID ... media dev std C.o.V. modulo 27.8 10.88 0.3914 procfs 2212.8 335.58 0.1516 ps 676.9 391.69 0.5786 con il comando associato al task da monitorare. In ultimo la curva in rosso rappresenta il tempo necessario ad ottenere l’informazione tramite la concatenazione di comandi Linux a partire dalla lettura con l’utility ps. Come si vede dalla figura la tecnica che fornisce i tempi migliori è quella del modulo caricabile. In tabella 4.1 vengono riportate media, deviazione standard e C.o.V. (Coefficient of Variation)1 delle strategie discusse. Il codice di prova è stato eseguito per 100 iterazioni in modo da ottenere 100 campioni per ognuna delle 3 tecniche. Dalla tabella risulta evidente una media dei tempi di esecuzione del modulo circa 30 volte inferiore alla strategia della concatenazione di comandi tramite le utility, e addirittura 80 volte inferiore al ciclo di lettura e parsing sul procfs, sebbene quest’ultima abbia un coefficiente di variazione inferiore. Nel grafico di figura 4.2 viene mostrato il confronto dei tempi di esecuzione, sempre in microsecondi, per il recupero di alcune informazioni di prova. Nello specifico le informazioni recuperate sono la quantità di memoria residente, vmrss, ed il numero di byte letti e scritti dal processo. In questo caso il confronto viene effettuato soltanto fra il modulo, risultato migliore dal punto di vista di tempo medio di esecuzione nel grafico precedente, ed il metodo basato su procfs, risultato migliore dal punto di vista della stabilità delle osservazioni (C.o.V. migliore). Anche in questo caso sono state effettuate 100 iterazioni per ottenere altrettanti campioni di entrambe le strategie. La curva in blu rappresenta sempre il modulo caricabile, mentre la curva in verde la lettura da 1 Il C.o.V. rappresenta il rapporto fra la deviazione standard e la media. 50 Panoramica delle risorse utilizzate dal framework Figura 4.2: Grafico confronto tempi per il recupero di informazioni Tabella 4.2: Tabella media, deviazione standard, e C.o.V. per il recupero di informazioni .. . media dev std C.o.V. modulo 14.35 4.0386 0.2814 procfs 69.48 23.0035 0.3310 procfs dei file /proc/[pid]/status, per la memoria residente, e /proc/[pid]/io, per i byte letti/scritti. Anche in questo caso si nota il vantaggio dell’utilizzo del modulo. Inoltre è da notare che, nel caso in esame, per il recupero delle informazioni era necessaria l’apertura di soli 2 file. Al crescere del numero di informazioni richieste, e della loro eterogeneità, il numero di file, e quindi l’overhead, aumenta. In tabella 4.2 sono riportate media, deviazione standard e C.o.V. anche per il secondo grafico discusso. In questo test la strategia basata su modulo kernel risulta migliore anche dal punto di vista della stabilità delle osservazioni. 51 Panoramica delle risorse utilizzate dal framework Essendo la tecnica che ha fornito i risultati migliori, il meccanismo del modulo kernel caricabile dinamicamente rappresenterà il nucleo del tool da implementare. Nelle sezioni seguenti si approfondiranno i concetti di modulo kernel caricabile dinamicamente, di file system procfs e gestione dei processi in un sistema operativo basato su kernel Linux, prima di procedere alla descrizione dell’architettura e dell’implementazione del tool. 4.2 Loadable Kernel Modules (LKM) Il kernel Linux ha un’architettura monolitica, la quale implica che l’intero codice del kernel è eseguito in kernel space, condividendo lo stesso spazio di indirizzamento. L’essere monolitico comporta la necessità di scegliere quali funzionalità abilitare o disabilitare a tempo di compilazione del kernel. Il processo di configurazione del kernel, infatti, consiste principalmente nella scelta di quali file includere nella fase di compilazione. Di conseguenza il modo più semplice per aggiungere funzionalità al kernel è quello di includere il codice sorgente nell’insieme dei file selezionati per la compilazione. Tutte le funzionalità, comprese quelle non utilizzate di frequente, saranno caricate al momento del boot del sistema. Un secondo modo di agire, introdotto dalla versione di Linux 1.2, è quello di aggiungere il codice dinamicamente, ossia durante l’esercizio del sistema, tramite il meccanismo dei loadable kernel modules (che tecnicamente ha reso Linux non più puramente monolitico). Un loadable kernel module è, quindi, una porzione di codice che può essere aggiunta al kernel Linux a runtime ogni volta che se ne presenta la necessità. Una volta caricato, il modulo diventa parte integrante del kernel. In base a quanto detto, si può effettuare una distinzione fra “kernel” e “base kernel”, ossia la parte di kernel consistente nell’immagine avviata al boot del sistema. Il nuovo modulo, quindi, comunicherà con il kernel di base, 52 Panoramica delle risorse utilizzate dal framework ma sarà parte del kernel. L’introduzione del meccanismo dei LKM ha portato diversi benefici, come: + L’aggiunta di un modulo, e quindi la sua esecuzione, non richiede una ricompilazione del kernel, permettendo un’estensione delle sue funzionalità senza dover effettuare un reboot. + Mantenere intatto il kernel di base previene l’introduzione di errori. Un modulo, infatti, in presenza di errori, si può escludere dal kernel in esecuzione, correggere e reinserire, senza dover ricompilare il kernel ogni volta. Inoltre ciò è utile in fase di diagnosi di un problema del sistema, potendo imputarne la causa ad un modulo particolare. + Quando i servizi forniti da un modulo non sono più necessari il modulo può essere rimosso liberando memoria. + Un approccio modulare è chiaramente più manutenibile. Questo approccio porta comunque con sé degli svantaggi. Un primo inconveniente è di tipo prestazionale. La memoria in cui risiede un modulo è leggermente differente da quella in cui risiede il kernel di base. Quest’ultimo è sempre caricato in una singola area contigua di memoria, i cui indirizzi reali sono uguali agli indirizzi virtuali. Ciò è possibile perché il kernel di base è il primo elemento, eccetto il loader, ad essere caricato, e pertanto ha a disposizione ampi spazi liberi di memoria. Inoltre, siccome il kernel Linux non può essere soggetto al paging 2 , esso rimarrà in quelle locazioni finché il sistema resta 2 Poiché i programmi non utilizzano contemporaneamente tutte le parti del codice e dei dati, la memoria associata al programma in esecuzione, ossia al processo, viene frammentata in unità di dimensione fissa, dette appunto pagine di memoria, che rappresentano il blocco minimo di memoria caricata dal disco verso la memoria centrale. Nel momento in cui serve un certo dato o viene eseguito un certo blocco di codice, viene caricata in memoria la pagina necessaria. In questo modo, occupando meno spazio, possono coesistere in memoria più programmi. 53 Panoramica delle risorse utilizzate dal framework attivo. Dal momento in cui viene caricato un LKM la memoria reale viene frammentata. Tuttavia un modulo necessita che almeno la memoria virtuale sia contigua, pertanto Linux usa la vmalloc per allocare un’area di memoria virtuale contigua nello spazio di indirizzamento del kernel. Anche questa memoria, comunque, non è soggetta al paging, rimanendo in queste locazioni fino alla rimozione del modulo. Come risultato di ciò, l’intero kernel di base può essere coperto da una sola entry nella page table e quindi nel TLB3 . Viceversa ad ogni LKM è associata una pagina differente, e quindi non tutti possono trovarsi contemporaneamente nel TLB. Conseguenza di ciò è un accesso leggermente più lento ai moduli piuttosto che al kernel di base. In realtà, però, questo calo di performance era percepibile con la tecnologia contemporanea all’introduzione del meccanismo dei LKM in Linux 1.2. Con i tempi di accesso alla memoria odierni il miss nel TLB comporta un calo di prestazioni, di fatto, non percepibile. Un secondo problema concerne invece la sicurezza. Il kernel ha un proprio spazio di indirizzamento, siccome un modulo consiste in codice che viene dinamicamente inserito o rimosso dal kernel, esso ne condivide lo spazio di indirizzamento (invece di averne uno proprio). Pertanto la segmentation fault 4 di un modulo provoca la segmentation fault del kernel. Oltre a creare problemi di sicurezza in maniera non maliziosa, un modulo kernel può essere utilizzato in maniera malevola per evitare la detection di processi o di file su un sistema compromesso. Linux pertanto consente di disabilitare il meccanismo LKM. Il sistema initramfs carica al boot solo i moduli necessari e poi disabilità questa funzione. 3 Il TLB (Translation Lookaside Buffer) è una tabella mantenuta dalla MMU (Memory Management Unit) integrata sul chip del processore. Nel TLB vengono memorizzate le entry della page table utilizzate più recentemente 4 La segmentazione è un approccio alla gestione della memoria e alla sua protezione in un sistema operativo. Essa è stata superata dal paging, ma gran parte della terminologia della segmentazione è ancora utilizzata, primo tra tutti lo stesso termine "errore di segmentazione". Un errore di segmentazione ha luogo quando un programma tenta di accedere ad una posizione di memoria alla quale non gli è permesso accedere, oppure quando tenta di accedervi in una maniera che non gli è concessa. 54 Panoramica delle risorse utilizzate dal framework Il codice sorgente di un loadable kernel module deve implementare necessariamente due funzioni: init_module : rappresenta la routine di inizializzazione (la entry function) che viene invocata al momento del caricamento del modulo nel kernel. Tipicamente registra degli handler nel kernel. cleanup_module : rappresenta la routine di pulizia che viene invocata come ultima operazione svolta dal modulo prima di essere rimosso in maniera sicura. A partire dalla versione 2.4 del kernel Linux è possibile nominare in altro modo le suddette funzioni, ed utilizzare le macro module_init e module_exit per specificare quali funzioni devono essere considerate come routine di inizializzazione e rimozione rispettivamente. Spesso è necessario passare dei parametri al modulo al momento dell’inserimento. A tale scopo è necessario dichiarare nel modulo delle variabili globali atte a contenere i parametri, ed utilizzare la macro module_param (o module_param_string come nel caso del modulo realizzato ai fini della tesi). A runtime la insmod, un’utility per il caricamento dei moduli descritta successivamente, riempirà queste variabili con gli argomenti passati ad essa a linea di comando. Un modulo kernel può comunicare con i processi in due modi: 1. attraverso il proc file system, sotto la directory /proc 2. attraverso i device file, sotto la directory /dev Il metodo utilizzato nell’ambito della tesi è la comunicazione tramite proc file system. 55 Panoramica delle risorse utilizzate dal framework obj-m += indirect_monitor.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean Figura 4.3: Codice del Makefile utilizzato 4.2.1 Compilazione di un LKM Un nuovo modulo, per poter essere aggiunto al kernel, deve essere preventivamente compilato. Un modo semplice ed efficiente di procedere è utilizzare il sistema di compilazione kbuild del kernel. Il codice del Makefile utilizzato è riportato in figura 4.3. Fra i prodotti della compilazione, il file di interesse sarà indirect_monitor.ko, ossia il modulo oggetto che verrà effettivamente caricato con il comando insmod . Un modulo kernel dopo la compilazione viene memorizzato nel file system sotto forma di file oggetto in formato ELF (Executable and Linkable Format). Un file oggetto in formato ELF è composto da varie sezioni delle quali la più importante, ai fini dell’utilizzo come LKM, è la .modinfo, visualizzabile con l’omonimo comando modinfo. In figura 4.4 è riportato l’output ottenibile dal comando modinfo per il modulo kernel del tool di monitoraggio indiretto. La insmod utilizza le informazioni contenute in questa sezione per formattare opportunamente i parametri, forniti a riga di comando alla insmod stessa, nelle strutture dati predisposte nel modulo (tramite macro come module_param, come già anticipato). Sempre in questa sezione sono riportati i dati di versioning. Queste informazioni vengono comparate con quelle del kernel in esecuzione prima dell’effettivo caricamento, in modo da prevenire problemi di compatibilità dovuti a moduli compilati per altre versioni del kernel. 56 Panoramica delle risorse utilizzate dal framework Figura 4.4: Output modinfo per il modulo kernel Il kernel inoltre tiene traccia dell’utilizzo di ogni modulo, cosicché esso non possa essere rimosso durante il suo utilizzo. Ovviamente esso considera soltanto i moduli caricati in RAM, da un comando come modprobe o insmod, e per essi alloca un’area di memoria atta a contenere: + il modulo oggetto; + una stringa rappresentante il nome del modulo; + il codice che implementa le funzioni del modulo. 4.2.2 Inserimento di un LKM nel kernel Il caricamento di un modulo in RAM può essere effettuato utilizzando due utilities: modprobe : legge la lista dei moduli, e delle loro dipendenze, dal file /lib/modules/$(uname -r)/modules.dep. Accetta come parametro il nome del file, con estensione .ko, cosi come è memorizzato in /lib/modules/$(uname -r). insmod : accetta come parametro il path del modulo in formato .ko, ed il modulo quindi non è vincolato a risiedere in /lib/modules/$(uname -r). Tuttavia esso non carica automaticamente anche le dipendenze. 57 Panoramica delle risorse utilizzate dal framework La differenza sostanziale consiste nel fatto che modprobe si preoccupa di caricare le dipendenze, se previste e nell’ordine in cui sono richieste, prima del caricamento di un modulo. Tuttavia al termine del calcolo delle dipendenze lo stesso modprobe invoca la insmod per il caricamento effettivo. Pertanto, non richiedendo alcuna dipendenza, ai fini della tesi verrà utilizzata l’utility insmod (in coppia con la relativa utility di rimozione, rmmod ). L’utility insmod [3] si appoggia all’utilizzo di system call 5 per la gestione dei moduli, allocazione di memoria e collegamento a moduli già esistenti, ed evolve nei seguenti passi: 1. Legge il nome del modulo che deve essere caricato. 2. Localizza il file contenente il codice oggetto del modulo. 3. Calcola la dimensione dell’area di memoria necessaria a memorizzare il codice del modulo, il suo nome, ed il modulo oggetto. 4. Invoca la system call create_module, per la gestione dei moduli. Questa chiamata serve a creare la entry del modulo nel kernel ed a riservare la memoria necessaria a contenerlo. Restituisce il kernel address al quale risiederà il modulo, mentre in caso di errore restituisce -1. 5. Utilizza la kernel symbol table, la module symbol table6 e l’indirizzo restituito dalla create_module per rilocare il codice oggetto incluso nel file del modulo. 5 Le system call sono funzioni built-in del kernel utilizzate per ogni operazione sul sistema In un linguaggio di programmazione un simbolo è un nome che rappresenta uno spazio di memoria, a cui può corrispondere sia una variabile sia una funzione. Una symbol table quindi non è altro che una lista contenente simboli con i relativi indirizzi di memoria. Nella kernel symbol table ci sono tutti i simboli esportati globalmente nello spazio kernel, con la macro EXPORT_SYMBOL. Essa risiede nella directory /proc/kallsyms. A sua volta anche un modulo ha la propria symbol table, la quale contiene i riferimenti esterni non risolvibili all’interno del modulo. 6 58 Panoramica delle risorse utilizzate dal framework 6. Alloca un’area di memoria in user space in cui carica una copia del modulo oggetto. 7. Invoca la system call init_module passandole l’indirizzo dell’area di memoria appena creata. Questa chiamata carica l’immagine rilocata del modulo in kernel space, ed esegue la funzione init_module definita dal modulo. 8. Rilascia la memoria in user space e termina. L’utility complementare, rmmod, che rimuove il modulo dal kernel, si articola nei seguenti passi: 1. Legge il nome del modulo che deve essere rimosso. 2. Invoca la system call delete_module per rimuovere tutte le entry non più utilizzate. Ritorna 0 in caso di successo, -1 viceversa. 4.3 Il proc file system (procfs) Il proc file system è uno speciale file system progettato originariamente per consentire un facile accesso alle informazioni inerenti i processi, da cui il nome della directory sotto cui risiede, ossia /proc. Nel proc file system sono presenti delle directory numerate, il cui numero corrisponde ad un identificativo di processo (PID) attivo. All’interno di queste directory ci sono sotto-directory, file e link simbolici che racchiudono le informazioni con granularità di processo. Tuttavia, data la sua utilità, ad oggi il proc file system viene utilizzato da molti elementi del kernel che abbiano qualcosa da segnalare, un esempio è la sotto-directory /proc/modules, in cui sono memorizzati moduli attivi nel kernel. 59 Panoramica delle risorse utilizzate dal framework Oltre a contenere informazioni prelevabili dallo user space, il procfs può anche essere usato per modificare alcuni parametri del kernel a runtime, infatti, sebbene la maggioranza dei file sia in sola lettura, ve ne sono alcuni accessibili in scrittura attraverso i quali è possibile cambiare delle variabili del kernel, allo scopo di rimodularlo. Possiamo quindi dire che il procfs funge da ponte fra il kernel e lo user space importando e/o esportando informazioni da e verso quest’ultimo. La caratteristica peculiare del procfs è di essere un file system virtuale, nel senso che esso non è associato ad alcun dispositivo a blocchi7 reale, ma esiste soltanto nella memoria centrale del sistema. Per questo motivo il procfs non contiene dei file “reali ”, bensì dei file virtuali , ossia informazioni sul sistema in esecuzione. Il procfs, dunque, ha la stessa dimensione della memoria fisica, ma non occupa lo stesso spazio poiché ognuno dei file è generato in tempo reale, al momento della sua effettiva richiesta. I virtual file, infatti, hanno tutti dimensione zero (fatta eccezione per i file kcore, mtrr e self ), a meno che non vengano copiati in un altro percorso esterno al procfs. Il motivo della dimensione nulla è che i virtual file non contengono alcun dato, ma agiscono soltanto come puntatori, ad esempio, alla reale locazione delle informazioni da esportare. In particolare il procfs lavora con delle call-back functions, ossia delle apposite funzioni che vengono richiamate all’atto di una specifica operazione sul file. Ad esempio all’atto di una lettura di un file viene richiamata la call-back function per la lettura. Il mapping fra le call-back e le operazioni per le quali vengono chiamate, avviene assegnando i campi di una particolare struttura, la struct file_operation. Un’istanza della struttura file_operation contiene, quindi, dei puntatori a funzioni che eseguono varie operazioni sui device. Pertanto è necessario definire il mapping dei campi manualmente. Ogni campo della struttura corrisponde all’indirizzo di una funzione definita dal 7 Un dispositivo a blocchi nei sistemi UNIX è uno speciale file che rappresenta una periferica. Un esempio di dispositivo a blocchi su un sistema basato su kernel Linux è il dispositivo /dev/sda che rappresenta un hard disk. 60 Panoramica delle risorse utilizzate dal framework static const struct file_operations indirect_monitor_fops = { .owner = THIS_MODULE, .open = indirect_monitor_open, .read = seq_read, .llseek = seq_lseek, .release = single_release, }; Figura 4.5: Struttura file_operation del modulo indirect_monitor.ko modulo per gestire la corrispondente operazione. Un esempio di struct file_operation, secondo la sintassi definita dalla revisione C99 [1] del linguaggio C, è riportato in figura 4.5. La sintassi C99 è raccomandata per motivi di compatibilità e portabilità. Con riferimento alla figura 4.5, bisogna specificare il campo owner come this_module se il procfs è utilizzato attraverso un modulo kernel. Il codice riportato assegna solo alcuni dei campi della struttura, tutti gli altri campi non esplicitamente assegnati verranno inizializzati a null dal compilatore gcc. Ogni volta che il modulo verrà aperto, il kernel richiama la funzione associata al campo open, indirect_monitor_open. 4.3.1 Creazione di una entry del procfs tramite LKM È possibile utilizzare il meccanismo degli LKM, descritti nella sezione 4.2, per aggiungere un virtual file al procfs. Per poterlo fare è necessario includere la libreria linux/proc_fs.h. Il procfs può contenere regular file, symlink e device, ma nell’ambito di questo lavoro di tesi il virtual file creato sarà un regular file. Il meccanismo per utilizzare il procfs prevede la creazione di una struttura, un’istanza della struct proc_dir_entry, contenente tutte le informazioni di cui ha bisogno un virtual file del procfs (fra queste, il campo proc_fops contenente l’istanza della struttura 61 Panoramica delle risorse utilizzate dal framework file_operation con il mapping delle call-back functions). La routine di inizializzazione del modulo, la init_module, ha il compito di registrare la struttura nel kernel. Viceversa la routine di pulizia si occupa della sua rimozione. La funzione da utilizzare per la creazione di un virtual file è la create_proc_entry, di cui si riporta la firma: struct proc_dir_entry* create_proc_entry(const char* name, mode_t mode, struct proc_dir_entry* parent) Questa funzione accetta come parametri il nome del file da creare, con i relativi permessi ed il suo path (se la sua posizione deve essere proprio sotto /proc, allora basta passare il valore null). Il valore di ritorno sarà un puntatore alla struttura proc_dir_entry in caso di successo, null in caso di fallimento. Tuttavia bisogna notare che la funzione create_proc_entry è considerata deprecata [2], in quanto l’assegnamento del campo proc_fops della struttura proc_dir_entry che va a creare, è suscettibile dell’introduzione di race condition 8 , poiché rimane a null per un periodo di tempo non deterministico. La funzione consigliata per evitare questo problema è la proc_create: static inline struct proc_dir_entry *proc_create(const char *name, mode_t mode, struct proc_dir_entry *parent, const struct file_operations * proc_fops) Per l’eliminazione della entry nel procfs appena creata si ricorre alla funzione remove_proc_entry: void remove_proc_entry(const char *name, struct proc_dir_entry *parent) 8 Una race condition, o corsa critica, è una condizione che si verifica nei sistemi multi-processo quando il risultato di un’operazione dipende dalla temporizzazione con cui vengono eseguiti due o più processi, e pertanto il risultato non è deterministico. 62 Panoramica delle risorse utilizzate dal framework Di seguito sono riportati gli esempi di firme per le call-back di lettura e scrittura: int read_func(char* page, char** start, off_t off, int count, int* eof, void* data) int write_func(struct file* file, const char* buffer, unsigned long count, void* data) Tuttavia, a causa dell’organizzazione della memoria in Linux, c’è differenza fra le due operazioni. L’unico segmento di memoria accessibile da un processo è il proprio, e poiché i dati da scrivere vengono dallo user space, è quindi necessario importarli nel kernel space con la routine copy_from_user. Per la lettura l’analoga funzione copy_to_user non è necessaria perché i dati sono già in kernel space. 4.3.2 La API seq_file Creare un file del procfs può risultare piuttosto macchinoso, e per questo motivo dalla versione di Linux 2.4.13 esiste una API, chiamata seq_file, che aiuta nella composizione ed al contempo fornisce un’interfaccia più sicura con il procfs perché si preoccupa anche di prevenire situazioni di overflow per il buffer di output, solleva il programmatore dal dover gestire la dimensione dei buffer allineati sulla dimensione di pagina e in ultimo non fa uso di lock. Essa interpreta il contenuto di un virtual file come una “sequenza” di oggetti, che si può scorrere attraverso 3 funzioni: start, next e stop. La API inizia la sequenza in corrispondenza dell’operazione di lettura del virtual file. La prima funzione ad essere invocata è la start, che inizializza la posizione di un iteratore e restituisce un iteratore al primo oggetto di interesse. Fintantoché la funzione start non restituisce il valore null viene chiamata la funzione next, e da questa la funzione show, che si occupa dell’effettiva scrittura dei dati nel buffer che sarà letto dall’utente. La 63 Panoramica delle risorse utilizzate dal framework funzione next incrementa la posizione dell’iteratore e ritorna la posizione del successivo iteratore. La funzione show interpreta l’iteratore restituito dalla next e genera la stringa di output. Una sequenza termina quando la funzione next ritorna il valore null, momento in cui viene chiamata la funzione stop. Da notare che al termine di una sequenza ne inizia una nuova, per cui il ciclo di chiamate termina soltanto quando sarà la funzione start a restituire null. La API seq_file fornisce anche delle funzioni predefinite per il mapping con i campi della struct file_operation, come ad esempio le seq_read e seq_lseek utilizzate nel codice di figura 4.5. Tuttavia essa non fornisce nessuna funzione predefinita per le operazioni di scrittura. In ultimo, esiste una versione dell’interfaccia ulteriormente semplificata, che è quella utilizzata ai fini di questo lavoro di tesi. Utilizzando questa interfaccia un modulo può definire soltanto la funzione show, a cui viene affidato il compito di generare tutto il contenuto del virtual file. Il metodo open del file richiama una particolare funzione: int single_open(struct file* file, int (*show)(struct seq_file* m, void* v), void* data) Al momento della lettura del file viene quindi chiamata la funzione show attraverso la single_open. I valori passati alla single open saranno accessibili dai campi privati della struttura seq_file. In corrispondenza dell’utilizzo della single_open il campo release della struttura file_operation dovrebbe essere associato ad un’altra funzione predefinita della API, la single_release, in luogo della seq_release per evitare che si verifichi un memory leak 9 . 9 Un memory leak si verifica quando un’area di memoria diventa irraggiungibile. Se si alloca memoria con una chiamata alla malloc, ad esempio, e poi si distrugge il puntatore, prima della deallocazione, si perde ogni riferimento all’area di memoria. 64 Panoramica delle risorse utilizzate dal framework 4.4 La gestione dei processi in Linux Il processo, definito come programma in esecuzione, è una delle astrazioni fondamentali in un sistema operativo UNIX. Esso è caratterizzato da 4 sezioni logiche: 1. Area codice: contiene istruzioni del programma. 2. Area variabili globali: contiene i dati statici e le variabili globali. 3. Area stack: contiene i dati per le chiamate a funzione. 4. Area heap: contiene i dati allocati dinamicamente. Ogni processo può poi avere uno o più flussi di esecuzione (thread ). Ad ogni processo viene associato un process descriptor che contiene le informazioni utilizzate dal sistema operativo per tenere traccia del processo in memoria. Alcune delle informazioni contenute sono ad esempio il PID (Process IDentifier ) del processo, ossia un valore intero che lo identifica univocamente nel sistema, il suo stato, i puntatori ai descrittori del processo padre e dei processi figli. 4.4.1 La creazione dei processi All’avvio del sistema viene effettuata una copia del kernel in memoria centrale. Esso inizializza le proprie strutture dati, effettua alcuni controlli ed infine crea il processo init. In pratica, il kernel, dopo aver eseguito i propri compiti, cede il controllo ad init che porta a termine il processo di boot eseguendo diversi compiti di amministrazione, come il controllo dei file system, la pulizia della directory /tmp e l’avvio di altri servizi. Esso ha quindi il compito di condurre il sistema in uno stato operativo, avviando i programmi e servizi necessari. Il codice eseguito da tale processo è posizionato in /sbin/init, mentre il suo file di configurazione è in /etc/inittab [17]. 65 Panoramica delle risorse utilizzate dal framework Come conseguenza di quanto appena detto, init è l’unico processo che non viene allocato dinamicamente, ma viene definito staticamente, a tempo di build del kernel, col nome di init_task. Pertanto il suo PID è sempre uguale ad 1. extern struct task_struct init_task; In Linux nessun processo è indipendente, nel senso che ognuno ha un processo padre da cui viene generato, chiaramente eccetto init. Se il processo padre termina prima del processo figlio, quest’ultimo viene “adottato” da init, motivo per il quale esso è considerato il padre di tutti i processi in Linux. Un processo tuttavia non viene propriamente creato, bensì viene clonato a partire dal suo processo padre. Nello specifico la creazione di un nuovo processo avviene attraverso la system call fork , la quale crea un nuovo descrittore di processo clonando quello del processo che la invoca. Avviene quindi una duplicazione, almeno del codice del programma, mentre i dati e lo stack saranno specifici per ognuno dei due processi. La fork ritorna 0 nel processo figlio ed il PID del figlio (o il valore -1 in caso di insuccesso) nel processo padre. Oltre al codice vengono condivisi anche i file aperti, mentre lo spazio di memoria, poiché deve contenere le variabili utilizzate dal processo, è invece specifico e privato per ognuno di essi. Tuttavia il meccanismo di clonazione della memoria virtuale è leggermente più complicato. Vengono allocate le strutture dati relative, ma all’atto della clonazione nessuna delle memorie virtuali dei processi viene ancora copiata. Linux utilizza una tecnica chiamata “copy on write” che consiste nel copiare la memoria virtuale soltanto quando uno dei due processi (il nuovo o il clonato) tenta di scrivervi. Viceversa, se la memoria non viene scritta, rimarrà condivisa fra i due processi senza che si verifichi alcun danno. In questo modo pagine di memoria a sola lettura, come quella del codice eseguibile, 66 Panoramica delle risorse utilizzate dal framework saranno sempre condivise. Per rendere funzionante questo meccanismo le pagine di memoria su cui è possibile effettuare la scrittura devono avere le loro entry nella page table marcate comunque come a sola lettura. Quando un processo tenta di scrivere nella sua memoria virtuale si verificherà un page fault 10 ed a questo punto il sistema operativo opererà effettivamente una copia della memoria aggiornando le strutture dati e le page table dei due processi. Condividendo lo stesso codice, dopo la fork entrambi i processi andrebbero però ad eseguire le stesse istruzioni. Per cui lo step successivo è la chiamata ad una system call della famiglia exec, le quali consentono di sostituire il codice che un processo sta eseguendo con uno differente. Le system call del tipo exec non hanno valori di ritorno poiché, rimpiazzando il codice in esecuzione, un eventuale valore di ritorno non avrebbe modo di essere intercettato. A questo punto il nuovo descrittore viene inserito in una struttura dati, gestita dal sistema operativo, atta a contenere l’insieme di tutti i descrittori di processo: la task list. 4.4.2 La task list Per tenere traccia dell’insieme dei processi presenti nel sistema, il kernel Linux memorizza i loro descrittori in una lista circolare doppiamente linkata che prende il nome di task list. Un esempio di tale struttura dati è schematizzato in figura 4.6. La lista ha ovviamente uno spazio finito che determina il numero massimo di processi che possono esistere simultaneamente nel sistema. 10 Il page fault è un’eccezione generata quando un processo cerca di accedere ad una pagina che è mappata nello spazio di indirizzamento virtuale, ma non è ancora caricata nella memoria fisica. 67 Panoramica delle risorse utilizzate dal framework Figura 4.6: Esempio di lista circolare doppiamente linkata L’implementazione di questa lista segue un approccio standard impiegato in tutti i sorgenti del kernel, ed è dichiarata in linux/list.h come segue: struct list_head { struct list_head *next, *prev; }; Grazie all’applicazione di un approccio standard si possono utilizzare tutte le macro e le funzioni definite nel kernel appositamente per scorrere gli elementi di una lista così definita. Avendo PID uguale ad 1, il primo elemento della lista è sempre il descrittore del processo init. L’intera lista concatenata si può scorrere attraverso la macro for_each_process [14], definita in linux/sched.h, che è implementata per iterare sui descrittori dei processi nel seguente modo: #define for_each_process(p) \ for (p = &init_task ; (p = next_task(p)) != & init_task ; ) Quindi partendo dal task init e scorrendo la lista finché non si raggiunge nuovamente il descrittore di tale task si possono conoscere tutti i processi presenti correntemente nel sistema. 68 Panoramica delle risorse utilizzate dal framework Tuttavia può risultare computazionalmente molto dispendioso iterare su tutta la task list quando su un sistema ci sono molti processi. Per fare una cosa del genere ci deve essere un’ottima ragione e nessuna alternativa [15]. 4.4.3 La task_struct Ogni nodo della lista circolare dei processi rappresenta un descrittore di processo. La struttura dati utilizzata in Linux per l’implementazione dei descrittori di processo è la task_struct, definita in linux/sched.h. I campi della task_struct rappresentano il contesto del processo, ossia ciò che costituisce lo stato corrente di un processo, quindi registri, stack pointer e così via. Quando un processo viene sospeso il suo contesto viene salvato nella task_struct, e da quest’ultima viene prelevato al momento della riattivazione. Ogni struttura di tipo task_struct ha un campo di tipo list_head, detto tasks, che rappresenta la lista circolare dei processi presenti nel sistema cui il descrittore appartiene. La task_struct è una struttura relativamente grande, infatti su una macchina a 32 bit ha una dimensione di circa 1.7 Kilobytes. Tuttavia se si pensa che essa contiene tutte le informazioni di cui il kernel ha bisogno per gestire un processo, allora la sua dimensione è anche piuttosto ridotta. Siccome la maggior parte del codice in spazio kernel tratta direttamente con la task_struct è utile essere in grado di ottenere il puntatore alla struttura relativa al processo corrente. Nei kernel precedenti a quelli della serie 2.6 la task_struct veniva memorizzata alla fine del kernel stack 11 di ogni processo. Ciò permetteva ad architetture con pochi registri di calcolare la locazione del descrittore del processo tramite lo stack pointer senza usare registri per mantenere l’indirizzo di tale locazione. 11 Un processo utilizza due stack, uno nello spazio utente ed uno nello spazio kernel, con politiche di protezione di accesso alla memoria differenti. 69 Panoramica delle risorse utilizzate dal framework Tuttavia la task_struct non occupa spazio nel vero senso della parola siccome viene allocata al termine del kernel stack, che sarebbe ad ogni modo allocato e quindi non disponibile per altri scopi [6]. Ovviamente con questo meccanismo lo spazio realmente utilizzabile come stack viene decrementato della dimensione della task_struct. Il vantaggio è che il kernel per accedere a questa struttura non deve accedere in memoria, il cui uso viene quindi ridotto. La task_struct viene sempre allineata al limite di pagina in modo da mantenere l’allineamento anche nella cache. A livello software è possibile ottenere il puntatore alla task_struct del processo corrente grazie alla macro current, la cui implementazione dipende dall’architettura della macchina. In alcune architetture tale puntatore viene memorizzato in un registro per un accesso più efficiente. Proprio per la sua caratteristica di mantenere tutte le informazioni di cui il kernel è in possesso riguardo i processi presenti nel sistema, il tool di monitoring realizzato in ambito di questo lavoro di tesi attinge alla task_struct per il reperimento delle informazioni necessarie al monitoraggio con granularità di processo. I campi della struttura che sono stati utilizzati saranno descritti contestualmente alla descrizione dei componenti del tool nella apposita sezione 5.2.4. 70 Capitolo 5 Implementazione del framework In questo capitolo verranno descritti i dettagli implementativi del framework, specializzando l’architettura presentata nel capitolo 3 per un sistema operativo basato su kernel Linux. Nella prima parte verrà presentata l’implementazione dell’applicativo demone, mentre nella seconda parte verrà trattata quella del modulo kernel che si occupa di recuperare le informazioni dalle strutture del kernel. 5.1 Implementazione del demone L’attività svolta dal demone implementa la logica di inserimento e rimozione del modulo kernel, la logica di interfacciamento con il procfs e di parsing delle informazioni, ed infine la creazione di un albero di directory dove memorizzare le informazioni in un formato facilmente utilizzabile per la successiva effettuazione di test. Esso interpreta il file di configurazione per impostare gli input da passare al modulo ed ottiene da quest’ultimo le informazioni da storicizzare nel sotto-albero di directory. In figura 5.1 si riporta la specializzazione dell’architettura del framework. 71 Implementazione del framework di monitoring indiretto Figura 5.1: Architettura specializzata del framework 5.1.1 Parsing del file di configurazione Possiamo individuare nel codice del demone due diverse “macro-operazioni”. Nello specifico, come mostrato negli estratti di codice a seguire, la prima sequenza operativa è quella che implementa il parsing del file di configurazione. Il formato del file prevede che i parametri commentati (con il carattere ’#’) siano considerati come disabilitati, mentre quelli non commentati vengono presi in considerazione all’atto del parsing. Il file viene letto in un ciclo tramite la funzione C fgets, la quale legge una riga alla volta (fino al carattere terminatore ’\n’) posizionando i caratteri letti in un buffer. La funzione chiave che permette il parsing è la funzione strstr del C, che scorre i caratteri del buffer alla ricerca dei token che rappresentano le parole chiave desiderate. 72 Implementazione del framework di monitoring indiretto I primi due valori da impostare sono il numero di iterazioni, niter, che deve compiere il demone, ossia quante rilevazioni effettuare, ed il periodo che deve intercorrere fra di esse, refresh. Entrambi i valori vengono riconosciuti leggendo il buffer con la funzione scanf. Si è consentito di poter passare entrambi questi parametri al demone anche attraverso la linea di comando, modalità, quest’ultima, che come si vede dall’implementazione bypassa la configurazione tramite file. for(dim_config=0; fgets(buf, sizeof(buf), f_config); ) { if(strstr(buf, "Num_iterazioni")!= NULL){ sscanf(buf,"%*128[^0-9]%d%*32[^\n\t]", &niter); if(arg != NULL) niter = arg[0]; } else if(strstr(buf, "Refresh")!= NULL){ sscanf(buf,"%*128[^0-9]%d%*32[^\n\t]", &refresh); if(arg != NULL) refresh = arg[1]; } Il successivo parametro è una stringa che contiene l’elenco di tutti i comandi associati ai processi che si desidera monitorare. L’elenco viene letto da file utilizzando una virgola come carattere separatore fra i comandi. Alla fine della stringa viene inserito, come convenzione di separatore, il carattere ’@’. A partire da questo punto i caratteri restanti di questa stringa verranno interpretati in altro modo, ossia come sequenza binaria. else if(strstr(buf, "Task_cmd")!= NULL){ enable_ptr += sprintf(enable_string + enable_ptr," enable_string"); sscanf(buf,"%*128[^=] %256[^\n]", enable_string + enable_ptr); enable_ptr += strlen(enable_string) - enable_ptr; enable_string[enable_ptr++]=’@’; } A questo punto inizia il parsing degli indicatori prestazionali di cui si desidera la produzione. L’elenco dei parametri è conosciuto sia dal modulo kernel sia dal demone, 73 Implementazione del framework di monitoring indiretto ed entrambi vi anno accesso in ordine posizionale. Pertanto è possibile sfruttare una stringa binaria come convenzione per abilitare (1) o disabilitare (0) la produzione delle specifiche informazioni derivanti dall’i-esimo parametro. Come si vede dall’estratto di codice seguente, se il primo carattere letto dal buffer è il carattere ’#’, cioè il parametro è commentato, allora nella stringa enable_string, già contente la lista di comandi, viene inserito uno 0, viceversa viene inserito un 1. // Statistiche globali else if(strstr(buf, "System info")!= NULL){ while(strstr(buf, "End system info")== NULL){ fgets(buf, sizeof(buf), f_config); if(buf[0]!=’#’) enable_ptr += sprintf(enable_string + enable_ptr,"1"); else enable_ptr += sprintf(enable_string + enable_ptr,"0"); } enable_string[--enable_ptr]=’\0’; } // I restanti sono i parametri da monitorare else if(strstr(buf, "Task info")!= NULL){ while(strstr(buf, "End task info")== NULL){ fgets(buf, sizeof(buf), f_config); if(buf[0]!=’#’){ // Ultimo carattere del buffer è la \n ... che viene eliminata buf[strlen(buf)-1]=’\0’; strcpy(info[dim_config++],buf); enable_ptr += sprintf(enable_string + enable_ptr,"1"); } else enable_ptr += sprintf(enable_string + enable_ptr,"0"); } enable_string[--enable_ptr]=’\0’; } } Risultato finale dell’operazione di parsing sarà una stringa composta da due parti, una prima contenente l’elenco dei processi da monitorare, separati da virgole, e la seconda contenente una stringa binaria che indica l’abilitazione/disabilitazione degli indici prestazionali da produrre. Le due parti sono separate dal carattere ’@’. 74 Implementazione del framework di monitoring indiretto 5.1.2 Parsing delle informazioni generate dal modulo Nella seconda sequenza operativa il demone genera un processo figlio a cui sarà affidato il compito di caricare il modulo di monitoring indiretto nel kernel. La tecnica per l’inserimento è quella di richiamare il comando insmod tramite la funzione execl messa a disposizione dalle API del linguaggio C. È importante che il demone generi un processo, quindi con la chiamata fork , e non un thread perché la execl sovrascrive il codice del processo con un altro. Pertanto se fosse richiamata da un thread, il quale condividerebbe l’area codice con il processo padre, il codice del processo verrebbe cancellato. Utilizzando un processo ex novo si evita questo problema. La execl ritorna soltanto in caso di errore, per cui se ciò si verifica il processo figlio termina con una chiamata alla _exit con codice di uscita exit_failure. In caso di valore di ritorno della fork pari a -1, il demone stampa un errore a video con la funzione perror. In condizioni di buon esito della generazione del processo figlio, il processo demone può continuare le sue operazioni. Per far ciò necessita dei valori prodotti nel procfs dal modulo appena inserito, per cui la prima cosa che fa il demone è quella di testare in polling, ossia con una verifica ciclica, l’avvenuto caricamento del modulo. Il polling è implementato tramite un ciclo while da cui il processo esce solo quando la condizione diventa falsa. int insert = fork(); if(insert==0){ execl("/sbin/insmod","insmod", module_path, (char*)enable_string, NULL); perror("[ERR] Errore nell’esecuzione della execl"); _exit(EXIT_FAILURE); } else if(insert==-1) perror("[ERR] Errore nella generazione del processo figlio"); // Continuo processo padre else { while( (fp = fopen("/proc/indirect_monitor","r")) == NULL ); 75 Implementazione del framework di monitoring indiretto ... Il demone a questo punto prosegue con il ciclo principale di letture dalla nuova entry creata nel procfs tramite il modulo appena inserito. Quindi effettua il parsing delle informazioni messe a disposizione dal modulo, riconoscendo messaggi di errore e le statistiche, con granularità di sistema e di processo, relative ai parametri abilitati. Il ciclo più esterno è quello controllato dai parametri niter e refresh. Per convenzione, un valore di niter uguale a zero indica un ciclo senza fine, condizione che viene verificata nella condizione di terminazione del ciclo for. La funzione usleep regola l’intervallo temporale fra due attivazioni successive del ciclo. Essa accetta un valore in microsecondi, mentre il parametro refresh adotta per semplicità un valore in millisecondi, per cui viene moltiplicato per mille. Il ciclo for più interno implementa il parsing vero e proprio con la stessa tecnica adottata precedentemente, ossia sfruttando la funzione fgets per leggere il virtual file per righe, e la funzione strstr per il riconoscimento dei token. Il ciclo è in grado di riconoscere: + stringhe di errore, indicanti situazioni di fallimento verificatesi nel modulo. + statistiche delle interfacce di rete. + statistiche sul carico di sistema. + il comando relativo al processo monitorato, che verrà usato per la creazione delle sotto-directory in cui memorizzare i file contenenti i dati di monitoraggio. Ogni file è nominato attraverso il timestamp di sistema per ottenere un ordinamento temporale. + il parametro da monitorare secondo un ordine posizionale, scandito dalla variabile k in un array contenente l’insieme dei parametri, info. 76 Implementazione del framework di monitoring indiretto for(int j=0; ((niter==0)?1:j<niter); j++){ for (int k=0; fgets(buf, sizeof(buf), fp); ){ if (strstr(buf, "[ERR]")!= NULL){ ... } else if (strstr(buf, "Network Interface")!= NULL) {...} else if (strstr(buf, "Load Avg")!= NULL) {...} else if (strstr(buf, "CmdLine")!= NULL) {...} else if (strstr(buf, info[k])!= NULL) {...} } usleep(refresh*1000); } Ogni ramo if contiene istruzioni analoghe, ossia effettua una scansione della stringa di buffer, apre il relativo file nell’albero di directory e scrive le informazioni. Per alleggerire la trattazione, nell’estratto di codice sono state omesse le istruzioni interne dei vari rami if. L’ultima sequenza di istruzioni, successiva al ciclo principale, è la creazione di un nuovo processo per la rimozione del modulo dal kernel, che analogamente al caso precedente utilizzerà la execl per invocare il comando rmmod . Allo stesso modo, il demone controlla in polling l’avvenuta rimozione della entry dal procfs. 5.2 Implementazione del modulo kernel Il modulo implementa la logica di raccolta dei dati dalle strutture del kernel esportandoli in un nuovo file generato nel file system virtuale procfs. Il modulo, per conoscere i task e gli indicatori da monitorare, prende in input un parametro dal demone all’atto dell’inserimento nel kernel. Questo parametro è rappresentato dalla stringa composta dalla concatenazione dei comandi associati ai processi da monitorare, e della stringa binaria di abilitazione delle funzioni. Il meccanismo offerto dal kernel per il passaggio dei parametri ad un modulo non utilizza le classiche variabili argc ed argv, bensì prevede di dichiarare delle variabili con 77 Implementazione del framework di monitoring indiretto visibilità globale e l’utilizzo di apposite macro, definite in linux/moduleparam.h. A tempo di esecuzione la insmod riempirà le variabili con gli argomenti passatile a riga di comando. In particolare, poiché l’argomento da scambiare è una stringa, la macro utilizzata nel modulo è la module_param_string. Essa prevede 4 parametri che sono, nell’ordine, il nome usato esternamente per il parametro, quello usato internamente al modulo, la dimensione della stringa, ed i permessi. module_param_string(enable_string, en_str, ENABLE_SIZE, 0); MODULE_PARM_DESC(en_str, "Stringa di abilitazione delle funzionalità del modulo"); Il modulo kernel utilizza le apposite API per la creazione di una entry nel procfs, come descritto più dettagliatamente nel capitolo precedente. Nello specifico la funzione di inizializzazione del modulo invoca la funzione proc_create, per la creazione della entry. Questa funzione associa alla entry la struttura di tipo file_operation che implementa il mapping tra le operazioni possibili sul nuovo file creato e le funzioni da invocare al verificarsi di tali operazioni. static int indirect_monitor_open(struct inode *inode, struct file *file){ return single_open(file, indirect_monitor_show, NULL); } static const struct file_operations indirect_monitor_fops = { .owner = THIS_MODULE, .open = indirect_monitor_open, ... }; static int __init indirect_monitor_init(void) { proc_create("indirect_monitor", 0, NULL, &indirect_monitor_fops); return 0; } All’operazione di apertura del file viene associata la funzione indirect_monitor_show, che in pratica funge da funzione main, in quanto richiama tutte le funzioni atte a rica78 Implementazione del framework di monitoring indiretto vare i dati di monitoraggio. Solo in occasione della prima apertura del file richiama la funzione split_enable_string, la quale ritorna la posizione del carattere ’@’ che funge da separatore fra la lista di comandi e la stringa binaria. In questo modo utilizzando una variabile intera con post-incremento è possibile scorrere posizionalmente la stringa binaria e, con un semplice controllo if, invocare o meno la i-esima funzione responsabile della produzione dell’i-esimo dato. Come esempio sono mostrate le invocazioni di due funzioni nell’estratto di codice seguente. L’ultima operazione della funzione “main” è quella di ottenere statistiche con granularità di processo per ognuno dei processi il cui pid è stato salvato dalla apposita funzione find_all_pids nel vettore pid_array. static int indirect_monitor_show(struct seq_file *m, void *v){ ... if(first_exec==1){ first_exec=0; separatore = split_enable_string(m,v); } iterator = separatore; find_all_pids(m,v); ... if(en_str[iterator++]==’1’) get_load_avg(m,v); if(en_str[iterator++]==’1’) get_mem_info(m,v); ... for(i=0; pid_array[i] > 0; i++){ if(get_task_stats(m, v, pid_array[i], iterator) == -1){ ... } La funzione di splitting effettua una tokenizzazione della stringa di input fino al carattere ’@’, sostituendo alle virgole fra i comandi il carattere terminatore di stringa, ’\0’. In questo modo la strcpy arresterà la lettura e salverà, nel vettore cmd, un comando alla volta. Infine effettua una correzione della seconda parte della stringa se i valori di quest’ultima non sono binari come ci si attende. 79 Implementazione del framework di monitoring indiretto static int split_enable_string(struct seq_file *m, void *v) { int i=0, j=0, index=0; for(j=i ; en_str[i]!=’@’; i++){ if(en_str[i]==’,’){ en_str[i]=’\0’; strcpy(cmd[index++],en_str+j); j=i+1; } } en_str[i]=’\0’; strcpy(cmd[index++], en_str+j); index = i+1; for( ; i<strlen(en_str); i++){ if(en_str[i] != ’1’) en_str[i]=’0’; } return index; } Una seconda funzione di fondamentale importanza per il funzionamento del modulo è la find_all_pids. Essa si occupa di ricavare i PID dei processi da monitorare a partire dalla stringa di comando loro associata, in quanto quest’ultima è l’unica informazione in possesso dell’utente. Il ciclo while più esterno itera sul numero di comandi da monitorare, salvati dalla precedente funzione nel vettore cmd. Il ciclo interno è invece una macro che consente l’iterazione su tutti i processi presenti sul sistema, e già descritta nel dettaglio nel capitolo precedente. Questa funzione quindi utilizza la macro per scorrere tutti i processi e confrontare il campo comm della task_struct di ogni processo con la stringa fornita dall’utente. Se c’è matching allora il processo è quello da monitorare, ed il suo PID viene salvato, se possibile, nell’apposito vettore. static void find_all_pids(struct seq_file *m, void *v) { ... while(strlen(cmd[++j]) > 0) { rcu_read_lock(); for_each_process(task) { num_task++; 80 Implementazione del framework di monitoring indiretto temp = kcalloc(64, sizeof(char), __GFP_NOFAIL); task_lock(task); strncpy(temp, task->comm, sizeof(task->comm)); pid = task->pid; task_unlock(task); if(strcmp(temp, cmd[j])==0){ if(i<N) pid_array[i++] = pid; else seq_printf(m,"\n[ERR] [pid_array] Spazio non disponibile per monitorare il task %d\n",pid); } kfree(temp); } rcu_read_unlock(); } } 5.2.1 Gestione delle strutture dati nel kernel Un campo molto importante, comune a molte strutture dati che implementano oggetti e risorse del kernel, è una variabile di tipo atomic_t. La dichiarazione del tipo atomic_t è la seguente: typedef struct { volatile int counter; } atomic_t; Il campo rappresenta un contatore, in particolare conta il numero di riferimenti alla struttura attualmente presenti nel sistema. Tale valore indica, quindi, il numero di processi che detengono la risorsa. Ad esempio la struttura mm_struct ha un campo di questo tipo, mm_count, che indica il numero di processi che utilizzano un riferimento alla struttura. Una variabile di tipo atomic_t è leggibile e modificabile attraverso apposite funzioni definite nel kernel, come ad esempio atomic_read o atomic_set, che si occupano della lettura e dell’assegnazione della variabile racchiusa nella struttura. 81 Implementazione del framework di monitoring indiretto Quando il processo clonato deve condividere una risorsa piuttosto che averne una nuova, il campo di tipo atomic_t della risorsa viene incrementato in modo che il sistema operativo non deallochi la risorsa fintantoché quest’ultima abbia ancora processi che ne fanno uso. 5.2.2 Statistiche delle interfacce di rete In un sistema operativo basato su kernel Linux ogni transazione di rete è effettuata attraverso l’uso di un interfaccia, che rappresenta un dispositivo in grado di scambiare dati con altri host. Tipicamente un’interfaccia è un dispositivo hardware, tuttavia esiste anche un’interfaccia puramente software, ossia l’interfaccia di loopback 1 . Un interfaccia di rete consente di inviare e ricevere pacchetti dati, e viene gestita dal sottosistema di rete del kernel. Esse non sono a conoscenza dei dettagli di connessione, né tengono traccia di flussi dati, permettono soltanto di gestire il traffico con granularità di pacchetto. La modalità con cui un sistema Unix-like fornisce l’accesso alle interfacce di rete è assegnando loro un nome univoco, come ad esempio lo per l’interfaccia di loopback, o eth0 per la scheda di rete primaria. Per la comunicazione con le interfacce di rete, il kernel utilizza delle funzioni specifiche per la trasmissione di pacchetti, in luogo delle classiche funzioni di lettura/scrittura usate per gli altri device. La seguente funzione, get_net_device_stats, consente di ricavare le statistiche di utilizzo delle interfacce presenti sul sistema. A tale scopo partendo dalla macro current, che restituisce un riferimento alla task_struct del processo correntemente in esecuzione, e seguendo una gerarchia di puntatori attraverso diverse funzioni, si ottiene un 1 L’interfaccia di loopback viene utilizzata nelle reti TCP/IP per identificare la macchina locale. Le comunicazioni tra programmi su TCP/IP prevedono la conoscenza dell’indirizzo IP dell’Host remoto. Gli Host hanno un indirizzo IP per ogni connessione alla rete Internet, quindi un Host non connesso alla rete non avrebbe nessun indirizzo IP. Di conseguenza, sarebbe impossibile per i programmi che sono in esecuzione su un Host non connesso colloquiare tra loro attraverso la rete. Per ovviare a questo problema, è stata creata una interfaccia “fittizia”, chiamata appunto interfaccia di loopback, presente su tutti gli Host TCP/IP anche quando non sono presenti altre interfacce di rete reali. 82 Implementazione del framework di monitoring indiretto riferimento ad una struttura di tipo net. Le funzioni get servono all’incremento del contatore atomico delle strutture per prevenire la deallocazione prematura, come descritto precedentemente. La struct net contiene il puntatore alla struttura net_device che rappresenta l’interfaccia di loopback, la prima interfaccia di una lista ordinata che le contiene tutte. Tale elenco si può scorrere attraverso la funzione next_net_device, che restituisce un riferimento alla successiva interfaccia di rete nella lista. Avendo a disposizione un riferimento iniziale ed un meccanismo per scorrere la lista, la consultazione di tutte le interfacce si può implementare con un ciclo for, come si vede dall’estratto di codice seguente. Dalla struttura net_device, si può ottenere un ulteriore riferimento ad una struttura che contiene i dati relativi alle statistiche richieste, ossia: + Pacchetti trasmessi e ricevuti; + Byte trasmessi e ricevuti; + Errori in trasmissione e ricezione; + Pacchetti scartati in trasmissione e ricezione; + Numero di collisioni. static int get_net_device_stats(struct seq_file *m, void *v){ ... task = current; if(task != NULL){ ns = task_nsproxy(task); if(ns != NULL){ get_nsproxy(ns); net = get_net(ns->net_ns); } } ... } 83 Implementazione del framework di monitoring indiretto else{ for(dev=net->loopback_dev; dev!=NULL; dev=next_net_device(dev)){ atomic_inc(& dev->refcnt); net_stats = dev->netdev_ops->ndo_get_stats(dev); seq_printf(m, "\n%s\n-----\nRx_Packets: %lu\nTx_Packets: %lu\n Rx_Bytes: %lu\nTx_Bytes: %lu\nRx_Errors: %lu\n Tx_Errors: %lu\nRx_Dropped: %lu\nTx_Dropped: %lu\n Multicast: %lu\nN_Collisions: %lu\n", dev->name, net_stats->rx_packets, net_stats->tx_packets, net_stats->rx_bytes, net_stats->tx_bytes, net_stats->rx_errors,net_stats->tx_errors, net_stats->rx_dropped, net_stats->tx_dropped, net_stats->multicast, net_stats->collisions); atomic_dec(& dev->refcnt); } ... } 5.2.3 LoadAVG: il carico di sistema Tipicamente il carico medio di sistema viene rappresentato in Linux attraverso 3 valori numerici, ai quali ci si riferisce come load avg , indicanti, nell’ordine, il carico medio negli ultimi 1, 5 e 15 minuti. Ciò significa che, leggendoli in tale ordine, si può esaminare la tendenza dello stato in cui il sistema versa. Tale stato è un indicatore del carico di CPU, da non confondere con la percentuale di utilizzo della stessa. Infatti il valore load avg misura esclusivamente il carico sulla CPU, non tenendo in considerazione processi che sono in uno stato qualsiasi di attesa. Load avg e percentuale di CPU sono quindi concettualmente differenti. La percentuale di CPU indica la quantità di tempo in cui un processo è attivo sulla CPU rispetto all’intervallo di campionamento, ossia il rapporto fra il tempo di attività sulla CPU ed il tempo totale di osservazione. Il load avg differisce dalla percentuale di CPU principalmente per due motivi [25]: 84 Implementazione del framework di monitoring indiretto 1. Esso non fornisce solo un valore istantaneo, ma misura una tendenza nell’utilizzazione della CPU. 2. Il suo valore è comprensivo di tutte le richieste di CPU, e non soltanto quanto quest’ultima è attiva al momento della rilevazione. In definitiva i valori di load avg forniscono una panoramica migliore dell’effettiva situazione del “traffico” sul sistema, e non soltanto quanto spesso un processo occupa la CPU. La situazione ideale sarebbe quella in cui la CPU sia sempre occupata e non ci siano processi in attesa. Un computer in stato di idle, ossia senza processi da schedulare, ha un valore di load avg pari a 0. Ogni processo appartenente alla run-queue o alla ready-queue, ossia un processo in esecuzione o in attesa della CPU incrementa di 1 il valore del carico. Linux ai due precedenti aggiunge anche i processi nello stato uninterruptible sleep. Per ottenere il valore del load avg, il kernel applica periodicamente (ogni 5 secondi) una media esponenziale mobile al numero corrente di task attivi ed ai loro precedenti valori. In questo modo le misurazioni recenti hanno un peso maggiore, mentre quelle storiche un peso sempre minore al passare del tempo. Per un sistema a singola CPU il load avg corrispondente ad un’utilizzazione del 100% è 1.00. Se uno dei valori è pari, ad esempio, a 3.95, ciò sta ad indicare che, potendo eseguire un solo processo alla volta, si ha un sovraccarico del 295%, poiché ci sono mediamente 2.95 processi in coda. Tuttavia il valore dei load avg non va interpretato in maniera assoluta, bensì in relazione al numero di core della macchina. Infatti, lo stesso valore di carico precedente, 3.95, per una macchina con 4 nuclei di elaborazione non indica sovraccarico, perché ognuno dei processi mediamente in coda può essere eseguito su uno dei 4 core. In questo caso il valore di carico che indica l’utilizzazione del 100% è pari a 4.00. In generale su una 85 Implementazione del framework di monitoring indiretto macchina ad N core, il carico a piena utilizzazione sarà N.00. static void get_load_avg(struct seq_file *m, void *v) { unsigned long loads[3]; get_avenrun(loads, FIXED_1/200, 0); seq_printf(m,"\nLoad Avg 1min - 5min - 15 min\n%lu.%02lu %lu.%02lu %lu.%02 lu\n", LOAD_INT(loads[0]), LOAD_FRAC(loads[0]), LOAD_INT(loads[1]), LOAD_FRAC(loads[1]), LOAD_INT(loads[2]), LOAD_FRAC(loads[2])); } Il codice dell’estratto precedente indica la funzione del modulo che richiama la get_avenrun del kernel per ottenere i valori di load avg. 5.2.4 Statistiche con granularità di processo Oltre alle statistiche globali come quella per il carico di sistema o quelle per le interfacce di rete, il framework fornisce anche statistiche con granularità di processo, ossia informazioni circa l’utilizzazione percentuale di risorse come la CPU o la memoria, da parte del singolo processo. Per rappresentare lo stato di esecuzione di un processo, la task_struct utilizza una variabile di tipo long. Il valore assegnato a questa variabile può essere uno fra i valori definiti dalle seguenti macro: #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_STOPPED 4 #define TASK_TRACED 8 /* in tsk->exit_state */ #define EXIT_ZOMBIE 16 #define EXIT_DEAD 32 /* in tsk->state again */ #define TASK_NONINTERACTIVE 64 86 Implementazione del framework di monitoring indiretto Tali macro possono essere usate per definire sia gli stati di esecuzione che gli stati di terminazione dei processi. Il valore TASK_RUNNING indica che il processo è eseguibile, quindi o in esecuzione oppure in una coda pronto per essere eseguito. Lo stato TASK_INTERRUPTIBLE indica che il processo è sospeso in attesa che si verifichi una condizione. Quando questa condizione si presenta passa nello stato TASK_RUNNING. Può altresì transitare in questo stato se riceve un segnale che lo risveglia. Lo stato TASK_UNINTERRUPTIBLE è identico al precedente eccetto per il fatto che non può essere risvegliato da alcun segnale. Siccome il task non risponde ai segnali non è possibile inviare nemmeno il segnale di terminazione SIGKILL. A causa di questa limitazione, salvo casi particolari, questo stato è meno usato del precedente. La macro TASK_STOPPED invece fa riferimento ad un processo che ha terminato la propria esecuzione e non è più schedulabile per tornare ad essere eseguibile. Lo stato di uscita ZOMBIE si presenta quando il processo termina ma il processo padre non ha ancora terminato la sua esecuzione e pertanto il descrittore in questo caso non viene deallocato fino alla terminazione del processo padre, in quanto quest’ultimo potrebbe avere ancora dei riferimenti attivi e quindi potrebbe ancora accedervi. In ultimo, se non si presentano situazioni come la precedente, il processo può terminare in maniera ordinaria con codice di uscita DEAD. 5.2.4.1 Percentuale di utilizzo di CPU Siccome ogni processo può generare diversi thread per il calcolo della percentuale di CPU occorre sapere quanto tempo ognuno di essi passa in esecuzione. A tale scopo si fa uso della funzione next_thread, che restituisce il riferimento alla task_struct del successivo thread in una lista circolare, abbinata ad un ciclo di tipo do-while che termina quando si è visitata l’intera lista circolare tornando al task di partenza. 87 Implementazione del framework di monitoring indiretto Per il calcolo della percentuale di CPU utilizzata da ciascun processo si è fatto uso di 5 campi della task_struct: utime: rappresenta il tempo passato ad eseguire in user space, di tipo cputime_t. stime: rappresenta il tempo passato ad eseguire in kernel space, anch’esso di tipo cputime_t. real_start_time: rappresenta l’istante di avvio del processo in modalità boot based , di tipo timespec. La modalità boot based prevede che l’istante temporale rappresenti il tempo trascorso dal boot del sistema, di contro la modalità monotonic, memorizzata nel campo start_time della task_struct, esclude dal conteggio il tempo che il sistema passa in stato di sospensione. prev_utime: per convenzione si è utilizzato per memorizzare il valore di utime della precedente rilevazione. prev_stime: per convenzione si è utilizzato per memorizzare il valore di stime della precedente rilevazione. Poiché il valore di utime ed stime è crescente, con la differenza fra utime e prev_utime si riescono a prendere solo i cicli di esecuzione per la misurazione corrente. In questo modo salva nel vettore utime solo la variazione effettiva. Il codice che implementa quando descritto è riportato nell’estratto seguente. temp_task = task; do{ cputime_t tmp = cputime_zero; index_CPU = task_CPU(temp_task); tmp = cputime_sub(temp_task->utime, temp_task->prev_utime); temp_task->prev_utime = temp_task->utime; utime[index_CPU] = cputime_add(utime[index_CPU], tmp); /*** codice analogo per stime ... ***/ num_thread++; 88 Implementazione del framework di monitoring indiretto Figura 5.2: Esempio intervallo temporale ... temp_task = next_thread(temp_task); } while(temp_task != task); Data la natura di file system virtuale del procfs, poiché le informazioni che esso fornisce non sono presenti in memoria, se non nel momento della loro effettiva richiesta, non è possibile implementare nel modulo il concetto di intervallo temporale. Pertanto, la cadenza temporale di aggiornamento, viene gestita attraverso il periodo di refresh nel demone. Il modulo, basandosi su questa logica, calcola l’intervallo temporale di possibile esecuzione per il task come differenza fra l’uptime, ossia il tempo corrente a partire dal boot, e il valore del campo real_start_time. Il valore dell’uptime è strettamente crescente, mentre quello dello start time è fissato. Pertanto il valore di questa finestra temporale viene aggiornato tenendo traccia del valore dell’ultima finestra temporale, la quale viene sottratta a quella calcolata correntemente, per ottenere la sola variazione rispetto all’ultima lettura. Si è scelto di utilizzare questa metodologia in quanto, non utilizzandola, il valore calcolato per la percentuale di CPU sarebbe storico dallo start time, e quindi eventuali 89 Implementazione del framework di monitoring indiretto picchi, di sovra-utilizzo o di sotto-utilizzo, verrebbero ad essere mediati dalla storia esecutiva del processo. Un esemplificazione della tecnica è mostrata in figura 5.2. Alla prima lettura da procfs la finestra temporale corrisponderà alla differenza fra uptime e real_start_time, e questo valore sarà memorizzato in una variabile temporanea, time. Quindi il campo real_start_time della task_struct viene aggiornato con il valore di uptime, in modo che, alla successiva lettura, la differenza con il nuovo uptime fornisca l’effettiva finestra di possibile esecuzione. Alla seconda lettura il valore della finestra corrisponderebbe alla linea indicata in figura come Time from start, mentre la tecnica adottata prevede di sottrarle il valore precedente (Old time in figura) per avere come finestra soltanto la variazione temporale rispetto all’ultima lettura (New time in figura): tempo ef f ettivo = uptime − task_start_time Lo start time del task viene aggiornato ad ogni iterazione con il corrente valore dell’uptime, in modo da escludere dal tempo effettivo la durata degli intervalli delle precedenti iterazioni. // Calcola l’uptime do_posix_clock_monotonic_gettime(&uptime); monotonic_to_bootbased(&uptime); time = cputime_sub(timespec_to_cputime(&uptime), timespec_to_cputime(&task-> real_start_time)); // assegna il valore di uptime al campo start_time in modo da salvare l’ ultimo istante in cui si è calcolato task->real_start_time = uptime; ... for(i=0; i<n_CPU; i++) usage[i] = cputime_add(utime[i], stime[i]); for(i=0; i<n_CPU; i++) { seq_printf(m, " %lu.%02d ", usage[i]*100/time, (int)((usage[i]*100.0/time usage[i]*100/time)*100)); } 90 Implementazione del framework di monitoring indiretto I valori dei campi utime ed stime vanno insieme a comporre il tempo totale che il processo passa in esecuzione, nel nuovo vettore usage. Il vettore ha dimensione pari al numero di CPU, in modo che nell’i-esima posizione ci sia il tempo che il processo ha eseguito sulla i-esima CPU. Il rapporto, per ogni CPU, fra il tempo passato in esecuzione e la finestra temporale rappresenta la percentuale di CPU calcolata. Tuttavia è necessario resettare il valore dei campi del vettore usage, fornendogli ogni volta soltanto le variazioni dei valori dei campi utime ed stime rispetto a relativi campi prev. La motivazione è esemplificata in figura 5.3. Oltre al processo padre, infatti, all’ammontare del tempo di esecuzione contribuiscono tutti i thread da esso generati. Data la natura multi-core, o multiprocessore, delle moderne CPU, ci possono essere un numero di thread in esecuzione parallela sui diversi nuclei esecutivi a disposizione. Tuttavia lo scheduling dei thread non fornisce alcuna garanzia che un thread eseguirà sulla stessa CPU nel corso del tempo. Pertanto memorizzare i vecchi valori dei tempi di esecuzione funzionerebbe soltanto a patto di avere una CPU a singolo core, oppure utilizzando un valore medio cumulativo. Nel secondo caso però, se si presentasse una situazione di utilizzo fortemente sbilanciata fra le CPU essa non verrebbe notata a causa della media. Nel caso multi-core, mostrato in figura 5.3, abbiamo che all’istante 100 il thread 0 esegue sulla CPU 0, i thread 1 e 2 sulla CPU 1, il thread 3 sulla CPU 2, ed i thread 4 e 5 sulla CPU 3. I valori percentuali di CPU sono mostrati in figura. All’istante 200, invece, in virtù di una differente schedulazione, vediamo che nella CPU 0, mantenendo memoria del precedente tempo di esecuzione su tale CPU, il valore percentuale passa dal 2% al 13%. Tuttavia il valore cumulativo dell’utime del thread 1 fa in modo che questa lettura sia alterata, in quanto sulla CPU 0 è stato in esecuzione soltanto per un tempo pari alla variazione rispetto al precedente valore dell’utime. Caso limite si presenta sulla CPU 2, in cui il valore memorizzato è superiore a quello che si presenta correntemente, portando 91 Implementazione del framework di monitoring indiretto Figura 5.3: Bug utime multi-core 92 Implementazione del framework di monitoring indiretto la percentuale di utilizzo ad un impossibile valore negativo. In virtù di quanto detto, la memorizzazione dei precedenti valori viene effettuata per ogni singolo thread, invece che per CPU, sfruttando i campi prev della task_struct, mentre i contatori per CPU vengono resettati ad ogni aggiornamento, ricevendo soltanto le variazioni del tempo di esecuzione. Un’ultima nota sul calcolo della percentuale di CPU riguarda il fatto che nelle funzioni di stampa del kernel, come appunto la seq_printf, non è consentito l’uso di numeri in floating point, pertanto per ottenere valori più accurati si è utilizzata la seguente formula: usage[i] ∗ 100/time, (int)((usage[i] ∗ 100.0/time − usage[i] ∗ 100/time) ∗ 100) In altri termini, non potendo usare numeri in virgola mobile, il valore percentuale è stato costruito affiancando due valori interi, rappresentanti rispettivamente la parte intera e la parte decimale di quello che sarebbe il numero in floating point. La percentuale tipicamente è un valore compreso fra 0 e 1 che viene per convenzione moltiplicato per 100. In questo modo quindi è possibile ottenere la parte intera del valore. Per ottenere un numero intero che rappresentasse la parte decimale si è effettuata la sottrazione fra il valore originale in floating point e la sua parte intera. In questo modo resta un valore minore di 1. Moltiplicando quest’ultimo per 100 si ottiene un intero che rappresenta la parte decimale del valore in floating point originale. 5.2.4.2 La memoria virtuale La memoria virtuale di un processo contiene l’immagine eseguibile del programma caricato. Tale file contiene tutte le informazioni necessarie per caricare il codice eseguibile ed i dati ad esso associati nella memoria virtuale del processo. Inoltre i processi possono 93 Implementazione del framework di monitoring indiretto Figura 5.4: Struttura mm_struct allocare memoria (virtuale) da usare durante la loro esecuzione, per memorizzare dei dati di interesse. Per poter essere utilizzata, questa memoria appena allocata necessita di essere linkata nella memoria virtuale già esistente del processo. In ultimo i processi Linux utilizzano delle librerie contenenti codice di utilità. Ha poco senso che ogni processo abbia una propria copia di queste ultime, per cui Linux fa uso di librerie condivise in modo che essere possano essere utilizzate allo stesso tempo da diversi processi in esecuzione. Anche il codice di queste librerie deve essere linkato nella memoria virtuale di ogni processo che deve condividerla. In un dato periodo un processo non userà tutto il codice ed i dati contenuti nella sua memoria virtuale. Pertanto sarebbe svantaggioso caricare tutto il codice ed i dati nella memoria fisica se potrebbero non essere utilizzati, specie se questo svantaggio si ha per ognuno dei processi presenti. 94 Implementazione del framework di monitoring indiretto Linux, invece, utilizza una tecnica detta paginazione a domanda, secondo la quale pagine di memoria virtuale del processo vengono effettivamente trasferite in memoria fisica soltanto quando il processo le richiede. Per fare questo Linux altera la page table dei processi facendo in modo da marcare le aree di memoria virtuale come esistenti ma non in memoria. Quando un processo tenta di accedervi viene generato un page fault ed il kernel provvede al caricamento della pagina richiesta. Linux a questo punto tenta di verificare se l’indirizzo virtuale a cui si sta facendo riferimento è presente nello spazio di indirizzamento virtuale del processo. In caso affermativo, Linux crea la entry appropriata nella page table ed alloca memoria fisica per il processo. Il kernel Linux necessita di gestire tutte queste aree di memoria virtuale ed il contenuto della memoria virtuale di ogni processo è descritto da una struttura dati, la mm_struct (in figura 5.4) puntata dalla struttura dati del suo descrittore, la task_struct. In questa struttura sono memorizzate informazioni sull’immagine eseguibile del processo ed un puntatore alla page table. Inoltre contiene puntatori ad una lista di strutture di tipo vm_area_struct, che rappresentano le aree in cui è suddivisa la memoria virtuale del processo. Il seguente estratto riporta la lettura dei campi della mm_struct da parte del modulo kernel. // Raccoglie info sulla memoria si_meminfo(&s_info); ram = s_info.totalram*s_info.mem_unit/KB; vmrss = PAGES_TO_KB(atomic_long_read(&(mm->_file_rss)) + atomic_long_read(&( mm->_anon_rss))); if(en_str[iterator++]==’1’) seq_printf(m, "\n%%MEM: %lu.%d", vmrss*100/ram, (int)((vmrss*100.0/ram vmrss*100/ram)*100)); if(en_str[iterator++]==’1’) seq_printf(m, "\nVmSize: %lu Kb", PAGES_TO_KB(mm->total_vm)); if(en_str[iterator++]==’1’) seq_printf(m, "\nVmPeak: %lu Kb", PAGES_TO_KB(mm->hiwater_vm)); if(en_str[iterator++]==’1’) seq_printf(m, "\nVmRss: %lu Kb", vmrss); 95 Implementazione del framework di monitoring indiretto if(en_str[iterator++]==’1’) seq_printf(m, "\nRssPeak: %lu Kb", PAGES_TO_KB(mm->hiwater_rss)); if(en_str[iterator++]==’1’) seq_printf(m, "\nVmLocked: %lu Kb", PAGES_TO_KB(mm->locked_vm)); if(en_str[iterator++]==’1’) seq_printf(m, "\nVmData: %lu Kb", PAGES_TO_KB(mm->total_vm - mm->shared_vm - mm->stack_vm)); if(en_str[iterator++]==’1’) seq_printf(m, "\nVmStack: %lu Kb", PAGES_TO_KB(mm->stack_vm)); if(en_str[iterator++]==’1’) seq_printf(m, "\nVmExe: %lu Kb", ((PAGE_ALIGN(mm->end_code)-(mm->start_code & PAGE_MASK)) >> 10)); 5.2.4.3 File descriptor aperti e socket La struttura files_struct contiene, per ogni processo nel sistema, informazioni su tutti i file che esso sta utilizzando. Il kernel Linux fa in modo che ogni risorsa, sia essa lo standard input/output/error, sia essa un dispositivo reale, venga trattata come se fosse un file. Di conseguenza la trasmissione dati avviene tramite funzioni di lettura/scrittura su file. Ogni file è identificato da un proprio descrittore, e la struttura files_struct può contenere fino a 256 descrittori, ognuno relativo ad un file utilizzato dal processo in esame. In figura 5.5 è mostrata la struttura della files_struct. Nella struttura dati che implementa il descrittore di processo è presente un riferimento alla sua struttura files_struct. Da quest’ultima è possibile risalire alla struttura fdtable che contiene l’array di file descriptor correntemente attivi. Come riportato nell’estratto di codice seguente, il calcolo del numero di file descriptor utilizzati viene effettuato utilizzando un contatore, n_openfiles, ed un ciclo while. Finché esiste un descrittore nell’array fd in posizione n_openfiles, quest’ultima variabile viene incrementata. Il ciclo termina quando non esistono più descrittori nell’array, ed il valore del contatore, decrementato per l’ultimo ciclo a vuoto, rappresenta il numero di descrittori utilizzati dal processo corrente. 96 Implementazione del framework di monitoring indiretto Figura 5.5: Struttura files_struct if(task != NULL){ ... files = task->files; if(files != NULL){ ... files_table = files_fdtable(files); while(files_table->fd[n_openfiles] != NULL) n_openfiles++; n_openfiles--; ... } Utilizzando il valore di n_openfiles come condizione di terminazione di un ciclo for, il cui indice intero, fd, viene usato come identificativo per i file descriptor, si può verificare quale descrittore di file detenuto dal processo rappresenta una socket. Attraverso la funzione fcheck_files si ottiene il puntatore alla struttura file partendo dalla struttura files_struct e dal fd. A partire da questo riferimento, tramite la macro S_ISSOCK, si può determinare se il file rappresenta una socket o meno. Il numero di riscontri positivi 97 Implementazione del framework di monitoring indiretto viene memorizzato nella variabile contatore n_socket. for(fd=0, index=0; files && (fd < n_openfiles); fd++){ ... file = fcheck_files(files, fd); if(file != NULL){ ... if(S_ISSOCK(file->f_path.dentry->d_inode->i_mode)){ n_socket++; index += sprintf(temp+index,"-%d",fd); } } ... } 98 Capitolo 6 Risultati sperimentali Si è già detto in precedenza che fra i principali problemi da affrontare quando si effettuano attività di monitoring vi è la necessità di ridurre al minimo l’impatto che l’infrastruttura di monitoraggio ha sulle prestazioni del sistema da monitorare. Ricordiamo infatti che il sistema monitorato sarà dato da: sistema monitorato = sistema reale + monitor Tuttora è piuttosto difficile creare un tool di monitoraggio del profilo comportamentale che non introduca un eccessivo overhead durante la rilevazione. Inoltre è ancora più complesso trovare il miglior rate di campionamento per le rilevazioni a causa del compromesso fra accuratezza della misurazione ed overhead introdotto da questa stessa operazione. Il sesto ed ultimo capitolo di questo elaborato di tesi, quindi, si concentra sulla realizzazione di appositi test sul framework sviluppato. Nella prima parte del capitolo si applicherà il framework di monitoraggio al dimostratore del progetto Miniminds a disposizione, allo scopo di verificare il funzionamento sul caso reale che esso rappresenta. 99 Risultati sperimentali Nella seconda parte si effettuerà una valutazione dell’impatto prestazionale introdotto dal framework grazie all’uso di una suite di benchmark. 6.1 Scenario d’uso: il dimostratore Miniminds L’architettura logica del dimostratore prevede la presenza di due nodi appartenenti al dominio FDD (Flight Data Domain), comunicanti tramite la piattaforma Miniminds. Sul primo nodo gira un applicativo contributor , mentre sul secondo un applicativo manager . Questi due ruoli prevedono le seguenti operazioni: + contributor : consente la visualizzazione dei piani di volo, e la modifica nel caso dei voli creati da se stesso. + manager : è responsabile della creazione, gestione e pubblicazione di un volo. Inoltre è l’unico a poter far richiesta di handover e a poter rigettare eventuali modifiche apportate dal contributor. In uno scenario reale quindi, entrambi sono sottoscritti ad uno stesso dominio, quello FDD, ma girano su due istanze separate del middleware, ossia distribuite su due nodi di rete differenti. Tuttavia, nel caso del dimostratore a disposizione, entrambe le istanze sono in esecuzione sulla stessa macchina virtuale. Di conseguenza la comunicazione può avvenire sfruttando l’interfaccia di loopback piuttosto che un’interfaccia di rete reale. Lo scenario prevede le seguenti operazioni: 1. Avvio del dimostratore tramite linea di comando; 2. Inizializzazione delle istanze dei container JBoss; 3. Deploy degli adapter e di entrambi i processi applicativi; 100 Risultati sperimentali 4. Dopo la sottoscrizione al relativo dominio di appartenenza, l’applicazione manager crea un nuovo volo, sul quale avrà il ruolo di manager ; 5. Nello stesso intervallo di tempo anche il contributor, sempre previa sottoscrizione, ha facoltà di creazione di voli, sui quali esercita il ruolo di manager ; 6. Dopo un certo lasso di tempo il manager chiede l’handover al contributor, il quale dopo aver rilevato la presenza del volo, potrà accettare o meno la richiesta di handover ; 7. Una volta accettato, il contributor diventa a sua volta manager del volo, potendo aggiornarne lo stato o qualsivoglia operazione di competenza di un manager ; 8. Il manager, dopo averne ripreso il controllo al fine di pubblicare l’elenco dei voli, potrà a sua volta accettare o rigettare le modifiche apportate dal contributor ; 9. De-sottoscrizione di entrambe le applicazioni e shutdown delle istanze JBoss. La sequenza di operazioni di entrambi i nodi applicativi è scandita grazie ad un polling effettuato su un file di sincronizzazione ad hoc. 6.1.1 Descrizione dei test di iniezione Per verificare l’effettivo contributo apportato dal framework di monitoraggio indiretto alla piattaforma Miniminds, si sono iniettati dei guasti mirati nel codice del dimostratore il cui obbiettivo è quello di emulare casi reali di malfunzionamenti. Ciò è stato reso possibile grazie all’accesso al codice sorgente dei due nodi applicativi, che in questo caso specifico ha reso non necessaria una fase preliminare di profiling, altrimenti indispensabile per poter rilevare delle anomalie comportamentali. Inoltre, essendo le esecuzioni di manager e contributor sincronizzate attraverso polling su un file condiviso, i failure 101 Risultati sperimentali generati da un guasto iniettato nel contributor possono altresì propagarsi nel manager, anche come failure di natura differente. Una prima emulazione di guasto è stata ottenuta forzando una variabile contatore di ciclo ad assumere costantemente un valore che non permettesse la terminazione del ciclo stesso, conducendo il dimostratore in uno stato di hang. In particolare, essendo un ciclo di esecuzione attiva del contributor ad essere stato alterato, l’hang indotto nel contributor è un hang attivo. Come conseguenza del meccanismo di sincronizzazione fra i due applicativi, l’anomalia si è propagata nel manager, conducendolo in uno stato di hang passivo, in quanto, questa volta, il processo resta bloccato in uno stato di sospensione. La seconda emulazione di guasto è stata realizzata iniettando nel codice una forzatura di un valore di ritorno a Null. In questo caso il valore di ritorno imprevisto e non gestito ha causato il crash del contributor, ed analogamente al caso precedente, la propagazione ha provocato un hang passivo del manager. 6.1.2 Profilo comportamentale della golden run I profili di comportamento risultanti nei casi descritti precedentemente vengono messi a confronto con il profilo di una tipica esecuzione del dimostratore nel caso in cui non si verifichi alcuna anomalia. Il profilo di tale esecuzione viene quindi assunto come golden run. Nelle seguenti figure sono riportati i grafici relativi ai parametri monitorati dal framework, sia quelli con granularità globale, quindi riferiti all’intero sistema, sia quelli con granularità di processo. Prima di procedere con il confronto tra i grafici relativi alla golden run e quelli relativi alle esecuzioni con i guasti indotti tramite iniezione, si ricorda che il dimostratore lancia 4 processi principali, corrispondenti alle due istanze del container JBoss, ed 102 Risultati sperimentali Tabella 6.1: Caratteristiche della macchina testata CPU Host RAM Host S.O. Host Software di virtualizzazione CPU Guest RAM Guest S.O. Guest Intel Xeon E5-1620 @ 3.70 GHz 16 GB DDR3 Windows 7 Professional S.P.1 64 bit Oracle VirtualBox 4.3.6 4 core 2 GB CentOS 6 64-bit, kernel 2.6.32 alle due istanze delle applicazioni contributor e manager. Nell’ordine, ci si riferisce ad essi come JBossNode0, JBossNode1, LegacyNode0 e LegacyNode1. Pertanto in ogni figura ci saranno 4 grafici rappresentanti ognuno dei processi. In particolare il processo LegacyNode0 avrà come container il processo JBossNode0, discorso analogo vale per gli altri due processi. Inoltre, come riportato in tabella 6.1, la macchina virtuale ha 4 core, pertanto ognuno dei grafici di CPU presenterà 4 curve, come evidenziato nelle rispettive legende. Una premessa necessaria riguarda l’asse delle ascisse dei grafici. Poiché il monitoraggio è realizzato, come già descritto nel dettaglio nel capitolo apposito, attraverso la struttura dati che implementa il descrittore di processo, se un processo non è correntemente presente sul sistema allora il monitor non può tracciare alcun profilo per esso. In base a quanto detto, le ascisse dei grafici, indicanti il tempo, sono dimensionate sull’effettivo periodo per il quale il processo è stato presente sul sistema. 103 Risultati sperimentali Figura 6.1: Byte trasmessi sulle interfacce di rete Golden run Figura 6.2: Byte ricevuti sulle interfacce di rete Golden run 104 Risultati sperimentali Figura 6.3: Pacchetti trasmessi sulle interfacce di rete Golden run Figura 6.4: Pacchetti ricevuti sulle interfacce di rete Golden run 105 Risultati sperimentali Figura 6.5: Errori in trasmissione sulle interfacce di rete Golden run Figura 6.6: Errori in ricezione sulle interfacce di rete Golden run 106 Risultati sperimentali Figura 6.7: Pacchetti in trasmissione scartati dalle interfacce di rete Golden run Figura 6.8: Pacchetti in ricezione scartati dalle interfacce di rete Golden run 107 Risultati sperimentali Figura 6.9: Percentuale utilizzo CPU Golden run Figura 6.10: Numero di thread generati Golden run 108 Risultati sperimentali Figura 6.11: Context Switch Volontari Golden run Figura 6.12: Context Switch Involontari Golden run 109 Risultati sperimentali Figura 6.13: Major Page Fault Golden run Figura 6.14: Minor Page Fault Golden run 110 Risultati sperimentali Figura 6.15: Percentuale di memoria occupata Golden run Figura 6.16: Dimensione della memoria virtuale Golden run 111 Risultati sperimentali Figura 6.17: Memoria virtuale residente in RAM Golden run Figura 6.18: Valore di picco della memoria residente Golden run 112 Risultati sperimentali Figura 6.19: Dimensione dell’area dati del processo Golden run Figura 6.20: Dimensione della memoria virtuale su cui esiste un lock Golden run 113 Risultati sperimentali Figura 6.22: Numero di byte letti dal disco Golden run Figura 6.21: Numero di byte scritti su disco Golden run 114 Risultati sperimentali Figura 6.23: Numero di file descriptor utilizzati Golden run Figura 6.24: Numero di socket aperte Golden run 115 Risultati sperimentali Figura 6.25: Carico di sistema Golden run La figura 6.9, relativa alla percentuale di utilizzo di CPU nella golden run, mostra come nella prima parte del periodo di osservazione, localizzabile nei primi 50 secondi di esecuzione, i processi JBoss monopolizzino i nuclei di elaborazione. Ciò si traduce appunto in una percentuale di utilizzo che sfiora quasi sempre l’80%. Nell’intervallo di tempo che va da 50 a 130 secondi il profilo di utilizzo di CPU delle istanze JBoss scende quasi al minimo eccetto che per eventuali picchi, corrispondenti a particolari richieste effettuate dai nodi manager e contributor tramite i rispettivi container JBoss, come ad esempio la sottoscrizione al dominio di interesse. In questo intervallo, quindi, il controllo passa ai due processi applicativi. La percentuale di utilizzo dei suddetti processi applicativi è pressoché nulla, ed infatti entrambi passano la maggior parte del tempo in polling per la sincronizzazione. Gli unici picchi di CPU si hanno inizialmente, in corrispondenza delle operazioni di sottoscrizione al dominio e delle operazioni di creazione e/o pubblicazione dei voli. In seguito i processi relativi alle istanze JBoss tornano ad occupare tempo di CPU per 116 Risultati sperimentali le operazioni di rimozione e terminazione dell’esecuzione del dimostratore. La durata dell’esecuzione, e quindi del periodo di osservazione del monitor, è di circa 3 minuti e 30 secondi. Un riscontro di quanto detto si può avere analizzando il grafico del carico di sistema in figura 6.25, in particolare la curva in blu rappresentante il carico nell’ultimo minuto. Si può vedere che il carico nei primi 60 secondi è mediamente di 1,8 processi in procinto di essere schedulati. Tale valore di carico scende mediamente a 1,5 processi durante il periodo in cui il controllo viene ceduto ai processi manager e contributor. Ciò avviene perché il numero di thread che generano questi ultimi due processi è inferiore a quelli generati dalle istanze JBoss. Dopo la terminazione dei processi applicativi il carico tende a scendere ancora di più, ad un valore medio di un singolo processo. 6.1.3 Osservazione di un hang attivo In questa sezione si procede al confronto fra i profili di comportamento dell’esecuzione utilizzata come golden run e quelli osservati in caso di emulazione di hang attivo, allo scopo di verificare la capacità del framework di fornire elementi sufficienti alla sua rilevazione (da parte di un futuro componente di detection). Nelle figure 6.26, 6.27 e 6.28 invece sono riportati i grafici per la condizione di hang attivo indotta nel dimostratore. L’anomalia è di immediata rilevazione attraverso il profilo di utilizzo percentuale della CPU del processo contributor. Tuttavia risultano alterati anche i profili delle occorrenze di page fault e dell’utilizzo percentuale di memoria. Il grafico di utilizzo percentuale di CPU del processo contributor nella golden run (figura 6.9), infatti, mostrava un utilizzo quasi nullo di CPU. Viceversa, in figura 6.26 si vede in maniera evidente come l’utilizzo percentuale dello stesso processo assume un valore medio del 45% per tutta la durata del periodo di osservazione. Inoltre si può vedere 117 Risultati sperimentali Figura 6.26: Percentuale di CPU Emulazione Hang Attivo Figura 6.27: Occorrenza minor page fault Emulazione Hang Attivo 118 Risultati sperimentali Figura 6.28: Percentuale di memoria occupata Emulazione Hang Attivo che, come conseguenza del loop infinito in cui versa il processo contributor, i processi manager e JBoss restano in polling sulle condizioni di sincronizzazione. In questo modo l’esecuzione del dimostratore non termina mai ed il periodo di osservazione raggiunge il limite impostato tramite configurazione, di 500 secondi. In questo intervallo il loro utilizzo di CPU è minimo, o addirittura nullo. Come riscontro di quanto osservato dal profilo di utilizzo di CPU, un’ulteriore deviazione dal profilo di esecuzione nominale è stata riscontrata negli andamenti delle occorrenze di page fault e di utilizzo percentuale di memoria. Il primo grafico, in figura 6.27, mostra infatti come l’occorrenza di minor page fault del processo contributor passi da un valore di circa 2 ∗ 104 , del profilo di riferimento, ad un valore quasi cinque volte superiore, circa 9 ∗ 104 . Un discorso analogo vale per il profilo di utilizzo percentuale di memoria, che varia da un valore di circa il 3%, del profilo di riferimento, ad un valore che supera anche il 10%. 119 Risultati sperimentali 6.1.4 Osservazione di crash ed hang passivo I grafici dei profili presi in considerazione per l’emulazione di malfunzionamento, tramite forzatura di un valore di ritorno a Null, sono invece riportati nelle figure 6.29, 6.30, 6.31 e 6.32. In questo caso i grafici sono relativi, nell’ordine, al carico di sistema, al numero di file descriptor utilizzati, alle socket aperte ed all’occorrenza di major page fault. Il grafico di figura 6.29 mostra in maniera evidente come il carico di sistema tenda a scendere bruscamente, da una media di 1.6 processi in coda ad una media quasi nulla, inferiore a 0.1. Figura 6.29: Carico di sistema Emulazione Hang Passivo Dai grafici relativi a file descriptor e socket, invece, si può dedurre un profilo di comportamento anomalo dovuto alla mancata deallocazione delle risorse utilizzate, come invece avviene nel caso della golden run, in cui sia i descrittori di file sia le socket aperte assumono valori nulli al termine dell’esecuzione del dimostratore. In base a questi profili 120 Risultati sperimentali Figura 6.30: File descriptor utilizzati Emulazione Hang Passivo Figura 6.31: Socket aperte Emulazione Hang Passivo 121 Risultati sperimentali Figura 6.32: Occorrenze major page fault Emulazione Hang Passivo si può dedurre o l’occorrenza di una situazione di stallo, o una terminazione imprevista del processo, che quindi non ha deallocato le risorse. L’ultimo grafico, in figura 6.32, riporta il profilo delle occorrenze di major page fault. Il framework di monitoring realizzato calcola tale valore come somma di tutti i major page fault del processo e dei thread che esso genera, pertanto il valore può crescere e decrescere in funzione della terminazione di alcuni thread, in quanto viene a mancare il loro contributo alla somma. Confrontando il grafico con quello della golden run si vede che il numero di major page fault generati da entrambe le istanze JBoss è inferiore di circa il 30%, ed inoltre il profilo di riferimento mostra un andamento irregolare della curva, ad indicare attività del processo. Il profilo di figura 6.32, invece, mostra un andamento costante nel tempo, ad indicare uno stato di inattività. Infine, analizzando il periodo di permanenza dei processi sul sistema (corrispondente alla durata dell’asse delle ascisse), è ragionevole supporre un crash del contributor, il quale non è più attivo e non ha deallocato le risorse, ed un hang passivo del manager che è attivo ma i suoi 122 Risultati sperimentali Figura 6.33: Percentuale utilizzo di CPU Emulazione Hang Passivo parametri mostrano una situazione di stallo. Si suppone un hang passivo in quanto il profilo di utilizzo di CPU, in figura 6.33, è pressoché nullo. 6.2 Descrizione dei test di benchmark Il processo di comparazione delle performance di due o più sistemi attraverso misurazioni è chiamato benchmarking, ed i workload 1 utilizzati per le misurazioni sono detti benchmark [26]. Un benchmark rappresenta quindi un’astrazione di un carico di lavoro reale, ed in virtù di ciò deve possedere diverse proprietà, ossia deve essere: + Rappresentativo di ciò che si vuole misurare nella realtà; + Portabile; + Ripetibile; 1 Per workload si intende un carico di lavoro da sottoporre al sistema. 123 Risultati sperimentali + Scalabile; + Non intrusivo; + Facile da usare e da comprendere. Molto spesso i risultati forniti da un benchmark non corrispondono a quanto atteso, e devono essere, pertanto, oggetto di interpretazione. Interpretare i risultati di un benchmark rappresenta, quindi, la parte più importante dell’attività di benchmarking stessa. In questa sezione, attraverso l’utilizzo di una suite di benchmark, viene effettuata una valutazione dell’impatto che l’impiego del framework sviluppato ha sulle prestazioni del sistema monitorato. Con il termine suite di benchmark si intende un insieme di test software volti a fornire una misura delle prestazioni della particolare macchina sulla quale i test vengono effettuati. Nello specifico la suite utilizzata è SysBench [27]. Quest’ultima è una suite modulare, cross-platform e multi-thread comprensiva di test per la valutazione delle performance della CPU, della memoria e delle operazioni di I/O su disco. Inoltre è piuttosto semplice nel suo utilizzo e facilmente reperibile dai repository standard di molte distribuzioni Linux. Queste caratteristiche sono state alla base della scelta del suo impiego per l’attività di benchmarking. 6.2.1 Pianificazione degli esperimenti La suite di benchmark impiegata prevede dei parametri globali, validi cioè per ognuno dei test che essa mette a disposizione, e dei parametri definiti ad hoc per ognuno dei test. Nello specifico sono stati utilizzati i parametri globali seguenti: –num-threads: che specifica il numero di worker thread che vengono lanciati dal tool. Di default è settato ad 1, ma, data la natura multi-core del sistema monitora124 Risultati sperimentali to, il suo valore è stato fatto variare durante i test per analizzare le prestazioni all’aumentare del grado di parallelizzazione delle richieste. –max-requests: limite per il numero di richieste generate. Di default è settato a 10.000. Se settato al valore negativo -1 indica un numero di richieste illimitato. –max-time: limite temporale per l’esecuzione del test. Di default è settato a 0, valore che indica che il limite è infinito. In particolare il numero di thread è stato fatto variare in un differente range a seconda dello specifico test, per cui verrà descritto in dettaglio nelle sezioni seguenti, mentre per ognuno dei test è stato lasciato il limite temporale al suo valore di default, in modo da utilizzare il tempo di completamento del test come parametro di valutazione. L’output prodotto2 , comune a tutti i test, è esemplificato di seguito: Test execution summary: total time: xx.xxx s total number of events: 10000 total time taken by event execution: xx.xxx per-request statistics: min: x.xx ms avg: x.xx ms max: x.xx ms approx. 95 percentile: x.xx ms Threads fairness: events (avg/stddev): xx.xxxx/x.xx execution time (avg/stddev): xx.xxxx/x.xx Nello specifico i parametri utilizzati ai fini della valutazione sono: Thread events (avg/stddev): ossia il valore medio e la deviazione standard del numero di richieste prese in gestione da ogni thread. 2 Gli esperimenti sono stati eseguiti attraverso degli script bash realizzati ad hoc. 125 Risultati sperimentali Thread execution time (avg/stddev): ossia il valore medio e la deviazione standard del tempo totale di completamento di tutti i thread. Per-request avg: ossia il tempo medio di completamento di una singola richiesta. I benchmark utilizzati sono quello di CPU e di lettura/scrittura in memoria. Il benchmark di I/O su disco non è stato utilizzato in quanto il framework realizzato, eseguendo le proprie elaborazioni on the fly ed esportando i risultati in un file system virtuale (quindi non fisicamente presente sul disco), non va a stressare sufficientemente il sistema di I/O. Inoltre, essendo le operazioni di I/O sincrone in Linux, i tempi di inattività, dovuti alla sospensione per le attese bloccanti, fornirebbero tempo a sufficienza per l’esecuzione CPU-bound del framework, non permettendo di registrare alcun risultato significativo. 6.2.2 Benchmark di CPU La suite di benchmark mette a disposizione un test per valutare le performance di CPU che si basa sul calcolo dei numeri primi. In particolare il tool implementa il calcolo dei numeri primi attraverso il metodo delle divisioni ripetute. Il test lancerà un numero N di thread, definito dall’utente, ed ognuno di essi eseguirà un certo numero di richieste dal numero totale definito attraverso il parametro –max-request. Ogni singola richiesta consiste nel calcolo di tutti i numeri primi fino ad un limite massimo definito dall’utente. Tale limite viene impostato attraverso il parametro –cpu-max-prime, unico parametro ad hoc per il test di CPU. I dati vengono raccolti attraverso uno script che esegue in ciclo il test di CPU tante volte quanto il numero di thread specificato dalla variabile j. In questo modo si può valutare anche il comportamento del sistema al crescere del grado di parallelismo. Nello specifico avendo a disposizione una macchina con 4 core, il numero di thread è stato fatto 126 Risultati sperimentali variare da 1 fino ad un massimo di 32, attendendosi una saturazione della curva dei dati dopo i 4 thread. Il comando di esecuzione risultante, specificato dal parametro run, è quello riportato di seguito, con la soglia di numeri primi da calcolare impostata a 30.000. sysbench --num-threads=$j --test=CPU --cpu-max-prime=30000 run L’output del comando verrà usato come input per una concatenazione di utility come grep, tr e sed , allo scopo di effettuare un parsing dei soli dati numerici. La prima parte dello script esegue il ciclo senza la presenza del monitor, mentre nella seconda parte viene lanciato il monitor e rieseguito lo stesso ciclo per valori decrescenti del periodo di attivazione del monitor. In questo modo si valuta l’impatto sulle prestazioni al crescere della frequenza di riattivazione del monitor. Ognuna delle esecuzioni del test viene ripetuta 5 volte, ed i grafici sono stati ottenuti eseguendo la media dei campioni ottenuti. In figura 6.34 sono riportate le curve indicanti la media e la deviazione standard del numero di richieste servite da ogni thread al crescere del numero dei thread stessi. Come si vede dal primo grafico in figura, la curva della media decresce esponenzialmente, in quanto il numero di richieste, fisso al valore 10.000 di default, viene ripartito su un numero sempre maggiore di worker thread. Il secondo grafico, relativo alla deviazione standard, mostra che la differenza del numero di richieste mediamente servite da ogni thread tende a decrescere all’aumentare dei thread, e, cosa più significativa, che questo comportamento non risulta influenzato dalla frequenza di attività del monitor, in quanto le curve sono praticamente quasi sempre appaiate. La figura 6.35 riporta il grafico della media del tempo di esecuzione totale di tutti i thread, con una versione zoomata dello stesso. Dai grafici si vede come il tempo di esecuzione decresca in maniera significativa da 1 a 4 thread, passando da circa 45 127 Risultati sperimentali secondi a circa 10 secondi. Ciò è giustificabile dal fatto che i thread vengono eseguiti in parallelo su core diversi, essendo proprio 4 il numero di core a disposizione della macchina virtuale. Oltre i 4 thread, non disponendo di altrettanti core, il tempo di esecuzione totale non scende ulteriormente, e le curve saturano ad un valore di circa 10 secondi. Per apprezzare le differenze di comportamento con e senza il monitor facciamo riferimento al grafico zoomato. Da esso si evince che le curve hanno lo stesso comportamento, e che, a meno delle curve rappresentanti le esecuzioni senza monitor e con il monitor a 100 ms (rispettivamente in rosso ed in arancione), che rappresentano l’upper ed il lower bound, il comportamento del sistema risulta influenzato in maniera minima dalla presenza del monitor, indipendentemente dalla sua frequenza di funzionamento. In figura 6.36 viene riportato il grafico dell’ultimo parametro utilizzato per il benchmark, il tempo medio di esecuzione di una singola richiesta. Ricordiamo che SysBench prevede un certo numero di richieste che i thread devono servire, ed il timer di servizio delle richieste viene settato all’avvio del benchmark. Pertanto le richieste servite dai thread in coda subiranno un ritardo dovuto all’accodamento. Poiché si è già visto che il tempo di esecuzione dei thread satura ad un valore costante, il tempo medio di servizio delle richieste sarà dominato dal crescere del tempo medio di accodamento, per cui ci si attende una crescita lineare del tempo medio di servizio, come effettivamente risulta analizzando la figura 6.36. Inoltre, dallo zoom sul grafico, si può notare che la differenza nel tempo medio di servizio di una richiesta fra le varie curve, relative ai diversi periodi di funzionamento del monitor, è inferiore al millisecondo, e che le curve rappresentanti i periodi del monitor fino a 300 ms sono tutte contenute in un range di 0.1 ms. Dai grafici analizzati si può ragionevolmente affermare che il framework di monitoraggio ha un impatto minimo sulle performance del sistema, e che l’unico periodo di riattivazione del monitor che presenta un overhead apprezzabile rispetto agli altri è quello 128 Risultati sperimentali (a) (b) Figura 6.34: Media (a) e deviazione standard (b) del numero di richieste servite da ogni thread 129 Risultati sperimentali (a) (b) Figura 6.35: Media del tempo di esecuzione totale di tutti i thread (a) con zoom (b) 130 Risultati sperimentali di 100 ms. Per questo motivo è stato eseguito un ulteriore stress test con il monitor a 100 ms, al fine di verificare le prestazioni spingendo il numero di thread generati dal benchmark fino a 100. I risultati sono mostrati nelle figure 6.38 e 6.37. Da esse si può notare che anche stressando al limite il sistema il comportamento continua ad essere come quello osservato nei grafici precedenti. Dalla figura 6.37 si evince che mediamente il tempo di servizio di una singola richiesta viene ritardato di circa 3 millisecondi in presenza del monitor alla massima frequenza di progetto. In particolare si possono prendere in esame i valori puntuali all’inizio e alla fine della parte della curva in cui inizia la crescita lineare, in modo da valutare la tendenza dell’overhead. I valori di overhead si sono calcolati per 4 thread, in modo da osservare l’impatto nel punto di massima parallelizzazione logica, e per 5, 50 e 95 thread. I risultati del calcolo sono i seguenti: overhead4−thread = overhead5−thread = 5, 585 − 5, 455 0, 13 = = 0, 024 = 2, 4% 5, 455 5, 455 overhead50−thread = overhead95−thread = 0, 11 4, 48 − 4, 37 = = 0, 025 = 2, 5% 4, 37 4, 37 55, 8 − 54, 3 1, 5 = = 0, 027 = 2, 7% 54, 3 54, 3 105, 4 − 102, 4 3 = = 0, 029 = 2, 9% 102, 4 102, 4 Analogamente, dalla figura 6.38 si osserva che il tempo di esecuzione totale dei thread in saturazione aumenta mediamente di soli 0.2 secondi in presenza del monitor a fre131 Risultati sperimentali (a) (b) Figura 6.36: Media del tempo di esecuzione di una singola richiesta (a) con zoom (b) 132 Risultati sperimentali (a) (b) Figura 6.37: Media del tempo di esecuzione di una singola richiesta (a) con zoom (b) 133 Risultati sperimentali (a) (b) Figura 6.38: Media del tempo di esecuzione totale dei thread (a) con zoom (b) 134 Risultati sperimentali Figura 6.39: Deviazione standard del tempo di esecuzione totale dei thread quenza massima. Assumendo 11 secondi come valore medio a saturazione del tempo di esecuzione totale, l’overhead introdotto dal monitor risulta essere, nel caso peggiore del monitor a 100 ms, pari a: overhead = 0.2 sec = 0.018 = 1, 8% 11 sec Il grafico dell’andamento della deviazione standard, del tempo totale di esecuzione, al crescere del numero dei thread è riportato in figura 6.39. Allo stesso modo prendiamo il valore di deviazione standard alla massima frequenza e con il massimo carico di thread generati. Dalla figura risulta essere: dev_std = 0.12 = 0, 010 = 1% 11 L’overhead introdotto ha un intervallo di variazione pari a (1.8 ± 1.0)% , e rispetta 135 Risultati sperimentali quindi i requisiti non funzionali richiesti dal progetto. 6.2.3 Benchmark di memoria Il benchmark di memoria fornito da SysBench consente di testare le prestazioni sia in lettura sia scrittura, a seconda del parametro specificato. Le opzioni relative allo specifico test utilizzate sono: –memory-total-size: ad indicare la dimensione totale della memoria da leggere/scrivere. –memory-oper: indica il tipo di operazione da eseguire: lettura o scrittura. Anche in questo caso i dati sono raccolti tramite l’esecuzione di uno script ad hoc che lancia i test prima in assenza del monitor e successivamente con il monitor attivo a frequenze via via crescenti. Ognuna delle misurazioni viene ripetuta 5 volte, ed i grafici sono stati ottenuti eseguendo la media dei campioni ottenuti. Il comando di run del test di memoria è il seguente: sysbench --num-threads=$j --test=memory --memory-total-size=1G --memory-oper= read run Per meglio comprendere la struttura del test di memoria, si riportano due estratti del codice sorgente di SysBench. Come si vede dagli estratti di codice a seguire, il test avviene su un’area di memoria allocata dinamicamente, quindi in area heap, tramite la funzione malloc del linguaggio C. In particolare una prima chiamata alla malloc alloca un vettore di puntatori, della dimensione di un carattere, pari al numero di thread specificati a linea di comando. Successivamente, per ognuno dei puntatori, viene allocata un’area di memoria della dimensione memory_block_size. Il test in pratica alloca in area heap un numero di 136 Risultati sperimentali porzioni di memoria, della dimensione specificata, pari al numero di worker thread, in modo che ognuno di essi operi su una porzione di memoria distinta. buffers = (int **)malloc(sb_globals.num_threads * sizeof(char *)); for (i = 0; i < sb_globals.num_threads; i++){ buffers[i] = (int *)malloc(memory_block_size); memset(buffers[i], 0, memory_block_size); } La distinzione delle operazioni da compiere avviene tramite una condizione di if preliminare (non riportato nell’estratto) che discerne se le letture/scritture devono avvenire in maniera casuale o sequenziale. Quindi effettua uno switch sulla condizione che indica il tipo di operazione. Nel caso della lettura esegue ciclicamente, fino alla terminazione dell’area di memoria assegnata al worker thread, un’assegnazione del contenuto della memoria puntata in una variabile temporanea. Analogamente, in senso contrario, nel caso della scrittura. buf = buffers[thread_id]; end = (int *)((char *)buf + memory_block_size); switch (mem_req->type) { case SB_MEM_OP_WRITE: for ( ; buf < end; buf++) *buf = tmp; break; case SB_MEM_OP_READ: for ( ; buf < end; buf++) tmp = *buf; break; ... Nelle figure da 6.40 a 6.43 vengono riportati i grafici dei risultati del test di memoria in lettura. La figura 6.40 riporta la media e la deviazione standard del numero di richieste servite da ogni thread, al crescere del numero dei thread stessi. Come si può osservare confrontando i grafici con quelli analoghi relativi al benchmark di CPU, la media di richieste gestite da ogni thread decresce esponenzialmente in quanto il numero 137 Risultati sperimentali di richieste resta fisso ed il numero di thread su cui ripartirle aumenta. Anche la deviazione standard, ossia lo scostamento del numero di richieste effettivamente servite rispetto alla media, tende a decrescere; ma soprattutto si nota una convergenza di tutte le curve relative ai diversi periodi di funzionamento del monitor ad indicare che la frequenza di funzionamento non influisce sul bilanciamento delle richieste fra i thread. La figura 6.41 mostra il rate di trasferimento, in MegaByte, in lettura. Come si vede esso tende a crescere bruscamente passando da 1 a 4 thread, in accordo con il fatto che fino a 4 thread si sfrutta il parallelismo logico della macchina. Aumentando ulteriormente il numero di thread essi saranno accodati in attesa di schedulazione, pertanto il rate di trasferimento tende a saturare. Dall’ingrandimento effettuato sul grafico, e riportato nella seconda parte della figura, si può apprezzare che le differenze fra le varie curve sono praticamente non significative. I picchi di massimo del rate appartengono alla curva senza monitor (in rosso), mentre quelli di minimo rate appartengono alla curva del monitor a 100 ms (in arancione), tuttavia le curve non risultano evidentemente distaccate dalle altre come si era osservato nel benchmark di CPU. Discorso analogo vale per il tempo medio di esecuzione totale dei thread, in figura 6.42. Anche dall’ingrandimento del grafico, infatti, risulta che la tendenza a saturare ad un valore di circa 3.4 secondi accomuna tutte le curve. La deviazione standard, riportata in figura 6.43, tende tuttavia ad essere non trascurabile al crescere del numero dei thread. Tuttavia questo è indice di un intervallo di confidenza che abbraccia tutte le curve, e ciò induce a confermare che anche nel caso della memoria, così come risultato dal benchmark di CPU, l’impatto del monitor, indipendentemente dalla frequenza di funzionamento, è minimo. Nell’ultima figura dei test di lettura si riporta il grafico del tempo medio di servizio di una singola richiesta, che, come atteso, cresce linearmente a causa della crescita dei ritardi di accodamento in seguito all’aumento del numero dei thread sui quali le richieste vengono ripartite. 138 Risultati sperimentali (a) (b) Figura 6.40: Media (a) e deviazione standard (b) del numero di richieste servite da ciascun thread in lettura 139 Risultati sperimentali (a) (b) Figura 6.41: Rate di MB trasferiti al secondo in lettura (a) con zoom (b) 140 Risultati sperimentali (a) (b) Figura 6.42: Media del tempo totale di esecuzione dei thread in lettura (a) con zoom (b) 141 Risultati sperimentali Figura 6.43: Deviazione standard del tempo totale di esecuzione dei thread in lettura Figura 6.44: Rate di MB trasferiti al secondo in scrittura 142 Risultati sperimentali Figura 6.45: Media del tempo di esecuzione totale dei thread in scrittura Figura 6.46: Deviazione standard del tempo di esecuzione totale dei thread in scrittura 143 Risultati sperimentali Risultati analoghi ai precedenti si ottengono anche nel caso del benchmark di memoria effettuato per l’operazione di scrittura. La stretta somiglianza dei grafici, anche nel rate di trasferimento raggiunto, è da ricercarsi nella mediazione effettuata dalla cache che interviene in ogni operazione. I grafici relativi al benchmark in scrittura sono riportati nelle figure da 6.44 a 6.46. 144 Conclusioni e sviluppi futuri Questa tesi ha avuto come obiettivo lo sviluppo di un framework di monitoraggio indiretto che acquisisse in tempo reale dati comportamentali (sulle performance e sulle risorse utilizzate) da sistemi informatici critici. Una fase di scouting preliminare delle infrastrutture di monitoraggio indiretto esistenti non ha permesso di individuare un tool che soddisfacesse pienamente le specifiche del progetto in ambito del quale è stato svolto questo lavoro di tesi, ossia un framework open source che consentisse un monitoraggio selettivo con granularità di processo, totalmente non-intrusive e con overhead minimo. Il lavoro è stato svolto nell’ambito del progetto Miniminds, che si prefigge lo scopo di realizzare una piattaforma per l’integrazione e l’interoperabilità di sistemi critici complessi. Il framework realizzato opererà affiancando un’infrastruttura di monitoraggio diretto prototipale già esistente, allo scopo di coadiuvarne l’azione di rilevazione di anomalie. Un possibile svantaggio dell’approccio indiretto al monitoraggio è rappresentato dal profiling necessario come fase preliminare, tuttavia, nel particolare caso reale preso in considerazione, il sistema presenta un comportamento regolare e ciclico, dovendo portare a termine sempre le stesse operazioni durante il proprio ciclo di vita. Inoltre è possibile ricercare anche alcune invarianti di ciclo, come condizioni per agevolare la fase 145 Conclusioni e sviluppi futuri di profiling, come ad esempio, nel caso reale preso in esame, all’allocazione di risorse (come file descriptor e socket) deve poi corrispondere la relativa deallocazione. Questa caratteristica di regolarità è in realtà comune a diversi sistemi critici, e ciò rende meno difficoltoso il tracciamento di un profilo comportamentale. L’architettura proposta per la realizzazione del tool è stata pensata per operare su un sistema operativo basato su kernel Linux. Tale architettura è composta da due elementi principali, un modulo kernel caricabile dinamicamente, ed un demone che implementa la logica di interfacciamento con l’utente, la logica di gestione del modulo e la presentazione dei risultati. Attraverso un file di configurazione si possono settare i processi e i parametri da monitorare, e la frequenza di funzionamento del monitor. I dati necessari sono stati ricavati interrogando le strutture dati gestite dal kernel Linux a partire dalla struttura dati che implementa il descrittore di processo, la task_struct. Sono stati effettuati dei test volti alla verifica dell’effettiva capacità del framework di riportare informazioni utili al rilevamento di situazioni di anomalia. A tale scopo sono stati introdotti dei fault software mirati nel codice sorgente di una delle applicazioni coinvolte nello scenario del caso reale. I grafici riportanti le curve dei parametri monitorati nel tempo hanno evidenziato un effettivo cambiamento nel profilo comportamentale dell’applicazione. Nella seconda parte del capitolo sono state eseguite delle attività di benchmark volte a valutare l’impatto che il framework stesso ha sul sistema monitorato. I dati ottenuti ci hanno consentito di affermare che l’overhead introdotto dal tool risulta minimo. In particolare si è visto dai test di CPU che, stressando il sistema con 100 thread generati dal benchmark in corrispondenza del monitor a massima frequenza, si ottiene un overhead che non supera il 3% (un valore medio del 1.8% con una deviazione standard del 1%). Un naturale sviluppo futuro potrebbe essere quello di ampliare ulteriormente il range 146 Conclusioni e sviluppi futuri dei parametri che il framework riesce a monitorare. Un’ulteriore evoluzione futura prevede lo sviluppo di un altro componente, il detector, che si occupi di effettuare un merge delle informazioni riportate sia dal monitor diretto che dal monitor indiretto, e, attraverso un processing on-line di queste ultime, di fornire degli alert in tempo reale. A tale scopo sarà necessaria un’approfondita campagna di profiling. 147 Bibliografia [1] Status of C99 features in gcc, http://gcc.gnu.org/c99status.html [2] History log of /linux/stable/Documentation/filesystems/seq_file.tx t, giugno 2014, http://code.metager.de/source/history/linux/stable/ Documentation/filesystems/seq_file.txt [3] D. Lepešs, University of Latvia, Loadable Kernel Modules, http://www.ltn.lv/~guntis/unix/kernel_modules.ppt [4] P. J. Salzman, M. Burian, O. Pomerantz, The Linux Kernel Module Programming Guide, cap. 5, maggio 2007 http://tldp.org/LDP/lkmpg/2.6/html/index.html [5] Bryan Henderson, Linux Loadable Kernel Module HOWTO, settembre 2006, http://www.tldp.org/HOWTO/Module-HOWTO/index.html [6] M. Bar, The Linux Process Model, marzo 2000, http://www.linuxjournal.com/article/3814 [7] A. Brooks, Operating System and Process Monitoring Tools, http://www.cse.wustl.edu/~jain/cse567-06/ftp/os_monitors 148 Bibliografia [8] Università degli studi di Roma “Torvergata”, Controllo del traffico aereo, http://radarlab.uniroma2.it/DIDATTICA/TTR/traffico% 20aereo.pdf [9] G. Di Pinto, Il controllo del traffico aereo, marzo 2011, http://www.mediterraneavirtual.com/public/corsi/dw/Air_ traffic_control.pdf [10] P. Tramontana, Testing: definizioni, pag. 6, http://www.federica.unina.it/ingegneria/ ingegneria-del-software-ingegneria/testing-definizioni [11] N. D. Evans, The new scope of mission-critical computing, settembre 2013, http://blogs.computerworld.com/high-performance-computing/ 22795/new-definition-mission-critical-computing [12] A. Bovenzi, On-line detection of anomalies in mission-critical software systems, aprile 2013 [13] G. Rusconi, Il downtime di Twitter? È costato 25 milioni di dollari al minuto, Il sole 24 ore, luglio 2012, http://www.ilsole24ore.com/art/tecnologie/2012-07-27/ downtime-twitter-costato-milioni-114441.shtml?uuid= AbgkMcEG [14] A. Saha, Learning about Linux Processes, dicembre 2006, http://linuxgazette.net/133/saha.html Bibliografia [15] J. Corbet, G. Kroah-Hartman, A. Rubini, Linux Device Drivers, 3rd Edition, febbraio 2005, cap.3, http://www.makelinux.net/books/lkd2/ch03lev1sec1 [16] D. A. Rusling, Creating a Process, http://www.science.unitn.it/~fiorella/guidelinux/tlk/ node51.html [17] Linux Magazine, Cos’è il processo init?, http://www.linux-magazine.it/Cos-è-il-processo-init.htm [18] M. Kerrisk, Linux Programmer’s Manual, maggio 2014, http://man7.org/linux/man-pages/man5/proc.5.html [19] D. Cotroneo, A. Pecchia, R. Pietrantuono, S. Russo, A Failure Analysis of Data Distribution Middleware in a Mission-Critical System for Air Traffic Control, 2009 [20] G. Sabatino, Il trasponder aeronautico, agosto 2012, http://www.zoomup.it/pilot-blog/il-transponder-aeronautico [21] The Guardian, U-2 spy plane causes Los Angeles air traffic control to crash, maggio 2014, http://www.theguardian.com/world/2014/may/06/ u-2-spy-plane-crashes-los-angeles-air-traffic-control [22] Capitolato Tecnico del progetto Miniminds, Middleware per l’integrazione e l’interoperabilità di sistemi critici per tempo e affidabilità, finanziato dal Ministero dell’Istruzione Università e Ricerca nell’ambito del Laboratorio Pubblico/Privato COSMIC Bibliografia [23] F. Daria, Una strategia di monitoring per un sistema di controllo del traffico aereo, Tesi di laurea triennale, 2013 [24] S. Russo, C. Savy, D. Cotroneo, A. Sergio, Introduzione a CORBA, McGraw-Hill, 2002 [25] R. Walker, Examining Load Avarage, Dicembre 2006 http://www.linuxjournal.com/article/9001?page=0,0 [26] R. Jain, Art of Computer Systems Performance Analysis Techniques For Experimental Design Measurements Simulation And Modeling, 1991, cap 4.6 pag.66 [27] SysBench: a system performance benchmark, https://launchpad.net/sysbench [28] Tivoli Monitoring, IBM Software http://www-03.ibm.com/software/products/it/tivomoni