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