Tesi Magistrale Carmine Carella

Transcript

Tesi Magistrale Carmine Carella
UNIVERSITÀ DEGLI STUDI DI MILANO - BICOCCA
Facoltà di Scienze Matematiche, Fisiche e Naturali
Dipartimento di Informatica Sistemistica e Comunicazione
Corso di Laurea Magistrale in Informatica
Automazione di Test di Sistemi Embedded
Relatore:
Prof. Mauro PEZZE’
Correlatori:
Lorena SIMONI
Giuseppe GORGOGLIONE
Tesi di Laurea di:
Carmine Carella
Matricola: 055465
Anno Accademico 2009-2010
Ringrazio il prof. Mauro Pezzè,
per avermi aiutato ad ideare questa tesi;
Ringrazio il gruppo APG di STMicroelectronics,
Lorena, Luca, GianAntonio,
per avermi supportato durante tutta l’attività di tesi;
e Giuseppe,
per il suo fondamentale supporto tecnico;
Un ringraziamento speciale
ai miei genitori e alle mie sorelle Brunilde e Milena,
per il supporto psicologico ed economico durante tutti i miei anni di studio;
e a Valeria,
per essermi stata sempre vicino.
Indice
Indice
viii
1 Introduzione
2 Sistemi Embedded
2.1 Caratteristiche dei sistemi embedded
2.2 Hardware . . . . . . . . . . . . . . .
2.3 Software di base . . . . . . . . . . .
2.4 I Sistemi Linux embedded . . . . . .
2.4.1 Sviluppo . . . . . . . . . . . .
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Testing di Software di Sistemi Embedded
3.1 Fasi del testing di sistemi embedded . . . . . . .
3.1.1 Simulazione . . . . . . . . . . . . . . . . .
3.1.2 Prototipazione . . . . . . . . . . . . . . .
3.1.3 Pre-produzione . . . . . . . . . . . . . . .
3.1.4 Post-produzione . . . . . . . . . . . . . .
3.2 V-Model prototipazione . . . . . . . . . . . . . .
3.3 Differenze con il testing di software tradizionale .
3.3.1 Testing nell’ambiente host . . . . . . . . .
3.3.2 Testing nell’ambiente target . . . . . . . .
3.4 Livelli software . . . . . . . . . . . . . . . . . . .
3.4.1 Impatto dei livelli software sul testing . .
3.4.2 Device driver in Linux . . . . . . . . . . .
3.4.3 I device driver - un elemento critico per la
3.5 Automazione . . . . . . . . . . . . . . . . . . . .
3.5.1 La personalizzazione degli strumenti . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
12
13
15
17
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
qualità
. . . . .
. . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
19
20
21
23
26
26
27
28
29
29
32
35
36
38
40
42
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
vi
4 Il Progetto Cartesio
4.1 Sviluppo del sistema embedded . . . . . . . . . . . . . . .
4.1.1 Hardware . . . . . . . . . . . . . . . . . . . . . . .
4.1.2 Software . . . . . . . . . . . . . . . . . . . . . . . .
4.1.3 Ambiente di sviluppo . . . . . . . . . . . . . . . .
4.2 Processo di testing del BSP Linux Cartesio . . . . . . . .
4.2.1 Test dei device driver delle memorie . . . . . . . .
4.2.2 Test del device driver CLCD . . . . . . . . . . . .
4.2.3 Test del device driver Touchpanel . . . . . . . . . .
4.2.4 Test del device driver Keyboard . . . . . . . . . .
4.2.5 Test del device driver Audio . . . . . . . . . . . . .
4.2.6 Test Report . . . . . . . . . . . . . . . . . . . . . .
4.3 Miglioramento del processo di testing . . . . . . . . . . . .
4.3.1 Criteri per la scelta degli strumenti di automazione
.
.
.
.
.
.
.
.
.
.
.
.
.
43
44
44
48
49
50
51
52
52
52
52
54
54
55
5 Analisi dell’Efficacia del Test
5.1 Analisi di copertura del codice . . . . . . . . . . . . . . . . .
5.2 Obiettivi dell’analisi di copertura nei sistemi Linux Embedded
5.3 Scelta dello strumento di automazione . . . . . . . . . . . . .
5.4 Gcov. Gnu Gcc Coverage Framework . . . . . . . . . . . . . .
5.5 Gcov per lo Spazio del Kernel . . . . . . . . . . . . . . . . . .
5.5.1 Configurazione . . . . . . . . . . . . . . . . . . . . . .
5.6 Utilizzo di Gcov Kernel nel Progetto Cartesio . . . . . . . . .
5.6.1 Esempio di utilizzo di gcov nello spazio del kernel . . .
5.6.2 Sviluppi futuri . . . . . . . . . . . . . . . . . . . . . .
58
59
63
63
64
67
68
69
72
73
6 Prestazioni del Codice nella Fase di Boot di Linux
6.1 Obiettivi del function tracing nei sistemi Linux Embedded
6.2 Scelta dello strumento di automazione . . . . . . . . . . .
6.3 Ftrace. Il framework di tracing del kernel linux . . . . . .
6.3.1 I Tracer del framework Ftrace . . . . . . . . . . . .
6.3.2 Il file system di Ftrace . . . . . . . . . . . . . . . .
6.3.3 Utilizzare un tracer . . . . . . . . . . . . . . . . . .
6.4 Il Function Graph Tracer . . . . . . . . . . . . . . . . . .
6.5 Personalizzazione di Function Graph Tracer . . . . . . . .
6.6 Function Duration Tracer . . . . . . . . . . . . . . . . . .
6.6.1 Utilizzo nella fase di boot . . . . . . . . . . . . . .
6.6.2 Strumento di post-analisi . . . . . . . . . . . . . .
6.7 Risultati sperimentali . . . . . . . . . . . . . . . . . . . .
6.7.1 Contributo . . . . . . . . . . . . . . . . . . . . . .
75
76
76
77
77
78
79
80
83
91
93
94
95
98
vii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7 Rilevazione di Memory Leak in Linux Embedded
7.1 Software aging . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2 Errori di programmazione causa di memory leakage . . . . .
7.2.1 Perdita del riferimento alla memoria allocata . . . . .
7.2.2 Mancanza delle system call per la deallocazione . . . .
7.2.3 Problemi di gestione della memoria con l’uso di array
7.2.4 Problemi di memory leakage in presenza di cicli . . . .
7.3 Conseguenze dell’occorrenza di memory leakage . . . . . . . .
7.4 Scelta dello strumento di automazione . . . . . . . . . . . . .
7.5 Kernel memory leak detector . . . . . . . . . . . . . . . . . .
7.5.1 Algoritmo . . . . . . . . . . . . . . . . . . . . . . . . .
7.5.2 Configurazione e utilizzo generale . . . . . . . . . . . .
7.5.3 Limiti e Svantaggi . . . . . . . . . . . . . . . . . . . .
7.6 Memory leakage nei device driver . . . . . . . . . . . . . . . .
7.6.1 Creazione istanza device . . . . . . . . . . . . . . . . .
7.6.2 Creazione e inizializzazione del device specifico . . . .
7.6.3 Esecuzione della funzione probe . . . . . . . . . . . . .
7.6.4 Occorrenze di memory leak nei device driver . . . . .
100
101
103
104
105
105
105
106
107
109
109
110
111
112
114
114
115
116
8 Prestazioni di Input/Output su Dispositivi a Blocchi
122
8.1 Obiettivi delle prestazioni di I/O nei sistemi Linux Embedded 123
8.2 Scelta dello strumento di automazione . . . . . . . . . . . . . 124
8.3 IOzone. Filesystem benchmark . . . . . . . . . . . . . . . . . 125
8.4 Personalizzazione di IOzone . . . . . . . . . . . . . . . . . . . 127
8.5 Risultati sperimentali . . . . . . . . . . . . . . . . . . . . . . 129
9 Conclusioni
135
A Processo di boot del kernel. Fase platform-dependent
A.1 Processo di boot del kernel Linux . . . . . . . . . . . . .
A.2 Fase platform-dependent del processo di boot del kernel
A.2.1 Registrazione dei device . . . . . . . . . . . . . .
A.2.2 Registrazione dei driver . . . . . . . . . . . . . .
A.2.3 Associazione tra driver e device . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
138
138
140
141
143
147
Bibliografia
149
Sitografia
151
Elenco delle Figure
153
Elenco delle Tabelle
155
Elenco dei Codici
156
viii
1
Introduzione
I sistemi embedded sono sistemi di elaborazione progettati per una determinata applicazione e supportati da una piattaforma hardware dedicata. La
loro diffusione è inarrestabile, le analisi di mercato mostrano la loro rilevanza in ogni settore applicativo. La presenza di tali sistemi serve talvolta per
ottenere le funzionalità desiderate nei prodotti finali o, spesso, è il veicolo
per introdurre dell’innovazione.
Essi sono una combinazione di hardware e software, tra loro fortemente
integrati. Negli ultimi anni sono diventati molto complessi, ed il software
contenuto in essi è diventato un elemento chiave, arrivando ad essere perfino
più importante dell’hardware stesso, in quanto è la componente che permette di aumentare le funzionalità del sistema e di ottenere un minimo di
flessibilità, in questi sistemi caratterizzati dall’essere poco versatili.
Conseguenza diretta dell’evoluzione e della criticità del software, è la
crescita della complessità e dell’importanza del processo di testing. I sistemi
embedded sono sempre più utilizzati in ambiti critici come quello avionico
e aerospaziale in cui la presenza di un difetto all’interno del software può
portare ad un fallimento del sistema e conseguentemente, compromettere
la vita umana e provocare ingenti perdite economiche. Rispetto ad altri
ambiti, quello embedded introduce requisiti di criticità nel sistema che hanno
bisogno di maggiore attenzione. Il testing è uno degli elementi per garantire
la qualità.
L’ambito embedded introduce nuove sfide e problemi da affrontare. Esso
ha delle caratteristiche peculiari che influenzano tutte le attività del processo di sviluppo. Il testing non è un’eccezione. Questo dominio estremamente
complesso ha: un processo di sviluppo che considera sia la parte hardware
che la parte software del sistema; specifiche complesse; vincoli platformdependent (cpu, memoria, consumo energetico, periferiche); requisiti di qualità molto stringenti; costi contenuti e vincoli di tempo che specificano un
time-to-market molto breve. Inoltre, anche la caratteristica di specificità
1
CAPITOLO 1. Introduzione
dei sistemi embedded influenza il testing. Ogni singolo settore applicativo
richiede di prendere in considerazione aspetti talmente peculiari da rendere
difficile l’identificazione di tecniche e strumenti di valenza davvero generale.
La diretta conseguenza è lo sviluppo di approcci ad hoc. Ancora, l’utilizzo
di uno strumento di automazione è vincolato da una notevole personalizzazione in cui vengono eseguiti adattamenti di basso livello all’architettura
di uno specifico processore per rendere funzionante lo strumento. Data la
complessità della personalizzazione, essa può concludersi con un successo o
un insuccesso influenzando la raccolta e l’analisi dei risultati per valutare e
migliorare la qualità.
Un ulteriore fattore che influenza il testing è il fatto che l’ambiente di
sviluppo del software embedded è diverso dall’ambiente di esecuzione, ovvero
il software viene sviluppato su una macchina con risorse computazionali maggiori e architettura differente detta host, rispetto alla macchina detta target
che coincide proprio con l’architettura embedded per la quale il software
viene creato e sulla quale deve essere eseguito. Questa suddivisione porta
ad avere due ambienti per il testing in cui applicare tecniche diverse per
valutare requisiti di qualità differenti. La forte relazione tra l’hardware e
il software del sistema embedded, in aggiunta alla presenza di vincoli realtime, rende pressoché impossibile sviluppare e testare il software indipendentemente dall’hardware su cui dovrà essere eseguito. Di qui la necessità
del testing nell’ambiente target. Questi due ambienti, nel testing di software
tradizionale, non esistono in quanto l’ambiente utilizzato per lo sviluppo è
anche quello di esecuzione. Questa differenziazione porta al problema di
definire la strategia di testing: quale test deve essere eseguito sull’ambiente
host e quale sull’ambiente target? Un certo requisito di qualità deve essere
valutato nell’ambiente giusto. Naturalmente questo andrà ad influenzare
le tecniche di testing da utilizzare sia nell’ambiente host che nell’ambiente
target.
La letteratura sul testing del software, contiene innumerevoli metodologie, tecniche e strumenti che supportano il processo di testing di software
tradizionale, ma esiste ancora poco per il testing di software embedded.
Negli ultimi anni sono aumentati i lavoro accademici e industriali, ma questa è un’area di ricerca ancora da esplorare. Alcune domande attendono
una risposta più precisa. Tra queste, quali sono i punti chiave del testing embedded?, in cosa differisce esattamente il testing embedded dal testing
tradizionale? quali sono le soluzioni e gli strumenti da applicare?. Ad oggi,
il testing di software di sistemi embedded è stato formalizzato solamente
attraverso la creazione di tecniche e strumenti proprietari (molto costosi) e
metodologie aziendali interne e di conseguenza non utilizzabili. Non esiste
di fatto ancora un approccio generale, che inglobi al suo interno strumenti
aventi licenze gratuite, e che raccolga pratiche comuni di test, attraverso le
quali sia possibile garantire uno sviluppo software di qualità. [SC02]
La prevalenza di approcci poco concreti nella scarsa letteratura sul test2
CAPITOLO 1. Introduzione
ing di software embedded, e la presenza di approcci ad-hoc e proprietari in
ambito industriale, evidenzia la mancanza di tecniche e strumenti utilizzabili
in contesti differenti. Questo aumenta la necessità di un approccio generale
attraverso il quale sia possibile garantire uno sviluppo software di qualità.
La tesi fornisce un contributo in questa direzione, per superare i problemi
di specificità del testing in questo dominio, attraverso la sperimentazione in
una realtà industriale complessa e affermata, di tecniche e strumenti per il
testing di caratteristiche funzionali e non funzionali di software embedded.
La tesi studia alcuni problemi di qualità specifici e propone delle tecniche
di testing che possono essere applicate nell’ambiente target per valutare classi di difetti rilevanti nei sistemi embedded. Inoltre, descrive l’adattamento e
l’utilizzo di strumenti di automazione per applicare le tecniche, affrontando
la scelta, in base a caratteristiche derivate dai vincoli imposti dall’ambiente
sperimentale, la personalizzazione e l’analisi dei risultati. I risultati ottenuti
dal lavoro di tesi possono essere riassunti come:
• un insieme di informazioni sulla personalizzazione degli strumenti per
la piattaforma ARM. Il lavoro svolto, può essere un’utile linea guida
per personalizzare gli stessi strumenti su altre architetture.
• un insieme di informazioni per la comunità open-source sul funzionamento degli strumenti e sulla presenza di problemi da risolvere per
migliorare l’utilizzo sull’architettura specifica.
• un insieme di risultati sperimentali, che convalidano le tecniche e gli
strumenti di testing proposti. Il lavoro di tesi ha migliorato il processo di testing utilizzato nell’ambiente sperimentale, aumentandone
il perimetro, attraverso l’individuazione di aree di qualità non ancora
esplorate e introducendo, per la valutazione di queste, strumenti di
automazione che riduco i costi e i tempi del processo.
L’ambiente sperimentale della tesi è quello della divisione Automotive di
STMicroelectronics (STM), la quale ha ideato una nuova famiglia di Systemon-Chip dal nome Cartesio con un alto livello di integrazione di device su un
singolo chip, un prodotto ideale per navigatori satellitari e per l’infotainment.
Oltre al core viene effettuato lo sviluppo di board sperimentali che comprendono tutti i dispositivi e le interfacce con le quali il chip può interagire. La
componente software è costituita dal porting del sistema operativo Linux
per la piattaforma target, Baseport (BSP), che comprende principalmente
lo sviluppo dei device driver necessari per la gestione dell’hardware specifico.
Tutto questo costituisce un ambiente di sperimentazione del chip completo
e funzionante utile per mostrare le funzionalità offerte ai committenti.
Il processo di testing del BSP Linux era inizialmente costituito da test
per la verifica del corretto interfacciamento del software con i device, principalmente test funzionali per le periferiche supportate dalla piattaforma.
3
CAPITOLO 1. Introduzione
Con il lavoro di tesi sono state introdotte tecniche di test e strumenti di
automazione per la valutazione di altre classi di problemi. Le tecniche di
test individuate sono state applicate con l’obiettivo di valutare la qualità del
codice platform-dependent. Ovvero dell’intero sistema operativo Linux l’attenzione del processo di testing è posta sull’area dei device driver, sviluppati
per la gestione delle componenti hardware specifiche.
Nell’ambito del progetto Cartesio, sono stati sperimentati approcci e
tecniche per il testing in ambiente target, utili all’identificazione di classi di
problemi di maggiore importanza in ambito embedded, attraverso l’utilizzo
di strumenti di automazione opportunamente selezionati, in base a diversi
criteri individuati nell’ambiente sperimentale. Per ogni strumento è descritta
in modo approfondito la personalizzazione, e in caso positivo l’applicazione
e l’analisi dei risultati. Le tecniche di testing sperimentate sono le seguenti.
Test di copertura. L’analisi di copertura del codice per la verifica dell’efficacia dei test funzionali esistenti.
Prestazioni del codice. Il profiling (function tracing) per la valutazione
della durata delle funzioni nel processo di boot.
Uso e gestione della memoria. Verifica dell’utilizzo della memoria da
parte del software alla ricerca di problemi di memory leakage;
Prestazioni di I/O su dispositivi a blocchi. Verifica delle prestazioni
delle operazioni di I/O (lettura, scrittura) sui dispositivi a blocchi,
disponibili sulla piattaforma target.
L’analisi di copertura ha l’obiettivo di valutare l’efficacia dei test funzionali esistenti, in termini di quantità di copertura del kernel, identificando il cosiddetto dead code dei device driver platform-dependent, il codice
che non viene esercitato durante l’esecuzione dei test. I risultati sono utili
alla creazione di casi di test addizionali per esercitare il codice, quindi incrementare la copertura dello stesso e anche il livello di qualità dei device
driver platform-specific sviluppati. Lo strumento scelto è Gcov. Gcov è un
framework per l’analisi di copertura fornito dal compilatore Gnu Gcc.
Il function tracing è una tecnica di profiling utilizzata nel caso specifico
per valutare le prestazioni delle funzioni del kernel eseguite nella fase di boot
del sistema. L’obiettivo è individuare le aree del processo di boot con i maggiori problemi di prestazioni, in particolare identificare le funzioni relative
alla parte di codice platform-dependent eseguite nella fase iniziale del kernel,
per poter intervenire e migliorare i tempi di boot, requisito importante in
ambito embedded. Lo strumento utilizzato è il Function Duration Tracer
che si basa sul framework di tracing di Linux, Ftrace.
Il memory leakage è il principale difetto della classe degli aging-related
bugs, relativo all’uso non corretto della memoria che provoca fallimenti
4
CAPITOLO 1. Introduzione
del sistema. La causa principale di questi difetti è legata a errori di programmazione. Nei sistemi embedded, la limitata quantità di memoria fisica disponibile e il fallimento del sistema, che può avere conseguenze catastrofiche, rendono il memory leakage un problema su cui soffermarsi maggiormente in quest’ambito rispetto ad altri. Lo strumento utilizzato è kmemleak
per la rilevazione nello spazio del kernel.
La valutazione delle prestazioni di I/O (operazioni di lettura e scrittura)
sui dispositivi a blocchi, periferiche con tecnologia a stato solido (Flash)
utilizzate come supporti di memorizzazione di massa nei sistemi embedded,
ha l’obiettivo di individuare possibili problemi di efficienza nei relativi device driver, misurando le velocità di trasferimento dei dati. Lo strumento
utilizzato in questo caso è IOzone.
La tesi, che relaziona il lavoro sperimentale svolto, è organizzato nel
seguente modo.
• Il capitolo 2, è una breve introduzione al mondo dei sistemi embedded,
presentandone le caratteristiche, fortemente condizionate dal settore
applicativo, per poi delineare le principali differenze con i sistemi desktop e gli aspetti basilari da considerare nella progettazione. Saranno
presentati, inoltre, alcuni dati di mercato che testimoniano la crescita
del settore embedded nel mercato globale, e in particolare il progresso
che ha avuto in questo ambito lo sviluppo del software ed il suo utilizzo
nei sistemi operativi open source, come Linux. In seguito, sarà presentata una delle principali architetture hardware e le caratteristiche del
software utilizzato nei sistemi embedded. La parte finale del capitolo
mostra le peculiarità, l’architettura e i principi di sviluppo dei sistemi
Linux embedded.
• Il capitolo 3, introduce inizialmente le fasi del processo di testing dei
sistemi embedded, focalizzandosi poi sulla fase di prototipazione e sul
testing della componente software. Successivamente, delinea le differenze con il testing di software tradizionale. Vengono approfondite le
tecniche di test che possono essere applicate nell’ambiente target. Vengono poi introdotti i livelli software di un sistema embedded, per identificare su quale concentrare maggiormente il controllo di qualità nel
porting di un sistema operativo. Infine, viene descritta l’automazione
del testing e la personalizzazione degli strumenti.
• Nel capitolo 4 è descritto l’ambiente sperimentale di STMicroelectronics. Inizialmente viene introdotto l’ambito automotive e il progetto
Cartesio. Successivamente viene descritto lo sviluppo del sistema embedded, approfondendo l’ambiente utilizzato, e la componente hardware e software. Viene descritto, poi, il processo di testing e le tecniche
adottate. Successivamente vengono delineati i punti di miglioramento
del processo di testing, tra cui copertura di nuove classi di problemi e
5
CAPITOLO 1. Introduzione
aumento dell’automazione. Infine sono mostrati i criteri utilizzati per
la scelta degli strumenti, utili per effettuare il testing delle classi di
problemi individuate.
• Il capitolo 5 è dedicato all’analisi dell’efficacia del test. Dopo una breve
introduzione al code coverage, vengono descritti gli obiettivi dell’analisi di copertura nell’ambiente sperimentale e la scelta dello strumento. Infine viene fornita una descrizione approfondita di Gcov e della
personalizzazione per l’architettura ARM.
• Il capitolo 6 è dedicato alla valutazione delle prestazioni del codice nella fase di boot del kernel. Vengono descritti gli obiettivi nell’ambiente
sperimentale e la scelta dello strumento. Successivamente viene introdotto il framework Ftrace e descritto in modo approfondito il Function Graph Tracer, attraverso la personalizzazione per l’architettura
ARM, l’utilizzo e l’analisi dei risultati sperimentali.
• Nel capitolo 7 viene inizialmente introdotto il software aging, e poi presentato il problema del memory leakage e le sue conseguenze. Successivamente, viene descritta la scelta dello strumento e in modo dettagliato
il Kernel Memory Leak Detector (Kmemleak). Infine vengono analizzati gli errori, causa di memory leakage, che possono essere commessi
nello sviluppo dei device driver e la rilevazione di questi da parte di
Kmemleak.
• Il capitolo 8 è dedicato alla valutazione delle prestazioni di I/O sui
dispositivi a blocchi. Vengono presentati gli obiettivi nell’ambiente
sperimentale e la scelta dello strumento. Successivamente viene descritto IOzone e la personalizzazione per l’architettura ARM. Infine
viene descritta l’applicazione per valutare un driver specifico e l’analisi
dei risultati sperimentali.
• Infine l’appendice A, descrive in modo approfondito il processo di boot
del kernel, focalizzandosi però sulla parte platform-dependent dell’architettura Cartesio. Questa appendice è di supporto in modo principale per il capitolo 6, ma anche per approfondire aspetti citati in altri
punti della tesi.
6
2
Sistemi Embedded
uesto capitolo è una breve introduzione al mondo dei sistemi embedded, presentandone le caratteristiche, fortemente condizionate dal
settore applicativo, per poi delineare le principali differenze con i sistemi
desktop e gli aspetti basilari da considerare nella progettazione. Saranno
presentati, inoltre, alcuni dati di mercato che testimoniano la crescita del
settore embedded nel mercato globale, e in particolare il progresso che ha
avuto in questo ambito lo sviluppo del software ed il suo utilizzo nei sistemi
operativi open source, come Linux. In seguito, sarà presentata una delle
principali architetture hardware e le caratteristiche del software utilizzato
nei sistemi embedded. La parte finale del capitolo mostra le peculiarità, l’architettura e i principi di sviluppo dei sistemi Linux embedded. Il principale
riferimento per questo capitolo è [BF07].
Q
2.1
Caratteristiche dei sistemi embedded
La presenza di sistemi digitali nella vita quotidiana è costante e molte volte
pressoché invisibile; nella maggior parte dei casi tali dispositivi raggiungono
un livello di complessità tale da includere un sistema a microprocessore:
telefono cellulare, testina di una stampante a getto d’inchiostro, navigatore,
carte di credito e così via.
La presenza di tali sistemi, detti appunto embedded in virtù della loro
forte interazione con l’ambiente nel quale sono immersi, serve talvolta per
ottenere le funzionalità desiderate nei prodotti finali o, spesso, è il veicolo
per introdurre dell’innovazione; la loro rilevanza in ogni settore applicativo
è mostrata nella figura 2.1, dove si osserva che all’incirca la metà del costo
finale di un prodotto è rappresentato dal sistema embedded.
Le caratteristiche fondamentali di un sistema embedded sono pertanto la
stretta interazione del sistema con l’ambiente in cui è immerso e la presenza
di interfacce spesso non visibili o conformi alle modalità d’interazione a cui
7
CAPITOLO 2. Sistemi Embedded
Figura 2.1: Incidenza percentuale dei sistemi embedded nel costo finale dei
prodotti
ci ha abituato il computer da tavolo; spesso si abusa di termini enfatici
come invisible computing per cogliere l’aspetto saliente della pervasività di
tali dispositivi nella vita quotidiana.
Delineare le caratteristiche generali di un sistema embedded è sicuramente una impresa ardua, essendo la sua struttura fortemente condizionata
dall’applicazione cui è destinato. Per tale motivo ogni settore applicativo
può essere caratterizzato da una particolare declinazione di tale definizione,
in cui vengono enfatizzate le peculiarità del mercato di riferimento, come
per esempio le dimensioni e il consumo di potenza per i sistemi portatili, la
capacità elaborativi per applicazioni legate all’elaborazione di immagini, o
il costo per i dispositivi dell’elettronica di consumo (consumer electronics).
Ogni singolo settore applicativo richiede di prendere in considerazione aspetti talmente peculiari da rendere difficile l’identificazione di architetture,
metodologie di progetto e strumenti di valenza davvero generale.
L’obiettivo di un sistema embedded e le conseguenti architetture realizzative sono duali rispetto a quelli di un normale elaboratore. Un calcolatore
come quello da tavolo viene realizzato in modo da essere principalmente
versatile, in grado cioè di adattarsi a una molteplicità di ambiti applicativi
semplicemente caricando programmi differenti: l’architettura hardware di
supporto contiene risorse in genere sovrabbondanti rispetto alle singole applicazioni, l’esigenza di genericità è infatti difficile da coniugare con gli obiettivi
di ottimizzazione. Un sistema dedicato ad una classe molto specifica di applicazioni può invece essere fortemente ottimizzato, venendo meno il requisito
di garantire una elevata versatilità. In tal modo sulla base di una conoscenza
approfondita dell’applicazione finale, si può dimensionare in modo corretto
la capacità di calcolo scegliendo opportunamente il microprocessore.
Progettare un sistema embedded è un’attività fortemente multi-disciplinare.
Gli sviluppatori, devono avere competenze trasversali rispetto ai vari settori
dell’ingegneria (informatica, elettronica, controllo dei sistemi, e così via) e
una conoscenza specifica dell’ambito applicativo.
La realizzazione di un sistema embedded è un processo che considera
8
CAPITOLO 2. Sistemi Embedded
in modo integrato le peculiarità dell’hardware e del software. I due domini
realizzativi hanno, infatti, aspetti fortemente complementari. L’hardware
è meno flessibile e richiede uno sviluppo abbastanza complesso e costoso.
Il software, sebbene non raggiunga le prestazioni dell’hardware, può avere
tempi di sviluppo e costi contenuti. Ad un alto livello di astrazione, un
sistema embedded può essere visto come una composizione di hardware e
software.
Sia il software sia l’hardware vengono scelti e organizzati in modo da
ottimizzare un più ampio ventaglio di obiettivi, che variano in base al settore
applicativo. Molti sistemi embedded, come per esempio quelli legati ad
applicazioni aerospaziali o medicali, che possono avere un ruolo determinante
per la sicurezza di chi ne viene a contatto, devono essere progettati in modo
da essere dependable, cioè devono considerare i seguenti aspetti:
Affidabilità disporre di una valutazione della probabilità che il sistema si
guasti.
Manutenibilità probabilità che un sistema possa essere riparato entro un
certo intervallo di tempo.
Disponibilità probabilità che il sistema sia funzionante, proprietà fortemente influenzata da quanto siano elevate l’affidabilità e la manutenibilità.
Safety proprietà legata alla possibilità che a fronte di un guasto il sistema
non provochi comunque dalle alle persone con cui interagisce
Sicurezza possibilità di proteggere le informazioni e verificare la loro autenticità
Gli aspetti di dependability, come ovvio, hanno importanza fortemente variabile in relazione ai settori applicativi e spesso in base al livello d’interazione
con gli utenti umani. Gli obiettivi di progetto più comuni sono quelli legati
all’efficienza della realizzazione.
Peso e dimensioni spesso l’ingombro fisico del sistema è determinante,
soprattutto per i dispositivi che non prevedono una collocazione fissa.
Costo Questo fattore è determinante soprattutto per le produzioni di elevato volume, come quelle per l’elettronica di consumo, dove il fattore
determinante, prima ancora delle prestazioni, è un prezzo accessibile.
Per tale motivo in un sistema embedded è presente solo quello che serve
(sia esso hardware o software) in una configurazione il più possibile
ottimizzata.
Consumo Essendo molti sistemi portabili, un basso livello di consumo energetico consente di avere batterie meno costose o di raggiungere durate
9
CAPITOLO 2. Sistemi Embedded
del sistema che ne rendano pratico l’uso. Essendo i miglioramenti tecnologici delle batterie abbastanza lenti, si deve necessariamente considerare tale aspetto in sede di progetto, sia per quanto concerne lo sviluppo dell’hardware, sia durante la stesura del codice dell’applicazione e
in sede di scelta del sistema operativo.
Dimensione del codice Nella maggior parte dei casi, i sistemi embedded
sono completi, ovvero hanno il codice all’interno di un supporto di
memoria permanente di tipo elettronico, spesso integrato nello stesso
chip del microprocessore. Questo vincolo si riflette, soprattutto per
motivi di costo e ingombro, sulla dimensione del codice che deve essere
il più possibile contenuta.
Prestazioni Le prestazioni non sono un obiettivo astratto, come accade
spesso nella progettazione di un computer general purpose, ma dipendono dall’applicazione. In generale si devono soddisfare due tipi di
vincoli temporali, relativi al tempo di reazione ad un evento e al tempo
necessario per l’esecuzione del codice.
Time-to-market e flessibilità le metodologie e le tecnologie scelte per il
progetto sono in genere quelle che consentono di arrivare al prodotto
entro tempi stringenti in modo da cogliere il massimo delle opportunità
di mercato.
Non è pertanto facile standardizzare l’architettura di un sistema embedded,
poiché anche a parità di requisiti funzionali, a seconda dei vincoli imposti
dall’applicazione, la realizzazione migliore può essere sensibilmente diversa.
Avere ad esempio l’obiettivo tassativo di realizzare il sistema in pochi mesi,
spesso sbilancia le scelte verso soluzioni principalmente software, mentre le
esigenze di raggiungere ingombri limitati o bassi costi unitari per elevati
volumi di produzione possono richiedere lo sviluppo di hardware dedicato.
La loro diffusione è inarrestabile, le analisi di mercato mostrano che sono
ormai presenti in ogni apparato sia in modo evidente, il cellulare né è un
esempio, sia in modo trasparente come accade per gli elettrodomestici o le
automobili. Secondo i dati del World Trade Statistics, il mercato e le applicazioni dominanti sono quelle dei sistemi embedded, visto che superano
di un fattore 100 quello dei computer desktop e dei server. Considerando il
mercato globale dell’hardware e del software per applicazioni embedded, il
fatturato totale del mercato, riportato in figura 2.2, mostra che il mercato
di maggior peso è quello dell’hardware, in particolare dei circuiti integrati,
sebbene il tasso di crescita maggiore sia quello del software, che comprende
i sistemi operativi, gli ambienti di sviluppo, gli strumenti per il testing e
gli strumenti per la progettazione di sistemi elettronici, Electronic Design
Automation(EDA). Globalmente il mercato dei sistemi embedded crescerà
con un tasso annuo medio del 14% nel prossimo futuro, valore decisamente
10
CAPITOLO 2. Sistemi Embedded
superiore a quello legato ai PC e ai server che si suppone non sarà in grado di
superare l’8%; Gli strumenti per il testing e i sistemi operativi hanno un mer-
Figura 2.2: Mercato globale dei sistemi embedded in miliardi di dollari
cato in crescita del 20% all’anno, ma il reale impatto del software è comunque
superiore a detti valori, poichè tali cifre non tengono conto dell’intero codice
che costituisce l’applicazione e/o il middleware, che viene realizzato ad-hoc
dal produttore finale del sistema embedded. Non essendo il software un
componente standard, non ricade nei volumi delle transazioni riportate nella figura 2.2. Il software può rappresentare anche la metà dell’attività di
sviluppo.
Dalla figura 2.3 si osserva una crescente presenza di sistemi operativi in
molte applicazioni embedded che testimonia l’interesse dei produttori, oltre
che verso i tradizionali microcontrollori, anche nella direzione dei processori di classe superiore (i più diffusi sono PowerPC e ARM), in grado di
ospitare un sistema operativo utile allo sviluppo di applicazioni complesse
con interfacce utente evolute.
Figura 2.3: Suddivisione del fatturato del software embedded per categoria.
(AAGR - Average Annual Growth Rate)
La tipologia di sistema operativo è decisamente varia, si spazia dall’open
source di Linux sino ai prodotti commerciali e proprietari, come WinCE. I
11
CAPITOLO 2. Sistemi Embedded
dati della figura 2.4 quantificano la frammentazione del mercato. La principale soluzione proprietaria per i nuovi progetti in ambito real-time è stata ultimamente Windriver (con il sistema VxWorks), mentre Microsoft (WinCE
e XP Embedded) lo è stato soprattutto per prodotti PDA/telefonia. Come
si può notare, l’utilizzo del sistema operativo Linux è cresciuto negli anni e
risulta il più utilizzato nell’ambito embedded. Le caratteristiche che rendono
preferibile Linux rispetto ad altri sistemi operativi sono descritte in dettaglio
nella sezione 2.4, ma la principale motivazione è che molti sistemi operativi
per applicazioni embedded di natura commerciale richiedono il pagamento
di licenze d’uso non inferiori a qualche dollaro, valore molte volte superiore
al costo dell’intero hardware e, di conseguenza, difficile da proporre a un
mercato consumer dove in molti casi il prezzo è il fattore determinante.
Figura 2.4: Utilizzo recente dei sistemi operativi per sistemi embedded
2.2
Hardware
Nella realizzazione dei sistemi embedded, le architetture possono essere
molto diversificate. Le più comuni sono ASIC, Microcontrollori, FPGA e
SoC. Approfondiamo l’architettura System-on-Chip (SoC). Nell’accezione
più generale, con il termine System-on-Chip o SoC si indica un sistema
completo realizzato integrando tutte le sue parti - circuiti digitali dedicati,
sezioni analogiche e analogico/digitali, memorie e microprocessori - su un
singolo chip di silicio. E’ peraltro evidente che alcune porzioni del sistema
non potranno essere integrate a causa delle loro caratteristiche elettriche e/o
meccaniche. Si pensi per esempio a batterie, grossi condensatori, induttori,
connettori, antenne e così via. Nella pratica, quindi, un system-on-chip è
12
CAPITOLO 2. Sistemi Embedded
un singolo dispositivo integrato che raccoglie tutte le funzioni principali di
un sistema, in modo da limitare al minimo il numero di componenti esterni
necessari. Un SoC, per essere utilizzabile, deve quindi essere montato su una
board che, in questo caso, sarà presumibilmente molto semplice. Le ragioni
per approcciare la progettazione di un sistema secondo il paradigma SoC
sono diverse e partono da considerazioni di natura differente. Tra queste vi
è il costo unitario, è necessario considerare il costo del sistema. Integrando
la gran parte delle funzionalità su un singolo chip si ottiene una sensibile
riduzione delle dimensioni e della complessità della board, nonché una diminuzione del costo dei componenti standard necessari. Un’altra ragione sono
le prestazioni, integrando tutti i blocchi funzionali su un singolo chip viene
a cadere la necessità di ricorrere a linee esterne - cioè tracce sulla board per l’interconnessione, queste risultano infatti molto più lente per via delle
dimensioni.
Nell’ambito embedded, l’utilizzo di microprocessori con costi anche inferiori alla decina di dollari, rispetto alle centinaia di quelli impiegati nei calcolatori permette di realizzare sistemi, come per esempio un telefono cellulare,
che contiene almeno un paio di microprocessori, un sistema di ricezione e
trasmissione a radiofrequenze, gestisce la tastiera e uno schermo grafico, al
costo complessivo di poche decine di dollari.
2.3
Software di base
L’attività di sviluppo del software per applicazioni embedded viene generalmente ed erroneamente, ritenuta più semplice rispetto a quella delle applicazioni general purpose. Tale assunzione è motivata spesso dalla dimensione
contenuta del codice e in alcuni casi, dall’assenza di un vero e proprio sistema
operativo o di un’interfaccia utente grafica sofisticata. In realtà, nonostante
i tempi di sviluppo ovviamente ridotti rispetto per esempio allo sviluppo di
editor di testi professionali, la scrittura di codice per applicazioni embedded
è spesso complicata e cruciale quanto la progettazione dell’hardware, per
almeno i seguenti motivi.
Strati software Gli strati software che compongono un sistema embedded
sono diversi, vanno dal firmware che è il livello più basso, passando dal
bootloader fino ad arrivare al kernel del sistema operativo con qualche
tipo di interfaccia, grafica o a riga di comando. I sistemi operativi
utilizzati in ambito desktop, concepiti per impieghi di carattere abbastanza generale, sono comunque adatti per applicazioni soft real-time,
invece quando i vincoli sono più stringenti, si richiede lo sviluppo di
soluzioni ad-hoc più snelle ed efficienti. L’utilizzo di sistemi operativi è per quei sistemi embedded più complessi che offrono maggiori
funzionalità.
13
CAPITOLO 2. Sistemi Embedded
Specificità Un software embedded è un software sviluppato per uno scopo
specifico, questo scopo è quello di soddisfare i requisiti di un sistema
embedded. Il livello di dipendenza dall’hardware è molto alto. Esso
viene progettato e sviluppato in modo specifico per gestire un determinato dispositivo hardware. Per questo quando si decide di utilizzare
un sistema operativo desktop, senza svilupparlo da zero, si effettua il
cosiddetto porting. Il sistema operativo viene opportunamente configurato secondo le esigenze e viene sviluppato lo strato software per la
gestione dell’hardware specifico, i device driver.
Prestazioni L’efficienza del codice influisce in modo determinante sui tempi di risposta globali del sistema. I tipici obiettivi di costo impediscono di accrescere semplicemente la potenza di calcolo per rispettare
eventuali vincoli temporali.
Validazione La forte interazione fra hardware e software, in aggiunta alla
presenza di vincoli real-time, rende pressoché impossibile sviluppare e
testare il software indipendentemente dall’hardware su cui dovrà essere
eseguito.
Energia L’organizzazione del software può influire pesantemente sui consumi energetici complessivi del sistema. Spesso l’intelligenza per la
gestione energetica del sistema è inclusa all’interno del software.
Tempi e costi di sviluppo La previsione dei tempi e costi di sviluppo del
software embedded è critica.
Complessità Spesso si usa il linguaggio C, assembler e, se presenti, sistemi
operativi di dimensioni contenute. Questa leggerezza apparente del
software in realtà è motivata dalla necessità di raggiungere prestazioni
ed efficienza spinte: la mancanza del sistema operativo non semplifica
la scrittura delle applicazioni, anzi obbliga il progettista a considerare
molti dettagli di frontiera con l’hardware.
Deployment L’attività di deployment del software differisce dal software
tradizionale. in cui è presente il concetto di plug and play, wizard di
installazione. Nell’ambito embedded il software deve essere caricato
in modo speciale in una EEPROM, attraverso metodi remoti con connessioni TFTP, ecc. Questo rende il processo di aggiornamento del
software molto lungo.
Manutenzione Spesso l’attività di manutenzione è quasi impossibile, poiché
il software potrebbe essere scritto su una memoria permanente interna al sistema (spesso inaccessibile). Inoltre, ogni modifica al sistema
dopo il momento della commercializzazione può comportare un costo
legato al richiamo che supera il costo industriale del dispositivo. Le
14
CAPITOLO 2. Sistemi Embedded
modifiche sono quindi accettabili in genere solo se risolvono criticità o
errori di progetto rilevanti.
Sicurezza I sistemi embedded interagiscono con l’ambiente e anche ovviamente con utenti umani. Per tale motivo gli aspetti di sicurezza devono
essere considerati accuratamente, tenendo conto sia degli impatti del
software che dell’hardware.
La trattazione del software richiede quindi di considerare in modo accurato i
tempi di esecuzione del software, l’impatto del sistema operativo, le modalità
di verifica e la presenza di opportune librerie che verranno utilizzate per lo
sviluppo delle applicazioni.
2.4
I Sistemi Linux embedded
In questa sezione viene preso in considerazione un sottoinsieme dei sistemi
embedded, ovvero i sistemi Linux embedded.
Linux è un kernel open-source Unix-like, distribuito liberamente sotto
i termini della licenza GNU General Public License. E’ stato sviluppato
inizialmente da uno studente Finlandese Linus Torvalds, e la prima release
ufficiale risale all’Ottobre del 1991.
E’ un sistema operativo multi-tasking, multi-user, multi-processor, e supporta una vasta gamma di piattaforme hardware, come x86, Alpha, Sparc,
MIPS, SuperH, PowerPC e ARM. Il kernel Linux è caratterizzato dall’utilizzo di una modalità protetta per la gestione della memoria, ciò significa che
il fallimento di un’applicazione non causa il fallimento di altre applicazioni
o del kernel stesso. [Len01]
Storicamente, Linux è stato sviluppato come un sistema operativo per
gli ambienti desktop e server, recentemente invece è cresciuto l’interesse
di adattarlo ai sistemi embedded. Vediamo quali sono le caratteristiche
principali che rendono Linux preferibile ad altri sistemi operativi nell’ambito
embedded:
Nessun pagamento per la licenza: per alti volumi di produzione il non
pagamento della licenza riduce notevolmente i costi di sviluppo.
Codice sorgente open source: il kernel e le applicazioni di supporto sono
completamente open source, questo permette un’accesso diretto al
codice sorgente. Gli sviluppatori possono vedere esattamente come
lavorano tutte le parti del sistema, possono personalizzarne il comportamento, e ottimizzarne le performance nelle sezioni di codice critiche;
tutto ciò è irrealizzabile utilizzando un qualsiasi altro sistema operativo commerciale. (Si fa notare inoltre che in alcuni ambiti, come
quello medico, è necessario certificare l’intero corpo del codice in esecuzione, ed è possibile solamente se l’intero codice sorgente del sistema
operativo è disponibile per l’ispezione e il test).
15
CAPITOLO 2. Sistemi Embedded
Affidabilità: Linux ha un’invidiabile reputazione per la sua robustezza.
L’Up-time è stato calcolato essere di settimane o anni, e il sistema
raramente deve essere riavviato.
Scalabilità: il kernel è molto modulare e scalabile. Questo rappresenta un
forte vantaggio quando viene utilizzato in ambienti che hanno risorse
limitate.
Elevato numero di programmatori: Linux è sviluppato da un numero
elevato di persone, che comunicano tra loro attraverso Internet, questa
moltitudine di programmatori consente a Linux di essere un sistema
operativo molto maturo.
Supporto: attraverso Internet è possibile accedere a molta documentazione
ed esistono molteplici aziende che forniscono supporto, formazione
e seminari. Inoltre è possibile iscriversi ad una delle tante mailing
list, all’interno delle quali si apprendono tante nozioni ed è possibile
effettuare qualsiasi domanda relativa ad un problema riscontrato.
Standard: tutte le componenti di Linux sono sviluppate secondo degli standard ben precisi.
Portabilità: è possibile sviluppare un’applicazione su di una macchina Linux host, e successivamente trasferirsi sulla macchina target, questo
riduce notevolmente il tempo impiegato per il debug.
Un sistema embedded è un sistema progettato per realizzare una o poche
funzionalità dedicate, spesso con vincoli di computazione real time. Come è
possibile osservare in figura 2.5, l’architettura generale di un sistema Linux
Embedded è costituito da tre componenti principali.
Al livello più basso troviamo l’hardware, il quale deve avere alcune caratteristiche specifiche per l’esecuzione del sistema Linux. Prima di tutto è
necessaria almeno una CPU di 32-bit contenente una unità di gestione della
memoria (MMU), dopodichè sono richieste una sufficiente quantità di RAM
e uno storage per ospitare il filesystem.
Sopra all’hardware risiede il kernel, il quale rappresenta il componente
principale del sistema operativo. Il suo scopo è quello di gestire coerentemente l’hardware mentre fornisce un alto livello di astrazione al software
del lato utente. All’interno del kernel esistono due categorie di servizi che
forniscono le funzionalità richieste dalle applicazioni. Le interfacce di basso
livello sono specifiche per la configurazione dell’hardware dove il kernel è
in esecuzione, e consentono il diretto controllo delle risorse hardware utilizzando delle API indipendenti dall’hardware (device driver). Al di sopra dei
servizi di basso livello forniti dal kernel, vi sono le componenti di alto livello,
16
CAPITOLO 2. Sistemi Embedded
Figura 2.5: Architettura generale di un sistema Linux Embedded
le quali hanno il compito di fornire un’astrazione comune a tutti i sistemi
Unix, inclusi processi, file, socket e segnali.
Sopra al kernel ci si aspetterebbe di trovare le applicazioni e le funzionalità che permettono l’utilizzo del sistema, invece i servizi esportati dal kernel
non possono essere utilizzati direttamente dalle applicazioni. Per questo motivo è necessario utilizzare le librerie di sistema, le quali interagiscono con il
kernel per fornire le funzionalità desiderate. Le librerie principali utilizzate
dalle applicazioni Linux sono le GNU C, ma spesso nei sistemi embedded
queste sono sostituite da librerie con funzionalità ridotte come Qt, XML o
MD5. [Yag03]
2.4.1
Sviluppo
Le risorse computazionali di un sistema embedded sono ridotte al minimo
per minimizzare i costi, lo spazio e il consumo energetico. A causa di questo,
essi non possono ospitare ambienti di sviluppo. Non è possibile eseguire
su di essi nessun IDE, editor di testo, compilatori e debugger. Tuttavia è
necessario scrivere applicativi per questi sistemi.
Per risolvere il problema, il codice viene sviluppato su una macchina detta host, ovvero una piattaforma con maggiori risorse computazionali, opportunamente configurata per generare il codice oggetto che sarà poi trasferito
ed eseguito sulla macchina target, la piattaforma embedded.
Il processo relativo alla costruzione di un programma su di un sistema
host per poter poi essere eseguito su di un sistema target è chiamato cross
compilazione, l’elemento fondamentale della cross compilazione è il cross
compilatore.
Quindi, nel processo di sviluppo di un sistema embedded esistono due
ambienti, un ambiente host e un ambiente target e attraverso il cross com-
17
CAPITOLO 2. Sistemi Embedded
pilatore il software embedded viene compilato per un particolare processore
presente sulla macchina target.
In ambito Linux, il GCC è il compilatore ottimale da utilizzare per costruire una cross toolchain. La costruzione di un cross compilatore e di una
cross toolchain non sono operazioni semplici. Per effettuare una cross compilazione non è sufficiente disporre di un cross compilatore configurato ed
ottimizzato per una particolare architettura hardware. Sono necessarie anche una serie di utility, che a loro volta devono essere costruite, ottimizzate
e configurate per poter contribuire alla cross compilazione per quella particolare architettura. Il cross compilatore richiede il supporto delle librerie C
e di altri eseguibili, come il linker, l’assembler, il debugger. L’insieme dei
tool, delle librerie e dei programmi usati per la cross compilazione si chiama
cross platform toolchain, o toolchain in breve.
Tutte le macchine atte alla compilazione di codice dispongono di una
toolchain. Se gli eseguibili prodotti sulla macchina host dovranno girare
su una macchina target dotata di una architettura simile all’host, allora
la toolchain è detta nativa. Se macchina host e macchina target hanno
differenti architetture allora la toolchain è detta cross platform.
Il GCC utilizza un particolare prefisso per identificare la piattaforma
per cui genererà i binari. Il prefisso ha la seguente forma CPUTYPEMANUFACTURER-KERNEL-OPERATINGSYSTEM. Per un generico compilatore per ARM il prefisso standard può essere il seguente arm-st-linux-gnu.
La cross toolchain può essere costruita a mano o si possono utilizzare dei
tool (crosstool, buildroot, crossdev) che cercano di automatizzare tutto il
processo di costruzione della toolchain. Il principale problema che si può
incontrare utilizzando questi tool è che potrebbero non funzionare e fallire
la costruzione della cross toolchain. Questa eventualità può accadere soprattutto se si cerca di utilizzare una versione del compilatore GCC, dei binutils,
delle librerie C o del kernel che ancora non sono supportati dal tool. In
questi casi l’unica alternativa è quella di costruire a mano la toolchain. La
costruzione della toolchain può rivelarsi un compito più o meno complicato a
seconda dei pacchetti software che si utilizzeranno per la sua realizzazione.
18
3
Testing di Software di Sistemi Embedded
l controllo della qualità del software [PY08] ha lo scopo di identificare e rimuovere i difetti durante tutto il processo di sviluppo, dallo
studio di fattibilità alla fase di mantenimento, e consiste di attività quali la
validazione e la verifica. La verifica controlla la consistenza tra artefatti intermedi(una specifica dei requisiti, un progetto, un sistema eseguibile, ecc.),
invece la validazione confronta gli artefatti intermedi con i requisiti richiesti
dal committente per il prodotto finale.
La validazione e la verifica vengono eseguite con approcci statici, analisi
e approcci dinamici, testing. Le tecniche di analisi, come l’ispezione non
automatica, sono quelle che non richiedono l’esecuzione di un programma.
Sono di fondamentale importanza nelle fasi iniziali del processo di sviluppo
(specifica dei requisiti e design), in cui c’è poco codice da eseguire. Il testing,
in realtà è un vero e proprio processo, costituito da attività che vengono
svolte nelle diverse fasi del ciclo di vita del software. Le principali attività
sono la pianificazione e il monitoraggio, in cui vengono identificati i requisiti
di qualità e pianificate le tecniche di test da utilizzare; generazione dei test;
esecuzione dei test e miglioramento del processo, in cui vengono raccolti i
dati sui difetti e analizzati. Le tecniche di test più importanti sono [Wan04]
test di unità (funzionale e strutturale), test di integrazione, test di sistema,
test di accettazione, test di regressione, utili a identificare e risolvere classi
di problemi differenti.
Il processo di testing assume varie forme in base al contesto di progetto,
al processo di sviluppo, al prodotto finale, alle risorse economiche disponibili e ai requisiti di qualità. Questo si traduce in una scelta dell’insieme di
tecniche ogni volta differente. Infatti, le tecniche di testing applicabili ad esempio al software procedurale o object-oriented, di solito sono parzialmente
applicabili alle altre tipologie di software. Domini come quello del software
real-time o safety-critical, necessitano la verifica di particolari proprietà.
Il dominio dei sistemi embedded ha delle caratteristiche peculiari che
I
19
CAPITOLO 3. Testing di Software di Sistemi Embedded
influenzano tutte le attività del processo di sviluppo. Il testing non è un’eccezione. Questo dominio estremamente complesso, richiede tecniche specifiche a causa di un processo di sviluppo differente che deve portare avanti
la creazione sia della parte hardware che della parte software del sistema;
requisiti di qualità molto stringenti; costi contenuti e vincoli di tempo che
specificano un time-to-market molto breve.
Esistono principalmente due categorie di sistemi embedded, critici (safetycritical), presenti in campo automobilistico, avionico, aerospaziale, ferroviario,
medico, nucleare e militare, e meno critici (less safety-critical), si pensi ai più
comuni elettrodomestici e prodotti elettronici di consumo, cellulari, lavatrici,
forni a microonde, condizionatori, ecc. Sempre più, il software è responsabile
della gestione e del corretto funzionamento di questi sistemi, diventando un
elemento chiave. La presenza di difetti all’interno del software minaccia la
stabilità dell’intero sistema, e causare fallimenti e conseguentemente, soprattutto per i sistemi critici, provocare ingenti perdite economiche e la perdita
di vite umane. Rispetto ad altri ambiti, quello embedded introduce requisiti
di criticità nel sistema che hanno bisogno di maggiore attenzione. Il testing
è uno degli elementi per garantire la qualità del sistema e valutare questi
requisiti.
Questo capitolo introduce inizialmente le fasi del processo di testing dei
sistemi embedded, focalizzandosi poi sulla fase di prototipazione e sul testing della componente software. Successivamente, delinea le differenze con
il testing di software tradizionale. Viene approfondito il testing nell’ambiente target, individuando le tecniche di test che possono essere applicate
in quest’ambiente. Vengono poi introdotti i livelli software, per descrivere
quale sia, in ambito embedded, quello su cui concentrare il controllo di qualità. Infine, viene descritta l’automazione del testing e la personalizzazione
degli strumenti.
3.1
Fasi del testing di sistemi embedded
In questa sezione descriviamo le varie fasi del processo di testing dei sistemi
embedded che vengono eseguite durante tutto il processo di sviluppo, in cui
il sistema è creato in una sequenza di prodotti che diventano sempre più reali.
Consideriamo l’architettura di riferimento del sistema riportata nella Figura
3.1, dove vengono schematizzate le varie componenti del sistema embedded
e dell’ambiente. Sulla sinistra vi è l’ambiente con cui il sistema è in grado
di interagire mediante sensori o attuatori. Sulla destra è mostrato il sistema
embedded, con le sezioni di interfacciamento e di eventuale conversione A/D;
la parte di elaborazione vera e propria legata a un processore e la possibilità
di interfacciarsi con altri sottosistemi con un estensione del sottosistema di
I/O. Tale schematizzazione, seppure nella realtà esistano molte varianti, è
sufficientemente generale e rappresentativa per i nostri scopi. Nel testing
del sistema embedded possono essere identificate le diverse seguenti fasi:
20
CAPITOLO 3. Testing di Software di Sistemi Embedded
Figura 3.1: Sistema embedded: schema di riferimento
Simulazione, Prototipazione, Pre-produzione, Post-produzione. Nel seguito è
presente una descrizione di dettaglio per ogni fase.
Quanto più ci si avvicina al sistema reale nel testing, tanto più le misure
saranno affidabili; i tempi di sviluppo e l’effort necessario cresceranno di
conseguenza, così come i rischi di progetto legati a uno sviluppo sempre più
avanzato del prodotto. Il motivo per introdurre il testing ai vari stadi dello
sviluppo serve per evitare di avere errori da correggere nelle fasi finali, dove
l’impatto sui costi potrebbe portare al fallimento degli obiettivi di profitto.
La transizione dal sistema simulato a quello reale segue alcuni passi
di raffinamento, che avvicinano in modo graduale il sistema in esame alla
configurazione del sistema finale, come mostrato nella Figura 3.2.
3.1.1
Simulazione
Nella fase di simulazione tutti gli elementi del sistema sono simulati. Gli obiettivi sono legati alla fase di progettazione e verifica funzionale del sistema,
tale fase non è pertanto strettamente obbligatoria.
Si possono identificare almeno tre possibili tipologie di testing basato su
simulazione: one-way, con feedback e prototipazione rapida.
One-way Nella simulazione one-way (Model Testing, MT) il comportamento dell’ambiente viene ignorato: vengono generati i segnali di input e
raccolti gli output corrispondenti (Figura 3.3). L’implementazione ricorre in genere a una simulazione del sistema embedded tramite un
21
CAPITOLO 3. Testing di Software di Sistemi Embedded
Figura 3.2: Transizione dal sistema simulato a quello reale
PC, dove si simulano i canali di comunicazione per l’input/output,
oltre ad avere il controllo e l’accesso diretto ai registri e alle variabili
del sistema simulato. Una interessate automazione di tale approccio
può essere il confronto automatico fra l’output atteso e quello reale.
Figura 3.3: Simulazione one-way
Feedback Nella soluzione con feedback (Model in the Loop, MiL), schematizzata nella Figura 3.4, viene invece simulato il comportamento dinamico dell’ambiente. Il modello di simulazione è più completo, racchiudendo sia il sistema embedded sia l’ambiente, ma è applicabile in
modo proficuo solo nei casi in cui siano derivabili modelli del sistema e
22
CAPITOLO 3. Testing di Software di Sistemi Embedded
dell’ambiente sufficientemente semplici, ma ragionevolmente accurati.
Figura 3.4: Simulazione con feedback
Prototipazione rapida La prototipazione rapida (Rapid Prototyping, RP)
è affine alla simulazione con feedback. Il comportamento dinamico dell’ambiente non viene simulato, ma è ottenuto direttamente dall’ambiente reale, o da una sua replica sufficientemente affidabile. Il modello
di simulazione non comprende l’ambiente, ma solo il sistema embedded,
come mostrato nelle Figura 3.5.
Figura 3.5: Prototipazione rapida
3.1.2
Prototipazione
Il livello successivo di dettaglio rispetto agli approcci simulativi prevede la
prototipazione. Alcuni degli elementi del sistema possono essere simulati,
altri prototipali ma anche reali. Gli obiettivi sono la verifica del modello
di simulazione (se sviluppato) e il riscontro che il sistema soddisfi i principali requisiti di progetto. Normalmente la prototipazione porta al rilascio
di unità da considerare come pre-produzione. In questa fase si procede sostituendo gradualmente, al costo di molte iterazioni, hardware e software
23
CAPITOLO 3. Testing di Software di Sistemi Embedded
simulati a vantaggio di quelli reali. Vi è pertanto la necessità di disporre
di interfacce fra hardware e software (simulati o meno), sapendo in anticipo
che alcuni dei segnali disponibili nei modelli simulati non lo saranno in quelli
reali. In questa attività risulterà utile disporre di strumentazione in grado
di rilevare, memorizzare e analizzare segnali digitali e/o analogici. Si può
procedere con il software arrivando ad avere il cosidetto SiL (Software Inthe-Loop), dove il software reale viene testato sull’ambiente simulato, sino ad
arrivare all’HiL (Hardware In-the-Loop), dove l’hardware reale viene testato
in un ambiente simulato.
Più in dettaglio, con riferimento alla precedente Figura 3.1, si possono
identificare diversi componenti simulabili del sistema complessivo, evidenziati con i numeri 1-4.
1. NVM embedded software - Il software può essere simulato e compilato
su una piattaforma host. Alternativamente si può simulare il softwate
compilato per la piattaforma finale su un emulatore di tale architettura
di calcolo eseguito sul computer host.
2. Unità di elaborazione - Il processore può essere sostituito da uno di
potenza superiore della stessa famiglia o comunque in grado di emulare
il processore della piattaforma finale.
3. Sistema embedded - La simulazione può avvenire mediante emulazione
sulla piattaforma host. Alternativamente si può realizzare una board
sperimentale prototipale.
4. Ambiente - Per la simulazione statica è sufficiente procedere alla generazione dei segnali, mentre la simulazione dinamica richiede l’interfacciamento con una piattaforma di simulazione (PC)
Le tipologie di testing a livello di prototipazione possono essere diverse.
• Test di unità software (Sw/U): verifica dei singoli componenti software.
• Test d’integrazione software (Sw/I): verifica delle interazioni fra le
componenti software.
• Test di unità hardware (Hw/U): verifica il comportamento delle componenti hardware in isolamento.
• Test d’integrazione hardware (Hw/I): verifica delle connessioni fra le
componenti hardware.
• Test d’integrazione hardware/software (Hw/Sw/I): verifica delle interazioni fra le componenti hardware e software.
• Test d’integrazione di sistema (SI): verifica che il sistema si comporti
secondo le specifiche di progetto.
24
CAPITOLO 3. Testing di Software di Sistemi Embedded
• Test ambientale (E): mette alla prova il sistema in presenza di particolari condizioni ambientali.
Test di unità e integrazione software. La funzionalità dei vari moduli
software è verificata usando una simulazione della piattaforma hardware.
Si possono avere due tipologie di test. Il primo prevede che il software
sia compilato per l’esecuzione sul computer host, il quale non ha vincoli
di risorse o performance. Inoltre grazie alla disponibilità di un più ampio
insieme di tool il testing è più facile che sull’ambiente target. L’obiettivo di
questa prima tipologia di test è di verificare il comportamento dei moduli
software. La seconda tipologia prevede la compilazione del software per
l’esecuzione sul processore dell’architettura target. Prima dell’esecuzione
sul target, il software compilato viene eseguito sulla macchina host su cui
è in funzione un emulatore della piattaforma finale. L’obiettivo di questo
test è verificare che il software verrà eseguito correttamente sul processore
target.
Test di unità e integrazione hardware. Le componenti hardware progettate e sviluppate vengono testate singolarmente e in connessione tra loro. Il
processore è quello reale, il resto del sistema è un prototipo. Si tratta di un
test di basso livello, effettuato in laboratorio. Sono necessari strumenti per
la verifica dei segnali elettrici, come oscilloscopi e strumenti per misure di
corrente, ecc.
Test d’integrazione hardware/software. Si basa su un sistema di test
dove i moduli software sono compilati per la piattaforma finale. Si utilizza
inoltre un sistema embedded sperimentale che racchiude buona parte della
piattaforma hardware finale. Il processore deve essere quello per cui il codice
è stato compilato, ovvero quello finale mentre l’ambiente può essere simulato.
Test d’integrazione di sistema. Questo test si verifica utilizzando la piattaforma hardware completa e il sistema embedded prototipale. I moduli
software sono compilati per la piattaforma finale, il processore è quello reale
e l’ambiente è ancora simulato.
Test ambientale. Il test ambientale ha un sistema di test composto da
una piattaforma hardware completa e il sistema embedded prototipale. I
moduli software sono compilati per la piattaforma finale, il processore è
quello reale e l’ambiente è ancora simulato. L’obiettivo di questa verifica
è identificare i problemi con l’ambiente prima di arrivare alla fase di preproduzione. Si cerca di comprendere come il sistema embedded influenzi
l’ambiente e, viceversa, come il comportamento del sistema embedded sia
influenzato dall’ambiente. Ad esempio, possono sorgere dei problemi spostando le funzionalità di un lettore mp3 portatile all’interno dei sistemi elettronici presenti in un’automobile. Temperature che possono superare i 100
gradi centigradi o livelli di disturbo elettromagnetico non previsti in sede di
progetto, potrebbero influenzare il comportamento in modo significativo.
25
CAPITOLO 3. Testing di Software di Sistemi Embedded
3.1.3
Pre-produzione
La pre-produzione prevede un test di sistema (ST) dove sia il software sia
l’hardware sono quelli reali. Fra i vari obiettivi, vi è sicuramente quello di dimostrare che tutti i requisiti di progetto sono soddisfatti e si può
procedere verso la produzione richiesta dal mercato o dal committente, a
cui normalmente tale sistema viene presentato e consegnato per le verifiche
di accettazione. Il testing a questo livello di avanzamento del prodotto
serve anche a dimostrare la conformità a standard per esempio imposti dai
marchi di qualità o da normative legislative. La possibilità di verificare il
comportamento nell’ambiente d’uso è utile per dimostrare l’affidabilità del
sistema.
3.1.4
Post-produzione
In questa fase solitamente viene verificata la catena di produzione, ispezionando la qualità del prodotto e monitorando tempi e costi effettivi di produzione. Dopo aver ispezionato a fondo i primi prodotti che escono dalla
linea di produzione, la cui analisi dei problemi serve a tarare la linea di
produzione e l’intera filiera dei fornitori di materiali, si può lasciare in esercizio solo un test di mantenimento. Tali verifiche sono volte essenzialmente
a garantire che la qualità del prodotto rimanga costante; in genere si svolgono controlli sommari su tutti i sistemi prodotti e verifiche approfondite
solamente su un loro campione. L’integrazione con la rete di assistenza che
effettua le riparazioni sui prodotti già immessi sul mercato sarebbe l’anello
ideale più esterno per garantire la qualità totale del prodotto rispetto ai
difetti di progetto e di produzione.
Il quadro riassuntivo degli approcci al testing per un sistema embedded
è riportato nella Tabella 3.1
Approccio
One way (MT)
Feedback (MiL)
Rapid Prototyping (RP)
Sw/U, Sw/I 1(SiL)
Sw/U, Sw/I 2(SiL)
Hw/U (HiL)
Hw/I (HiL)
Hw/U, Sw/I (HiL)
SI (HiL)
E (Hil/ST)
Pre-produzione
Software
simulato
simulato
sperimentale
sperimentale/reale(host)
reale(finale)
reale(finale)
reale(finale)
reale(finale)
reale(finale)
Processore
sperimentale
host
emulatore
reale(finale)
reale(finale)
reale(finale)
reale(finale)
reale(finale)
reale(finale)
Sistema
sperimentale
simulato
simulato
prototipo
prototipo
sperimentale
prototipo
prototipo maturo
reale
Tabella 3.1: Caratteristiche degli approcci al testing per sistemi embedded
26
Ambiente
simulato
reale
simulato
simulato
simulato
simulato
simulato
simulato
simulato
reale
CAPITOLO 3. Testing di Software di Sistemi Embedded
3.2
V-Model prototipazione
Fino ad ora, abbiamo visto come un sistema embedded viene sviluppato in
una sequenza di passi che permettono di ottenere una serie di prodotti intermedi, che vanno dal sistema simulato per poi evolvere nel sistema reale.
Il V-Model multiplo, mostrato in Figura 3.6, basato sul ben noto V-Model,
è un modello di sviluppo che prende in considerazione questa caratteristica della progettazione dei sistemi embedded. Ciascun prodotto intermedio
Figura 3.6: V-Model
(modello, prototipi, prodotto finale) segue un completo ciclo di sviluppo
a V che include progettazione, implementazione e testing. L’essenza del VModel multiplo è lo sviluppo di differenti versioni fisiche dello stesso sistema,
ciascuna avente gli stessi requisiti funzionali. Questo si traduce nel fatto che
le tutte le funzionalità del sistema possono essere verificate sia nel modello
sia nel prototipo che nel prodotto finale.
Focalizziamo la nostra attenzione, su una delle quattro fasi descritte:
la prototipazione, in quanto è quella che inquadra meglio l’ambito sperimentale di questa tesi. Poiché i sistemi embedded sono costituiti da una
componente hardware e una componente software dedicata per il controllo
dell’hardware, lo sviluppo di questi sistemi viene fatto in modo da dividere
le funzionalità hardware e software, e quindi nasce la necessità di un testing
dell’hardware, un testing del software e la verifica del sistema completo con
hardware e software integrati. Questa tesi prende in considerazione il testing della componente software fino alla verifica del sistema completo. Non
viene considerato il testing dell’hardware.
Esplicitiamo il V-Model per la prototipazione, mostrando le attività di
progettazione e di testing per la componente software del sistema embedded.
27
CAPITOLO 3. Testing di Software di Sistemi Embedded
Il risultato è mostrato in Figura 3.7
Figura 3.7: V-Model Prototipazione: attività di progettazione e testing della
componente software
3.3
Differenze con il testing di software tradizionale
La differenza principale tra il testing di software embedded e il testing di
software tradizionale, deriva dal processo di sviluppo utilizzato in ambito
embedded. Come descritto nel capitolo 2, l’ambiente di sviluppo è diverso dall’ambiente di esecuzione, ovvero il software viene sviluppato su una
macchina con risorse computazionali maggiori e architettura differente detta host, rispetto alla macchina detta target che coincide proprio con l’architettura embedded per la quale il software viene creato e sulla quale deve
essere eseguito. Il motivo principale di questo approccio è che l’ambiente di
sviluppo non è supportato in termini di risorse hardware e software dalla
piattaforma target. Questa suddivisione tra host e target porta ad avere
due ambienti per il testing, in cui applicare tecniche diverse per valutare
requisiti di qualità differenti. Questi due ambienti, nel testing di software
tradizionale non esistono in quanto l’ambiente utilizzato per lo sviluppo e anche quello di esecuzione per cui il testing è effettuato in un singolo ambiente.
Questa differenziazione porta al problema di definire la strategia di testing:
28
CAPITOLO 3. Testing di Software di Sistemi Embedded
quale test deve essere eseguito sull’ambiente host e quale sull’ambiente target? Un certo requisito di qualità deve essere valutato nell’ambiente giusto.
Tutto questo influenza le tecniche di testing da utilizzare nell’ambiente host
e nell’ambiente target.
3.3.1
Testing nell’ambiente host
Il testing del software nell’ambiente host viene effettuato nelle fasi iniziali dello sviluppo del sistema embedded (fase di simulazione, principalmente, ma
anche in parte nella prototipazione) e non evidenzia differenze con il testing
di software tradizionale. In buona parte nelle fasi iniziali sono applicabili
le classiche soluzioni sviluppate nell’ambito dell’ingegneria del software, opportunamente adattate sulla base dei linguaggi utilizzati e della dimensione
del codice. Devono essere verificate le caratteristiche funzionali del software
in esecuzione in un ambiente che simula la piattaforma reale, attraverso le
tecniche di testing utilizzate anche nel software tradizionale, come il test di
unità e integrazione. Il test di unità ha lo scopo di rilevare difetti all’interno
della più piccola unità software (modulo) sia dal punto di vista strutturale
che funzionale. Il test di integrazione è utile per identificare i difetti nella
trasmissione di dati o messaggi tra i moduli software. Il testing nell’ambiente
host viene effettuato per diversi motivi:
• per verificare la correttezza del software, pur essendo eseguito su un
ambiente che non è quello reale. L’identificazione dei difetti relativi alla logica e struttura nella fasi iniziali, rende la rilevazione e la
risoluzione dei difetti più facile, veloce ed economica;
• per iniziare il testing molto presto, disaccoppiandolo dalla disponibilità dell’hardware specifico, che potrebbe non essere ancora stato
sviluppato o costare troppo per essere utilizzato durante lo sviluppo;
• gli strumenti per l’esecuzione automatica dei test potrebbero non essere supportati dall’ambiente target.
Un approccio per il testing di software embedded nell’ambiente host,
che utilizza una simulazione della piattaforma hardware reale è descritto
in [EGW06]. Il testing basato su simulazione è un buon modo per ridurre
la complessità del testing di sistemi embedded, riducendo costi e tempi, e
aumentando la copertura.
3.3.2
Testing nell’ambiente target
Le maggiori differenze emergono quando il testing del software deve essere spinto al livello più basso di astrazione, considerando anche peculiarità
dell’architettura di calcolo su cui andrà eseguito.
29
CAPITOLO 3. Testing di Software di Sistemi Embedded
L’introduzione del testing nell’ambiente target avviene perché bisogna
considerare le dipendenze del software con l’hardware specifico. Esso si
focalizza sulla verifica dell’interazione tra l’hardware e il software. La forte
relazione tra questi due componenti del sistema embedded, in aggiunta alla
presenza di vincoli real-time, rende pressoché impossibile sviluppare e testare
il software indipendentemente dall’hardware su cui dovrà essere eseguito.
Anche se il testing nell’ambiente host verifica che il software è eseguito
correttamente, non si ha la certezza che la stessa cosa valga per l’ambiente
target. Appena l’hardware diventa disponibile, si può verificare che il codice
operi correttamente sulla piattaforma sotto condizioni realistiche.
Il testing di un sistema embedded non si conclude con i test separati del
software e dell’hardware, ma richiede una verifica congiunta delle funzionalità e del corretto interfacciamento del software con l’hardware che esso deve
gestire.
Il test effettuato nell’ambiente target è un test di integrazione dell’hardware e del software.
Per la dipendenza dall’hardware e per lo sviluppo parallelo del software
con l’hardware stesso, nascono vari problemi per il testing nell’ambiente
target:
• il testing può essere ritardato a causa della disponibilità di hardware,
non sempre tempestiva, aumentando inoltre il costo di correzione dei
difetti;
• il testing può essere rallentato dal numero ridotto di unità del nuovo
hardware, che devono essere condivise dal team di test;
• i modelli di hardware su cui testare il software possono essere diversi.
Un modello può avere molte revisioni. Il software e i test possono
cambiare in base al modello e alla revisione;
• oltre al concetto di copertura software, è presente anche quello di copertura hardware. Il software deve essere testato per ogni modello hardware e revisione. Inoltre devono essere verificate le funzionalità per
ogni periferica disponibile sul modello o sulla revisione. Deve essere
creata una matrice di copertura hardware;
• un alta percentuale di difetti hardware può essere scoperta durante
questo test. Localizzare il difetto, che può essere relativo all’hardware
o al software non è un compito facile. Per questo il team deve essere
composto anche da membri con competenze sull’architettura e sugli
strumenti di debugging come ad esempio analizzatori di bus, strumenti
con interfaccia JTAG e oscilloscopi;
• il livello di automazione non è massimo. Esiste ancora un considerevole
percentuale di test manuali a causa dell’interazione con l’hardware che
non può essere facilmente automatizzata.
30
CAPITOLO 3. Testing di Software di Sistemi Embedded
I requisiti di qualità del software e le tecniche di testing per valutarli, cambiano in base al dominio applicativo.
Le tecniche di testing esistono in un complesso spazio di trade-off e spesso
hanno punti di forza e debolezze complementari. Non esiste una singola
tecnica che è utile per tutti gli scopi, ma la combinazione di tecniche serve
ad identificare diverse classi di problemi.
Descriviamo alcune tecniche che possono essere utilizzate nell’ambiente
target per valutare requisiti di qualità di maggiore interesse per il software
di sistemi embedded. L’elenco presentato nella tabella 3.2 non è esaustivo. Prendiamo in considerazione solo le tecniche utilizzate nell’ambiente
sperimentale e descritte approfonditamente nei capitolo successivi.
Devono essere verificate le caratteristiche funzionali del software, questa
volta in esecuzione sulla piattaforma reale, dove le misure sono più affidabili, e le caratteristiche non funzionali che possono essere valutate soltanto in quest’ambiente con l’effettiva interazione del software con l’hardware
dedicato.
Tecnica di Testing
test funzionale
strutturale
e
test di copertura
prestazioni codice in
fase di boot
uso e gestione della
memoria
prestazioni di I/O su
dispositivi a blocchi
Descrizione
Classe di Problemi
verifica del software di basso livello nella gestione dei
dispositivi hardware
verifica della copertura del
codice da parte dei test
funzionali e strutturali
applicazione di tecniche di
profiling (function tracing)
per la valutazione della durata delle funzioni nel processo
di boot
verifica come viene utilizzata la memoria dal software
verifica delle prestazioni
delle operazioni di scrittura
e lettura sui dispositivi a
blocchi della piattaforma
target
difetti nelle operazioni che
il software permette di eseguire sull’hardware
problemi nell’efficacia dei
test
correttezza
funzionale
punti critici del software
con maggiore tempo di esecuzione nella fase di boot
prestazioni
memory leak
prestazioni,
affidabilità
prestazioni
scarsa efficienza delle operazioni di I/O
Requisiti di
Qualità
copertura
Tabella 3.2: Tecniche di testing nell’ambiente target
Forniamo una descrizione più approfondita delle tecniche di testing che
possono essere applicate nell’ambiente target:
Test funzionale e strutturale il software si occupa della gestione dei dispositivi e fornisce le operazioni per utilizzarli. Questo test si rende necessario per verificare la correttezza delle operazioni. Queste possono
31
CAPITOLO 3. Testing di Software di Sistemi Embedded
essere semplici e interessare un singolo device, ad esempio la lettura
di un file da un dispositivo USB, oppure più complesse coinvolgendo
più dispositivi, ad esempio l’esecuzione di un playback mp3 da una
memoria USB. L’analisi di copertura è utile per verificare l’efficacia
dei test.
Prestazioni l’ottimizzazione del software per la piattaforma target è importante. Valutare le prestazioni del software con l’hardware specifico
è possibile solo nell’ambiente target, in cui i valori sono quelli reali.
Una prima valutazione delle prestazioni è possibile con il profiling, utilizzato per ottimizzare la fase di boot. Con le informazioni di profiling
sulle funzioni chiamate durante la fase di boot, è possibile determinare
quali porzioni di codice sono più lente di quanto ci si sarebbe aspettato. Queste sezioni di codice sono quindi delle ottime candidate ad
essere riscritte per rendere l’esecuzione del programma maggiormente
veloce. Una seconda valutazione, è data dal calcolo delle prestazioni
delle operazioni di I/O sui dispositivi a blocchi del sistema. Mentre
i test funzionali verificano la correttezza delle operazioni, l’analisi di
prestazioni permette di verificare non solo l’efficacia nella gestione dei
dispositivi ma anche l’efficienza delle operazioni.
Utilizzo della memoria questo tipo di test verifica che il software utilizzi la memoria in modo corretto. L’attenzione è posta sul primo
tra i problemi di gestione della memoria, che potrebbe compromettere
l’affidabilità e le prestazioni del software e del sistema embedded in
generale, il memory leakage.
3.4
Livelli software
Un sistema embedded è una combinazione di hardware e software tra loro
fortemente integrati. Il processo di sviluppo procede in due direzioni, da un
lato, lo sviluppo della parte hardware e dall’altro lo sviluppo della parte software. In questa sezione ci concentriamo su quest’ultima, delineando i livelli
software esistenti in un sistema embedded [SKCL08]. Infine discutiamo su
quale livello sia più importante concentrare il processo di testing. La figura
3.8 mostra i livelli software di un sistema embedded.
32
CAPITOLO 3. Testing di Software di Sistemi Embedded
Figura 3.8: Livelli software di un sistema embedded
Sopra il livello dell’hardware vero e proprio, si trova il firmware, un
piccolo programma costituito da istruzioni di basso livello che permette di
inizializzare i componenti elettronici. E’ memorizzato nella memoria ROM
del System-on-Chip. Sopra il livello del firmware è presente il bootloader, il
programma che carica il kernel di un sistema operativo e ne permette l’avvio.
Il livello successivo è il Board Support Package o Base Port (BSP), l’insieme
dei device driver che offrono il primo e più basso livello di astrazione dell’hardware, e consente al sistema operativo di essere eseguito sulla piattaforma specifica. Al livello successivo è presente il kernel del sistema operativo.
Gli ultimi livelli sono rappresentati da interfacce grafiche complesse oppure
più semplici, a riga di comando, e dal software applicativo.
L’utilizzo di un sistema operativo, in ambito embedded, generalmente
non significa sviluppare da zero le sue funzionalità, ma piuttosto si intende,
effettuare una configurazione e strutturazione, partendo da una struttura
generica di un sistema operativo disponibile, al fine di adattarlo alle esigenze
del software applicativo da un lato, e del supporto hardware dall’altro. Si
effettua il cosiddetto porting1 del sistema operativo. Il porting consiste di
1
Il porting è l’operazione con cui un programma, sviluppato originariamente per una
piattaforma, viene modificato nel suo codice sorgente in modo da poter essere utilizzato
in un’altra piattaforma.
33
CAPITOLO 3. Testing di Software di Sistemi Embedded
due operazioni principali: sviluppo del BSP e definizione delle caratteristiche
del kernel.
Il BSP fornisce un’interfaccia più astratta del livello hardware, nascondendo i dettagli della piattaforma specifica al sistema operativo. Possiamo
vedere il BSP come uno strato software di astrazione dell’hardware su cui
si poggia il sistema operativo per poter essere eseguito sulla piattaforma,
senza preoccuparsi di dettagli di basso livello. Nei sistemi embedded il BSP
non è un livello separato ma è integrato con il kernel in un unico eseguibile.
Quindi il kernel non è completamente generico, ma integra al suo interno
le definizioni dei driver e dei device dell’architettura, attraverso l’utilizzo
di file di descrizione. Questo avviene per ragioni di ottimizzazione, al contrario dei sistemi discreti, come ad esempio i personal computer basati su
core x86, provvisti di un data base di driver che vengono caricati al volo
in funzione dei dati ricavati da un ulteriore livello software, il BIOS (Basic
Input Output System). Il BIOS è un componente software di solito assente
nei sistemi embedded. Infatti si trova nei sistemi a componenti discreti. In
questi sistemi infatti la CPU, la scheda grafica, il controller della seriale, il
controller ethernet, ecc. sono componenti presenti su board distinte, interconnesse ed intercambiabili. Il BIOS è quel software che consente alla CPU
di guardarsi attorno per capire a quali altri dispositivi è connessa (tipo di
dispositivi, marca, modello, ID), e come risultato produce la compilazione di
una struttura dati in memoria che descrive appunto la topologia del sistema
con tutte queste informazioni, e viene poi passata al kernel del sistema operativo che la usa per capire quali driver caricare, in quale ordine e con quali
parametri di ingresso (base addrress, canale IRQ, ecc.). In questi sistemi il
BSP esiste come strato software separato dal kernel del sistema operativo,
che è completamente generico e non include la definizione di nessun driver
e nessun device, in quanto ricavati dinamicamente con il BIOS.
Definire le caratteristiche del kernel significa scegliere quali delle funzionalità tipiche si vogliono utilizzare nel sistema operativo e come esse devono
essere configurate. Tra queste le più rilevanti sono le seguenti.
• Scheduling. Definisce che tipo di meccanismo di scheduling utilizzare
(fixed, priority based, round-robin, preentable e così via).
• Gestione della memoria. Specifica in primo luogo se si vuole o meno
disporre di un meccanismo di gestione della memoria e, in secondo luogo, come questa deve essere organizzata e quali funzionalità si desidera
avere. Tra questa troviamo, per esempio, paginazione, segmentazione,
protezione,gestione della memoria condivisa e altre ancora.
• Comunicazione tra processi. Per i sistemi multitasking o multithreading, il problema della comunicazione e della sincronizzazione
tra task è di fondamentale importanza. Nella letteratura e nelle implementazioni reali si trovano diversi meccanismi quali, per esempio,
34
CAPITOLO 3. Testing di Software di Sistemi Embedded
semafori, mailbox, code, memoria condivisa, pipe, socket e altri ancora. Nella fase di configurazione del kernel, il progettista deve scegliere
quali di questi meccanismi utilizzare.
• File system. In alcuni sistemi sono presenti memorie di massa allo
stato solido - tipicamente memorie Flash - sulle quali si pensa di organizzare dati in modo strutturato. In questo caso è necessario prevedere
l’uso di un file system e configurarlo in modo adeguato alle necessità
specifiche.
• Networking. Sempre più spesso i sistemi embedded offrono la connettività verso il mondo esterno mediante interfacce wired e/o wireless.
Questo richiede, oltrre all’hardware di supporto, la disponibilità di tutto lo stack protocollare necessario alla comunicazione. Quale tipo di
protocollo, quali servizi e quali caratteristiche questo deve avere sono
oggetto di scelta da parte dello sviluppatore.
Questi appena elencati sono solo alcuni degli aspetti che necessitano di un’attenta configurazione. Si possono anche definire quali servizi di più alto livello
si vogliono includere nel sistema operativo. Tra questi i due di maggiore interesse sono il supporto per la grafica (raster, vettoriale e così via) e l’insieme
dei vari servizi applicativi e protocolli di rete (HTTP, FTP, ecc.) [BF07]
3.4.1
Impatto dei livelli software sul testing
La strutturazione del software di un sistema embedded influenza il processo
di testing, che può concentrarsi su diversi livelli.
Il test di integrazione hardware/software nell’ambiente target prevede
di verificare le funzionalità del software nella gestione dell’hardware reale.
Bisogna però capire quale sia il livello giusto dove applicare il test per eseguirlo in modo corretto e inoltre capire quali sono i componenti standard
e non, nello sviluppo del software, che richiedono un maggior controllo di
qualità.
Il firmware e il bootloader sono due strati software molto legati all’hardware sottostante, la loro verifica non è certamente da escludere ma per poter
fare dei test funzionali, essi non permettono il giusto livello di astrazione.
Lo stato delle interfacce e delle applicazioni è un livello generico che si
poggia su altri strati software e non ha un’interazione diretta con l’hardware.
I livelli più importanti in questo contesto sono quello del kernel e dei
device driver. Nell’esecuzione del porting, il kernel del sistema operativo è
considerato un elemento standard, uno strato software già disponibile con
tutte le sue funzionalità, che può essere considerato di buona qualità. Il
suo sviluppo comprende un importante processo di testing, dimostrato da
molte iniziative, nell’ambito open source, che hanno l’obiettivo di garantire
un kernel di qualità, tra queste LTP (Linux Test Project) [18]. L’attenzione,
35
CAPITOLO 3. Testing di Software di Sistemi Embedded
invece, deve esser posta, sullo strato software che viene sviluppato da zero
nel porting di un sistema operativo su una piattaforma hardware specifica e
che quindi potrebbe includere la maggior parte dei difetti.
Il testing nell’ambiente target è concentrato sulla verifica delle componenti software meno visibili all’utente, quelle componenti di livello più basso
che sono a più diretto contatto con l’hardware ma che allo stesso tempo
forniscono il giusto livello di astrazione per il sistema operativo: i device
driver. Questo livello software deve essere testato in maniera integrata con
l’hardware, per verificare la gestione e l’utilizzo corretto dei device presenti
sulla piattaforma target.
3.4.2
Device driver in Linux
I device driver consistono, dal punto di vista user space, in scatole nere
che permettono a un particolare hardware di rispondere a delle ben definite programming interface. Le attività degli utenti vengono quindi eseguite
attraverso un set di operazioni che sono indipendenti dalla specificità del
driver, ed è questo che si occupa di renderle specifiche per l’hardware effettivo. Un driver è in sostanza un software layer di astrazione interposto tra le
applicazioni e il dispositivo (vedi figura 3.9), e chi lo scrive si pone nel ruolo
di colui che dovrà decidere come un componente hardware dovrà apparire.
In realtà, un driver può limitarsi a essere esclusivamente un’astrazione: il
driver di un’interfaccia Ethernet potrebbe infatti utilizzare hardware non
Ethernet, o addirittura non usare nessun hardware (ad esempio, il driver di
loopback). E’ quindi il driver il responsabile della definizione delle funzionalità che un dispositivo può offrire: differenti driver potrebbero mostrare
aspetti o funzionalità differenti dello stesso dispositivo. La tentazione di
renderli ricchi di funzionalità avanzate si scontra tuttavia con la necessità di
mantenerli, per questioni di efficienza, manutenibilità e sicurezza, il più semplici possibile, in particolare scrivendo software il più possibile policy free, e
lasciando le problematiche complesse agli strati più alti del sistema. Molti
driver vengono comunque rilasciati insieme a programmi di utilità o librerie
che semplificano l’interfaccia con il driver e l’hardware, con configurazioni di
default che comunque implicano qualche politica di default, decisa a priori
dallo sviluppatore.
I device in Linux
In Linux si individuano tre fondamentali tipi di device. I loro driver possono
essere sviluppati all’interno di moduli a sé stanti, che possono essere caricati
e rimossi on-the-fly dal kernel. Ogni modulo può implementare uno (o più)
di questi tipi di driver, che sono classificati nel seguente modo:
Char (a caratteri) possono essere acceduti come uno stream di byte. Ad
esempio, device come le porte seriali o la console testuale possono
36
CAPITOLO 3. Testing di Software di Sistemi Embedded
Figura 3.9: Device driver in Linux secondo [CRKH05]
37
CAPITOLO 3. Testing di Software di Sistemi Embedded
essere agevolmente modellati come uno stream. Tali device vengono
visti come nodi del filesystem (es. /dev/ttyS0), e solitamente (ma non
sempre) si differenziano dai file normali per il fatto che possono essere
acceduti solo sequenzialmente.
Block (a blocchi) vengono acceduti anch’essi come nodi del filesystem, e
possono gestire operazioni di I/O su blocchi di dati. I device a blocchi possono quindi ospitare filesystem. La differenza con i device a
carattere è trasparente per gli utenti, ma quelli a blocchi sono dotati
di un’interfaccia con il kernel completamente diversa rispetto a essi.
Network (di rete) si tratta di device che possono scambiare dati con altri
host attraverso un’interfaccia (hardware o software), e sfruttano il sottosistema network del kernel che si occupa delle questioni di alto livello,
come le connessioni, i protocolli, eccetera. Tali dispositivi non possono
essere mappati come nodi sul filesystem, non essendo stream-oriented,
e quindi l’accesso, da parte degli utenti, è radicalmente differente da
quello dei device a carattere e a blocchi.
In generale, ogni classe di dispositivi possiede un’interfaccia peculiare con
il kernel, studiata per i suoi particolari scopi, e ogni driver può implementarne le funzionalità. Parallelamente, ogni driver può implementare, in vari
modi, una certa quantità di ulteriori capabilities che si riflettono sulle possibilità operative dell’utente. Questo può avvenire, ad esempio, tramite la
creazione di nodi del filesystem che permettono di utilizzare lo stesso dispositivo in modi diversi, l’implementazione di particolari funzionalità per il
controllo e la configurazione dell’hardware, la fornitura di strumenti esterni
per l’implementazione di politiche, eccetera.
3.4.3
I device driver - un elemento critico per la qualità
L’approccio ‘comunitario’ allo sviluppo di tutte le componenti del kernel di
Linux tende, per sua natura, alla generazione di una certa disomogeneità all’interno dello stesso. Basti pensare alle porzioni di codice che implementano
i sottosistemi core: essi vengono eseguiti e testati, anche involontariamente,
da tutti quelli che hanno a che fare con tale sistema operativo, e si trovano
sotto gli occhi della gran parte degli sviluppatori. Questo ragionamento
non vale per i device driver; soltanto una piccola porzione della comunità
possiede o conosce un certo dispositivo, e solo questa potrà contribuire in
modo significativo al suo sviluppo.
Bisogna inoltre considerare che lo sviluppo di un device driver richiede
conoscenze approfondite sia riguardo l’hardware effettivo che riguardo le
particolari metodologie e pratiche di sviluppo del sistema operativo. Questa
duplice caratteristica non sempre è riscontrabile nel programmatore medio,
almeno in alcuni degli scenari possibili: ad esempio, chi scrive il driver
38
CAPITOLO 3. Testing di Software di Sistemi Embedded
potrebbe non essere chi ha prodotto effettivamente il dispositivo; le specifiche utilizzate potrebbero essere incomplete, o riguardanti vecchie revisioni,
o addirittura basate su procedure di reverse engineering; all’altro lato, chi
conosce esattamente il dispositivo potrebbe avere capacità di sviluppo di
driver adatti solo a particolari sistemi operativi.
Esistono inoltre delle sostanziali differenze nell’organizzazione dello sviluppo dei driver nel mondo di Linux rispetto a quella di sistemi operativi concorrenti. Innanzi tutto, per volontà dello stesso Linus Torvalds, l’approccio
allo sviluppo per i driver è del tipo merge early, ovvero i criteri per l’accettazione degli stessi nel kernel sono molto laschi rispetto a quelli delle
altre sezioni2 . Il consenso generale, molto più ufficiale che ufficioso, sembra affermare che un driver, per essere accettato nel kernel, debba avere le
seguenti caratteristiche:
• compila e sembra funzionare;
• non ha problemi di sicurezza (security holes) evidenti;
• ha un maintainer attivo;
• non incide sulle persone che non possiedono quell’hardware;
• non introduce interfacce in user space non necessarie.
Il tutto si traduce in due effetti, uno negativo e uno positivo:
• nel kernel si possono ritrovare driver scadenti o con review insufficiente;
• ai driver viene garantita una grande visibilità, o almeno molta di più
rispetto a driver rilasciati con sistemi proprietari, che porta a un loro
più rapido sviluppo, grazie al naturale processo di review pubblico.
Un’altra importante variabile, che rende utile l’early merge, è che Linux
non ha né un’ABI (Application Binary Interface), né un’API (Application
Programming Interface) stabile verso i driver. Ciò significa che non c’è
garanzia che l’interfaccia offerta in una versione del kernel sarà presente nella
successiva, e anzi queste cambiano continuamente ad ogni release. L’early
merge risulta quindi anche una necessità, in particolare per tutti quegli
IHV (Independent Hardware Vendor) che non hanno grandi interessi nella
manutenzione continua ed eterna dei loro driver.
In altri sistemi operativi, con procedure e metodologie di sviluppo dei
driver differenti, le cose non vanno meglio. Nel momento del boom di Microsoft Windows XP e di Windows Server 2003, ad esempio, si è stimato (si
veda anche [SBL05]) che gli oltre 35.000 differenti driver esistenti, in tutte le
loro 120.000 principali versioni, erano la causa di circa l’85% dei fallimenti
di tali sistemi operativi.
2
a tal proposito si veda http://lwn.net/Articles/270960/
39
CAPITOLO 3. Testing di Software di Sistemi Embedded
Si può in conclusione affermare che l’approccio ‘comunitario’ allo sviluppo delle zone di frontiera del sistema operativo offra l’innegabile vantaggio
di poter disporre rapidamente di funzionalità e supporto con una qualità
accettabile per usi non critici. Altrettanto innegabilmente, tuttavia, il contenuto della cartella drivers del kernel di linux non può vantare una qualità paragonabile alle varie altre cartelle: mm (memory management), fs (file
system support), ipc (inter process comunication), net (networking subsystem). E’ inoltre doveroso menzionare il fatto che tale cartella contiene circa
il 70% del codice dell’intero kernel, e che anche per la sola semplice probabilità la maggior parte dei difetti si troverà in essa. Per confermare l’attualità
di queste affermazioni, basta analizzare i dati della sezione summary reports
del Kernel Bug Tracker3 , il sistema (basato sulla piattaforma Bugzilla 3) attualmente utilizzato per il posting e la gestione dei bug del kernel mainline.
Nel grafico riportato nella figura 3.10 è possibile vedere la diffusione dei bug
(negli stati NEW, ASSIGNED e REOPENED, di qualsiasi severità esclusa
enhancement, e nella sola versione corrente al momento della scrittura) nei
vari sottosistemi del kernel.
Come si può notare i sottosistemi in cui sono presenti la maggior parte
dei difetti sono, prima di tutto Drivers, e questo conferma quanto detto finora, ma anche il sottosistema Platform Specific/Hardware, che coincide con la
cartella arch (architecture dependent subsystems), nella quale è presente il
codice relativo alle piattaforme specifiche. Nell’ambito del porting del kernel
su sistemi embedded, i device driver sviluppati sono inseriti nella cartella
arch, se essi sono driver (ad esempio GPIO) relativi a dispositivi peculiari
per l’architettura che non devono essere registrati a nessun sottosistema di
driver del kernel e nella cartella Drivers se sono relativi a dispositivi più standard (ad esempio USB) che devono essere registrati nei relativi sottosistemi
del kernel. Questi due sottosistemi del kernel hanno un’alta percentuale di
difetti in quanto contengono il codice dello strato software, device driver,
che è chiaramente uno dei punti critici della qualità di un sistema operativo. Il livello di qualità dei device driver lascia molto a desiderare e per essi
non esiste un valido sistema di certificazione ufficiale. Per questo è lo strato
software sul quale concentrare gli sforzi del testing nell’ambito del porting
del kernel Linux su una piattaforma hardware specifica.
3.5
Automazione
Il costo del testing, spesso, è più della metà dell’intero costo dello sviluppo
software, e aumenta considerevolmente quando viene eseguito con modalità
manuali. La complessità dei sistemi embedded attuali rende il test manuale
molto difficoltoso e dispendioso in termini di tempo. Gli esseri umani sono
lenti e tendenti a commettere errori quando si trovano di fronte a processi
3
http://bugzilla.kernel.org/
40
CAPITOLO 3. Testing di Software di Sistemi Embedded
Figura 3.10: Diffusione dei bug nelle sezioni del kernel
ripetitivi, è quindi necessario eseguire più operazioni possibili in modalità
automatica.
L’utilizzo di strumenti di automazione, permette di migliorare l’efficacia
e l’accuratezza delle attività di qualità, rilevando un maggior numero di
difetti, e l’efficienza, diminuendo il costo e il tempo dell’esecuzione.
In ambito embedded, è vero comunque, che non tutti i test possono
essere automatizzati, a causa delle interazioni con l’hardware nell’esecuzione
di alcuni test, ma è anche vero che l’automazione permettere l’esecuzione di
operazioni che manualmente non sarebbero di fatto implementabili.
L’automazione in ambito embedded soffre, però, di diversi problemi.
Specificità teniche e strumenti il testing di un telefono cellulare è estremamente differente dal testing di un sistema di cruise control interno ad una macchina, necessitano entrambi di tecniche e strumenti specifici per testare le loro particolarità intrinseche. E’ possibile
affermare quindi che non esiste un unico approccio per effettuare il
test di ogni sistema embedded. Le tecniche e gli strumenti applicabili ad un sistema, generalmente, non sono applicabili ad un altro e
per questo vengono sviluppati approcci e strumenti di testing ad hoc
[KBE06]. Questo causa la mancanza di strumenti generici, applicabili
in qualunque contesto.
Risorse ambiente target Le risorse computazionali dell’ambiente target,
ridotte rispetto all’ambiente host, influenzano la scelta di quale strumento utilizzare.
41
CAPITOLO 3. Testing di Software di Sistemi Embedded
3.5.1
La personalizzazione degli strumenti
Nel contesto embedded, l’automazione del testing è fondamentale. L’automazione passa però, attraverso la personalizzazione degli strumenti [BJJ+ 07].
La personalizzazione consiste di adattamenti di basso livello all’architettura
di uno specifico processore per rendere funzionante lo strumento.
In alcuni casi, la complessità degli adattamenti, rende la personalizzazione molto difficile, ed essa può concludersi con un successo o un insuccesso, influenzando la raccolta e l’analisi dei risultati per valutare e migliorare
la qualità.
La complessità della personalizzazione, rende poco vantaggioso l’utilizzo
di strumenti esistenti nello specifico sistema embedded, e questo porta molto
spesso allo sviluppo di soluzioni ad hoc.
Nei capitoli successivi, è descritta in maniera approfondita la personalizzazione degli strumenti scelti per applicare le tecniche di testing della
tabella 3.2 nell’ambiente sperimentale. Nel corso dei capitoli, viene fornita
una dimostrazione pratica di quanto può essere difficile, in alcuni casi, la
personalizzazione, portando addirittura al fallimento di utilizzo dello strumento nel sistema specifico, e quanto, in altri casi, non comporta problemi
particolari.
La personalizzazione di uno strumento di automazione comprende:
• scelta dello strumento, in base ai vincoli del contesto applicativo (architettura specifica) e ai requisiti di qualità da valutare;
• ricerca di pattern seguiti in letteratura per personalizzazione in sistemi
simili, con la stessa architettura;
• personalizzazione dello strumento seguendo le linee guida del pattern
ricercato.
42
4
Il Progetto Cartesio
’ambiente sperimentale della tesi è la divisione Automotive Product
Group (APG) di STMicroelectronics (STM), www.st.com. STM è una
multinazionale italo-francese con sede a Ginevra (Svizzera), leader mondiale
nel campo dei semiconduttori. All’interno di APG è stato ideato un nuovo
System-on-Chip (SOC) dal nome Cartesio1 .
L’area automotive è un grande mercato che interessa tutte quelle componenti elettroniche che vengono sviluppate per aggiungere nuove funzionalità
alle moderne autovetture per aumentare la sicurezza o l’efficienza del motore.
Tali componenti sono sempre più programmabili e complessi e la centralità
del software è sempre maggiore. Frasi come ‘‘più del 90% dell’innovazione
all’interno di una moderna autovettura è data dal software” indicano l’importanza del software embedded nella progettazione di un’automobile. Il
corretto funzionamento dei vari componenti di un’automobile, come airbag,
freni, ecc. rende necessario integrare il testing delle componenti hardware e
delle componenti software che le controllano nel processo di sviluppo di tali
sistemi embedded.
Per comprendere meglio l’importanza del testing per i sistemi embedded
in campo automotive, approfondiamo la descrizione dei diversi componenti
di un’automobile che possono essere controllati dal software. Questi possono
essere divisi in tre categorie: engine, cabin e infotainment. I componenti engine sono i più critici e quelli entertaiment sono i meno critici. I componenti
engine sono le parti come freni e sterzo che sono caratterizzati da vincoli
fortemente real-time. I componenti cabin sono meno critici ma comunque
importanti, e sono per esempio i dispositivi per il controllo dei finestrini
e dell’aria condizionata. I componenti infotainment includono device quali
L
1
In tutto il resto della tesi il nome Cartesio è usato per indicare in modo generale
l’intera famiglia di SOC, includendo STA2062 e STA2065. Inoltre sono utilizzati come
sinonimi i termini Cartesio, progetto Cartesio, piattaforma Cartesio, architettura Cartesio,
per indicare l’ambiente sperimentale, in generale
43
CAPITOLO 4. Il Progetto Cartesio
sistemi di navigazione gps, CD player, TV, ecc. Il software che controlla le
componenti engine necessita di un testing rigoroso, attraverso metodi certificati. Per il software delle componenti cabin un testing meno formale ma
rigoroso, in funzione della richieste del cliente, può essere sufficiente; il software relativo alle componenti infotainment necessità più di un’analisi delle
prestazioni per assicurare che i vincoli soft-real time siano soddisfatti.
Quindi come si deduce dalla descrizione del dominio automotive, il testing del software di sistemi embedded può avere differenti livelli, per controllare difetti nelle funzionalità o difetti di prestazione. Un sistema embedded,
deve rispettare vincoli hard o soft real time nell’interazione del sistema con
l’ambiente. Effettuare stime accurate dei tempi di risposta del sistema è uno
degli obiettivi del testing dei sistemi embedded.
4.1
Sviluppo del sistema embedded
Questa sezione, descrive la componente hardware e la componente software del sistema embedded Cartesio. Inoltre mostra l’ambiente di sviluppo
utilizzato.
4.1.1
Hardware
Cartesio è una nuova famiglia di SOC con un alto livello di integrazione di
device su un singolo chip. Il core è costituito dal microcontrollore ARM a 32
bit che permette il supporto per un vasto insieme di periferiche e interfacce
di I/O (Input/Output). Comprende un GPS engine che lo rende un prodotto
ideale per navigatori satellitari, Portable Navigation Device (PND), e per
l’infotainment.
Attualmente le versioni principali di Cartesio sono due, denominate
Cartesio STA2062 e la versione successiva Cartesio plus STA2065.
Per lo sviluppo, Cartesio è montato su board di validazione (EVB), progettate da STM per entrambe le versioni del SOC. Si hanno così le board
EVB20262 per Cartesio e EVB2065 per Cartesio plus. Queste board, permettono di verificare il funzionamento del SOC con le periferiche esterne.
Inoltre le board, sono utilizzate dai clienti come reference design, ovvero
come esempio per realizzare i loro design di prodotto.
In figura 4.1 è mostrato il diagramma a blocchi che fornisce un overview
del SOC, mostrando come il microcontrollore ARM, le periferiche e il GPS
engine sono interfacciati.
Come è possibile osservare in figura 4.1, il SOC deve gestire un numero
elevato di device presenti sia sullo stesso chip che esterni ad esso, ovvero presenti sulla board. L’elenco dei device è mostrato in tabella 4.1. Descriviamo
brevemente i device presentati nella tabella 4.1. L’area Storage include i
device relativi alle varie tipologie di memoria flash, memorie a stato solido
44
CAPITOLO 4. Il Progetto Cartesio
Figura 4.1: Diagramma a blocchi Cartesio plus
45
CAPITOLO 4. Il Progetto Cartesio
Area
Storage
HMI
Audio
Core
I/O
GPS
Grafica
NET
Device
embedded MMC
NAND 64
NAND 1G
NAND 2G
sdMMC0
sdMMC1
USB1.1 host
USB1.1 gadget
USB2.0 host
USB2.0 gadget
USB2.0 extPHY
USB2.0 intPHY
Clcd
Touchpanel
Keyboard
AC97 Audio Codec
Power Manager
DFS
DMA
Timer Sistema
FSMC
GPIO
UART
I2C
SPI
Gps subsystem
SGA Smart Graphic Accelerator
Ethernet Network Adapter
Tabella 4.1: Dispositivi con cui il SoC Cartesio interagisce
46
CAPITOLO 4. Il Progetto Cartesio
non volatili a lettura e scrittura che si differenziano per dimensioni, capacità
e velocità di accesso.
• SD/MMC: (Secure DIgital/MultiMedia Card) memoria flash presenti
sulla board della piattaforma Cartesio con due slot per l’inserimento
di schede esterne e una memoria interna, già integrata sulla board.
• NAND: sono memorie rapide e capienti con accesso sequenziale, al
contrario delle NOR che hanno una bassa velocità di lettura e scrittura
e un accesso ai dati random. Sulla board sono presenti delle NAND
da 64MB, 1G e 2G.
• USB: (Universal Serial Bus) standard di comunicazione seriale. Nell’architettura Cartesio, sono presenti controller USB che supportano gli
standard 1.1 (Full-Speed) e 2.0 (High-Speed), che possono funzionare
sia in modalità host (una memoria USB inserita nello slot della board
è rilevata dal controller e il suo contenuto può essere visualizzato nella
piattaforma target) che device (una memoria USB inserita nello slot
della board è rilevata dal controller e il suo contenuto può essere visualizzato nella piattaforma host. Cartesio stesso può essere rilevato come
mass-storage). Il transceiver può essere sia esterno al SOC, quindi
sulla board (extPHY) che interno ad esso (intPHY).
L’area HMI include i device che permettono l’interazione del sistema con
il mondo esterno, quindi abbiamo una tastiera (keyboard), un touchpanel e
il display a cristalli liquidi (CLCD). L’area Audio comprende i device per
l’audio. L’area Core include device che sono interni al SOC e permettono
alcune delle sue funzionalità:
• POWER MANAGER: il device che permette le funzionalità di risparmio
energetico.
• DFS: (Dinamic Frequency Scaling) permette di modificare la frequenza di un microprocessore in modo dinamico per ridurre il consumo
energetico.
• DMA: (Direct Memory Access) permette ad alcuni sottosistemi hardware di accedere la memoria di sistema per leggere e/o scrivere indipendentemente dalla process unit centrale.
• TIMER SISTEMA: device utilizzato per la temporizzazione del sistema.
• FSMC: (Flexible Static Memory Controller) interfaccia con diversi tipi
di memoria esterna statiche e dinamiche (NOR, NAND).
L’area I/O (Input/Output), contiene i seguenti device.
47
CAPITOLO 4. Il Progetto Cartesio
• GPIO: (General Purpose Input/Output) interfaccia per la connessione
di periferiche esterne. Le connessioni (pin GPIO) sono raggruppate in
gruppi (GPIO port). Le GPIO port possono essere configurate come
input o output e per produrre CPU interrupt.
• UART: (Universal Asynchronous Receiver Transmitter) interfaccia utilizzata per la trasmissione e la ricezione in seriale.
• I2C: (Inter Integrated Circuit) sistema di comunicazione seriale bifilare
(master/slave) utilizzato tra circuiti integrati.
• SPI: (Serial Peripheral Interface Bus) standard di comunicazione seriale sincrona ideato da Motorola che opera in modalità full duplex.
L’area GPS è relativa al sottosistema GPS presente in Cartesio. L’area
Grafica contiene il device per l’accelerazione grafica SGA. Infine l’area NET
è relativa al controller Ethernet, standard di comunicazione per le local area
networks (LAN).
4.1.2
Software
Nella divisione APG, affianco allo sviluppo hardware viene fatto anche lo
sviluppo software del sistema embedded. Questo consiste nel porting del
sistema operativo GNU/Linux per la piattaforma Cartesio. Con il termine sistema operativo GNU/Linux intendiamo il kernel con un middleware
platform-dependent (hardware accelerated OPENGL, DIRECTFB, GPS LIBRARY), senza ulteriori strati software, quali interfacce grafiche e applicazioni.
Come descritto nella sezione 3.4 del capitolo 3, effettuare il porting vuol
dire soprattutto sviluppare il base port (BSP), lo strato software di base
che contiene i device driver che astraggono il SOC e la board e consentono
di eseguire il kernel linux sulla piattaforma specifica. Lo strato software
sviluppato in APG per la piattaforma Cartesio, consiste del kernel Linux e
del BSP integrato nel kernel, e prende il nome di BSP Linux Cartesio.
Il BSP Linux Cartesio è multi-core e multi-board, in quanto supporta le
diverse versioni di Cartesio e le relative board. Inoltre è molto facile fare il
porting su una nuova board. La struttura del BSP Linux Cartesio è fatta in
modo da concentrare le modifiche necessarie per il funzionamento del BSP
su una nuova board, in pochi file, separati dai driver, che contengono le
definizioni dei device. Questi file sono descritti nell’appendice A.
L’obiettivo del BSP Linux Cartesio è fornire un sistema operativo funzionante e capace di gestire l’architettura specifica del SOC e della board.
Il BSP viene fornito al cliente come strato software di base che può essere
modificato per la board proprietaria, su cui sarà integrato il SOC Cartesio,
ed esteso con livelli software di più alto livello, come ad esempio interfacce
grafiche (GUI) e applicazioni.
48
CAPITOLO 4. Il Progetto Cartesio
I device driver sviluppati hanno le seguenti caratteristiche:
• parametrici: nel codice dei driver, non sono presenti dati relativi alla
specifica board (base address, IRQ channels, ecc.), in modo tale che
un driver possa gestire device su board differenti: EVB2062, EVB2065,
customer board e altre.
• gestione di un device: esiste una relazione 1 a 1 tra un driver ed un
device, nel senso che un driver gestisce una sola periferica. Se esistono
più device identici (ad esempio 4 GPIO controllers, 2 DMA controllers,
ecc.) il driver viene istanziato più volte per ciascuna periferica.
• basati su framework standard: vengono esposte dal driver API
standard verso altri driver e le applicazioni utente.
4.1.3
Ambiente di sviluppo
Abbiamo già descritto nella sezione 2.4.1, come viene sviluppato un software
per un sistema embedded. Ricordiamo che esistono due ambienti, host, una
macchina con architettura differente e risorse computazionali maggiori, dove
viene sviluppato il software e una macchina target che è l’architettura specifica sulla quale verrà eseguito il software. Un cross-compilatore permette di
compilare il codice sulla macchina host per il processore del target. Nello
sviluppo del BSP Linux Cartesio abbiamo:
• ambiente target: piatttaforma composta dalla board di validazione e
il SOC Cartesio;
• ambiente host: un personal computer con un installazione nativa o una
macchina virtuale VMware Workstation di Novell OpenSUSE 11.2 o
successiva;
• GNU cross-compilatore: ospitato nella macchina host. Può essere utilizzata la toolset sviluppata in APG, basata su pacchetti software GNU
standard, GCC, GLIBC, BINUTILS, GDB, ecc. oppure una di tipo
commerciale quale CodeSourcery Sourcery G++ [19].
Inoltre per l’attività di debugging durante lo sviluppo del BSP, viene utilizzato un JTAG tool quale Lauterbach PowerDebug with Trace32.
Una volta che il BSP è cross-compilato, esso può essere caricato ed eseguito nell’ambiente target in differenti modi: JTAG, in una NAND preflashed o da SD/MMC. Per interagire con GUN/Linux eseguito sulla target
board si utilizza un cavo seriale che connette il taget con l’ambiente host e
un’applicazione terminale (Windows HyperTerminal).
49
CAPITOLO 4. Il Progetto Cartesio
4.2
Processo di testing del BSP Linux Cartesio
Oltre allo sviluppo, il gruppo APG svolge anche un attività di testing del
BSP, sulla piattaforma hardware di riferimento, al fine di garantire la piena
qualità del prodotto finale, trovando i difetti. La figura 4.2 mostra un ciclo
del processo di testing del BSP che ha una durata di due settimane, e inizia
quando gli sviluppatori rilasciano una release candidate K che viene sottoposta a testing. I difetti identificati vengono tracciati con uno strumento di
bug tracking e viene rilasciato il test report dell’esecuzione dei test funzionali. La risoluzione dei difetti risolvibili subito avviene nella stessa release
K, gli altri invece vengono assegnati agli sviluppatori che nel corso del ciclo
di sviluppo della release candidate K+1 aggiungeranno nuove funzionalità e
correggeranno i difetti della precedente release. A questo punto terminato
lo sviluppo della release K+1, inizia un nuovo ciclo di testing. Dopo ciascun
ciclo di testing, la release software (BSP), il test report e la documentazione
vengono pubblicati nel repository proprietario InfoCenter. I test verificano
Figura 4.2: Processo di Testing adottato in STM per il BSP Linux
le funzionalità dei driver, ovvero che essi gestiscano le periferiche relative offrendo le corrette operazioni per interagire, mentre il software è in esecuzione
sull’hardware per cui è stato progettato. Ci sono due tipi di test:
1. test funzionale: verifica che il driver di una periferica operi correttamente (ad esempio, l’operazione di scrittura di un file su un device
USB);
50
CAPITOLO 4. Il Progetto Cartesio
2. test strutturale: verifica di funzionalità più complesse che coinvolgono
più driver (ad esempio il playback di un file audio da un device MMC)
Ci sono anche dei test di compilazione per intercettare errori di merge di
nuove funzionalità. L’esecuzione dei test, è sia manuale, a causa delle interazioni con l’hardware e sia automatica. In quest’ultimo caso, il test è eseguito con test suite che contengono casi di test scritti in linguaggio C, compilati
sulla macchina host con il cross-compilatore, copiati su una memoria esterna
ed eseguiti nell’ambiente target dalla memoria esterna (ad esempio, MultiMedia Card) o in base al caso specifico da una memoria interna (ad esempio,
NAND). Descriviamo quali sono le test suite esistenti.
4.2.1
Test dei device driver delle memorie
Il primo insieme di test riguarda i device driver relativi alle memorie flash,
MMC, USB host, NAND. Il test funzionale si basa sul verificare le diverse
operazioni che possono essere fatte sui dispositivi, attraverso la gestione del
driver. Questi test sono automatici e implementati utilizzando le system call
di Linux.
1. test di formattazione con diversi tipi di file system (fat e ext2): comando mkfs;
2. test di riconoscimento del device e accessibilità: comandi mount/umount;
3. test delle operazioni sui files: creazione, apertura, lettura, scrittura e
rimozione del file con i comandi creat, open, read, write;
4. test di operazioni di scrittura con chunks di diverse dimensioni: comando write con diverse dimensioni di file, 1byte, 1KB, 2KB, 4KB,
16KB;
5. test di operazioni di copia di file tra directories: comando cp;
I test strutturali sono manuali: verifica del corretto play di un file audio
situato nelle memorie.
I test dell’USB gadget sono eseguiti manualmente. In modalità gadget,
una memoria all’interno della board (ad esempio MMC) viene associata
allo porta USB che lavora in questa modalità, specificando dei parametri
al driver. Con un cavo USB si collega la board ad un PC tramite la porta
in modalità gadget. La memoria (MMC) e quindi la board viene vista dal
PC come un dispositivo USB sul quale effettuare le operazioni di scrittura
e lettura dei file. I casi di test sono: 1) connessa la board ad un pc, verifica
che la board venga riconosciuta dal PC come un dispositivo di memoria; 2)
verifica del trasferimento di un file da PC alla board e viceversa; 3)esecuzione
di un play audio dal PC di un file memorizzato nella memoria della board.
51
CAPITOLO 4. Il Progetto Cartesio
4.2.2
Test del device driver CLCD
Le test suite per questo device driver, includono casi di test per verificare
le operazioni di scrittura e lettura del driver e le diverse risoluzioni per il
display. Di seguito un esempio di esecuzione dei questi test.
# ./cartesio_clcd_app_test -A
Unable to open result files for logging
PASSED : t_CLCD_LINUX_01_01 : Testing of the success of the CLCD driver open operation (Read Only)
PASSED : t_CLCD_LINUX_01_02 : Testing of the success of the CLCD driver open operation (Read and Write)
PASSED : t_CLCD_LINUX_01_03 : Testing of the success of the CLCD driver open operation (Write Only)
PASSED : t_CLCD_LINUX_01_04 : Testing of the success of the CLCD driver close operation (Read Only)
PASSED : t_CLCD_LINUX_01_05 : Testing of the success of the CLCD driver close operation (Read and Write)
PASSED : t_CLCD_LINUX_01_06 : Testing of the success of the CLCD driver close operation (Write Only)
PASSED : t_CLCD_LINUX_02_01 : Testing of the success of the CLCD driver write operation
PASSED : t_CLCD_LINUX_02_02 : Testing of the success of the CLCD driver read operation
PASSED : t_CLCD_LINUX_03_01 : Testing of the success of the CLCD driver to frame buffer mapping operation
PASSED : t_CLCD_LINUX_03_02 : Testing of the success of the operation of writing to the CLCD through the frame buffer
PASSED : t_CLCD_LINUX_04_01 : Validating the fixed screen information
PASSED : t_CLCD_LINUX_05_01 : Testing the 480x272 variable screen resolution
PASSED : t_CLCD_LINUX_05_02 : Testing the 240x136 variable screen resolution
PASSED : t_CLCD_LINUX_05_03 : Testing the 480x136 variable screen resolution
PASSED : t_CLCD_LINUX_05_04 : Testing for illegal value of horizontal variable screen resolution (>480)
PASSED : t_CLCD_LINUX_05_05 : Testing for illegal value of vertical variable screen resolution (>272)
PASSED : t_CLCD_LINUX_05_06 : Testing for illegal value of horizontal variable screen resolution (>640)
PASSED : t_CLCD_LINUX_05_07 : Testing for illegal value of vertical variable screen resolution (>480)
PASSED : t_CLCD_LINUX_05_08 : Testing for illegal value of horizontal variable screen resolution (<480)
PASSED : t_CLCD_LINUX_05_09 : Testing for illegal value of vertical variable screen resolution (<272)
4.2.3
Test del device driver Touchpanel
I test di questo device verificano che le coordinate XY restituite dal driver
toccando una particolare area del LCD siano all’interno di un certo range.
In particolare il test è eseguito per i 4 angoli del LCD (alto/basso sinistra e
alto/basso destra) e l’area centrale. L’esecuzione dei test è semi-automatica.
I test avviano la verifica di una particolare operazione ma per completare
l’esecuzione è necessario interagire con il device, toccando l’area richiesta
sul display. Un esecuzione di questo test è mostrata in figura 4.3.
4.2.4
Test del device driver Keyboard
I test per questo tipo di device verificano che la tastiera sia correttamente
mappata. Il test chiede di digitare un certo tasto e controlla che esso sia
corrispondente alla mappa del driver. L’esecuzione di questo test è mostrata
in figura 4.4.
4.2.5
Test del device driver Audio
Le test suite per il driver audio, verificano il playback di file audio a diverse frequenze (8000Hz, 16000Hz, ecc.) con diversi tipi di campioni. La
creazione di forme d’onda sinusoidali, alternando i canali destro e sinistro,
è utile per verificare distorsioni e/o inversioni di canale. Per i test con i
diversi file audio, viene utilizzata l’applicazione ALSA2 audio e la relativa
libreria. ALSA Audio è fornita dal kernel e per poter essere utilizzata sulla
piattaforma specifica viene effettuato un collegamento con il driver specifico
2
www.alsa-project.org
52
CAPITOLO 4. Il Progetto Cartesio
Figura 4.3: Esecuzione dei test per il touchpanel
Figura 4.4: Esecuzione dei test per la keyboard
53
CAPITOLO 4. Il Progetto Cartesio
della componente audio, tramite un livello di funzioni di collegamento. Essa
è utilizzata per verificare le funzionalità del driver, operazioni più comuni
play, stop, pausa e per ricercare problemi di qualità nell’audio, come per esempio glitch audio, ovvero dei salti sulla traccia che provocano brevi rumori
e interruzioni nel playback.
4.2.6
Test Report
I risultati dei test sono raccolti utilizzando dei file Excel. Sono presenti i report per ogni release del BSP per entrambe le versioni di Cartesio (STA2062
e STA2065). Questi report sono organizzati in fogli per ogni device per i
quali è disponibile il test. Questi report includono i risultati sia dei test
automatici che manuali. Un parte di un report della versione 2.3.0 del BSP
per il Cartesio STA2062 è mostrato nella figura 4.5. sono interfacciati.
Figura 4.5: Esempio di report del testing del BSP Linux Cartesio
4.3
Miglioramento del processo di testing
Analizzando il processo di testing effettuato sul BSP Linux Cartesio, notiamo che le tecniche di testing mancanti sono quelle per la valutazione delle
caratteristiche non funzionali del software. Si è deciso quindi di inserire tecniche di testing e strumenti di automazione in questa direzione e anche per
valutare l’efficacia degli attuali test. Lo scopo del lavoro di tesi è intervenire
nel processo di testing con i seguenti obiettivi:
1. aumentare il perimetro del processo, individuando aree di qualità non
ancora prese in considerazione. Individuare le classi di problemi di
qualità non ancora esplorate dal testing.
54
CAPITOLO 4. Il Progetto Cartesio
2. introdurre per le aree di qualità individuate, degli strumenti che permettano di effettuare il testing in modo automatico, riducendone i costi
e i tempi;
Il raggiungimento di tali obiettivi, ha come conseguenza il miglioramento del
processo di testing e l’aumento del livello di qualità del software sviluppato.
Le tecniche di testing applicate nel contesto sperimentale di STM sono
quelle identificate nella sezione 3.3.2:
• test di copertura per valutare la qualità dei test funzionali esistenti;
• valutazione delle prestazioni del codice per ottimizzare il tempo di
boot del sistema;
• rilevazione di problemi di memory leakage
• verifica delle prestazioni di I/O dei dispositivi di memoria (a blocchi).
4.3.1
Criteri per la scelta degli strumenti di automazione
Per ogni area di qualità individuata, vengono selezionati gli strumenti di automazione e viene effettuato un confronto utile per la scelta dello strumento
da adottare. Il confronto è basato su caratteristiche derivate da politiche
aziendali, dal software che deve essere testato, dal sistema embedded e dalle
funzionalità dello strumento.
• Instrumentazione: molti strumenti operano utilizzando l’instrumentazione del codice. Questa tecnica prevede l’inserimento dei probe in
un programma prima o durante la sua esecuzione con lo scopo di monitorarlo. Un probe è un insieme di linee di codice che quando eseguite
generano la registrazione di informazioni su un evento. Questo evento indica che l’esecuzione del programma è passata nel punto in cui
il probe è inserito. Esistono due tipi di instrumentazione, instrumentazione del codice sorgente e instrumentazione del codice oggetto. Il
primo tipo è relativo all’inserimento di probe nel codice sorgente e
questo richiede la ricompilazione del programma. Non può essere utilizzato quando il codice sorgente non è disponibile, come nel caso di
codice di terze parti. Il secondo tipo è relativo all’inserimento di probe
nel file oggetto, dopo la compilazione del sorgente, per cui in questo
caso non è necessaria la ricompilazione. Un altro tipo di classificazione,
divide l’instrumentazione in esplicita, quando è lo sviluppatore a dover
specificare i probe nel programma e lo strumento si occupa della registrazione delle informazioni durante l’esecuzione, ed implicita, quando
i probe sono inseriti in modo automatico dallo strumento. Ci sono
due tipi di overhead associati all’instrumentazione, off-line overhead
55
CAPITOLO 4. Il Progetto Cartesio
che riguarda l’overhead richiesto per l’inserimento dei probe nel programma e run-time overhead, l’overhead di esecuzione dei probe per
la registrazione degli eventi durante l’esecuzione del programma.
• Licenza degli strumenti: esistono strumenti con licenza commerciale, con maggiore supporto e strumenti con licenza free software
GNU GPL. Gli strumenti commerciali hanno il vantaggio di offrire
un integrazione di funzionalità, difficilmente è ottenibile adottando
strumenti diversi, che permette una migliore attività di testing. Ad
esempio alcuni strumenti per il code coverage [YLW06], offrono oltre
all’analisi di copertura, la generazione di casi test strettamente legata alla copertura e anche il profiling per identificare quelle parti del
programma meno prestazionali.
• Linguaggio di programmazione: questa caratteristica riguarda il
linguaggio di programmazione con il quale è scritto il software che deve
essere testato. Alcuni strumenti sono utilizzabili con più linguaggi ma
altri sono specializzati per un singolo linguaggio di programmazione.
• Sistema Operativo: alcuni strumenti possono essere eseguiti su più
sistemi operativi, ma alcuni esistono solamente per sistemi operativi
specifici.
• Architettura: l’utilizzo di uno strumento è vincolato anche dal tipo
di architettura hardware utilizzata. La ricerca di strumenti che supportano le architetture dei sistemi più generici (personal computer),
come ad esempio x86 è molto più semplice rispetto alla ricerca di strumenti che supportano architetture per sistemi embedded, quali ARM
e MIPS. Di solito l’utilizzo degli strumenti per queste architetture è
vincolato dalla presenza di patch che eseguono alcune modifiche fondamentali per il loro funzionamento. Questo perché gli strumenti sono
prima sviluppati per le architetture più comuni e poi adattati anche
per architetture più specifiche. Inoltre gli strumenti che supportano
architetture per sistemi embedded devono rispettare anche i vincoli
imposti dall’ambiente di esecuzione, ovvero le risorse disponibili, quali
memoria, spazio del disco per la memorizzazione dei risultati, processore, ecc., per questo sono sviluppati per non essere troppo costosi in
termini di risorse. L’informazione sul tipo di architetture supportate
non è sempre reperibile dalla documentazione degli strumenti, questo
è soprattutto vero per le architetture embedded dove l’unico modo per
verificare il supporto è provare lo strumento. Di solito il suo utilizzo
non è immediato.
• Ambito di utilizzo e integrazione: gli strumenti che lavorano con
il codice nello spazio delle applicazioni utente (user-space) hanno di
solito differenze sostanziali nel loro funzionamento dagli strumenti che
56
CAPITOLO 4. Il Progetto Cartesio
operano nello spazio del sistema operativo (kernel-space). Uno strumento per lo spazio utente non può essere di solito utilizzato per lo
spazio del kernel, in quanto quest’ultimi sono oggetto di modifiche rilevanti per operare nell’ambiente kernel. Gli strumenti user-space sono
più ricchi dal punto di vista dell’interfaccia e anche più integrabili nell’ambiente di lavoro, in alcuni casi la loro integrazione è trasparente,
come ad esempio gli strumenti offerti come plug-in di IDE quali Eclipse.
Gli strumenti kernel-space, invece sono più semplici dal punto di vista
dell’interfaccia ma la loro integrazione può essere molto complessa per
motivi derivati dalla versione del kernel e dall’architettura utilizzata.
• Interfaccia e Reporting: l’interfaccia offerta dallo strumento può
essere un elemento decisivo nella scelta. Alcuni strumenti forniscono
una GUI (user-friendly graphic interface), altri invece un’interfaccia
a riga di comando, command-line oppure entrambe. Uno strumento
che lavora nello user-space fornisce sicuramente una GUI la quale permette anche la generazione di report sofisticati in formati quali html,
pdf, xml, latex, ecc., al contrario per il kernel-space che è un ambiente
di lavoro in cui l’interazione avviene tramite console e quindi un’interfaccia command-line che non permette la generazione di report ricchi
ed esplicativi e di solito sono basati su semplici file di testo difficili da
leggere. Per questo motivo i report forniti da questi strumenti sono
processati da strumenti di post-analisi che permettono di presentare i
dati in un formato più comprensibile ed esplorabile, che permette una
facile lettura e interpretazione dei risultati.
Nei capitoli specifici nei quali viene effettuato il confronto saranno utilizzate tutte o un sottoinsieme delle caratteristiche descritte in base alle informazioni disponibili per gli strumenti. Questo insieme può anche essere
esteso con caratteristiche specifiche degli strumenti per quella particolare
area di qualità.
57
5
Analisi dell’Efficacia del Test
n ambiti industriali fortemente competitivi, dove il software è una componente chiave del prodotto, la soddisfazione del cliente è altamente correlata con la qualità del software. In alcuni contesti industriali, come quello
automotive o avionico, i difetti software potrebbero mettere in pericolo la
vita umana, introducendo considerevoli conseguenze negative sia dal punto
di vista economico che delle relazioni con i clienti. Per questo, in tali ambiti
industriali l’attenzione verso il miglioramento della qualità del software è in
forte crescita. Un modo per garantire la qualità è il testing del software.
Esso è un processo complesso e dispendioso in termini di risorse, che occupa
dal 40% al 80% del costo totale dello sviluppo software. La comprensione
delle risorse richieste dal testing è un trade-off tra budget, tempo e obiettivi
di qualità. In molte aziende si cerca di misurare la completezza e la bontà del testing per stabilire per quanto tempo e con quale costo deve essere
effettuato per raggiungere gli obiettivi di qualità definiti. Un modo per effettuare tale misurazione è l’analisi di copertura, detta code coverage o anche
test coverage.
L’obiettivo di questo capitolo, è mostrare la scelta e l’adattamento dello strumento di code coverage più adatto all’analisi di copertura dei device
driver nei sistemi Linux embedded e dare un indicazione dello stato dell’automazione del code coverage in questo ambito. In questo capitolo vengono
inizialmente forniti alcuni concetti base del test coverage, successivamente
vengono descritti gli obiettivi del test coverage in ambito Linux embedded
e la scelta dello strumento da utilizzare nel contesto specifico del progetto
Cartesio di STM. Viene descritto lo strumento in maniera dettagliata sia
dal punto di vista del funzionamento che della configurazione, sottolineando il fatto che uno strumento utilizzabile nello spazio utente necessità di
modifiche sostanziali per operare nello spazio kernel. Infine, viene descritto l’utilizzo dello strumento scelto nel progetto Cartesio, che nella nostra
sperimentazione fallisce evidenziando come la fase di personalizzazione degli
I
58
CAPITOLO 5. Analisi dell’Efficacia del Test
strumenti utili per automatizzare il processo di testing sui sistemi embedded può essere molto complessa a causa di vincoli architetturali e condiziona
l’utilizzo e la raccolta dei risultati di test.
5.1
Analisi di copertura del codice
L’analisi di copertura, è una tecnica di testing del software applicabile in
qualunque fase del processo di testing: test d’unità, test d’integrazione o
test di sistema. In particolare può essere utilizzata con il test strutturale e
con il test funzionale.
Il test strutturale (white-box testing) esamina come lavora il codice e si
basa sulla sua struttura e sulla sua logica. Il test funzionale (black-box testing) invece, esamina le funzionalità del programma sulla base delle specifiche
dei requisiti funzionali, che stabiliscono quali funzionalità devono essere espletate dal codice sotto test, senza preoccuparsi di come lavora internamente.
La differenza tra i due riguarda il criterio utilizzato nella derivazione dei casi
di test. Nel black-box testing i casi di test vengono derivati dalle specifiche
dei requisiti funzionali, invece nel white-box testing vengono derivati dalla
struttura stessa del programma, in particolare dal control flow graph.
L’analisi di copertura è il processo che permette di misurare la percentuale di codice all’interno di un’applicazione che viene eseguita durante
un processo di testing e conseguentemente di trovare quelle aree del programma che non vengono esercitate dalle test suite. Ha i seguenti vantaggi
e svantaggi.
• fornisce una misura quantitativa per valutare i progressi e migliorare la
qualità del processo di testing. Fornisce soltanto una misura indiretta
della qualità del prodotto software finale. Il processo di testing può
essere migliorato utilizzando i risultati del test coverage per creare i
casi di test aggiuntivi che esercitano quelle aree del programma non
coperte.
• migliorare il processo di testing vuol dire aumentare le sue capacità
nella rilevazione dei difetti e di conseguenza anche l’affidabilità del software. Purtroppo non sempre la qualità è proporzionale alla copertura.
La piena copertura (100%) di un programma da parte di un test, non
sempre garantisce l’assenza di difetti. Questo perchè il test coverage è
un attività complessa e per ottenere un valore di copertura affidabile
bisogna effettuare una scelta oculata di metriche di copertura tra loro
complementari.
• permette di definire la priorità nella generazione ed esecuzione dei casi
di test (Test Case Prioritization), selezionando i test che forniscono la
maggiore copertura del codice;
59
CAPITOLO 5. Analisi dell’Efficacia del Test
• rileva i casi di test ridondanti, che devono essere rimossi da una test
suite in quanto la loro esecuzione è costosa in termini di tempo e risorse
e non migliora la rilevazione dei difetti;
• deve essere valutata la relazione tra copertura e sforzo richiesto per
raggiungerla. Una percentuale di copertura maggiore richiede uno
sforzo maggiore nella generazione dei casi di test. In un certo contesto
aziendale, una buona copertura può essere rappresentata anche da una
percentuale del 70%, in quanto il raggiungimento del 100% sarebbe
troppo dispendioso in termini di tempo e costi. Si utilizza quindi una
percentuale come valore soglia, raggiunta la quale l’attività di testing
viene considerata troppo costosa. E’ chiaro che occorre raggiungere un
giusto compromesso tra il raggiungimento di una percentuale di copertura, lo sforzo richiesto e gli obiettivi di qualità definiti nel processo
di testing.
Il code coverage può misurare la copertura a diversi livelli di granularità
utilizzando diverse metriche: statement coverage, branch coverage, path
coverage, function/method coverage, class coverage e execution state space
coverage. Ognuna produce una differente vista di come i test eseguono
il codice. Tra tutte le metriche, statement(basick block) coverage e branch
coverage sono quelle maggiormente utilizzate e implementate negli strumenti
di automazione. Il capitolo si focalizza su queste due metriche.
Un control flow graph o CFG di un programma definito in [PY08] è
un grafo diretto i cui nodi rappresentano regioni del codice sorgente e gli
archi rappresentano i percorsi, dalla fine di una regione direttamente all’inizio di un’altra, che possono essere intrapresi dal programma durante la
sua esecuzione, eseguendo il flusso sequenziale o tramite un branch. Un
branch è una diramazione all’interno del programma che interrompe il normale flusso di esecuzione. L’attivazione di un branch è dovuta al verificarsi
di una condizione valutata da istruzioni condizionali (if-else, while, for) e
consiste nell’esecuzione degli statement appartenenti al branch. I nodi del
grafo possono rappresentare statement oppure basic block. Uno statement
è l’elemento minimale del codice di un programma che può essere eseguito.
Un basic block è una sequenza di statement consecutivi nel quale il flusso
di esecuzione non subisce nessuna alterazione. All’interno di un basic block
non esistono branch e esso ha un solo punto di ingresso e un solo punto di
uscita.
La prima metrica di copertura è chiamata statement coverage o basic block coverage in base alla granularità della rappresentazione del CFG,
ovvero se i nodi del grafo rappresentano statement oppure basic block. Lo
statement coverage [PY08] richiede che ciascuno statement sia eseguito almeno una volta da un caso di test. Un caso di test copre uno statement se
eseguendo il test viene eseguito anche lo statement.
60
CAPITOLO 5. Analisi dell’Efficacia del Test
Sia T una test suite per un programma P, T copre P se e solo se per
ciascuno statement S di P esiste almeno un caso di test in T che provoca
l’esecuzione di S. Questo è equivalente a dire che ogni nodo nel CFG del
programma P è visitato con almeno un cammino di esecuzione esercitato
da un caso di test in T. Lo statement coverage Cstatement di T per P è il
rapporto tra il numero di statement eseguiti da almeno un caso di test in T
e il numero totale di statement di P.
Cstatement =
numero di statement eseguiti
numero di statement
(5.1)
Per ottenere una copertura completa si richiede che vengano eseguiti dai
casi di test tutti i nodi del CFG almeno una volta.
Se qualunque istruzione viene eseguita in un basic block allora ogni altra
istruzione del basic block è eseguita lo stesso numero di volte. L’instrumentazione dei basic block è un modo più efficiente di calcolare quante volte
ciascuna linea di codice in un programma viene eseguita rispetto all’instrumentazione di ogni singolo statement. Lo statement coverage può essere
inferito dal basic block coverage.
L’assunzione fondamentale dell’analisi di copertura è che un difetto in un
programma non può essere rilevato senza eseguire lo statement o il branch
che lo contiene. Quando una metrica di copertura ha delle debolezze significa
che ci sono alcuni statement o branch nel programma che la metrica non
riesce a coprire. Una metrica esegue solo parte dell’analisi di copertura
richiesta. Un uso combinato di più metriche permette di trattare situazioni
diverse e quindi di migliorare la copertura e la rilevazione dei difetti.
La debolezza dello statement coverage è la non sensibilità ad alcune
strutture di controllo [8]. Per esempio consideriamo il frammento di codice
Java 5.1, che mostra uno dei principali problemi di questa metrica.
Codice 5.1: Debolezza dello statement coverage
1
2
3
4
5
Integer var = null ;
if ( condizione ) {
var = new Integer (0);
}
return var . toString ();
L’esempio 5.1 mostra un semplice if statement senza clausola else. Per
ottenere il 100% di copertura con questa metrica è sufficiente che il caso di
test esegua il codice con la condition = true e non serve testare il caso in cui
la condition = false. Non esiste nessun codice per il ramo false (mancanza
della clausola else) e quindi per esso lo statement coverage non può essere
misurato.
Lo statement coverage non è adatto nel calcolare la copertura del test
di una istruzione if in quanto non prende in considerazione il caso dei due
rami true e false.
61
CAPITOLO 5. Analisi dell’Efficacia del Test
Lo statement coverage fornisce come risultato che questo frammento di
codice è pienamente coperto solo con un caso di test che verifica la condizione true. Questo risultato può ingannare indicando al test designer che
non c’è bisogno di nessun altro caso di test per questo frammento di codice.
Ma tale copertura non è totale, in quanto manca il caso di test che verifica
la condizione false che è proprio quella che espone il difetto di questo codice,
ovvero la generazione dell’eccezione NullPointerException. Quindi il risultato di 100% di copertura con la metrica statement coverage potrebbe non
necessariamente implicare che il programma è ben testato.
Per risolvere la debolezza dello statement coverage descritta, è richiesto
l’utilizzo di una seconda metrica di copertura chiamata branch coverage. Il
branch coverage [PY08] richiede che ciascuno branch sia eseguito almeno
una volta da un caso di test. Sia T una test suite per un programma P, T
copre P se e solo se per ciascun branch B di P esiste almeno un caso di test
in T che provoca l’esecuzione di B. Questo è equivalente a dire che ogni arco
nel CFG del programma P appartiene ad almeno un cammino di esecuzione
esercitato da un caso di test in T. Il branch coverage Cbranch di T per P è il
rapporto tra il numero di branch eseguiti da almeno un caso di test in T e
il numero totale di branch di P.
Cbranch =
numero di branch eseguiti
numero di branch
(5.2)
Per ottenere una copertura completa si richiede che vengano eseguiti dai
casi di test tutti gli archi del CFG almeno una volta.
Il branch coverage si focalizza sulle espressioni condizionali che attivano
i diversi branch di un programma.
Nell’esempio 5.1 il branch coverage rileva che quel frammento non ha
una copertura completa, in quanto il ramo false dell’istruzione if non è mai
testato, identificando una falla nel processo di testing.
Sebbene il branch coverage fornisce una copertura più accurata rispetto
allo statement coverage non è comunque una metrica infallibile. La debolezza del branch coverage si verifica quando le espressioni condizionali che
attivano i rami sono espressioni complesse costituite dal concatenamento di
operatori logici.
Per esempio consideriamo il frammento di codice Java 5.2, che mostra il
principale problema di questa metrica.
Codice 5.2: Debolezza del branch coverage
1
2
3
if (( somma != null ) || ( somma . equals ( " " ))) {
System . out . println ( " Somma non e ’ null " );
}
Se il frammento di codice 5.2 viene eseguito da un test con somma !=
null, si ottiene 100% di copertura, ma la seconda condizione booleana non
viene coperta e contiene un errore.
62
CAPITOLO 5. Analisi dell’Efficacia del Test
5.2
Obiettivi dell’analisi di copertura nei sistemi Linux Embedded
Per poter intervenire in un processo di qualità bisogna comprendere due
aspetti, 1) qual’è lo stato del processo di testing, 2) qual’è la sua efficacia.
Nell’ambito del progetto Cartesio, il processo di testing del sistema Linux
embedded consiste di un insieme di test suite per il test funzionale delle singole periferiche. I casi di test verificano principalmente quelle parti di codice
del kernel che sono direttamente legate alle periferiche, ovvero l’area dei
device driver e in particolare i driver sviluppati per la piattaforma specifica.
La prima attività da svolgere per migliorare il processo di testing è
introdurre un’analisi di copertura.
Quindi l’analisi di copertura ha l’obiettivo di valutare l’efficacia dei test
funzionali esistenti, in termini di quantità di copertura del kernel, identificando il cosiddetto dead code dei device driver platform-dependent, il codice
che non viene esercitato durante l’esecuzione delle test suite. I risultati sono
utili alla creazione di casi di test addizionali per esercitare il codice, quindi
incrementare la copertura dello stesso e anche il livello di qualità dei device
driver platform-specific sviluppati.
5.3
Scelta dello strumento di automazione
Un’analisi di copertura efficace ed efficiente non può prescindere dall’utilizzo di uno strumento di automazione. Nella tabella 5.1 viene riportato il
confronto tra gli strumenti di automazione selezionati, in base alle caratteristiche individuate nel capitolo 5. Tale insieme di caratteristiche viene esteso
in questo caso con la caratteristica relativa al tipo di copertura supportata
dallo strumento. Basandoci sul lavoro di [YLW06], in cui vengono studiati
e confrontati 17 strumenti per il test coverage, abbiamo effettuato una prima selezione considerando solo gli strumenti relativi al linguaggio C/C++,
quindi questa caratteristica viene eliminata dalla tabella di confronto. Gli
strumenti analizzati sono Bullseye Coverage, CodeTest, Gcov, Intel Code
Coverage, Generic Coverage Tool (GCT) e Coverage Meter. Per l’analisi
Strumento
Instrum.
Licenza
Ambito
Bullseye [10]
source code,
implicita
source code,
implicita
source code,
implicita
source code,
implicita
source code,
implicita
source code,
implicita
comm.
user-space,
kernel space
user-space
CodeTest [11]
Gcov [12]
Intel [13]
GCT [14]
Meter [15]
comm.
free
comm.
user-space,
kernel-space
user-space
free
kernel-space
free
user-space
Sistema
Operativo
Windows,
Linux
Linux
Interfaccia
Copertura
GUI
Linux
command
Windows,
Linux
Linux
command
Windows,
Linux,
Mac-OS
GUI
branch,
function
statement,
branch
statement,
branch
statement,
function
branch, condition
statement,
branch
entrambe
command
Tabella 5.1: Confronto degli strumenti per l’analisi di code coverage
63
CAPITOLO 5. Analisi dell’Efficacia del Test
di copertura sul sistema linux embedded del progetto Cartesio, cerchiamo
uno strumento che sia distribuito con licenza free e possa essere eseguito sul
sistema operativo Linux.
Un’altra caratteristica che lo strumento deve possedere è l’interazione
tramite riga di comando, in quanto questa è l’unica modalità disponibile sul
nostro sistema. Ma, la caratteristica cardine nella scelta è che lo strumento
deve permettere il test coverage del codice nello spazio del sistema operativo
(kernel space).
Osservando i risultati del confronto, la nostra scelta è ricaduta su Gcov.
Gcov rispetta i requisiti da noi richiesti (licenza free, linguaggio C e O.S.
Linux). Per la scelta è stato determinante il fatto che Gcov è uno strumento
che permette di misurare la copertura sul codice dello spazio del kernel. E’
uno strumento che è possibile configurare a partire dalla versione 2.6.31 del
kernel, la versione da noi utilizzata è la successiva 2.6.32, senza l’applicazione
di patch [17]. Supporta due tipi di copertura, statement coverage e branch
coverage, descritti precedentemente.
Come vedremo nel resto del capitolo, è molto problematico integrare
questi strumenti di validazione nel kernel, considerando che molti di questi
richiedono adattamenti di basso livello all’architettura di uno specifico processore, e Linux supporta una ventina di famiglie diverse di processori, tutti
da supportare separatamente.
Gcov, è uno strumento che ha un basso overhead nell’instrumentazione
del codice sorgente. Le stime effettuate in [LHRF03], si basano sul confronto
dei tempi di compilazione per il kernel 2.5.50 con e senza il framework Gcov
abilitato. Più precisamente, viene calcolato per Gcov Kernel, la versione di
Gcov per lo spazio kernel, un overhead del 3%.
5.4
Gcov. Gnu Gcc Coverage Framework
Gcov è un framework per l’analisi di copertura fornito dal compilatore Gnu
Gcc. Permette di ottenere i dati relativi alla copertura del codice, per programmi compilati attraverso il compilatore Gcc. Gcov è in grado di fornire
lo statement coverage e il branch coverage. Il codice del kernel e quindi anche i device driver platform-dependent sono compilati con il Gcc, per questo
Gcov rappresenta la migliore soluzione per i nostri scopi.
Gcov è uno strumento nato per funzionare nello spazio delle applicazioni
utente. Nel seguito descriviamo il funzionamento di Gcov in questo ambito e
successivamente le modifiche necessarie per il suo funzionamento nello spazio
del kernel. Gcov opera basandosi su quattro fasi:
1. instrumentazione del codice durante la compilazione
2. raccolta dei dati durante l’esecuzione del codice
3. estrazione dei dati alla terminazione dell’esecuzione del codice
64
CAPITOLO 5. Analisi dell’Efficacia del Test
4. elaborazione e presentazione dei dati sulla copertura
In questo capitolo ci riferiamo all’infrastruttura Gcov fornita dalla versione
4.4.1 del compilatore Gcc. Nelle versioni precedenti i file e i nomi delle
funzioni sono differenti.
Per ottenere i dati relativi alla copertura è necessario compilare il codice
con Gcc, aggiungendo i flag -fprofile-arcs e -ftest-coverage. Queste opzioni
indicano al compilatore di inserire delle istruzioni aggiuntive (probe) all’interno del codice sorgente. Questo nuovo codice incrementa dei contatori che
identificano le sezioni del codice che vengono eseguite. Quando il programma è in esecuzione questi contatori vengono incrementati quando la specifica
porzione di codice viene eseguita. Una volta che il programma ha terminato
la sua esecuzione, questi contatori vengono trasferiti in un apposito file, che
viene usato da Gcov per determinare la copertura del codice target.
Per ogni file compilato con Gcc e con le opzioni per la copertura, il compilatore crea il CFG del programma, individuando i basic block da instrumentare. A ciascun basic block è assegnato un ID, blockno. Viene allocato un
vettore di contatori dei basic block, counts che è indicizzato dal basic block
ID. In ciascun basic block il compilatore genera il codice per incrementare
il relativo elemento counts[blockno] nel vettore. Successivamente, viene allocata una struttura struct bbobj che contiene tre campi che identificano
il nome del file compilato, la dimensione del vettore counts e il puntatore
ad esso. Il compilatore definisce all’interno del file una funzione costruttore _GLOBAL_.I.FirstfunctionnameGCOV che invoca una funzione globale
__bb_init_func(struct bb*) con il bbobj passato come argomento. Viene
inserito un puntatore alla funzione costruttore nella sezione .ctors del file
oggetto. Il linker successivamente raggruppa tutti i puntatori alle diverse funzioni costruttore dei vari file instrumentati collocate nei file *.o in un’unica
sezione .ctors dell’eseguibile finale. L’instrumentazione e la trasformazione
del codice sono illustrate in 5.3, che mostra un file sorgente compilato con
Gcc e con le opzioni per l’analisi di copertura.
Codice 5.3: Instrumentazione del codice per l’analisi di copertura
1
2
3
4
5
6
7
8
9
10
11
12
13
static ulong counts [ numbbs ];
static struct bbobj = { numbbs , & counts , " file1 . c " };
static void _GLOBAL_ . I . fooBarGCOV ()
{ __bb_init_func (& bbobj ); }
void fooBar ( void )
{
counts [ i ]++;
<bb -i >
if ( condition ) {
counts [ j ]++;
<bb -j >
} else {
<bb -k >
65
CAPITOLO 5. Analisi dell’Efficacia del Test
14
15
16
}
}
SECTION ( " . ctors " ){ & _GLOBAL_ . I . fooBarGCOV }
Nel codice 5.3, le linee di codice 1,2,3,4,7,10,16 sono inserite dal compilatore
e <bb-x> denota l’x-esimo basic block.
Nelle versioni precedenti la 4.4.1, Gcc genera due file di output per
ciascun codice sorgente sourcefile.c compilato.
• sourcefile.bb: contiene i numeri di linea corrispondenti ai basic
block all’interno del file.
• sourcefile.bbg: contiene una lista degli archi del CGF del programma.
La libreria C runtime glibc linkata con il file eseguibile, definisce la funzione
globale __bb_init_func(struct bb*) la quale collega l’oggetto passato ad
una lista bbobj globale bb_head. Glibc invoca inoltre tutte le funzioni i cui
puntatori si trovano nella sezione .ctors dell’eseguibile.
Quando il programma è in esecuzione, l’elemento del vettore counts è
incrementato ogni volta che il relativo basic block instrumentato viene eseguito. Quando il programma termina viene letta la lista bb_head e per ogni
bbobj viene creato un file sourcefile.da con le informazioni sulla dimensione del vettore e dei contatori contenuti nel vettore stesso. Nella versione
4.4.1 di Gcc vengono utilizzati dei file differenti:
• sourcefile.gcno: questo file include i dati contenuti nei file .bb e
.bbg. Viene generato quando il file sorgente è compilato da Gcc con
l’opzione -ftest-coverage.
• sourcefile.gcda: questo file include i dati contenuti nei file .da.
Viene generato quando il file oggetto del programma costruito da Gcc
con l’opzione -fprofile-arcs è eseguito.
Una volta che il programma ha terminato l’esecuzione e il file .gcda contenete i dati dei contatori viene creato, Gcov è in grado di analizzare il file
e produrre le informazioni sulla copertura. Un esempio di file di output di
Gcov è mostrato nel codice seguente.
int main() {
int i;
printf("starting example\n");
for(i=0;i<10;i++) {
printf("Counter is at %d\n",i);
}
/* this is a comment */
1
if(i==1000) {
1
1
11
10
10
66
CAPITOLO 5. Analisi dell’Efficacia del Test
######
printf("This line should never run\n");
######
}
1
return 0;
1 }
Per le righe instrumentate che sono eseguite almeno una volta Gcov inserisce
un numero che indica quante volte la riga è stata eseguita. La stringa
###### indica le righe instrumentate che non sono mai eseguite. Nessuna
informazione è aggiunta da Gcov per le righe non instrumentate, come i
commenti.
Le informazioni fornite da Gcov sono utili ma non molto organizzate per
un esplorazione semplice. E’ possibile utilizzare il toolkit Lcov [9] che è in
grado di estrarre automaticamente i dati prodotti da Gcov e produrre un
report HTML navigabile e molto esplicativo come mostrano le figure 5.2 e
5.2.
5.5
Gcov Kernel. Gnu Gcc Coverage Framework per lo
Spazio del Kernel
Per ragioni tecniche che da qui a poco spiegherò, Gcov non lavora in modo
nativo nello spazio del kernel per due motivi:
1. sebbene la compilazione dei file del kernel genera i costruttori bbobj e
le sezioni .ctor per ogni file, il kernel non dispone di una funzione per
chiamare i costruttori presenti nelle sezioni .ctor.
2. il kernel non esiste come applicazione in esecuzione e i file .gcda non
possono essere creati.
Per poter implementare le funzionalità di Gcov per lo spazio del kernel sono
state necessarie delle modifiche allo strumento e al compilatore. In particolare, il primo problema è stato risolto definendo in modo esplicito le sezioni
.ctor nel kernel. Il simbolo __CTOR_LIST_ è posto all’inizio della sezione e reso visibile al linker. Quando l’immagine del kernel è linkata viene inserito nella sezione il vettore di puntatori alle funzioni costruttore dei vari file instrumentati. Il vettore è delimitato dalle variabili ctors_start = &__CTOR_LIST_
e ctors_end = &__DTOR_LIST_, dove &__DTOR_LIST_ è l’indirizzo iniziale
della sezione .dtors che contiene le funzioni distruttore. Il secondo problema, è stato risolto introducendo una nuova interfaccia specifica mediante
Debugfs. Debugfs è il nome proprio di una tecnologia preesistente nel kernel
di Linux, sfruttata per fornire un’interfaccia anche a GCOV oltre ad ulteriori sottosistemi. In questo modo vengono esportati i risultati di copertura
calcolati da Gcov nel codice del kernel in esecuzione allo spazio utente.
Gcov Kernel può essere utilizzato per valutare la copertura del codice di
un kernel in esecuzione e conseguentemente la copertura dei device driver. E’
67
CAPITOLO 5. Analisi dell’Efficacia del Test
disponibile dal 2002 attraverso l’applicazione di una patch, ma dalla versione
2.6.31 del kernel la patch è integrata e Gcov Kernel può essere configurato
prima della compilazione, senza necessità di applicare una patch esterna.
Gcov kernel inoltre permette di analizzare la copertura dei moduli dinamici. I moduli dinamici sono un importante caratteristica che riduce le dimensioni del kernel compilando i device driver in modo separato e caricando il
codice nel kernel quando uno specifico driver è richiesto. La documentazione
per Gcov Kernel è disponibile in /Documentation/gcov.txt.
5.5.1
Configurazione
Per descrivere la configurazione di Gcov Kernel, ci riferiamo alla versione
2.6.32 del kernel. Prima di tutto, bisogna modificare il file .config attraverso
lo strumento di configurazione xconfig, abilitando le opzioni CONFIG_DEBUG_FS
per montare il file system debugfs e CONFIG_GCOV_KERNEL per abilitare l’infrastruttura Gcov. Per ottenere la copertura dell’intero codice del kernel si
può abilitare l’opzione CONFIG_GCOV_PROFILE_ALL, ma ovviamente questo
aumenterà le dimensioni dell’immagine e diminuirà la velocità di esecuzione
del kernel. Quest’ultima opzione non è supportata da tutte le architetture,
come ad esempio Arm.
Per abilitare l’analisi di copertura su file specifici o su intere directory:
• per il singolo file, viene inserita l’opzione GCOV_PROFILE_nomefile.o:=y
nel makefile nella directory dove è situato il file.
• per tutti i file in una directory, viene inserita l’opzione GCOV_PROFILE:=y
nel makefile posto nella directory.
Le informazioni sulla copertura dei moduli dinamici possono essere conservate nel debugfs o eliminate quando il driver viene scaricato, specificando
l’opzione gcov_persist=1 o gcov_persist=0 rispettivamente.
Dopo aver configurato le opzioni e compilato il kernel, bisogna creare
un root file system, il file system per la macchina target montato nella fase
di boot, che deve contenente i file sorgenti per i quali è stata abilitata la
copertura, nella stessa posizione della macchina host. Questa preparazione
speciale, è dovuta al fatto che di default Gcov lavora su kernel compilati ed
eseguiti sulla stessa macchina. Ma in ambiente embedded la compilazione
avviene sulla macchina host attraverso un cross-compiler che produce una
versione del kernel eseguibile sulla macchina target. Un root file system può
essere creato con strumenti quali BuildRoot [16].
Dopo la fase di boot, Gcov crea la directory /sys/kernel/debug/gcov
contenente il file per il reset dei contatori, reset, e i file .gcno e .gcda di ogni
sorgente instrumentato, con i dati sulla copertura che sono stati raccolti fin
dalla fase di boot del kernel.
68
CAPITOLO 5. Analisi dell’Efficacia del Test
Figura 5.1: Configurazione di Gcov Kernel
5.6
Utilizzo di Gcov Kernel nel Progetto Cartesio
Nel contesto del progetto Cartesio, Gcov Kernel è stato analizzato in modo
approfondito, giungendo ad una conclusione riguardo il suo utilizzo. La conclusione mette in evidenza come la fase di personalizzazione degli strumenti
nell’ambito dell’automazione del processo di testing di sistemi embedded, è
la fase che richiede più sforzo e non sempre termina con un successo. Questo
è il caso dell’introduzione di Gcov Kernel, come strumento per l’analisi di
copertura, nel processo di testing del sistema operativo Linux embedded per
la piattaforma Cartesio. Abbiamo analizzato il fatto che questo strumento
non è generale come sembra e la personalizzazione per il nostro sistema non
è riuscita e non è stato possibile utilizzarlo.
Sarebbe stato meglio aver calcolato i dati sulla copertura dei device driver specifici per la piattaforma, necessari per migliorare il test funzionale, ma
ci siamo comunque concentrati sull’analisi del fallimento della personalizzazione di Gcov Kernel, comprendendo i motivi della sua non applicabilità.
Questo ci ha portato a scoprire un difetto non specifico per il nostro caso
e che può essere generalizzato per i casi in cui lo strumento è utilizzato nei
sistemi Linux embedded su piattaforma ARM.
Iniziamo la descrizione del processo che abbiamo seguito per arrivare
all’identificazione del difetto. Per la sperimentazione, abbiamo utilizzato uno
dei driver platform-specific, il gpio driver che permette di gestire le interfacce
General Purpose Input/Output. Un sistema di solito ha una o più connessioni
gpio, che permettono di interfacciarsi con i device esterni. Possono essere
configurate come input per leggere segnali digitali provenienti da altre parti
69
CAPITOLO 5. Analisi dell’Efficacia del Test
del sistema oppure come output per inviare segnali ad una data periferica.
Le interfacce gpio, permettono l’espansione di porte seriali in quei chipset
dove le porte di I/O sono insufficienti. Sono collegate a bus standard quali
SPI e I2C e sono configurabili sia nella direzione della comunicazione sia nei
device che possono essere connessi. Sul nostro sistema embedded, il driver
gpio è collocato nella directory platform-specific, /arch/arm/mach-cartesio.
La prima operazione è stata la configurazione di Gcov Kernel, in cui
abbiamo abilitato le opzioni per l’infrastruttura Gcov CONFIG_GCOV_KERNEL
e per il debugfs CONFIG_DEBUG_FS, nel file .config.
La seconda operazione è stata l’inserimento dell’opzione per abilitare
la copertura sul driver gpio.c. Quindi abbiamo inserito nel makefile della
directory /arch/arm/mach-cartesio in cui il driver è situato, il comando
GCOV_PROFILE_gpio.o := y. Il driver gpio è un driver fondamentale, per
questo è linkato direttamente nel kernel e non è modulo dinamico. Quindi in
questa descrizione non viene considerato il caso dell’utilizzo di Gcov Kernel
con i moduli dinamici.
A questo punto abbiamo compilato il kernel ed è stato creato in /arch/arm/ mach-cartesio, il file gpio.gcno.
La terza e ultima operazione di configurazione è stata la creazione del
root file system con BuildRoot [16], in cui abbiamo inserito la directory
/arch/arm/mach-cartesio con tutto il suo contenuto e nella stessa posizione.
Dopo la fase di boot del kernel, abbiamo verificato la presenza del file
system Gcov /sys/kernel/debug/gcov, notando che il framework era stato inizializzato correttamente. Ma il file gpio.gcda, contenente i dati sui contatori
per ricavare la copertura del driver, non era stato creato. Questo file viene
creato appunto in fase di esecuzione del codice instrumentato, al contrario
del file gpio.gcno che viene creato al momento della compilazione, e poichè il
driver gpio è uno dei primi ad essere inizializzato nella piattaforma Cartesio,
il file gpio.gcda doveva essere creato durante la fase di boot.
Quindi siamo andati alla ricerca della causa di questo problema che
impediva il corretto funzionamento di Gcov Kernel. Abbiamo analizzato
con uno strumento di debugging, quale il Lauterbach, il kernel alla ricerca delle chiamate alle funzioni di Gcov. Abbiamo scoperto che la funzione globale __gcov_init che è la funzione per avviare l’analisi di copertura per il driver instrumentato, non veniva mai invocata dalla funzione
costruttore del driver. Abbiamo quindi verificato la compilazione di tale
funzione da parte del kernel, andando a controllare nel file system.map la
presenza della relativa riga che indica la corretta esportazione della funzione. Questa era presente. Infine abbiamo inserito all’interno della funzione
l’istruzione pr_err("I’m in the __gcov_init function") che effettua la
stampa su console del messaggio in fase di boot, per verificare che il kernel
eseguisse questa funzione. Ma dopo la ricompilazione e il riavvio del kernel
il messaggio non era stato visualizzato.
70
CAPITOLO 5. Analisi dell’Efficacia del Test
Assodato che la funzione __gcov_init non era la causa principale del
nostro problema abbiamo verificato il corretto funzionamento dei costruttori di Gcov. Sono proprio le funzioni costruttore create all’interno di
ogni file instrumentato ad invocare la funzione __gcov_init. Abbiamo
per questo verificato la corretta generazione e il corretto collegamento dei
costruttori sia all’interno del codice del driver sia nell’eseguibile finale del
kernel. Per fare questo abbiamo prima di tutto controllato che l’opzione
CONFIG_CONSTRUCTORS=y era presente nel file .config. Questa opzione era
abilitata. Se il supporto per i costruttori di Gcov è disponibile, allora nell’eseguibile vmlinux del kernel, deve essere presente la sezione relativa delimitata dai simboli __ctors. Se per un file è stata abilitata la copertura il file
conterrà il costruttore Gcov è quando tale file viene linkato al vmlinux, il
costruttore viene inserito nella sezione __ctors dell’eseguibile del kernel.
Quindi abbiamo analizzato il driver gpio.o alla ricerca del costruttore
Gcov con il comando objdump -t gpio.o | grep .ctors non trovando
nessuna riga nel risultato. La funzione costruttore per il driver non era
stata creata.
Abbiamo allora verificato la presenza della sezione dei costruttori e il suo
contenuto nel vmlinux con il comando |objdump -t vmlinux | grep _ctors_,
ottenendo come risultato:
c0025bf0 g
c0025bf0 g
.init
.init
00000000 __ctors_start
00000000 __ctors_end
in cui sono presenti i due simboli __ctors_ che confermano che il supporto
dei costruttori è disponibile ma come si può notare avevano lo stesso indirizzo. Questo indica che la sezione è vuota e nessun costruttore Gcov è stato
linkato, e quindi i costruttori del driver gpio non sono stati inseriti in quanto
non generati.
Questo procedimento è stato ripetuto utilizzando anche un’altra versione del compilatore Gcc, passando dalla 4.4.3 (toolchain STM) alla 4.4.1
(toolchain Codesourcery). Il risultato comunque non è cambiato.
L’ultima prova che abbiamo effettuato è stata valutare lo strumento Gcov
Kernel anche su un’architettura differente, ovvero x86. Abbiamo compilato
il kernel per questa piattaforma utilizzando un file di configurazione nativo
all’interno dell’ambiente host Suse Linux, abilitando il framework Gcov e instrumentando il file string_32, non essendo disponibile il file del driver gpio,
in quanto specifico della piattaforma Cartesio. Lo scopo di questa prova era
verificare che i costruttori venissero correttamente generati. Eseguendo il
comando |objdump -t vmlinux | grep _ctors_ nell’eseguibile del kernel
per la piattaforma x86, abbiamo ottenuto i seguenti risultati. Confrontandoli
con i risultati ottenuti dall’utilizzo di Gcov Kernel per l’architettura ARM,
notiamo che la sezione ctors per l’architettura x86 ha indirizzi differenti e
quindi non è vuota. Il costruttore del file instrumentato è stato generato e
inserito correttamente, al contrario dell’architettura ARM.
71
CAPITOLO 5. Analisi dell’Efficacia del Test
Cross-compiler ARM
c0025bf0 g .init 00000000 __ctors_start
c0025bf0 g .init 00000000 __ctors_end
Native-compiler x86
c048d178 g .init 00000000 __ctors_start
c048d198 g .init 00000000 __ctors_end
Tabella 5.2: Sezione ctors di vmlinux per l’architettura ARM e x86
In conclusione possiamo affermare che il difetto quindi non è relativo al
framework Gcov, ma al compilatore Gcc, in particolare alla parte platformdependent per ARM di Gcc, che per questa architettura non crea le funzioni
costruttore richieste da Gcov per i file in cui l’opzione di copertura viene
abilitata e non inserisce i puntatori a tali funzioni nella sezione .ctors del file
eseguibile del kernel.
Sul sito http://gcc.gnu.org/bugzilla/, dove è possibile visualizzare
e pubblicare i difetti per il compilatore Gcc, abbiamo trovato un difetto che
conferma il nostro problema. La descrizione è disponibile all’indirizzo
http://gcc.gnu.org/bugzilla/show_bug.cgi?id=26313
con il titolo arm-gcc with gcov option do not work. La descrizione riporta
che per Gcov Kernel utilizzato su ARM la funzione __gcov_init non viene
chiamata, senza ulteriori dettagli.
La nostra analisi è stata condotta in modo approfondito mettendo in luce
le cause di questo problema. Il nostro lavoro lavoro può essere utilizzato per
completare la descrizione del difetto trovato su Gcc Bugzilla e per questo può
essere pubblicato con l’obiettivo che possa essere risolto per poter sfruttare
il framework Gcov Kernel anche su piattaforma ARM.
5.6.1
Esempio di utilizzo di gcov nello spazio del kernel
Non disponendo dei risultati della nostra analisi di copertura, riportiamo un
esempio che mostra i dati elaborati da Gcov Kernel e interpretati da Lcov,
per dare un’idea dell’utilità dello strumento. La figura 5.2 riporta l’esempio
di un’analisi di copertura per un kernel su piattaforma ppc64. Questa è
una vista globale della copertura sull’intero kernel. Sono presenti le diverse
directory, tra cui quelle contenenti i driver, e per ognuna la percentuale
di copertura totale relativa ai diversi file contenuti, la percentuale per lo
statement e il branch coverage. Sono presenti dati numerici sugli statement
e branch eseguiti e anche la percentuale totale di copertura del codice del
kernel.
La figura 5.3, riporta le informazioni di dettaglio di una delle directory
presente nei risultati globali. Il report è navigabile e come una normale
pagina html e basta effettuare un click per visualizzare ulteriori informazioni.
E’ possibile notare informazioni sulle linee instrumentate ed eseguite e la
percentuale di copertura per ogni file della directory. Inoltre è presente anche
la percentuale di copertura totale relativa alla directory stessa. L’ultimo
72
CAPITOLO 5. Analisi dell’Efficacia del Test
Figura 5.2: Esempio di risultati con gcov e lcov
livello di dettaglio riguarda il singolo file, nel quale è possibile vedere quali
sono effettivamente le linee eseguite e non eseguite.
5.6.2
Sviluppi futuri
Proprio nel periodo di scrittura di questo capitolo è stata pubblicata su
http://article.gmane.org/gmane.linux.kernel/993549 in data 01 Giugno 2010, una possibile soluzione al problema individuato che prevede l’applicazione di una patch per, come riferito nella descrizione, abilitare il supporto
Gcov su architettura Arm e risolvere il problema dei costruttori. Purtroppo per ragioni di tempo non siamo riusciti a valutare questa soluzione, la
sua pubblicazione è giunta in concomitanza con la fine del periodo di tesi.
Rimane comunque un punto di partenza per un’eventuale continuazione del
lavoro.
73
CAPITOLO 5. Analisi dell’Efficacia del Test
Figura 5.3: Esempio di risultati di dettaglio con gcov e lcov
74
6
Valutazione delle Prestazioni del Codice nella Fase
di Boot di Linux Embedded
l profiling è una strategia per valutare le prestazioni di un sistema. Si
utilizzano metodi di profiling per capire dove sono i percorsi critici (fastpath che vengono eseguiti più spesso) e i colli di bottiglia sui quali vale la
pena di concentrarsi maggiormente per eseguire ottimizzazioni e migliorare
le prestazioni. Attraverso il profiling è possibile osservare dove un programma sta utilizzando il suo tempo e quali funzioni sono chiamate mentre il
programma viene eseguito. Con le informazioni di profiling, è possibile determinare quali porzioni di codice sono più lente e quindi delle ottime candidate
ad essere riscritte o eliminate per rendere l’esecuzione del programma più
veloce. Conoscendo anche il numero di volte che ogni funzione viene chiamata, è possibile determinare dove concentrare gli sforzi di ottimizzazione
del codice per aumentare le prestazioni complessive del programma. Il profiling è importante soprattutto nei sistemi embedded, ma molto spesso è un
tipo di valutazione che viene ignorata. Il profiling durante le fase di validazione del software è volto a individuare e risolvere problemi di prestazioni
del software, ma non di meno l’analisi approfondita può anche portare ad
un miglioramento del design hardware. Molto spesso, i problemi di scarse
prestazioni sono legati al software. Non effettuare un’analisi di profiling può
portare alla scelta affrettata di sovradimensionare un sistema con processori
e RAM più veloci. Questa può non essere la scelta più vantaggiosa. Infatti
aumentano i costi rendendo il prodotto meno competitivo, i margini di profitto si abbassano e aumenta il consumo energetico, tutti fattori che sono da
tenere in considerazione.
Il profiling può essere influenzato da diversi fattori che modificano il
tempo di esecuzione di una funzione, tra questi il principale è il caso degli
interrupt e altre eccezioni che possono avvenire durante l’esecuzione.[Ber01]
Questo capitolo descrive una tecnica di profiling, il function tracing, tracciamento delle funzioni. Il function tracing, nel contesto di un sistema op-
I
75
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
erativo rappresenta una buona tecnica per individuare le funzioni del kernel
con maggiori problemi di velocità di esecuzione per poi investigare le cause
e proporre una possibile soluzione. In ambito embedded questa tecnica può
essere sfruttata per valutare il tempo necessario alla fase di boot uno dei
requisiti più stringenti di un sistema embedded. Il capitolo descrive l’adattamento di uno strumento di function tracing, nel processo di testing del
sistema operativo Linux embedded per piattaforma Arm, nel contesto del
progetto Cartesio di STMicroelectronics, per la valutazione delle prestazioni
delle funzioni eseguite nella fase di boot del sistema. Il capitolo analizza lo
strumento, la personalizzazione per il sistema specifico e i risultati ottenuti.
L’utilizzo di questo strumento nell’ambito sperimentale, permette di valutare
un aspetto della qualità del software non considerato precedentemente con
un buon livello di automazione.
6.1
Obiettivi del function tracing nei sistemi Linux Embedded
Nell’ambito del testing del BSP Linux Cartesio, il function tracing viene utilizzato per valutare le prestazioni delle funzioni del kernel eseguite nella fase
di boot del sistema. L’obiettivo è individuare le aree del processo di boot
con i maggiori problemi di prestazioni, in particolare identificare le funzioni
relative alla parte di codice platform-dependent eseguite nella fase iniziale
del kernel, per poter intervenire e migliorare i tempi di boot, requisito importante in ambito embedded. Quando si parla di prestazioni delle funzioni
si intende il tempo impiegato dal kernel per eseguire le funzioni, un valore
quantitativo per questa misura è dato dalla durata delle funzioni, calcolata come la differenza tra l’istante in cui il kernel è entrato nella funzione,
iniziando la sua esecuzione, e l’istante in cui è uscito dalla funzione e ha
terminato la sua esecuzione. Un altro fattore che incide sulle prestazioni
delle funzioni è il numero di volte che la funzione è chiamata.
6.2
Scelta dello strumento di automazione
Mentre per le classi di problemi analizzate negli altri capitoli, la scelta degli
strumenti di automazione era abbastanza ampia, in questo capitolo per la
tecnica adottata, considerando i vincoli dell’ambiente sperimentale, abbiamo
trovato solamente due strumenti, Ftrace e Bootchart1 . Quest’ultimo è uno
strumento non adatto per i sistemi embedded ed il motivo principale è che
richiede molte risorse per la sua esecuzione. Diversi progetti alternativi sono
stati ideati ma ancora in fase di sviluppo nel momento in cui scriviamo, tra
questi EmBootchart2 . La nostra scelta è ricaduta su Ftrace.
1
2
www.bootchart.org
http://tree.celinuxforum.org/CelfPubWiki/ELC2006Presentations
76
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
6.3
Ftrace. Il framework di tracing del kernel linux
Ftrace è framework del kernel Linux, di recente sviluppo disponibile dalla
versione 2.6.27 del kernel, per il tracing delle funzioni in esecuzione nel kernel. Può essere utilizzato come strumento di debugging, analisi delle latenze
e per la rilevazione di problemi di prestazioni che possono verificarsi al di
fuori dello user space. Ftrace è per definizione un function tracer, ma in realtà esso è uno strumento complesso che include un’infrastruttura composta
da diversi tipi di tracer con differenti scopi e funzionalità. L’architettura a
plugin di Ftrace permette di aggiungere in modo incrementale i tracer man
mano che vengono sviluppati, in questo modo la lista dei tracer disponibili
cresce da una versione all’altra del kernel. In questa descrizione ci riferiamo
alla versione 2.6.32 del kernel Linux. Questa sezione fornisce una descrizione
sommaria del framework, in quanto una descrizione dettagliata esula dagli
scopi del capitolo. Si focalizza invece sugli obiettivi esposti precedentemente
e approfondisce la descrizione e la successiva personalizzazione di un tracer appartenente al framework. La documentazione dettagliata di Ftrace è
reperibile nella directory /Documentation/trace situata nella root di compilazione del kernel, i cui file descrivono i differenti tracer e i parametri che
possono essere utilizzati per ognuno, i file nel debug file system che sono utilizzati per inizializzare e sospendere un trace, per modificare la dimensione
del trace log, per settare i parametri del filtraggio e per personalizzare il
formato di output del trace log.
6.3.1
I Tracer del framework Ftrace
I tracer disponibili e configurabili si possono dividere in due categorie, function tracer e latency tracer. Dei function tracer fanno parte function,
function graph e sched-switch. Nei latency tracer rientrano irqsoff, preemptoff, preemptirqsoff e wakeup. Esitono altri due tipi di tracer particolari,
hw-branch-tracer che utilizza il supporto hardware x86 per il tracing dell’esecuzione dei branch e quindi è utilizzabile solo per le architetture x86; nop è
un tracer speciale il quale indica che nessun tracer è selezionato.
Function Tracer
Di questa categoria fanno parte i tracer function, function-graph e schedswitch.
• function: permette di tracciare le informazioni sulle funzioni in esecuzione nel kernel, nell’istante in cui parte l’esecuzione della funzione
(entry-point).
• function-graph: mentre il function traccia le funzioni sui loro entrypoint, il function-graph effettua il tracciamento basandosi sia sull’entry77
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
point e sia sull’exit-point delle funzioni. Fornisce inoltre un grafo di
chiamate delle funzioni.
• sched-switch: esegue il tracing dei cambi di contesto (context switches) e dei wakeups tra i thread.
Latency Tracer
In questa categoria ci sono i tracer irqsoff, preemptoff, preemptirqsoff e
wakeup.
• irqsoff : permette di effettuare il tracing delle aree del kernel che
disabilitano gli interrupt, memorizzando il valore di massima latenza;
• preemptoff : è simile a irqsoff ma si concentra sull’analisi della quantità di tempo per il quale la preemption è disabilitata;
• wakeup: è un tracer che rileva la massima latenza del task con la più
alta priorità per essere schedulato dopo il suo risveglio.
• preemptirqsoff : simile a irqsoff e preemptoff, traccia il tempo maggiore nel quale gli interrupt e/o la preemption è disabilitata.
6.3.2
Il file system di Ftrace
Ftrace utilizza il debugfs file system per memorizzare i file di configurazione
e i file dei risultati e per rendere disponibili essi allo spazio utente. Quest’interfaccia permette di usare i tracer senza avere a disposizione un’ applicazione utente. Quando sia debugfs che ftrace sono configurati nel kernel,
viene creata una directory /sys/kernel/debug/tracing contentente i file di
configurazione e di output.
Per configurare debugfs in modo tale che esso venga montato al boot
del kernel si abilita l’opzione CONFIG_DEBUG_FS del kernel hacking nel file
.config, utilizzando l’utility xconfig (Figura 6.1) I principali file contenuti in
/sys/kernel/debug/tracing sono:
• current_tracer: utilizzato per impostare e visulizzare il tracer attualmente configurato;
• available_tracer: contiene i differenti tipi di tracer che sono stati
compilati nel kernel;
• tracing_enabled: permette di abilitare (valore uno nel file) o disabilitare (valore zero nel file) il tracer attualmente configurato;
• trace: questo file contiene i risultati del tracer in un formato leggibile.
I risultati di un tracer possono avere tre formati differenti: function,
78
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Figura 6.1: Configurazione del file system debugfs
sched-switch e latency trace. Il formato viene modificato in base al tipo
di tracer utilizzato. Approfondiremo nelle sezioni successive il formato
function.
6.3.3
Utilizzare un tracer
Per utilizzare, ovvero per selezionare, attivare, fermare e leggere i risultati
di un tracer si eseguono i seguenti comandi. La visualizzazione dei tracer disponibili nel sistema, compilati con il kernel avviene per mezzo del
comando:
$ cat available_tracers
La selezione di un tracer tra quelli disponibili è data da:
$ echo "nome tracer" > current_tracer
Una volta selezionato, il tracer deve essere attivato con il comando:
$ echo 1 > tracing_enabled
Dopo aver eseguito il proprio programma il tracer deve essere disabilitato:
$ echo 0 > tracing_enabled
e i risultati possono essere visualizzati digitando:
$ cat trace
79
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
6.4
Il Function Graph Tracer
Tra i tracer disponibili nel framework quello che riveste maggiore interesse
per i nostri obiettivi è il function graph tracer. Può essere considerato una
derivazione del tracer function, con il quale condivide alcuni dettagli di
funzionamento. Questo tracer traccia le funzioni considerando sia il punto
di ingresso (entry-point) sia il punto di uscita (exit-point), al contrario del
function che utilizza solo gli entry-point della funzione. Questo aggiunge
interessanti caratteristiche al tracer con il quale si può misurare il tempo di
esecuzione di una funzione, durata della funzione, e costruire un grafo delle
chiamate, per osservare le relazioni tra le funzioni. Esso può essere utilizzato
in diverse situazioni quali:
• avere un overview del kernel e studiare quali operazioni avvengono
in dettaglio nelle diverse area o in un’area specifica alla ricerca di
anomalie;
• trovare le aree time-consuming del kernel in esecuzione che possono
mettere in luce problemi di prestazioni;
• trovare quale percorso viene intrapreso da una specifica funzione;
• analizzare cosa accade nel processo di boot, quali funzioni vengono
eseguite e con quale durata.
L’ultimo utilizzo descritto è sicuramente quello di nostro interesse. Il nostro
scopo è utilizzare function graph di Ftrace per misurare la durata delle funzioni che vengono eseguite al boot di sistema, concentrandoci in particolare
sulle funzioni del codice platform-dependent, i device driver, per indagare
eventuali problemi di prestazioni e migliorare il tempo di esecuzione della
fase di boot del BSP Linux Cartesio.
Il function graph tracer utilizza l’instrumentazione implicita. Il kernel
consiste di milioni di funzioni C, utilizzare un’instrumentazione esplicita
sarebbe dispendioso 5. Per instrumentare il codice quando il supporto per il
tracing è abilitato, il kernel viene compilato con l’aggiunta dell’opzione -pg,
utilizzata proprio per scopi di profiling e tracing, ai flag utilizzati dal compilatore gcc. Questo aggiunge del codice nel prologo di ciascuna funzione
che permette di invocare una routine assembly chiamata mcount ogni volta che inizia l’esecuzione di una funzione. E’ una routine platform-specific,
situata nel file arch/arm/kernel/entry-common.S per le architetture ARM.
All’interno del kernel ci sono milioni di funzioni, per ognuna viene chiamata la mcount, se essa non ha un basso overhead, le prestazioni del sistema
possono diminuire in modo considerevole. La figura 6.2, mostra un esempio
di codice assembly del file del BSP fs/sync.o generato dalla compilazione
con (figura destra) e senza (figura sinistra) l’opzione -pg. La dimensione del
file senza instrumentazione è 75KB, invece il file con l’instrumentazione è
80
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
79KB. Vi sono 4KB di differenza causati dall’inserimento del codice per la
chiamata di mcount, che consiste di poche istruzioni e quindi l’esecuzione
di queste ha un basso overhead durante l’esecuzione della funzione stessa.
A questo overhead si aggiunge quello per l’esecuzione delle istruzioni che
compongono mcount. Quando il kernel configurato con il tracer, è in ese-
Figura 6.2: Instrumentazione con e senza l’opzione -pg
cuzione, la funzionalità di tracing è disabilitata finché l’utente non l’attiva e
in questa situazione, la routine mcount ritorna il più velocemente possibile
alla funzione instrumentata e il kernel continua l’esecuzione della funzione.
Quando il tracing è abilitato, mcount chiama la funzione corrispondente al
tracer selezionato dall’utente la quale registra le informazioni di tracing nel
trace log.
La memorizzazione delle informazioni di tracing viene effettuata nel trace
log che è implementato con un’apposita struttura del kernel chiamata ring
buffer, la quale è stata appositamente creata per memorizzare i dati di tracing permettendo un veloce ed esclusivo accesso ai dati, gestendo le letture e le
scritture simultanee. Inoltre tale struttura permette la gestione automatica
dei timestamp utilizzati nei dati di tracing.
Infine, le informazioni di tracing possono essere accedute da un utente
attraverso i file situati nel debugfs. I dati sono formattati in modo che
siano leggibili e anche processabili da uno strumento di post-analisi. Si può
accedere ai dati sia durante l’esecuzione del trace che dopo la sua terminazione. Come detto in precedenza, il formato dei risultati utilizzato dal
function graph è il function format. A titolo d’esempio viene mostrato un
risultato con questo formato.
81
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
# tracer: function_graph
#
#
TIME
CPU TASK/PID
#
|
|
|
360.774522 |
1)
sh-4802
360.774522 |
1)
sh-4802
360.774523 |
1)
sh-4802
360.774524 |
1)
sh-4802
360.774524 |
1)
sh-4802
360.774525 |
1)
sh-4802
360.774525 |
1)
sh-4802
360.774527 |
1)
sh-4802
360.774528 |
1)
sh-4802
360.774528 |
1)
sh-4802
360.774529 |
1)
sh-4802
360.774529 |
1)
sh-4802
360.774530 |
1)
sh-4802
DURATION
|
|
0.541 us
4.663 us
+0.541 us
6.796 us
7.952 us
!9.063 us
0.615 us
0.578 us
0.594 us
|
|
|
|
|
|
|
|
|
|
|
|
|
FUNCTION CALLS
|
|
|
|
}
}
__wake_up_bit();
}
}
}
journal_mark_dirty();
__brelse();
reiserfs_prepare_for_journal() {
unlock_buffer() {
wake_up_bit() {
bit_waitqueue() {
__phys_addr();
Esso si compone di un header con il nome del tracer utilizzato e di diversi
campi nel risultato possono essere abilitati o disabilitati.
• CPU: è il numero della cpu sulla quale la funzione viene eseguita.
• DURATION: è il tempo di esecuzione della funzione, espresso in microsecondi. Esso include anche il tempo speso nell’esecuzione delle
funzioni figlie e nella gestione degli interrupt.
• OVERHEAD: questo campo precede il campo DURATION nel caso
venga raggiunta la soglia di durata predefinita. I valori sono + e !
• TASK/PID: mostra il thread e il pid del thread che ha eseguito la
funzione.
• TIME: visualizza il tempo assoluto fornito dal clock di sistema, da
quando il kernel è stato avviato a quando la funzione è acceduta. La
parte decimale mostra la risoluzione in microsecondi.
• FUNCTION CALLS: mostra il grafo delle chiamate delle funzioni.
Alcune righe del risultato non hanno il valore nel campo DURATION, questo
perchè il tracer specifica il valore sulle righe che rappresentano l’exit-point
di una funzione.
L’opzione -pg del compilatore aggiunge l’instrumentazione soltanto per
gli entry-point delle funzioni, per poter controllare anche gli exit-point, il
meccanismo di tracing per il function graph, a differenza del tracer function, subisce delle modifiche che interessano i valori memorizzati nello stack
e le sequenze di chiamate delle funzioni. In particolare viene utilizzato un
return trampoline. Questo è mostrato in figura 6.3. Quando Ftrace è chiamato sull’entry-point della funzione (1), esso memorizza l’indirizzo di ritorno
reale, il quale è l’indirizzo del caller dal quale è stata chiamata la funzione
82
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Figura 6.3: Return trampoline per il tracing dell’exit-point
instrumentata, nello stack. Poiché più funzioni saranno annidate prima che
l’indirizzo di ritorno sia processato le informazioni su esse sono conservate
in uno stack di indirizzi di ritorno (2). Dopo che Ftrace invoca la funzione
relativa al function graph tracer, esso sostituisce l’indirizzo di ritorno con
l’indirizzo di una routine di Ftrace per la gestione degli exit-point (3). Successivamente Ftrace torna alla funzione instrumentata così da poter essere
eseguita (4). Quando la funzione termina la sua esecuzione torna anziché
al caller originale, alla routine di Ftrace, che chiama nuovamente il function
graph tracer per gestire i dati sull’exit-point (5). Infine, Ftrace recupera
l’indirizzo di ritorno reale dallo stack e ritorna al caller originale (6-7).
6.5
Personalizzazione di Function Graph Tracer
Il function graph tracer è stato sviluppato originariamente per l’architettura
x86. Non funziona in modo nativo per ARM. A questa conclusione ci siamo
arrivati osservando come nella configurazione del kernel del BSP Cartesio per
la piattaforma ARM, nella sezione relativa al tracing, l’opzione per abilitare
il Function Graph Tracer non è presente. Attualmente per poter utilizzare
questo tracer sulla piattaforma ARM, bisogna eseguire una personalizzazione
dello strumento. Dopo una fase di ricerca, nella letteratura scientifica e
in ambito industriale, di pattern seguiti per personalizzazioni con esigenze
simili, abbiamo trovato il pattern descritto in [Bir09] e per personalizzare
83
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
lo strumento nel nostro contesto abbiamo seguito le linee guida citate. Il
pattern prevede l’applicazione di alcune patch al kernel reperibili all’indirizzo
[17]. Il BSP Linux Cartesio utilizza la versione 2.6.32 del kernel. Le patch
utilizzate sono relative a questa versione.
Analizziamo le modifiche necessarie per l’architettura ARM. La prima
modifica è aggiungere lo strumento function graph tracer, registrandolo nel
framework Ftrace. Poi viene estesa la routine ARM mcount in arch/arm
/kernel/entry-common.S per invocare il tracer function graph, registrato
precedentemente, e viene aggiunto il return trampoline per il tracciamento
dell’exit point. Nella figura 6.4 è mostrata l’applicazione della patch per
applicare queste modifiche al BSP Linux Cartesio. Abbiamo notato che
Figura 6.4: Applicazione delle modifiche per aggiungere il Function Graph
Tracer nell’architettura ARM
le patch 2.6.32-rc5 non corrispondono esattamente alla nostra versione del
kernel, nonostante questa sia la 2.6.32. Questo lo si può notare dalla presenza
delle righe hunk generate durante l’esecuzione delle patch. La creazione di
una patch che effettua delle modifiche ad un file avviene basandosi sul file
stesso contenuto in una certa versione del kernel. La patch indica al suo
interno il numero di riga corrispondente all’inizio del frammento di codice che
deve essere modificato. I file però possono cambiare da una versione all’altra
del kernel e se la patch viene utilizzata su una versione del kernel differente
da quella in cui è stata creata può generare una non corrispondenza tra la
riga indicata nella patch e quella contenuta nel file, che è stato modificato
e a quella riga non è più presente il frammento da sostituire. La patch
comunque viene applicata cercando la parte di codice interessata con un
metodo euristico, in particolare individuando le parti di codice prima e dopo
il frammento da modificare. Quando trovato la patch viene applicata e viene
generata la riga hunk che indica che la corrispondenza delle righe non è esatta
e l’applicazione è avvenuta con un certo offset dalla riga indicata.
La seconda modifica è registrare nel framework Ftrace il tracer ottimizzato per ARM chiamato function duration tracer. Esso aggiunge un duration
filtering e la possibilità di utilizzare il tracing per la fase di boot. La nostra
84
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
sperimentazione si basa sull’utilizzo di questo tracer. Nel seguito è descritto
in modo approfondito in una sezione dedicata. La figura 6.5 mostra l’applicazione della patch che aggiunge questa funzionalità al BSP Linux Cartesio.
Figura 6.5: Applicazione delle modifiche per aggiungere il Function Duration
Tracer
La terza modifica è relativa al compilatore e alla funzione mcount. Le
toolchain ARM utilizzate per la cross-compilazione del kernel, sono composte da diversi strumenti tra cui il compilatore gcc. Le versioni gcc precedenti la 4.4.0 instrumentano il codice con chiamate alla funzione mcount.
Le versioni successive instrumentano il codice con chiamate alla stessa funzione ma con nome diverso, ovvero __gnu_mcount_mc. Poiché la toolchain
utilizzata per il BSP Linux Cartesio include un copilatore gcc 4.4.1, bisogna
aggiungere la funzione __gnu_mcount_mc nel file arch/arm/kernel/entrycommon.S. La figura 6.6 mostra l’applicazione delle modifiche descritte al
BSP Linux Cartesio.
Figura 6.6:
Applicazione
__gnu_mcount_mc
delle
modifiche
per
la
creazione
di
Infine, abbiamo scelto uno strumento di post-analisi per visualizzare i
risultati in modo tale che siano interpretabili più velocemente rispetto ai dati
contenuti nel trace log di Ftrace. Lo strumento è FDD. La figura 6.7 mostra
l’aggiunta dello strumento al BSP Linux Cartesio. Dopo l’applicazione di
queste modifiche, il function graph tracer risulta configurabile nel kernel del
BSP Linux Cartesio. Dopo la configurazione, abbiamo utilizzato il Function
85
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Figura 6.7: Aggiunta dello strumento FDD
Duration Tracer. I risultati ottenuti, mostrano però un problema: i valori
di durata delle funzioni non sono precisi.
Analizziamo i risultati ottenuti dall’esecuzione del tracer sulla piattaforma Cartesio.
# tracer: function_duration
#
# tracing_thresh=0
# CPU TASK/PID
CALLTIME
# |
|
|
|
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
0)
sh-433
|
311.450000000
DURATION
|
|
|
0.000
|
0.000
|
0.000
|
0.000
|
0.000
|
0.000
|
0.000
|
0.000
|
0.000
|
0.000
|
0.000
us
us
us
us
us
us
us
us
us
us
us
|
|
|
|
|
|
|
|
|
|
|
FUNCTION CALLS
|
|
|
|
sub_preempt_count
__mutex_unlock_slowpath
mutex_unlock
inotify_inode_queue_event
__fsnotify_parent
inotify_dentry_parent_queue_event
fsnotify
vfs_write
sys_write
add_preempt_count
expand_files
Come è possibile notare dal frammento di risultati mostrato, il campo DURATION, che rappresenta la durata di esecuzione della funzione, contiene
per ogni funzione sempre il valore zero microsecondi e così per tutte le altre
righe del risultato, circa 39.000. E’ improbabile che ogni funzione abbia
durata zero, come se nessuna di esse fosse mai eseguita. Inoltre il campo
CALLTIME, che rappresenta il tempo assoluto (dal boot del kernel) di chiamata della funzione, contiene per molte righe sempre lo stesso valore, ad
esempio 311.450000000 per 2223 righe, cosi per 311.460000000, ecc. Tali
valori ci dicono che molte funzioni sono chiamate nello stesso istante, ovvero
le 2223 sono invocate tutte nello stesso tempo 311.450000000. Analizzando questi dati ci siamo resi conto di problemi rilevanti nell’accuratezza dei
valori.
Descriviamo l’analisi e la soluzione per risolvere questo problema. Tutti
i dati relativi al tempo, utilizzati dal function graph tracer, ma in generale
dai tracer del framework Ftrace, sfruttano i dispositivi hardware della piattaforma sottostante utilizzati come sorgenti di clock di sistema e le API
messe a disposizione dai sottosistemi di gestione del tempo (time manage-
86
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
ment) del kernel per recuperare i valori temporali (timestamp) da utilizzare
nel calcolo della durata delle funzioni.
Descriviamo brevemente quali sono i sottosistemi messi a disposizione
dal kernel. Per un sistema operativo la temporizzazione è un concetto fondamentale. I valori temporali recuperati da una sorgente hardware, il kernel
li interpreta e utilizza per differenti scopi. Non tutte le attività svolte dal
kernel hanno gli stessi requisiti temporali, la schedulazione dei processi, la
comunicazione con un dispositivo hardware, la generazione dei timeout nelle
implementazioni TCP e altre ancora. Per questo il kernel deve gestire differenti timer per fornire valori temporali con risoluzioni diverse. Il kernel Linux
distingue due sottosistemi (framework) che forniscono timer differenti: timer
a bassa risoluzione (LRTimer) e timer ad alta risoluzione (HRTimer), inseriti dalla versione 2.6. Gli LRTimer forniscono una risoluzione dell’ordine dei
millisecondi e si basano su tick periodici che avvengono ad intervalli regolari.
Gli eventi possono essere identificati e gestiti in questi intervalli. In molti
casi, una risoluzione dell’ordine dei millisecondi non è sufficiente, per esempio
quando la misurazione del trascorrere del tempo serve alla macchina (vedi
lo scheduling). Per tutti quei processi legati all’interazione uomo-macchina
la risoluzione può anche essere inferiore.
Inoltre negli ultimi anni sono stati sviluppati dispositivi hardware che
forniscono valori temporali più precisi con i quali è possibile ottenere una
risoluzione nell’ordine dei nanosecondi. Per sfruttare questi nuove sorgenti
di clock, nel kernel è stato introdotto il sottosistema che fornisce timer ad
alta risoluzione.
Indipendentemente dalla risoluzione, il kernel distingue due tipi di timer:
1. Alarm: sono utilizzati per esempio, con eventi quali la gestione dei
pacchetti di rete da parte del sottosistema network del kernel. Un
pacchetto ha un certo periodo di tempo per essere recapitato, un timer
viene settato e rimosso dopo che il periodo è concluso, ma anche prima
se il pacchetto arriva in tempo. La risoluzione per questi tipi di timer
non è molto importante. Quando il kernel definisce che un pacchetto
deve essere spedito entro 10 secondi dopo l’acknownledgment, non è
rilevante se il timeout avviene dopo 10 secondi o 10.001 secondi.
2. Free-running clock: sono utilizzati per implementare sequenze temporali. Per esempio, il driver di una scheda audio che invia i dati
all’hardware in intervalli di tempo periodici. Questi timer richiedono
una risoluzione migliore rispetto ai time-out.
Nella figura 6.8 è mostrata una vista generale della struttura del sottosistema di time management del kernel, con i componenti principali e le loro
interazioni. Al livello più basso è presente l’hardware. In base all’architettura specifica, di solito ci sono più dispositivi timer, implementati da diversi
clock chip, che forniscono valori di temporizzazione e sono utilizzati come
87
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Figura 6.8: Struttura del sottosistema di time management del kernel
sorgenti di clock di sistema. Alcuni sono a bassa risoluzione, altri ad alta
risoluzione. I clock chip hardware devono essere gestiti dai driver platformspecific, ma il framework fornisce un livello di astrazione clock source che
è un interfaccia generica per tutti i clock chip, per effettuare ad esempio
l’operazione di lettura del valore del timer. Alcuni dispositivi timer possono
fornire eventi in momenti arbitrari, al contrario dei dispositivi che lo fanno ad intervalli regolari. Un ulteriore livello di astrazione è necessario per
gli eventi periodici e per questo si ha il livello dei clock event. Quindi due
oggetti gestiscono la temporizzazione nel kernel, clock source e clock event.
Ciascuno di loro è rappresentato da una speciale struttura dati del kernel.
[Mau08]
Tornando al problema identificato, possiamo ricondurre la causa di esso
al fatto che non viene utilizzato dal tracer un timer ad alta risoluzione.
Prima di tutto, ci assicuriamo che sulla nostra piattaforma sia presente un
clock chip che fornisce valori ad alta risoluzione. Utilizzando il comando
cat /proc/timer_list è possibile vedere su un kernel in esecuzione la lista
dei timer utilizzati con la loro risoluzione. Il risultato di questo comando
sul BSP Linux Cartesio è mostrato di seguito.
# cat /proc/timer_list
Timer List Version: v0.5
HRTIMER_MAX_CLOCK_BASES: 2
now at 1681664395673 nsecs
cpu: 0
clock 0: mtu0-timer1
.base:
c042c2f8
.index:
0
.resolution: 1 nsecs
.get_time:
ktime_get_real
88
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
.offset:
18446744073127135596 nsecs
active timers:
clock 1: mtu0-timer0
.base:
c042c328
.index:
1
.resolution: 1 nsecs
.get_time:
ktime_get
.offset:
0 nsecs
Il BSP utilizza due timer, mtu0-timer0 come clock source e mtu0-timer1
come clock events. I timer hanno una risoluzione (.resolution) di 1 nanosecondo, quindi sfruttano clock chip ad alta risoluzione che la piattaforma
Cartesio mette a disposizione.
Siccome l’hardware che permette di ottenere valori di tempo accurati è
presente, la ricerca della causa del problema si restringe al tracer e a come
esso recupera le informazioni sul tempo.
Dopo un’analisi approfondita del codice sorgente del tracer possiamo affermare che il function graph tracer utilizza la funzione sched_clock per
leggere le informazioni sul tempo, dall’oggetto clock source che sfrutta il
clock chip mtu0-timer0. Tale funzione deve essere presente nel driver del
timer, nel nostro caso nel file /arch/arm/mach-cartesio/time.c, la funzione non esiste in quanto il nostro driver è stato adattato al nuovo HR
Timer framework di Linux e la funzione, appartenente al vecchio framework,
è stata eliminata. La funzione è mostrata nel codice 6.1.
Codice 6.1: Funzione sched_clock
1
2
3
4
5
unsigned long long notrace sched_clock ( void )
{
return clocksource_cyc2ns ( clock_source . read (& clock_source ) ,
clock_source . mult , clock_source . shift );
}
Essa è una funzione wrapper che per recuperare le informazioni sul tempo
dal dispositivo hardware si avvale della funzione di lettura definita nella
struttura clocksource del driver time.c, mostrata nel codice 6.2.
Codice 6.2: Struttura clock source
1
2
3
4
5
6
7
8
9
static struct clocksource clock_source = {
. name
= " mtu0 - timer0 " ,
. rating = 300 ,
. read
= clock_source_read ,
. mask
= CLOCKSOURCE_MASK (32) ,
. shift = 20 ,
. flags = CLOCK_SOURCE_IS_CONTINUOUS ,
. resume = clock_source_resume ,
};
89
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Tale struttura è la struttura di stato del driver che contiene un insieme di
proprietà per l’astrazione dei clock chip presenti sulla piattaforma hardware.
In particolare è specificato il nome del clock chip hardware, in questo caso mtu0-timer0 utilizzato come sorgente di clock principale; quale funzione
il driver implementa per leggere i valori dall’hardware e costituisce l’API
del timer framework, in questo caso clock_source_read; il rating che stabilisce quanto è affidabile il timer (precisione), nel nostro caso il valore è
300 e secondo quanto descritto in [Mau08], un valore tra 300 e 399 sono
considerati timer veloci ed accurati. Nel codice 6.3 è mostrata la funzione
clock_source_read.
Codice 6.3: API per la lettura delle informazioni temporali dall’hardware
1
2
3
4
static cycle_t notrace clock_source_read ( struct clocksource * cs )
{
return ~ readl ( mtu0 + MTU_REG_T0VAL );
}
Quindi possiamo concludere che per la risolvere il problema identificato con
il function graph tracer, bisogna effettuare delle modifiche al driver del timer
della piattaforma specifica per far si che il tracer utilizzi un timer ad alta
risoluzione, dopo essersi assicurati che esiste un chip hardware che può essere
utilizzato come sorgente di clock ad alta risoluzione. Queste modifiche sono:
• se non già presente inserire nel driver la API del timer framework utilizzata dal tracer per leggere i valori sul tempo, sched_clock. In questo
modo si è sicuri che il driver e il proprio BSP supportano al meglio
il function graph tracer, permettendogli di utilizzare valori temporali
accurati.
• la funzione sched_clock deve utilizzare la routine read di una struttura che rappresenta un timer ad alta risoluzione. Tale struttura e la
sua routine fanno parte del timer framework. La routine deve essere
implementata dal driver per l’architettura specifica.
• bisogna aggiungere l’attributo notrace alla sched_clock e a tutte le
funzioni chiamate da essa, quindi clock_source_read, che permette
non includere nei risultati del tracer le funzioni di gestione di interrupt
del timer.
Il miglioramento nell’accuratezza dei valori temporali si può notare dai risultati mostrati di seguito, ottenuti eseguendo il function duration tracer sul
BSP Linux Cartesio dopo la personalizzazione dello strumento.
90
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
# tracer: function_duration
#
# tracing_thresh=0
# CPU TASK/PID
CALLTIME
# |
|
|
|
0)
ls-447
|
181.694794331
0)
ls-447
|
181.694812485
0)
ls-447
|
181.694810639
0)
ls-447
|
181.694819562
0)
ls-447
|
181.694828793
0)
ls-447
|
181.694834946
0)
ls-447
|
181.694844793
0)
ls-447
|
181.694849408
0)
ls-447
|
181.694852793
0)
ls-447
|
181.694860177
0)
ls-447
|
181.694848485
6.6
DURATION
|
|
|
7.385 us
|
1.538 us
|
5.538 us
|
0.923 us
|
1.538 us
|
2.462 us
|
0.615 us
|
0.615 us
|
4.923 us
|
0.616 us
| + 14.461 us
|
|
|
|
|
|
|
|
|
|
|
FUNCTION CALLS
|
|
|
|
down_write
arch_get_unmapped_area
get_unmapped_area
cap_file_mmap
find_vma
kmem_cache_alloc
add_preempt_count
add_preempt_count
vma_prio_tree_insert
sub_preempt_count
__vma_link_file
Function Duration Tracer
Il function duration tracer è un function graph tracer ottimizzato per l’architettura ARM e utilizzabile nella fase di boot del kernel. Queste caratteristiche non sono presenti nel function graph tracer originale di FTrace.
L’obiettivo della nostra attività di testing è trovare le funzioni del linux
kernel invocate nella fase di boot con il tempo di esecuzione maggiore (timeconsuming) per ridurre il tempo di startup del kernel. Il Function Duration
Tracer è lo strumento adatto in questo contesto.
A differenza del Function Graph Tracer, il Function Duration è stato
sviluppato in modo specifico per supportare un duration filtering, per aumentare la finestra di tempo che il tracer può coprire. Il kernel esegue
milioni di funzioni per secondo, i dati di tracing vengono memorizzati in un
file di log che ha una dimensione finita. A causa di questo, è possibile che
non vengano registrati tutti i dati relativi alle milioni di funzioni invocate
ottenendo una perdita di informazioni di tracing che può compromettere
l’analisi dei risultati e l’individuazione di possibili problemi. Non tutti i dati
prodotti sono di notevole importanza, per questo aggiungendo un duration
filtering che esegue un filtraggio dei dati prima essi vengano memorizzati nel
file di log, permette di catturare gli eventi di maggiore interesse e aiutare ad
isolare le aree con possibili problemi. Per fare questo viene utilizzato il file
tracing_thresh in debugfs. La durata delle funzioni calcolata nell’exit-point,
viene confrontata con la soglia specificata nel file e gli eventi sono scartati se
non rispettano la soglia. Tale soglia indica una durata minima specificata in
microsecondi. Qualunque funzione che viene eseguita con una durata minore
rispetto alla soglia viene omessa dal risultato. L’utilita del duration filtering è quella di mettere in risalto le funzioni del kernel più time-consuming,
eliminando dai risultati le funzioni con la durata più breve. Per i dettagli
su come questa miglioria è implementata e per approfondire i suoi punti di
forza, rimandiamo a [Bir09]. Per impostare la soglia si esegue il comando
$ echo "soglia" > tracing_thresh
91
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
specificando un valore numerico che rappresenta la soglia in microsecondi.
Il function duration tracer è utilizzabile sia nella fase di boot che nella
fase di lavoro standard del kernel. La differenza tra le due modalità è la
configurazione dello strumento. Descriviamo la configurazione e l’utilizzo
del tracer per la fase di lavoro standard. Nel seguito ci concentriamo invece
sulla fase di boot.
Il function duration tracer è compilato nel kernel abilitando l’opzione
CONFIG_FUNCTION_DURATION_TRACER. La figura 6.9 mostra questa operazione
nel BSP Linux Cartesio. Deve inoltre essere abilitato il debugfs file system
Figura 6.9: Configurazione del Function Duration Tracer nel BSP Linux
Cartesio
con l’opzione CONFIG_DEBUG_FS.
Quando il kernel è in esecuzione, dopo il boot, per utilizzare il Function
Duration Tracer si eseguono alcuni passi. Dalla directory /sys/kernel/debug/tracing, si disabilita un tracer precedentemente attivato:
$ echo 0 >tracing_enabled
si seleziona il function duration tracer:
$ echo function_duration >current_tracer
se necessario, si può modificare la dimensione del trace log. Se il tracer non
copre tutto il tempo di esecuzione ovvero non riesce a catturare tutti i dati
quando essi sono tanti. Per modificare la dimensione si deve prima vedere
la dimensione corrente.
$ cat buffer_size_kb
$ echo 1000 >buffer_size_kb
92
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
si imposta la soglia:
$ echo 500 >tracing_thresh
si abilita il tracer:
$ echo 1 >tracing_enabled
si esegue il proprio codice e successivamente si disabilita il tracer:
$ echo 0 >tracing_enabled
Disabilitare il tracer subito dopo aver eseguito la propria parte di codice è
utile per non sovrascrivere gli eventi di proprio interesse, in quanto il log
utilizza un ring buffer, che scrive i dati in modo circolare. Si salva il log in
un file di testo:
$ cat trace >/tmp/trace.txt
La memorizzazione dei dati nel log avviene in ordine di exit delle funzioni.
Per avere i risultati in ordine di entry della funzione si ordina il log in base
al campo CALLTIME, il quale è il secondo per default. Se il formato del
log è stato modificato, tale campo potrebbe avere un numero differente
$ cat /tmp/trace1.txt | sort -k2 >/tmp/trace.txt.sorted
6.6.1
Utilizzo nella fase di boot
Il function duration è il solo tracer che può essere utilizzato per calcolare i
tempi delle funzioni eseguite durante la fase di boot. Per questo motivo è
fondamentale per raggiungere i nostri obiettivi.
Per configurare il tracer per questa modalità, si utilizza come al solito
l’utility xconfig per modificare il file di configurazione del kernel .config.
In particolare si modifica l’opzione CONFIG_CMDLINE, kernel command
line, aggiungendo nella riga il frammento seguente:
ftrace = function_duration tracing_thresh="valore" trace_stop_fn="nome funzione"
in cui viene indicato il nome del tracer da utilizzare nella fase di boot, ovvero
function duration tracer, una soglia per filtrare le funzioni nel risultato,
abbiamo scelto il valore 100 in modo tale che le funzioni con tempo minore
di 100 microsecondi vengano scartate dal risultato e prese in considerazione
quelle più time-consuming, e uno stop trigger, una funzione su cui fermare
l’esecuzione del tracer, soprattutto per evitare di sovrascrivere i risultati con
informazioni di funzioni non appartenenti alla fase di boot. Per questo la
funzione stop trigger deve quella segna il confine tra la fase di boot e la fase
di lavoro standard del kernel. Nel nostro caso abbiamo scelto la funzione
93
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
cpu_idle(), trace_stop_fn=cpu_idle, la quale mette in attesa la cpu dopo
la fase di boot finché un nuovo processo non viene eseguito.
Una volta configurato con la funzionalità di tracing, il kernel deve essere
compilato per la piattaforma target. Dopo la fase di boot, durante la quale
sono raccolti i dati dal tracer, i risultati possono essere recuperati accedendo
al file system debugfs, salvando i risultati su un file di testo e infine settando
il tracer nop.
$ cd /sys/kernel/debug/tracing
$ cat trace > /tmp/boot-trace.txt
$ echo nop > current_tracer
6.6.2
Strumento di post-analisi
Come già anticipato dopo aver eseguito il tracer nella fase di boot sulla
macchina target, i risultati possono essere salvati su un file di testo e successivamente portati sulla macchina host, ad esempio utilizzando una scheda
di memoria, e analizzati da uno strumento di post-analisi. Lo strumento
scelto nel nostro contesto è FDD [17] scritto in Python. Questo strumento
permette di effettuare un’analisi statistica dei dati recuperati dal tracer, per
una migliore interpretazione e rilevazione dei problemi.
Le informazioni che lo strumento riesce a ricavare dai dati sono espresse
dai seguenti campi, identificati da un ID (specificato tra parentesi):
• Function: (f) nome della funzione
• Count: (c) numero di volte che una funzione è stata chiamata durante
il tracing
• Time: (t) tempo totale di esecuzione di una funzione (cumulativo
rispetto a tutte le volte che è stata chiamata)
• Average: (a) tempo medio per una singola chiamata della funzione
• Local time: (l) Time - tempo trascorso in tutte le funzioni chiamate
durante l’esecuzione della funzione stessa. Questo include non solo
le funzioni figlie, chiamate direttamente, ma anche gli interrupt e il
tempo trascorso in funzioni dello user space e di altri processi del
kernel. E’ un buon indicatore del tempo effettivo di una funzione
• Range: (r) indica il minimo e massimo tempo per una singola chiamata
• Sub-time: (s) tempo trascorso nella sotto funzioni
• Max sub-routine: (m) nome della sotto funzione con il maggior
tempo
94
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
• Max sub-routine count: (n) numero di volte che la sotto funzione
con il maggior tempo è stata chiamata
• Max sub-routine cost: (x) tempo speso nelle chiamate alla sotto
funzione con il maggior tempo
Per lanciare lo strumento, posizionandosi nella directory dove è stato
salvato, di solito /<kernel-src-root>/script, si può eseguire il comando
./fdd -S"campo" -n"k" -f fctalrsmnx /boot-trace.txt > fdd-analysis.txt
dove il comando ha le seguenti opzioni:
• -s : permette di ordinare i risultati in base al campo specificato con
l’ID. Può essere uno dei campi descritti nell’elenco precedente.
• -n : mostra soltanto le prime <k> funzioni più time-consuming.
• -f : modifica il formato del risultato, specificando, attraverso gli ID,
quali campi deve contenere.
La stringa /boot-trace.txt indica il nome e il percorso del file di testo salvato
sulla macchina target e portato sulla macchina host, contente i dati raccolti
dal tracer. Infine fdd-analysis.txt è il file in cui viene salvata l’elaborazione
dei dati effettuata da FDD.
6.7
Risultati sperimentali
Mostriamo i risultati ottenuti dall’esecuzione del function duration tracer
sul BSP Linux Cartesio nella fase di boot. L’esecuzione è stata effettuata
sulla macchina target, successivamente i risultati sono stati salvati su un file
di testo function_duration_boot.txt, e portati sulla macchina host per
l’analisi con FDD. Il primo risultato ottenuto, figura 6.10, processando il file
contenente i dati del tracer con FDD attraverso il comando
./fdd -n 20 -f fctalrsmnx /CARTESIO/fdd_input/function_duration_boot.txt > /home/stm/CARTESIO/result.txt
mostra le prime 20 funzioni con il maggior tempo di esecuzione ordinate in
base al campo Time. Poichè tale campo specifica la durata totale di una
funzione considerando tutte le volte che è stata chiamata (campo Count),
questo primo risultato visualizza tra le funzioni più time-consuming, funzioni generiche e proprie dei sottosistemi del kernel, che sono quelle maggiormente invocate per eseguire le diverse attività. Infatti si possono notare,
le funzioni chiamate ogni volta che un task deve essere schedulato (schedule,
schedule_timeout, preempt_schedule), le funzioni per la registrazione dei
driver e dei device (driver_register, bus_add_driver, amba_driver_register,
e per la loro associazione (driver_attach, driver_probe_device, amba_probe)
e alcune funzioni per la gestione della memoria (exit_mm0, mm_release).
95
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Figura 6.10: Risultati dell’analisi con FDD ordinati in base al campo Time
Questo primo insieme di risultati ci fornisce un’informazione utile. Tra
queste prime 20 funzioni, la maggior parte rientrano nell’insieme delle funzioni generiche utilizzate dal kernel per eseguire la fase platform-dependent
del processo di boot (appendice A). Le funzioni di registrazione dei driver e dei device e quelle per l’associazione sono eseguite per tutti i driver
che caratterizzano il BSP Linux Cartesio. Questi risultati non forniscono,
però, nessuna indicazione su quali sono le funzioni con la maggiore durata
appartenenti ai driver specifici.
Poiché il nostro obiettivo è analizzare le funzioni dei driver platformspecific per ottimizzare le singole funzioni oppure per individuare driver
non utili da poter eliminare e velocizzare la fase di boot, abbiamo utilizzato
un secondo insieme di risultati. Essi sono mostrati in figura 6.11 e sono
stati ottenuti ordinando i dati con FDD in base al campo Average che specifica il tempo medio di una funzione per una singola chiamata. Il comando
utilizzato è mostrato di seguito.
./fdd -s a -n 20 -f fctalrsmnx /CARTESIO/fdd_input/function_duration_boot.txt > /home/stm/CARTESIO/result.txt
La motivazione di questa scelta è che non si vogliono visualizzare, nel risultato, le funzioni che vengono chiamate più spesso (valore campo Count elevato)
che di solito sono quelle generiche del kernel. Invece, si vogliono mettere in
evidenza le funzioni del codice platform-specific chiamate meno frequentemente. Infatti le funzioni di un driver, durante la fase di boot, sono invocate
pochissime volte, ad esempio una volta per la registrazione e massimo n volte
se vi sono n device da associare. Eseguendo questo tipo di analisi, si ottengono le funzioni ordinate in base alla durata di una chiamata e quindi non
in base al tempo cumulativo per tutte le invocazioni. Quindi, si riescono ad
isolare le funzioni che hanno maggiore durata indipendentemente da quante
volte sono state chiamate. Questa è l’analisi corretta da effettuare con FDD,
in quanto le funzioni dei driver sono chiamate poche volte rispetto alle altre generiche del kernel. Infatti il risultato 6.11, che mostra le prime 20
96
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Figura 6.11: Risultati dell’analisi con FDD ordinati in base al campo Average
funzioni con la durata maggiore, è cambiato considerevolmente rispetto al
precedente 6.10, e notiamo che la maggior parte delle funzioni relative al
codice dei driver specifici, oltre che essere presente qualche funzione generica apparsa anche nel risultato precedente. Possiamo dire che le funzioni più
time-consuming del BSP Linux Cartesio appartengono ai seguenti driver:
• driver Seriale: pl011_init, pl011_probe, uart_add_one_port;
• driver I2C: cartesio_i2c_init, cartesio_i2c_probe;
• driver DMA: pl080_dma_init, pl080_dma_probe;
• driver GPIO: cartesio_gpio_init;
• driver USB: usb_init;
La funzione con la durata maggiore è la pl011_init situata nel file /driver
/serial/amba-pl011.c del driver dell’interfaccia seriale. Essa è la funzione
di registrazione del driver e impiega circa 0,18 secondi per essere eseguita,
considerando che le funzioni di init degli altri driver hanno durate che variano
da 0,02 a 0,05 secondi.
La causa di una durata maggiore rispetto alle altre funzioni, come si nota
dal grafo delle chiamate della pl011_init mostrato in figura 6.12, è dovuta al fatto che durante la sua esecuzione invoca le funzioni pl011_probe,
uart_add_one_port. Queste appaiono nei risultati della figura 6.11 al settimo e ottavo posto rispettivamente il che indica che hanno una durata
abbastanza alta in quanto sono chiamate ben 4 volte (valore campo Count)
poichè nella piattaforma Cartesio sono presenti 4 controllori e ognuno di essi
deve essere registrato e associato al driver. Inoltre è da tenere in considerazione che nell’esecuzione della pl011_init avviene anche la gestione di un
interrupt per l’esecuzione delle funzioni che stampano su console proprio i
trace di debug del kernel. Questa è uno dei tanti motivi che influenzano
97
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
Figura 6.12: Grafo delle chiamate della pl011_init
la valutazione delle prestazioni e questo caso ci suggerisce come i risultati
devono essere analizzati al netto della console.
Per l’ottimizzazione della fase di boot del BSP Linux Cartesio, possiamo concludere che le funzioni con durata maggiore sono relative ai driver
specifici della piattaforma. Alcuni di essi sono fondamentali per il corretto funzionamento del sistema embedded e non possono essere eliminati in
quanto gestiscono componenti core, tra questi il driver I2C, DMA, GPIO.
Altri sono relativi alla gestione delle periferiche, come driver USB e driver
Seriale. Le funzioni di quest’ultimo sono quelle che hanno la durata maggiore. Gran parte del tempo di boot è dedicato all’inizializzazione della
seriale. Considerando che questa interfaccia è molto utile in fase di sviluppo
e debugging ma raramente è presente in un prodotto finale, al contrario di
un’interfaccia USB. Un prodotto come un PND possiede una porta USB ma
non una porta seriale che è molto più ingombrante e inutile per un prodotto
del genere. Quindi come risultato di quest’analisi possiamo affermare che in
fase di produzione i driver dell’interfaccia seriale possono essere disabilitati
dal BSP Linux Cartesio per velocizzare la fase di boot.
6.7.1
Contributo
Per svolgere il lavoro descritto in questo capitolo, mi sono avvalso anche dei
consigli ricevuti dal responsabile del function graph tracer per ARM, Tim
Bird della Sony Corporation, il quale ha ritenuto il lavoro un ottimo contributo per poter far integrare lo strumento ottimizzato per la piattaforma
ARM direttamente nel kernel a partire dalla versione 2.6.34, senza l’utilizzo
98
CAPITOLO 6. Prestazioni del Codice nella Fase di Boot di Linux
di patch. Per questo il lavoro è stato pubblicato sui siti relativi a Linux
embedded3 e Linux ARM kernel4 .
3
4
http://vger.kernel.org/vger-lists.html#linux-embedded
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
99
7
Rilevazione di Memory Leak in ambiente Linux
Embedded
uesto capitolo descrive il problema del memory leakage, il principale
difetto della classe degli aging-related bugs, relativo all’uso non corretto
della memoria che provoca fallimenti del sistema. La causa principale di
questi difetti è legata a errori di programmazione.
Nei sistemi embedded, la limitata quantità di memoria fisica disponibile
e il fallimento del sistema che può avere conseguenze catastrofiche rendono il
memory leakage un problema da affrontare maggiormente che in altri ambiti.
La rilevazione dei difetti di memory leakage è difficile e per questo si
utilizzano strumenti automatici. Il capitolo descrive l’adattamento di uno
strumento per automatizzare l’attività di rilevazione dei memory leak nel
contesto del processo di testing dei sistemi linux embedded. La rilevazione è
focalizzata sui difetti che avvengono nello spazio del kernel. Esistono pochi
strumenti che permettono di effettuare tale rilevazione. Il capitolo valuta
uno strumento che permette di rilevare i kernel memory leak con un basso
impatto sulle prestazioni del sistema, requisito fondamentale in ambiente
embedded e che può essere completamente integrabile nel kernel linux.
Per mostrare l’utilità dello strumento, il capitolo descrive la sua applicazione con i device driver. Definisce gli errori di programmazione che possono essere commessi nello sviluppo di un driver e che causano difetti di
memory leak. Descrive quindi i risultati della rilevazione di memory leak
causati dagli errori definiti.
Q
100
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
7.1
Software aging
Il termine software aging identifica quella classe di difetti software che portano il sistema ad uno stato di instabilità in modo progressivo accumulando
errori durante la fase di esecuzione.
Il software aging è un problema noto sia per applicazioni critiche sia
per applicazioni di massa. In [CCN+ 08] vengono descritti diversi studi che
mostrano attraverso valutazioni sperimentali sui sistemi operativi che le condizioni di errore maturano col tempo e/o col carico di lavoro portando ad
una degradazione delle prestazioni e/o fallimenti. I fallimenti possono essere inconsistenze e/o corruzione dei dati che fanno riferimento alla memorizzazione di valori non corretti per le variabili, frammentazione dei file
che riguarda la memorizzazione non ottimizzata dei file su disco, rilascio
dei file-locks non corretto che fa riferimento alla gestione non corretta degli
accessi ai file, memory leakage che riguarda l’utilizzo non efficiente della
memoria principale e fallimenti di tipo crash o hang. Quando un crash si
manifesta un’applicazione (processo) scompare. Quando accade un hang,
un’applicazione (processo) è ancora in memoria ma non risponde ai comandi dell’utente o a qualunque altra richiesta. Se un crash interessa il sistema
operativo, la principale manifestazione sarà una schermata blu o un reboot.
Se invece accade un hang, il sistema sarà bloccato, come congelato (freeze).
Chiariamo la differenza tra crash e hang, in quanto esiste un po di confusione
tra i due termini. Sebbene qualche volta un hang è una diretta conseguenza
di un crash, la maggior parte delle volte l’hang accade indipendentemente.
Il loro modo di manifestarsi è differente.
Localizzare e risolvere i difetti di software aging durante l’attività di
testing del ciclo di vita del software è difficile per due motivi:
• non è semplice rilevare il fallimento. Il loro effetto è spesso nascosto e
i fallimenti non vengono scoperti fino all’evidenza. Ad esempio il memory leak è un problema che emerge solo quando lo spazio di memoria
diminuisce al punto che questa risorsa non può più essere utilizzata
efficientemente.
• non è semplice riprodurre le condizioni che portano al fallimento.
In effetti, in alcune classificazioni questi difetti vengono classificati in modo
diverso. Il modo tipico di classificare i difetti software, proposto da Gray
in [Gra86], distingue tra Bohrbugs e Heisenbugs. I Bohrbug sono difetti di
progettazione deterministici. Possono essere individuati facilmente e eliminati durante la fase di testing e debugging del ciclo di sviluppo del software. Gli Heisenbug, invece appartengono alla classe di difetti temporanei
e sono intermittenti. Per meglio dire, sono difetti permanenti le cui condizioni di attivazione però occorrono raramente oppure non sono facilmente
riproducibili e quindi causano fallimenti sporadici ad esempio fallimenti che
101
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
possono non ripetersi se il software viene riavviato. Per questa ragione gli
Heisenbug sono estremamente difficili da identificare attraverso il testing in
quanto non deterministici. Trivedi in [VT01] estende la classificazione di
Gray con gli aging-related bugs che appartengono sempre alla classe degli
Heisenbugs ma li aggiunge per sottolineare il modo diverso di localizzarli e
risolverli. Si possono risolvere difetti di tipo Heisenbug solo con azioni di
Figura 7.1: Classificazione dei difetti software proposta da Trivedi in [VT01]
tipo reattivo, invece gli aging-related bugs possono essere risolti sia in modo
reattivo ma anche in modo proattivo, eseguendo un processo di prevenzione
come il software rejuvenation, descritto in seguito.
L’identificazione dei difetti di software aging avviene principalmente durante la fase di esecuzione, utilizzando degli strumenti di monitoraggio delle
risorse per determinare lo stato di salute delle applicazioni e anche utilizzando in modo combinato strumenti di monitoraggio ed eseguendo dei test di
stress per un lungo periodo. Una metodologia per la rilevazione e la stima
del software aging è descritta in [GVVT98]
Per risolvere i difetti di software aging viene proposto un approccio
chiamato software rejuvenation. Tale approccio si basa sul concetto di terminare periodicamente un’applicazione o un sistema e riavviarlo successivamente in un nuovo stato interno pulito. Questo processo rimuove gli errori
accumulati durante l’esecuzione, liberando le risorse del sistema operativo
ed è un modo proattivo per evitare le costose e non prevedibili interruzioni
di sistema dovute al software aging. Esiste però un aspetto da non trascurare. Questo processo di prevenzione, deve essere eseguito nei tempi giusti
del sistema, ad esempio quando il carico di lavoro è molto basso, nei quali
il costo della prevenzione risulta minore del costo di una possibile azione di
recovery reattiva ad un fallimento provocato da un difetto di software aging.
E’ la scelta di questi tempi ottimali che giustifica l’utilizzo del software reju-
102
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
venation. Per ulteriori dettagli sul software rejuvenation, sui suoi vantaggi,
svantaggi e esempi di applicazione rimandiamo a [VT01] e [HKKF95].
Nei sistemi embedded, una scarsa valutazione di questi difetti in fase
di pre-produzione, aumenta le probabilità che nella fase operazionale del
sistema possano presentarsi situazioni di fallimento del software, che per
sistemi critici può essere catastrofico. Una tecnica come quella del software
rejuvenation è difficilmente applicabile per questi sistemi che devono sempre
garantire la loro operatività. Questo aumenta l’importanza della rilevazione
di tali difetti prima che il sistema diventi operativo.
I difetti relativi all’utilizzo e gestione della memoria, rappresentano la
maggiore fonte di aging-related bugs. Questo giustifica i nostri studi sul
memory leakage, considerato come uno tra i principali difetti nella gestione
della memoria.
La memoria centrale di un computer è una risorsa importante e critica
che deve essere utilizzata correttamente dalle applicazioni e dal sistema operativo. Il memory leakage è un particolare tipo di consumo di memoria non
intenzionale causato da un programma che non è più in grado di liberare un
zona di memoria che ha acquisito e non più necessaria. Questa condizione
è il risultato di un errore software che impedisce di rilasciare la memoria
dinamica dopo il suo utilizzo. Sebbene il memory leakage è strettamente
riferito all’esistenza di una qualunque zona di memoria non utile che non è
stata deallocata, molto spesso è associato ad un errore di programmazione
con il quale il software perde la capacità di rilasciare la memoria non utile.
Per comprendere meglio il memory leakage, descriviamo nel prossimo
paragrafo alcuni errori di programmazione che causano questo difetto e
portano ad un utilizzo improprio della memoria.
7.2
Errori di programmazione causa di memory leakage
Nello scenario tipico di allocazione della memoria tra sistema operativo e
applicazioni utente, l’applicazione richiede una zona di memoria, il sistema
operativo verificata la disponibilità e alloca la memoria richiesta. Infine
restituisce all’applicazione il puntatore all’indirizzo iniziale della zona di
memoria allocata.
In condizioni normali, terminato l’utilizzo della memoria, l’applicazione
notifica tramite system call che la memoria può essere liberata. In alcuni
casi questa interazione tipica tra sistema operativo e applicazioni non viene
rispettata e la zona di memoria allocata diventa non raggiungibile e non
utilizzabile per altri scopi. Si verifica quindi un difetto di memory leakage.
La causa principale del memory leakage sono gli errori di programmazione. Il caso in cui questi errori sono più frequenti è quando si utilizza
un linguaggio di programmazione sprovvisto di gestione automatica della
memoria, garbage collection [TVG06], come C e C++. Ad esempio, durante
103
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
la scrittura del codice con questi linguaggi uno sviluppatore dopo aver allocato con specifiche istruzioni all’interno del programma i blocchi di memoria
necessari ai suoi scopi, non scrive le oppurtune istruzioni per deallocare tali
blocchi. Un altro esempio è il caso in cui il riferimento ad una locazione
di memoria allocata dinamicamente non viene salvato o viene sovrascritto, rendendo effettivamente la locazione non più raggiungibile e utilizzabile.
Comunque anche i linguaggi di programmazione provvisti di meccanismi automatici di gestione della memoria, come Java e C# non sono totalmente
immuni da questi difetti; una descrizione approfondita dei problemi di memory leakage in questi tipi di linguaggi esula dagli scopi di questo capitolo e
per i dettagli rimandiamo a [GR08]. Descriviamo invece i principali errori di
programmazione che possono causare memory leak nei linguaggi sprovvisti
di garbage collector.
7.2.1
Perdita del riferimento alla memoria allocata
La prima situazione che descriviamo è il caso di memory leak provocato
dalla perdita del riferimento alla zona di memoria allocata. Di solito viene
utilizzata una variabile puntatore come riferimento; in alcuni casi però, può
capitare che un errore sovrascriva il puntatore con un altro valore oppure
che l’indirizzo restituito da una system call usata per l’allocazione non venga
memorizzato nella variabile rendendo l’area di memoria irrangiungibile e
impedendo al programma di utilizzare o liberare la memoria.
Codice 7.1: Primo esempio codice C.
1
2
3
4
5
6
char * string1 = ( char *) malloc (100);
char * string2 = ( char *) malloc (200);
scanf ( " % s " , string2 );
string1 = string2 ;
free ( string2 );
free ( string1 );
Nell’esempio 7.1, i 100 byte di memoria puntati da string1, non vengono
liberati e provocano un memory leak. Infatti, string1 inizialmente punta
ad un blocco di memoria di 100 byte e successivamente ad un altro blocco,
lo stesso di string2. A questo punto non c’è più nessun puntatore che referenzia il blocco di memoria da 100 byte e l’istruzione free(string1) fallisce
poiché cerca di liberare lo stesso blocco di memoria puntato da string2 che
è già stato rilasciato dalla precedente istruzione free(string2). Quindi dopo
l’uscita dalla funzione free(string1), esiste in memoria un blocco di 100 byte
isolato e non appartenente a nessun processo.
104
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
7.2.2
Mancanza delle system call per la deallocazione
La seconda situazione è il caso della mancanza delle system call per la deallocazione a fronte della presenza di system call per l’allocazione. irrangiungibile e impedendo al programma di utilizzare o liberare la memoria.
Codice 7.2: Secondo esempio codice C.
1
2
char * string1 = ( char *) malloc (100);
char * string2 = new char [200];
Come mostra il codice 7.2, non vengono utilizzate le system call free/delete
a fronte delle system call malloc/new. In questo caso un blocco di memoria
è allocato e non liberato successivamente, rendendo la memoria allocata non
utilizzabile dal sitema operativo. In questi tipi di linguaggi le system call
malloc/new e free/delete dovrebbero essere usate sempre in coppia.
7.2.3
Problemi di gestione della memoria con l’uso di array
La terza situazione riguarda il caso in cui non viene rilasciata totalmente la
zona di memoria assegnata ad un array.
Codice 7.3: Terzo esempio codice C.
1
2
char * p = new char [10];
delete p ;
Il codice 7.3, definisce un array di caratteri di lunghezza 10 byte; quando
utilizza delete p per liberare la memoria, solo un blocco di 1 byte viene rilasciato. Questo causa un memory leak. Per liberare invece tutta la memoria
allocata all’array deve essere utilizzata l’istruzione delete [] p.
7.2.4
Problemi di memory leakage in presenza di cicli
La quarta situazione è relativa all’utilizzo delle system call di allocazione/deallocazione in presenza di cicli.
Codice 7.4: Quarto esempio codice C.
1
2
3
4
5
6
char * p = NULL ;
for ( int i =0; i <5; i ++)
{
p = new char ;
}
delete p ;
Nel caso di allocazioni di memoria all’interno di un ciclo, devono essere presenti in esso non solo le istruzioni di assegnazione dei blocchi di memoria ma
anche le istruzioni di rilascio. Come mostra l’esempio 7.4, il programma alloca la memoria in un ciclo for() che si ripete per cinque volte. L’istruzione di
105
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
rilascio della memoria delete p è posta fuori dal ciclo, ma in quella posizione
non libera tutti i blocchi di memoria allocati, ma soltanto l’ultimo assegnato
al quinto ciclo del for(). Gli altri puntatori sono andati persi. Una soluzione
possibile in questo caso è inserire i puntatori in un array con un istruzione
all’interno del ciclo for(), per poi rilasciare i puntatori scandendo l’array in
un nuovo ciclo.
7.3
Conseguenze dell’occorrenza di memory leakage
Il memory leakage è una tra le principali cause di problemi di affidabilità e
scarse prestazioni del sistema.
Sebbene la diminuzione dei costi della memoria ha permesso di produrre
sistemi con una notevole quantità di memoria fisica, i memory leak hanno
ancora un impatto negativo sulle prestazioni e sull’affidabilità soprattutto in
un ambiente multi utente. Nello scenario più critico, un software che utilizza
in modo intenso la memoria e soggetto a errori di programmazione responsabili della generazione di memory leak diminuisce la quantità di memoria
fisica utilizzabile al punto che le richieste di allocazione di nuova memoria da
parte dello stesso programma o di altri programmi, non possono essere soddisfatte dal sistema operativo. L’impossibilità del sistema nel soddisfare le
richieste di nuova memoria, portano al fallimento del programma richiedente
o anche ad un errore di sistema se è lo stesso sistema operativo a richiedere
tale risorsa. Se il sistema operativo supporta il concetto di memoria virtuale
e di spazio di swap, una parte della memoria virtuale può essere trasferita
sul disco, liberando la corrispondente quantità di memoria fisica. Sfortunatamente però se l’area di memoria trasferita è ancora usata da un altro
programma, si incorre in un overhead di prestazioni per effettuare nuovamente il trasferimento da disco a memoria fisica portando al thrashing del
sistema, in cui la memoria virtuale è continuamente trasferita da memoria
fisica a disco e viceversa.
Esistono casi in cui l’analisi del memory leakage è fondamentale. Alcuni
di questi sono:
• Esecuzione prolungata nel tempo: programmi che sono eseguiti
per molto tempo e utilizzano in modo incrementale la memoria, ad
esempio processi che girano in background su un server e in particolare
software di sistemi embedded il quale può essere eseguito per anni
senza interruzione.
• Allocazione frequente di memoria: applicazioni in cui nuova memoria è allocata frequentemente, ad esempio video game e animazioni
video.
106
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
• Sistemi Operativi: un sistema operativo è un tipo di software molto
particolare, di solito scritto con linguaggi di basso livello che non
mettono a disposizione una gestione automatica della memoria.
• Device Driver: I device driver sono quello strato software più vicino
all’hardware che permette di gestirlo e renderlo utilizzabile da parte
delle applicazioni utente. Considerando che nella progettazione di un
sistema operativo la scrittura dei driver occupa una parte rilevante,
bisogna garantire che questi siano scritti in modo da gestire correttamente la memoria che richiedono per allocare le risorse necessarie.
• Risorse Limitate: quando le risorse sono limitate in dimensione,
come ad esempio la memoria nei sistemi embedded, è fondamentale
che essa venga utilizzata in modo efficiente dalle applicazioni e dal
sistema operativo.
7.4
Scelta dello strumento di automazione
Il notevole impatto negativo che il memory leakage può avere sulle prestazioni
e sull’affidabilità di un sistema, ha portato allo sviluppo di una grande quantità di strumenti per la rilevazione di memory leak e l’identificazione degli
errori di programmazione che possono causarlo. Nella tabella 7.1 viene riportato il confronto tra gli strumenti di automazione selezionati, in base
alle caratteristiche individuate nel capitolo 5. In questo confronto viene utilizzata un’ulteriore caratteristica, tipo di analisi: esistono strumenti che
analizzano il codice sorgente effettuando un’analisi statica, senza eseguire il
codice, per identificare costrutti programmativi che possono provocare un
memory leak. Altri strumenti effettuano un’analisi dinamica, durante l’esecuzione del software. Gli strumenti analizzati sono Coverity Static Analysis,
IBM Rational Purify, Parasoft Insure++, Valgrind, Leak Tracer, Memwatch,
Mtrace, Dmalloc e Kmemleak. Per l’analisi dei memory leak sul sistema linux embedded della nostra piattaforma, cerchiamo uno strumento che effettui
l’analisi dinamica, che sia distribuito con licenza free, che supporti l’analisi
di software scritto in linguaggio C e possa essere eseguito sul sistema operativo Linux. Un’altra caratteristica che lo strumento deve possedere è
l’interazione tramite riga di comando, in quanto questa è l’unica modalità
disponibile sul nostro sistema. Ma, la caratteristica cardine nella scelta è
che lo strumento deve permettere l’identificazione dei difetti di memory leak
nello spazio del sistema operativo (kernel space), la rilevazione di tali difetti
nel codice del kernel in esecuzione.
Infine un altro parametro da considerare è l’architettura su cui deve
essere eseguito lo strumento, nel nostro caso ARM. Nella documentazione di
alcuni strumenti vengono esplicitamente indicate le piattaforme sulle quali il
funzionamento è stato provato, come nel caso di Valgrind che nella sezione
107
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
Strumento
Instrum.
Licenza
Linguaggio
Ambito
Coverity [1]
source code,
implicita
comm.
C,
C++,
Java, C#
user-space
Purify [2]
byte
code,
implicita
comm.
C,C++
user-space
Insure++ [3]
byte
code,
implicita
comm.
C,C++
user-space
Valgrind [4]
byte
code,
implicita
byte
code,
implicita
byte
code,
implicita
byte
code,
implicita
byte
code,
implicita
byte
code,
implicita
Leak tracer [5]
Memwatch [6]
Mtrace [6]
Dmalloc [6]
Kmemleak [7]
free
C
user-space
free
C++
user-space
Sistema
Operativo
Linux,
Windows,
Solaris,
Mac-OS
IBM-Aix,
Linux,
Windows,
Solaris
IBM-Aix,
Linux,
Windows,
Solaris
Linux,
Mac-OS
Linux
Interfaccia
Analisi
GUI
statica
entrambe
dinamica
GUI
dinamica
N.D.(*)
dinamica
command
dinamica
free
C
user-space
Linux
command
dinamica
free
C
user-space
Linux
command
dinamica
free
C,C++
user-space
Linux
command
dinamica
free
C
kernel-space
Linux
command
dinamica
(*) N.D. indica informazione non disponibile
Tabella 7.1: Confronto degli strumenti per la rilevazione di memory leak
supported platforms del sito web indica per il sistema operativo Linux le
seguenti piattaforme: x86, Amd64, ppc32, ppc64, invece l’architettura ARM
non è attualmente supportata ed è inserita nel porting plans ed è previsto il
suo inserimento futuro.
Osservando i risultati del confronto tra gli strumenti, la nostra scelta
è ricaduta su Kmemleak. Kmemleak, rispetta i requisiti da noi richiesti
(analisi dinamica, licenza free, linguaggio C e O.S. Linux) e principalmente
è l’unico strumento che lavora nello spazio del kernel. Come riporta la
documentazione di kmemleak only the ARM and x86 architectures are currently supported è lo strumento adatto per la nostra architettura ARM e
non necessità di una fase di personalizzazione.
Una peculiarità di kmemleak, non presente in altri strumenti è la sua
semplice integrazione nel kernel linux. E’ uno strumento che è possibile
configurare a partire dalla versione 2.6.32 del kernel (quella da noi utilizzata). Inoltre, la sua leggerezza in termini di risorse utilizzate, lo rende uno
strumento adatto all’ambito embedded, in quanto è semplice e richiede poca
memoria per essere utilizzato.
Descriviamo sommariamente, per poi entrare nei dettagli nel prossimo
paragrafo, come lavora kmemleak. Il suo funzionamento è simile ad altri
strumenti quali Memwatch, Mtrace e Dmalloc. Questi strumenti lavorano
sostituendo le funzioni della libreria del linguaggio C per la gestione della
memoria, ad esempio malloc(), free(), ecc. Dispongono di un codice che intercetta le chiamate a queste funzioni durante l’esecuzione di un programma
e tracciano le informazioni di ogni richiesta di allocazione/deallocazione della
memoria. In quest’approccio l’unico overhead è rappresentato da una fase di
108
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
instrumentazione del codice sorgente che aumenta il tempo di compilazione e
le dimensioni dell’immagine del kernel da caricare nella memoria del sistema
embedded in fase di boot. Quindi kmemleak ha un basso grado di intrusività
nel codice sorgente e un overhead dovuto alla fase di instrumentazione.
7.5
Kernel memory leak detector
Il kernel memory leak detector o kmemleak, è uno strumento che fornisce
un modo di rilevare possibili kernel memory leak in modo simile ad un
garbage collector, con la differenza che gli oggetti orfani non sono eliminati
ma soltanto segnalati. Lo stesso metodo di kmemleak è usato anche da
Valgrind per rilevare i memory leak nello spazio delle applicazioni utente.
7.5.1
Algoritmo
Sommariamente, kmemleak traccia le allocazioni di memoria per mezzo delle
istruzioni kmalloc, vmalloc, kzalloc, ecc. e memorizza in un prio search tree i
puntatori alle zone di memoria allocate insieme con la dimensione e lo stack
trace. Quando sono identificate le corrispondenti funzioni di deallocazione,
i puntatori vengono rimossi dalla struttura dati di kmemleak.
Un blocco di memoria allocato è considerato orfano se scandendo la
memoria, non viene trovato nessun puntatore al suo indirizzo iniziale o a
qualunque locazione all’interno del blocco. Questo vuol dire che il kernel
non ha nessun modo di passare l’indirizzo del blocco a una funzione di
deallocazione e quindi il blocco di memoria allocato è considerato un memory
leak.
Descriviamo nel dettaglio l’algoritmo utilizzato per capire come i memory leak vengono rilevati all’interno del kernel. Kmemleak instrumenta lo
slab allocator, componente del kernel per la gestione della memoria utilizzato per l’allocazione. Per ogni blocco allocato dallo slab allocator, vengono
inserite nella struttura dati di kmemleak, prio search tree che è un radix
tree, le informazioni: puntatore al blocco, dimensione del blocco e stack
trace. Quando il blocco è liberato viene eliminat l’elemento corrispondente
nel radix tree. I passi dell’algoritmo sono i seguenti:
1. tutte le allocazioni di blocchi di memoria sono considerati memory leak
e inserite in una white list. I blocchi sono marcati come white object.
2. la memoria viene scansionata, partendo dalla sezione dati e dallo stack,
alla ricerca di puntatori ai blocchi allocati. I valori in memoria sono
confrontati con le informazioni nel prio search tree. Se un puntatore
ad un white object viene trovato, il blocco viene aggiunto in una gray
list.
109
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
3. vengono scansionati tutti gli oggetti nella gray list in quanto tali oggetti potrebbero contenere puntatori a oggetti nella white list, permettendo di aggiungere questi alla fine della gray list.
4. dopo aver controllato la gray list, ogni blocco di memoria raggiungibile
dal kernel è stato localizzato. I blocchi rimasti nella white list sono
zone di memoria orfane e considerati memory leak. I risultati sono
inviati allo spazio utente attraverso il file dei risultati nel debug file
system.
7.5.2
Configurazione e utilizzo generale
Per rendere disponibili i risultati, kmemleak utilizza il file /sys/kernel/debug/kmemleak. Per poter essere utilizzato, kmemleak va configurato prima
della compilazione del kernel. A questo proposito bisogna modificare il file
.config del kernel, con il tool di configurazione avviato con il comando make
xconfig, abilitando l’opzione CONFIG_DEBUG_KMEMLEAK in Kernel
hacking. Bisogna inoltre abilitare l’opzione CONFIG_DEBUG_FS sempre
in Kernel hacking per abilitare e montare il debug file system per poter
leggere e scrivere il file dei risultati. Dopo aver configurato lo strumento e
Figura 7.2: Configurazione kernel memory leak detector
compilato il kernel si ottiene un immagine contenente il supporto per la rilevazione del memory leakage nello spazio kernel. Kmemleak viene lanciato
al boot ed eseguito in un thread che ha il compito di scansionare la memoria ogni 10 minuti (per default) e scrivere nel file dei risultati gli oggetti in
memoria non referenziati che ha identificato. Per visualizzare su console il
file dei risultati si esegue il comando
110
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
#cat /sys/kernel/debug/kmemleak
Per pulire la lista di tutti i possibili memory leak si esegue il comando
#echo clear > /sys/kernel/debug/kmemleak
Per forzare una scansione della memoria si esegue su console il comando
#echo scan > /sys/kernel/debug/kmemleak
Molti parametri di kmemleak possono essere modificati a run-time, come ad
esempio l’intervallo di tempo per la scansione automatica. Una lista completa dei parametri è reperibile nella documentazione /Documentation/kmemleak.txt
Kmemleak permette anche di tracciare le allocazioni e deallocazioni di
memoria che avvengono in fase di boot prima che lo strumento sia inizializzato, poichè in questa fase attiva un early log buffer in cui sono immagazzinate
tutte le informazioni su questa azioni. Il buffer è poi letto da kmemleak per
identificare possibili leakage. La dimensione del buffer è modificabile configurando l’opzione CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE.
7.5.3
Limiti e Svantaggi
Kmemleak può incorrere in casi di falsi negativi e falsi positivi.
• Falsi negativi: sono memory leak reali (blocchi orfani) non rilevati da
kmemleak poichè durante la scansione della memoria sono stati trovati
valori che puntano a tali oggetti.
• Falsi positivi: sono oggetti erroneamente identificati come memory
leak.
Lo strumento mette a disposizione un’insieme di API, (/include /linux /kmemleak.h per i dettagli), che usate in combinazione permettono di ridurre
il numero di falsi positivi e falsi negativi.
Alcuni dei memory leak identificati possono essere solo temporanei, poichè
i puntatori ad esempio possono essere memorizzati per un breve intervallo
nei registri CPU che non vengono controllati da kmemleak. Per ridurre
questi casi kmemleak definisce una variabile MSECS_MIN_AGE che rappresenta la quantità minima di tempo per un oggetto per essere identificato
come memory leak. In pratica è un intervallo di tempo nel quale il puntatore
può essere trasferito in memoria e rilevato dallo strumento.
Il principale svantaggio di kmemleak è la riduzione delle prestazioni nell’esecuzione delle funzioni di allocazione/deallocazione della memoria, in
quanto queste vengono instrumentate per eseguire operazioni aggiuntive necessarie per gli scopi dello strumento. Comunque lo strumento dovrebbe essere utilizzato in fase di testing e debugging del codice dove le prestazioni
non sono tra i requisiti più importanti.
111
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
7.6
Memory leakage nei device driver
Questo paragrafo approfondisce la descrizione della fase di associazione devicedriver e anche delle funzioni probe e remove, in quanto analizza gli errori
di programmazione che possono avvenire nella scrittura di queste due funzioni causando problemi di memory leakage. Per comprendere questi errori
è necessario studiare il meccanismo di registrazione dei device e dei driver
e approfondire la fase di associazione e il ruolo delle funzioni di probe e
remove.
Gli errori di programmazione, causa di memory leakage, che si verificano all’interno del codice di un device driver, sono errori legati alla comprensione del processo di registrazione e associazione tra device e driver del
kernel linux. Anche se la fase più interessante di un driver è la fase di runtime, che evidenzia la logica di funzionamento con la periferica, dal punto
di vista della rilevazione dei leakage, l’analisi della fasi di inizializzazione e
de-inizializzazione sono più importanti, in quanto è in esse che avvengono la
maggior parte delle allocazioni e de-allocazioni di risorse, per diminuire la
deframmentazione della memoria, che possono creare memory leak. E’ per
questo che ci concentriamo sulle funzione di init e exit del driver.
Come già descritto nell’appendice A, la fase platform-dependent del processo di boot ha lo scopo di descrivere al kernel l’hardware dell’ architettura
specifica e inizializzare tutte le periferiche in modo che possano essere gestite
e utilizzate. Questo viene fatto attraverso un processo che si divide in tre
passi:
• Registrazione dei device;
• Registrazione dei driver;
• Associazione tra driver e device.
Dopo le fasi di registrazione dei device e dei driver, il kernel ha in memoria
una tabella dei device della piattforma e una tabella dei driver con cui
gestirli. Appena un driver non associato viene rilevato inizia una fase di
confronto, basata sul controllo degli identificatori, con l’intento di avviare
l’associazione tra il driver e un device. Se il confronto risulta positivo allora
vuol dire che è stato trovato un device che può essere gestito con quel driver
e come passo finale viene eseguita l’associazione, che si divide ulteriormente
in creazione istanza device, creazione e inizializzazione del device specifico e
esecuzione della funzione probe. Descriviamo in modo approfondito la fase
di associazione mostrando come esempio il codice del driver SGA di Cartesio.
112
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
Figura 7.3: Associazione tra device e driver
113
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
7.6.1
Creazione istanza device
Il kernel alloca in memoria una istanza device definita nella libreria device.h,
che rappresenta una generica periferica. Le parti più rilevanti di questa
struttura sono mostrate nel codice 7.5.
Codice 7.5: Struct device
1
2
3
4
5
6
7
8
9
10
11
12
13
struct device {
struct device_private
*p;
/* which driver has allocated this device */
struct device_driver * driver ;
/* initial name of the device */
const char * init_name ;
/* semaphore to synchronize calls to its driver */
struct semaphore
sem ;
/* type of bus device is on */
struct bus_type * bus ;
/* type of device */
struct device_type * type ;
}
7.6.2
Creazione e inizializzazione del device specifico
Oltre ad allocare una struct device, il driver alloca in memoria anche una
struttura che contiene informazioni sulla periferica specifica presente sulla
piattaforma. Nel progetto Cartesio, il driver SGA è un driver che gestisce un
device di tipo Amba (vedi appendice A per ulteriori dettagli) e per questo
che viene creata un’istanza della struct Amba device definita nella libreria
/include/linux/amba/bus.h. Essa è una derivazione di device e rappresenta
per il kernel una periferica Amba. La derivazione è implementata con un
puntatore ad un’altra struttura. La creazione di questa struttura dipende dal
tipo di device, infatti nel caso del device SGA viene creata poichè esso è un
Amba device. Ma se avessimo preso in considerazione una periferica di altro
tipo, ad esempio I2C, sarebbe stata creata un’istanza di una struttura I2C.
Così per device di altro tipo. La parte più rilevante della struct amba_device
è mostrata nel codice 7.6.
Codice 7.6: Struct amba device
1
2
3
4
struct amba_device {
struct device dev ;
unsigned int periphid ;
}
I campi principali sono struct device dev che realizza la derivazione e int
periphid utilizzato per memorizzare l’identificativo della periferica. L’Amba
device creato viene inizializzato con le informazioni peculiari della periferica
specifica e con informazioni di carattere privato al kernel.
114
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
7.6.3
Esecuzione della funzione probe
Dopo la creazione della struttura device e della struttura per la periferica specifica, viene chiamata la funzione probe del driver passando sia il
puntatore della struttura specifica sia l’ID della periferica. Nel codice 7.7
mostriamo solo alcune parti più rilevanti della probe del driver SGA.
Codice 7.7: Funzione probe
1
2
3
4
5
6
7
8
9
10
11
12
static int cartesio_sga_probe ( struct amba_device * dev , struct amba_id * id )
{
struct cartesio_sga * sga ;
/* allocate and set device context */
sga = kzalloc ( sizeof ( struct cartesio_sga ) , GFP_KERNEL );
if (! sga ) {
dev_err (& dev - > dev , " not enough memory for driver status \ n " );
ret = - ENOMEM ;
goto exit_clk_disable ;
}
amba_set_drvdata ( dev , sga );
}
La prima operazione importante della probe è dichiarare la struttura cartesio_sga attraverso un puntatore *sga. Definita sempre all’interno del driver,
è detta struttura di stato. Il suo codice è mostrato in 7.8. Contiene informazioni relative allo stato del device e informazioni di interfacciamento nel
caso il driver deve essere registrato presso un sottosistema del kernel.
Rappresenta la periferica specifica che il driver deve gestire, e come si
nota dal codice 7.8, con il campo struct device *dev, è ancora una derivazione
di device.
La dichiarazione e poi la creazione di un’istanza di questa struct definisce
un particolare contesto di utilizzo del driver. Più precisamente, considerando
che il driver può gestire device dello stesso tipo ma comunque differenti
l’uno dall’altro, la struttura di stato permette di personalizzare il driver per
la gestione di quella particolare periferica. Mentre le altri parti di codice
del driver sono generiche per tutti i device, questa struttura è la parte del
driver che viene personalizzata per ogni utilizzo del driver con una periferica
specifica con informazioni peculiari ad essa. Per questo motivo la struttura
prende anche il nome di device context.
Codice 7.8: Struttura di stato driver Cartesio SGA
1
2
3
4
5
6
7
8
/* SGA controller status */
struct cartesio_sga {
void __iomem * base ;
int irq ;
struct clk * clk ;
struct device * dev ;
int major ;
struct dentry * debugfs ;
115
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
/* firmware */
void __iomem * scheduler_virt ;
dma_addr_t scheduler_phys ;
/* global resource tracking */
DECLARE_BITMAP ( task_alloc_table , SGA_NUM_TASKS );
DECLARE_BITMAP ( sem_alloc_table , SGA_NUM_PROGSEMS );
DECLARE_BITMAP ( int_alloc_table , SGA_NUM_PROGINTS );
struct completion done [ SGA_NUM_TASKS ];
void __iomem * batch [ SGA_NUM_TASKS ];
spinlock_t lock ;
9
10
11
12
13
14
15
16
17
18
19
}
La seconda operazione della funzione probe è la creazione di un’istanza
della struttura di stato attraverso l’allocazione in memoria con l’istruzione
sga = kzalloc(sizeof(struct cartesio_sga), GFP_KERNEL) La terza è ultima operazione è l’associazione tra la struttura di stato e il device, istanza
della struct device. Il kernel gestisce una periferica attraverso quest’ultima
struttura. Fino a questo momento la struct device non è associata a nessuna periferica in particolare, esiste come struttura in memoria ma il kernel
non può ancora utilizzarla per gestire una periferica. Affinché questo sia
possibile, bisogna associare la struttura di stato, che rappresenta all’interno
del kernel la particolare periferica, con l’istanza della struct device creata
all’inizio della fase di associazione. Nella funzione probe questo viene effettuato con l’istruzione amba_set_drvdata(dev,sga) dove dev è un Amba
device derivazione di device e sga è la struttura di stato struct cartesio_sga.
Nel dettaglio l’associazione avviene facendo puntare il campo struct device_private *p; della struttura struct device alla struttura di stato. Il flusso
di collegamento è quindi
amba_device --> device --> device_private --> cartesio_sga
Il kernel a questo punto è a conoscenza che il device che deve gestire è un
amba device, questo amba device è la periferica SGA della piattaforma, e il
driver con il quale gestirlo è /arch/arm/mach-cartesio/sga.c.
7.6.4
Occorrenze di memory leak nei device driver
In un device driver, il primo errore di programmazione che può causare un
memory leak coinvolge sia la funzione probe che la funzione remove. Dopo
aver descritto la probe spendiamo qualche parola per la funzione remove.
Una parte della funzione remove del driver Cartesio SGA è mostrato nel
codice 7.9.
Codice 7.9: Funzione remove
1
2
3
static int __devexit cartesio_sga_remove ( struct amba_device * dev )
{
struct cartesio_sga * sga = amba_get_drvdata ( dev );
116
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
kfree ( sga );
a m b a _ r e l e a s e _ r e g i o n s ( dev );
dev_info (& dev - > dev , " module stopped and unloaded \ n " );
return 0;
4
5
6
7
8
}
La funzione remove viene chiamata quando il driver caricato come modulo
separato dal kernel, viene rimosso con il comando rmmod oppure nel caso
di periferiche che supportano il meccanismo di hotplug, quando la periferica
viene fisicamente staccata dal sistema. Lo scopo principale della funzione
remove è disfare tutto il lavoro eseguito dalla funzione probe. Attraverso amba_device ricevuto in ingresso recupera la struttura di stato e quindi tutte
le informazioni relative alla periferica gestita con il driver (riga 3). Successivamente, libera la zona di memoria che era stata allocata alla struttura di
stato (riga 4). Come ultima operazione dealloca la memoria che era stata
riservata alla struct amba_device (riga 5).
Il primo errore di programmazione, possiamo definirlo come mancanza
dell’istruzione di deallocazione della struttura di stato. La struttura
di stato è allocata nella funzione probe con l’istruzione
sga = kzalloc(sizeof(struct cartesio_sga), GFP_KERNEL)
Nella funzione remove viene liberata la zona di memoria, deallocando la
struttura di stato, con l’istruzione
kfree(sga)
Quando una richiesta di rimozione del driver è ricevuta, esso deve liberare
tutte le zone di memoria che ha allocato alle sue risorse, tra cui la struttura
di stato. Ma può succedere che l’istruzione di deallocazione kfree non venga inserita nella funzione remove. In questo modo quando un driver viene
scaricato, il puntatore sga alla struttura di stato verrà eliminato e non essendoci la kfree che dealloca la struttura di stato, rimarrà una zona della
memoria non referenziata contenente delle informazioni non più utili su una
periferica , escludendo l’uso di quella stessa parte di memoria per altri scopi.
Nel nostro sistema Linux embedded, abbiamo utilizzato per la rilevazione di
questo memory leak, il kernel memory leak detector (kmemleak).
Descriviamo il risultato dell’esecuzione di kmemleak all’interno del kernel per la rilevazione dell’errore di mancanza dell’istruzione di deallocazione
della struttura di stato. Nella figura 7.4 sono riportati i risultati ottenuti. Kmemleak rileva la zona di memoria non referenziata specificandone la
dimensione size 512. Nel momento della registrazione del driver nel kernel, con il comando insmod, viene avviato il processo descritto precedentemente fino alla chiamata della funzione probe in cui vi è un’allocazione che
non viene rilasciata. La sezione hex dump, riporta il contenuto dei primi
32 bytes della zona di memoria in questione in formato esadecimale. La
117
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
Figura 7.4: Kmemleak: rilevazione errore deallocazione struttura di stato
sezione backtrace del risultato, invece è lo stack di chiamate a funzioni che
ha portato alla rilevazione del memory leak. Esso si legge dal basso verso
l’alto. Si può scoprire in questo stack in quale funzione è avvenuto il memory
leak. Ci sono alcune funzioni generali del kernel e altre funzioni relative al
bus Amba. Si possono notare delle funzioni relative alla registrazione del
driver da amba_driver_register fino a __driver_attach e delle funzioni del
kernel che permettono di chiamare la funzione probe del driver SGA, da
driver_probe_device fino a amba_probe. Non è presente in questo risultato
alcun riferimento alle funzioni proprie del driver SGA. Sono presenti due
righe dello stack in cui al posto del nome della funzione vi è un numero
esadecimale. Le funzioni di queste due righe sono proprio le chiamate alla
funzione cartesio_sga_init per la registrazione del driver SGA e alla funzione cartesio_sga_probe per l’associazione del driver al device. Proprio in
quest’ultima è presente l’errore che ha provocato il memory leak.
Limite di Kmemleak con driver compilati come moduli
La sostituzione del nome delle funzioni con numeri esadecimali che rappresentano gli indirizzi delle funzioni in memoria è legata alla modalità di
caricamento del driver. In questo caso il driver è inserito come modulo nel
kernel e a differenza dei driver inseriti normalmente, tutte le informazioni
sulle sue funzioni non vengono inserite nel binario (vmlinux) del kernel. Inoltre per tutte le funzioni del driver non vengono inserite le rispettive righe
nel file system.map. Tale file creato in fase di compilazione viene utilizzato come tabella di simboli per la ricerca di un indirizzo di memoria di
una funzione o di una variabile. Le righe di questo file sono costituite da
118
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
coppie <indirizzo di memoria, nome funzione/variabile>. Un esempio del
system.map ottenuto dalla compilazione del kernel per la piattaforma Cartesio con il driver SGA compilato come modulo è mostrato nel seguito. Il file
è composto da 23436 righe in quanto comprende tutte le funzioni e variabili
del kernel.
...
c000e06c
c000e150
c000e2c0
c000e310
c000e404
c02a0fd8
c02a2fa0
...
t
t
t
T
t
t
t
cartesio_core_init
cartesio_board_init
cartesio_gpio_init
cartesio_vic_init
cartesio_sys_timer_init
cartesio_gpio_probe
cartesio_i2c_probe
Esplorando il file abbiamo notato che non sono presenti le funzioni cartesio_sga_probe e cartesio_sga_init. In conclusione, un driver compilato
come modulo non permette di caricare i simboli del driver nel kernel, ovvero
nel system.map non ci sarà alcuna riga relativa al driver. Questo nel caso di kmemleak si traduce nel fatto che lo strumento non riesce a tradurre
l’indirizzo delle funzioni del driver SGA con i rispettivi nomi, proprio per
l’assenza della linea di traduzione nel system.map. Nella figura 7.5, è mostrato il caso del driver SGA non caricato come modulo e nella rilevazione dello stesso memory leak si può notare che le funzioni cartesio_sga_probe e
cartesio_sga_init sono presenti con i loro nomi, invece che con indirizzi di
memoria non tradotti.
Figura 7.5: Kmemleak: driver non compilato come modulo
119
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
Il secondo errore di programmazione nel codice di un device driver, possiamo definirlo mancanza salvataggio dell’handle della risorsa allocata.
Questa volta l’errore interessa soltanto la funzione probe e non la funzione remove. Nella funzione probe viene allocata la struttura di stato, che contiene
le informazioni sul device utili al driver, con l’istruzione
sga = kzalloc(sizeof(struct cartesio_sga), GFP_KERNEL)
il puntatore alla struttura di stato restituito dalla kzalloc, detto handle, viene
salvato in una variabile automatica struct cartesio_sga *sga, la quale cessa
di esistere dopo l’esecuzione della funzione probe, in quanto è una variabile
locale. Per non perdere il riferimento alla struttura di stato, il kernel viene
in aiuto, allocando una struttura astratta struct device per salvare lo stato
del device, che è costituita da una parte platform-independent gestita completamente dal kernel e una parte che estende la struttura con informazioni
sul particolare device che servono al driver per gestirlo correttamente. Il salvataggio dell’handle in una struttura stabile che sopravvive alla terminazione
della funzione probe, avviene per mezzo dell’istruzione
amba_set_drvdata(dev,sga);
Con la funzione amba_set_drvdata, viene creato un altro riferimento, oltre
al puntatore sga, alla struttura di stato, ovvero il puntatore struct device_private *p nella struct device. Se questa funzione non viene inserita,
la zona di memoria allocata alla struttura di stato, dopo che la probe termina diventerà orfana, causando un memory leak. La rilevazione di questo
memory leakage da parte di kememleak è mostrata nella figura 7.6.
Figura 7.6: Kmemleak: mancanza salvataggio dell’handle della risorsa
allocata
120
CAPITOLO 7. Rilevazione di Memory Leak in Linux Embedded
In realtà per salvare l’handle della struttura di stato, si potrebbe rendere la variabile automatica struct cartesio_sga *sga globale. Questo però
non permetterebbe al driver di essere inizializzato più volte per gestire più
device, in quanto ogni volta la variabile sarebbe riscritta e le strutture di
stato allocate perderebbero l’unico riferimento, provocando un leakage. Per
evitare questo al posto della variabile globale si potrebbe utilizzare un array di puntatori alle strutture di stato, ma questo implicherebbe il fatto di
dover contare il numero di volte che il driver viene inizializzato per poter
incrementare l’indice dell’array, ma questo complicherebbe enormemente il
codice del driver.
Gli errori descritti, sono errori banali, ma considerando la quantità di
driver esistenti all’interno del kernel per le diverse periferiche e considerando
che un singolo driver può gestire più periferiche, allocando più di una struttura di stato, questo errore può veramente compromettere le prestazioni
e la stabilità del kernel. Come abbiamo già detto un’analisi statica del
codice non rileva tutti gli episodi di memory leak in quanto le condizioni
che lo causano sono variabili e si rende necessario un monitoraggio in fase
di esecuzione, con uno strumento automatico, come kmemleak, che effettua
un’analisi dinamica.
121
8
Valutazione della Prestazioni di Input/Output su
Dispositivi a Blocchi
egli ultimi decenni, i sistemi embedded sono cresciuti, oltre che nella
potenza computazionale, anche dal punto di vista della capacità di
memorizzazione dei dati, grazie allo sviluppo di tecnologie allo stato solido
per la realizzazione di memorie, come ad esempio le memorie Flash, utilizzate
come supporti di memorizzazione in molti personal computer ma soprattutto
nei sistemi embedded, come cellulari e PND (Portable Navigation Device).
L’uso della tecnologia Flash è prevalente nei sistemi embedded ed utilizzata per la realizzazione di memorie di massa, in quanto a causa dei vincoli di
spazio e costo, l’utilizzo di tecnologie magnetiche, come gli hard disk, molto
più ingombranti, non è adatto. Le memorie Flash hanno delle caratteristica
importanti, quali la dimensione ridotta e la robustezza e resistenza agli urti,
rispetto agli hard disk. Queste proprietà dipendono principalmente dalla
tecnologia utilizzate in queste memorie, ma intraprendere una discussione
su questo tema, esula dagli scopi del capitolo.
Esistono molte soluzioni differenti sia nella capacità di memorizzazione
sia nella tecnologia che utilizzano, e nella velocità di trasferimento dati: Compact Flash (CF), Sony Memory Stick (MS), Secure Digital (SD), Multimedia
Card (MMC) e USB flash drive.
Una piattaforma dedicata per i sistemi embedded possiede una o più
soluzioni e le board del sistema sono equipaggiate con i controllori, gli slot
e i bus per comunicare con questo tipo di periferiche. Nel sistema operativo
Linux questo tipo di dispositivi è classificato come block devices, dispositivi
a blocchi, come descritto nella sezione 3.4.2.
Dal punto di vista software, per poter utilizzare tali dispositivi ed eseguire le operazioni di Input/Output (I/O), quali scrittura e lettura dei
dati, sono sviluppati i device driver specifici per le periferiche.
I device driver devono essere verificati sia dal punto di vista funzionale,
esaminando la correttezza delle operazioni, come visto nel capitolo 4, ma an-
N
122
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
che dal punto di vista delle prestazioni, valutando con quale velocità queste
vengono effettuate.
Obiettivo del capitolo è descrivere l’adattamento di uno strumento per
valutare le prestazioni di I/O sui dispositivi a blocchi tipici di un sistema
embedded, le memorie Flash. Viene descritto inizialmente lo strumento
selezionato e la sua personalizzazione per l’architettura specifica Cartesio.
Successivamente viene descritta l’applicazione nel BSP Linux Cartesio e
analizzati i risultati ottenuti.
8.1
Obiettivi delle prestazioni di I/O nei sistemi Linux Embedded
La piattaforma Cartesio è equipaggiata con due tipologie principali di periferiche di memorizzazione: MMC (MultiMedia Card), USB. Le MMC sono
presenti come embedded (EMMC), in cui la memoria è presente direttamente sulla board insieme al controller, e come esterne, in cui sulla board è
presente il controller e lo slot per ospitare le schede di memoria. I dispositivi USB sono presenti nei due standard, 1.1 e 2.0 e possono funzionare in
entrambe le modalità host e device.
Il BSP Linux Cartesio include i device driver sviluppati per queste periferiche. Nell’ambito del testing del BSP, abbiamo già descritto come questi
driver vengono verificati dal punto di vista funzionale, valutando che le operazioni di scrittura (write) e lettura (read) siano eseguite correttamente,
ovvero che l’operazione vada a buon fine e sia garantita l’integrità dei dati
dopo il trasferimento. Quello che manca è un test di tipo non funzionale,
per valutare la velocità nel trasferimento dei dati su questi dispositivi.
Questo tipo di test è influenzato da una serie di fattori, che bisogna
prendere in considerazione. Questi sono:
Caratteristiche periferica le prestazioni dipendono anche dal tipo di periferica hardware che viene utilizzata, ovvero la specifica card o usb key,
la sua dimensione e tecnologia; queste sono prestazioni intrinseche del
dispositivo di memoria.
Filesystem il filesystem, caratteristica tipica dei dispositivi a blocchi, permette di organizzare i dati in una struttura gerarchica in directory e
file, e di gestire metadati quali (proprietario, diritti di accesso, ecc.).
Permette inoltre di fare le operazioni tipiche utilizzando funzioni di
alto livello. Linux supporta diversi filesystem, più o meno complessi,
con più o meno funzionalità, che introducono quindi overhead differenti
nelle operazioni di I/O (ad esempio VFAT e EXT2 non sono jounaled1 ,
1
Il journaling è una tecnologia utilizzata da molti filesystem per preservare l’integrità
dei dati da eventuali cadute di tensione
123
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
EXT3 e altri lo sono; VFAT ha strutture dati a 16 bit, EXT2 e EXT3
a 32bit, etc).
Scenario di utilizzo lo scenario di utilizzo dei dispositivi di I/O include
quali operazioni vengono effettuate, con quale numero e dimensione
dei file e altro ancora.
Gerarchia di memoria la presenza di livelli di memoria, sia hardware
(cache) che software (buffer cache), aggiuntivi alla RAM principale.
Quindi le prestazioni di I/O devono essere valutate mettendo in piedi
un ambiente di test che riproduca il più possibile i casi reali di utilizzo, in
modo comparativo con dispositivi simili sul mercato.
Nell’ambito sperimentale del BSP Linux Cartesio, l’analisi delle prestazioni
è utilizzata per valutare
• le operazioni di I/O su diversi dispositivi con diversi filesystem. L’analisi dei filesystem è importante. Per poter utilizzare un filesystem su un
sistema operativo, quest’ultimo deve avere un implementazione dello
stesso. Per analizzare quali filesystem un sistema operativo supporta
e in quale maniera, correttezza delle operazioni e velocità, la valutazione delle prestazioni può mettere in evidenza la presenza di difetti
nel supporto ad alcune tipologie di filesystem.
• per valutare l’andamento delle prestazioni a fronte di modifiche ai
driver in versioni successive del BSP,
• per valutare le prestazioni dei driver che utilizzo il DMA e driver che
non utilizzano questo componente nei trasferimenti dati.
Lo studio delle prestazioni inoltre, può rilevare problemi nell’architettura
del sistema. Ovvero se un device driver è stato sviluppato per leggere dal
dispositivo blocchi di 4 Kbyte di memoria e questa dimensione si rivela poco
performante, allora bisogna correggere la progettazione del driver.
8.2
Scelta dello strumento di automazione
In questo contesto, esiste un numero importante di strumenti. Descriviamo
i principali scelti in basa ai vincoli dell’ambiente sperimentale, quali supporto per il sistema operativo Linux, disponibilità di un’interfaccia a riga di
comando, licenza free e architettura supportata.
Bonnie e Bonnie++ Bonnie [20] è un filesystem benchmark per Unix,
sviluppato da Tim Bray nel linguaggio di programmazione C. E’ stato progettato per determinare i colli di bottiglia nei filesystem degli
hard disk dei server. Permette di eseguire operazioni di scrittura e
124
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
lettura sequenziali (per carattere e blocchi) e stabilire il numero di
random seek per secondo. Esso utilizza le funzioni, lseek, read, getc,
write e putc per accedere ad un file. Inoltre opera con lo scopo di
ridurre l’influenza della cache di sistema sulle operazioni. Bonnie++
[21], è una versione migliorata di Bonnie scritta nel linguaggio C++ da
Russel Coker, con l’obiettivo di valutare le prestazioni del filesystem
dei database su macchine server. Permette di creare diversi file per il
test, di dimensione superiore a 2GB, attraverso le funzioni creat, stat
e unlink, utilizzate su server mail e proxy. Esso supporta anche una
modalità di testing RAID.
Tiobench Tiobench [23] è uno strumento di filesystem benchmark multithreading per sistemi operativi POSIX che supportano la libreria pthread,
la quale mette a disposizione tutto l’occorrente per la gestione dei
thread nell’ambiente UNIX. Può essere utilizzato per determinare le
latenze medie e massime per le operazioni di lettura e scrittura su differenti filesystem e il throughput medio di lettura e scrittura sequenziale
o random.
IOzone IOzone [22] è un filesystem benchmark disponibile per molte architetture. Supporta diverse operazioni che lo rendono uno strumento adatto per valutare in modo completo le prestazioni. Permette
di eseguire scritture sincrone che trasferiscono i dati immediatamente
sul dispositivo (flush), senza rimandare la scrittura successivamente
conservando i dati in memoria.
Bonnie e Bonnie++ sono orientati verso la valutazione di prestazioni su
dispositivi quali hard disk, infatti includono la valutazione dei tempi di seek.
Tiobench, permette di valutare le prestazioni con uno stress test di letture
e scritture eseguite da più thread contemporaneamente. Il nostro obiettivo
è valutare le operazioni su dispositivi a blocchi, in cui non esistono i tempi
di seek dovuti alla rotazione dei dischi e al posizionamento delle testina
sul settore corretto. Inoltre poiché lavoriamo in ambiente embedded dove
l’utilizzo di uno strumento multi-threading può saturare la limitata quantità
di memoria, abbiamo scelto IOzone, in quanto dalla nostra analisi risulta lo
strumento più completo e adatto in questo contesto e ai nostri obiettivi.
Per la scelta dello strumento ci siamo basati su queste valutazioni in
quanto una confronto approfondito degli strumenti era al di là di quelli che
erano i tempi di stage e tesi.
8.3
IOzone. Filesystem benchmark
IOzone è uno strumento di filesystem benchmark. Esso misura una moltitudine di operazioni sui file. In questa sezione forniamo le informazioni più
rilevanti e di interesse per i nostri scopi. L’obiettivo non è duplicare la
125
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
documentazione esistente [22], ma fornire quelle che sono le linee guida per
l’utilizzo dello strumento in ambiente Linux embedded.
Le operazioni possibili sui file con sono moltissime, quelle di nostro
interesse sono:
• Read: questo test misura le prestazioni nella lettura di un file esistente.
• Write: questo test misura le prestazioni nella scrittura di un nuovo
file. Quando un nuovo file viene scritto esso deve essere prima creato,
per cui non vengono memorizzati solo i dati ma anche informazioni
aggiuntive, i metadati, che consistono di informazioni quali nome della
directory, spazio allocato, ecc. Ovviamente la scrittura dei metadati
porta ad un overhead che fa aumentare le prestazioni. Un’operazione
di scrittura ha operazioni minori rispetto all’operazione di riscrittura.
• Re-read: questo test misura le prestazioni nella lettura di un file
che è stato recentemente letto. Normalmente il sistema operativo
mantiene i dati di un file appena letto in cache per appunto migliorare
le prestazioni.
• Re-write: questo testo misura le prestazioni della scrittura di un file
già esistente. Quando un file esistente viene scritto nuovamente il lavoro richiesto è minore in quanto non devono essere ricreati i metadati,
e quindi le prestazioni sono maggiori.
Altre operazioni possibili sono random read/write, read-backwards, strideread, fread, fwrite, pread, pwrite e molte altre.
Ogni operazione è effettuata con dimensioni di file differenti (per default
con file da 64KB a 512MB) trasferiti con record di diverse dimensioni (per
default i file sono trasferiti con record da 4K a 16MB). Un’altra caratteristica
di IOzone è che è possibile generare output compatibili con Excel per la
creazione di grafici.
Dopo aver scaricato i sorgenti e compilato IOzone per la piattaforma
target (vedi sezione 8.4), per utilizzare lo strumento bisogna copiare IOzone
sul dispositivo di memoria, ed eseguirlo da quella posizione con il comando
seguente.
time ./iozone -a p o e -g"dimfile" -S"dimcache" -Rb "nomefile.xls" -i0 -i1 > "nomerisultato".txt
time è il comando Linux che permette di misurare il tempo di esecuzione
del programma lanciato. Le opzioni principali di IOzone sono:
• a utilizzata per attivare la modalità automatica. Permette di eseguire
tutte le operazioni con file da 64KB a 512MB e dimensione dei record
da 4K a 16MB. La modalità automatica può essere modificata con le
opzioni specificate nel comando.
126
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
• p permette di svuotare la cache del processore prima di ciascuna operazione e quindi di eseguirle senza l’accelerazione della cache della
cpu.
• S specifica la dimensione della cpu cache in Kilobytes, in quanto a
livello applicativo questo dato relativo all’hardware non può essere
ricavato.
• o con questa opzione, le scritture sono sincronizzate subito con il dispositivo. IOzone apre un file con il flag O_SYNC, il quale forza il completamento dell’operazione di scrittura prima di ritornare il controllo
allo strumento.
• e permette di includere le operazioni fsync e fflush per la scrittura
sincrona sul dispositivo. Il tempo di scrittura dall’esecuzione di queste
operazioni.
• g imposta la massima dimensione del file per la modalità automatica,
specificata in Kbytes.
• Rb utilizzata per creare un file dei risultati compatibile con Excel.
• i opzione utilizzata per specificare quali test eseguire (0=write/rewrite, 1=read/re-read).
Per effettuare un test completo, la dimensione massima del file deve essere
maggiore della dimensione della memoria RAM presente nel sistema. Questo
per poter vedere le prestazioni con l’influenza della cache della cpu, del buffer
cache e infine le prestazioni reali del dispositivo.
8.4
Personalizzazione di IOzone
IOzone supporta molte architetture e sistemi operativi, come descritto nella documentazione. Il makefile presente nei sorgenti mostra la lista delle
architetture supportate e per poterlo compilare per la propria piattaforma
basta specificare make "nome architettura".
Nel seguito è mostrato il frammento del makefile in cui sono specificate
le architetture supportate.
#
# Version $Revision: 1.128 $
#
# The makefile for building all versions of iozone for all supported
# platforms
#
CC = cc
C89 = c89
GCC = gcc
CCS = /usr/ccs/bin/cc
NACC = /opt/ansic/bin/cc
CFLAGS =
S10GCCFLAGS
= -m64
S10CCFLAGS
= -m64
FLAG64BIT
= -m64
127
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
all:
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
@echo
""
"You must specify the target.
"
->
AIX
"
->
AIX-LF
"
->
bsdi
"
->
convex
"
->
CrayX1
"
->
dragonfly
"
->
freebsd
"
->
generic
"
->
ghpux
"
->
hpuxs-11.0 (simple)
"
->
hpux-11.0w
"
->
hpuxs-11.0w
"
->
hpux-11.0
"
->
hpux-10.1
"
->
hpux-10.20
"
->
hpux
"
->
hpux_no_ansi
"
->
hpux_no_ansi-10.1
"
->
IRIX
"
->
IRIX64
"
->
linux
"
->
linux-arm
"
->
linux-AMD64
"
->
linux-ia64
"
->
linux-powerpc
"
->
linux-powerpc64
"
->
linux-sparc
"
->
macosx
"
->
netbsd
"
->
openbsd
"
->
openbsd-threads
"
->
OSFV3
"
->
OSFV4
"
->
OSFV5
"
->
linux-S390
"
->
linux-S390X
"
->
SCO
"
->
SCO_Unixware_gcc
"
->
Solaris
"
->
Solaris-2.6
"
->
Solaris7gcc
"
->
Solaris8-64
"
->
Solaris8-64-VXFS
"
->
Solaris10
"
->
Solaris10cc
"
->
Solaris10gcc
"
->
Solaris10gcc-64
"
->
sppux
"
->
sppux-10.1
"
->
sppux_no_ansi-10.1
"
->
TRU64
"
->
UWIN
"
->
Windows (95/98/NT)
"
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(64bit)
(64bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(64bit)
(32bit)
(32bit)
(64bit)
(64bit)
(32bit)
(64bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(64bit)
(64bit)
(64bit)
(32bit)
(64bit)
(32bit)
(32bit)
(32bit)
(32bit)
(32bit)
(64bit)
(64bit)
(32bit)
(64bit)
(32bit)
(64bit)
(32bit)
(32bit)
(32bit)
(64bit)
(32bit)
(32bit)
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
<-"
Come si può notare l’architettura ARM, presente nella piattaforma Cartesio
è nella lista del makefile. Ma prima di effettuare la compilazione con il
comando make linux-arm bisogna modificare il makefile. In particolare,
bisogna sostituire il valore della variabile CC del makefile,
CC = cc
C89 = c89
GCC = gcc
CCS = /usr/ccs/bin/cc
NACC = /opt/ansic/bin/cc
CFLAGS =
S10GCCFLAGS
= -m64
S10CCFLAGS
= -m64
FLAG64BIT
= -m64
128
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
specificando per la variabile CC la toolchain utilizzata nel progetto, armstm-linux-gnueabi-gcc
CC = arm-stm-linux-gnueabi-gcc
C89 = c89
GCC = gcc
CCS = /usr/ccs/bin/cc
NACC = /opt/ansic/bin/cc
CFLAGS =
S10GCCFLAGS
= -m64
S10CCFLAGS
= -m64
FLAG64BIT
= -m64
8.5
Risultati sperimentali
IOzone è stato utilizzato nell’ambiente sperimentale, per valutare i miglioramenti di prestazioni dei device driver relativi alla periferica MMC. In particolare, la valutazione è stata effettuata su due release successive del BSP,
2.3 e 2.4, in cui il driver MMC è stato modificato per l’utilizzo del DMA.
Il test è stato eseguito su Cartesio plus (STA2065), utilizzando una MMC
card Sandisk da 2GB formattata con il filesystem FAT. La versione di IOzone
è la 3.327. Il comando utilizzato è mostrato di seguito.
time ./iozone -aoe -g256M -S64 -Rb mmc1_fat_c.xls -i 0 -i 1
Il Cartesio plus ha una cpu cache di 64KB e una RAM di 128MB. Come detto
in precedenza la dimensione massima del file di test deve essere superiore
alla memoria RAM, per poter osservare le prestazioni in tre situazioni: con
l’aiuto della cpu cache, con l’aiuto della buffer cache e le prestazioni reali
del dispositivo. Per questo la dimensione massima scelta è 256MB. Il test
sarà eseguito con file da 64KB a 256MB trasferiti con dimensioni di record
da 4KB a 16MB. Le operazioni eseguite sono la read, write, re-read, rewrite. Per ognuna di esse viene attivata la modalità sincrona. I risultati
sono salvati in un formato leggibile con Excel. IOzone ha impiegato circa 6
ore per l’esecuzione del test.
Analizziamo i risultati relativi all’operazione di write, mostrati in figura
8.1. IOzone per i file superiori a 32MB, effettua le operazioni con record
size maggiore di 64KB. Quindi per questi file le misurazioni per i valori
di record size da 4KB a 32KB non sono fornite. Questo perché trasferire
un file molto grande con record size molto piccoli, abbassa notevolmente
le prestazioni poiché devono essere fatti molti più trasferimenti per completare l’operazione. Ad esempio per scrivere un file da 512MB con record
da 4KB bisogna effettuare 131072 trasferimenti. Se la dimensione del record
aumenta, ad esempio 64KB, i trasferimenti diventano 8192.
129
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
Figura 8.1: Prestazioni dell’operazione write
130
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
Come si nota dalla figura 8.1, il driver MMC della versione 2.4 del BSP
migliora nettamente le prestazioni dell’operazione di scrittura. Nella versione 2.3 il valore medio delle prestazioni è di 307 KB/s, invece nella 2.4 il
valore medio delle prestazioni è di 2389 KB/s.
Nella scrittura di alcuni file, ad esempio nella figura 8.1 nella versione
2.4, il file da 256KB ha dei trasferimenti con alcune dimensioni di record
(vedi 64KB e 256KB) con prestazioni maggiori. Questo è dovuto alla presenza o meno dei dati da trasferire nelle cache del sistema che migliorano le
prestazioni. Di solito le cache sono di due tipi:
• cache del processore (cpu cache): è una memoria fisica (cache hardware) aggiuntiva alla RAM del sistema, di solito di dimensioni molto
più piccole rispetto alla memoria principale. E’ utilizzata dal processore per memorizzare i dati e le istruzioni che devono essere elaborate
in tempi brevi, secondo il noto principio di località.
• cache del filesystem (buffer cache): è un’area della memoria primaria
(RAM) che il kernel alloca in fase di inizializzazione, e contiene una
copia dei blocchi del disco usati più di recente. Utile per limitare gli
accessi al disco durante le operazioni di lettura e scrittura, è implementata tramite una struttura software. Tutte le operazioni di lettura e
scrittura di blocchi passano attraverso il buffer cache. Come la cache
del processore, la buffer cache è utile per migliorare le prestazioni. Ma
il suo utilizzo è pericoloso in fase di scrittura, in quanto per limitare un
eccessivo tasso di swapping trattiene dei blocchi in memoria primaria
per scriverli su disco solo successivamente sulla base di opportuni algoritmi, e quindi l’utente può spegnere il sistema in modo disordinato o
estrarre una supporto rimovibile credendo che la scrittura si è conclusa
quando in realtà non è ancora stata completata.
Dai risultati di IOzone e conoscendo le dimensione delle cache, è possibile
capire qual’è il vantaggio in termini di prestazioni dell’utilizzo di questi
livelli di memoria aggiuntivi. Nella figura 8.2 sono mostrate le prestazioni
dell’operazione di lettura sulla MMC. Si possono notare tre differenti livelli
di prestazioni:
• miglioramento delle prestazioni dovuto alla cache del processore. Nel
trasferimento del file da 64KB, vi è l’aiuto della cpu cache che ricordiamo sulla piattaforma Cartesio è di 64KB.
• miglioramento delle prestazioni dovuto alla cache del filesystem (buffer
cache). Questo lo si può notare nelle operazioni di lettura dei file con
dimensione da 128KB a 32MB.
• prestazioni dell’operazione di lettura dopo che entrambe le cache sono
state riempite e quindi senza l’accelerazione dovuta ad esse. Lettura
dei file di dimensione da 64MB a 256MB.
131
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
Figura 8.2: Prestazioni dell’operazione read
132
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
Come si nota dalla figura 8.2 per la versione 2.4 del BSP, la lettura con l’aiuto
della cache del processore è quella che ha le migliori prestazioni, con 86821
KB/s (valore medio tra tutti i record size per il file da 64KB), la lettura
con la buffer cache ha un valore di 70546 KB/s (calcolato come media tra
tutti i record size dei file da 128KB a 32MB) e infine le prestazioni senza
l’utilizzo di questa cache in quanto sature, si abbassano notevolmente con
un valore di 9204 KB/s (calcolato come media tra i record size dei file da
64MB a 256MB).
Dalla versione 2.3 alla 2.4 del BSP Linux, il driver della MMC ha migliorato le prestazioni dell’operazione di lettura, passando da un valore (medio)
di 50207 KB/s a 57642 KB/s. Si possono inoltre osservare le prestazioni
della lettura senza l’aiuto delle due cache, per poter vedere di quanto effettivamente il driver ha migliorato le prestazioni. La figura 8.3, riporta i
grafici dell’operazione di lettura per i soli file da 64MB a 256MB (per tali
dimensioni le cache sono sature) e mostra il confronto per le due versioni
del BSP. Nel BSP 2.3 la velocità media della lettura è di 1018 KB/s, invece
nella versione 2.4 del BSP, la velocità media è di 9204 KB/s.
Da un confronto generale, si può notare come solitamente una MMC card
ha velocità di lettura maggiore della velocità di scrittura (basti osservare le
scale dei grafici). Le operazioni di lettura dipendono molto dall’architettura
che interagisce con il device (presenza di cache fisica e del sistema operativo,
buffer cache). Le operazioni di scrittura invece dipendono dal tipo di device
utilizzati, dalla particolare card e dalle tecnologie NAND, in cui le scritture
vengono effettuate dopo un operazione di erase.
In conclusione, l’infrastruttura di test messa in piedi con IOzone è servita
da un lato a confermare che il kernel era configurato correttamente: la cache
dati L1 del processore era correttamente abilitata (Linux supporta vari core
ARM dai più vecchi senza cache a quelli più rencenti con cache dati L1,
cache istruzioni L1 e cache L2, e quindi l’abilitazione di queste infrastrutture
hardware è opzionale), anche la cache del filesystem era stata correttamente
abilitata. Dall’altro lato è anche servita al programmatore del driver per
testare le modifiche che di volta in volta venivano applicate al codice per
tentare di migliorarne le prestazioni. Infatti il processo di ottimizzazione
del codice se in alcuni casi può essere eseguito in modo scientifico (algoritmi
matematici, codec, ecc.) in molti altri casi è un processo che va avanti
per tentativi ragionati. Appunto sperimentando e verificando i risultati con
strumenti di misura e profiling come quelli che descrivi nella tesi.
133
CAPITOLO 8. Prestazioni di Input/Output su Dispositivi a Blocchi
Figura 8.3: Prestazioni dell’operazione read senza l’accelerazione delle cache
134
9
Conclusioni
Dopo aver compreso il problema del testing di software di sistemi embedded nell’ambiente sperimentale STM, la tesi ha analizzato quattro classi
di problemi di qualità specifici e ha proposto tecniche di test e strumenti
di automazione per la loro valutazione nell’ambiente target e sulla componente software platform-dependent (device driver) del BSP Linux Cartesio,
producendo una serie di informazioni:
• un insieme di informazioni sulla personalizzazione degli strumenti per
la piattaforma ARM. Il lavoro svolto, può essere un’utile linea guida
per personalizzare gli stessi strumenti su altre architetture.
• un insieme di informazioni per la comunità open-source sul funzionamento degli strumenti e sulla presenza di problemi da risolvere per
migliorare l’utilizzo sull’architettura specifica.
e un insieme di risultati sperimentali, che convalidano le tecniche e gli
strumenti di testing proposti:
• Per lo strumento di analisi di copertura, Gcov, la personalizzazione ha
impedito l’utilizzo e la raccolta dei dati sulla copertura. In alternativa
è stata effettuata un’analisi sulle cause, che ha portato alla scoperta di
un bug relativo alla parte platform dependent ARM del compilatore
Gcc, che esclude un problema nell’architettura stessa dello strumento.
Le informazioni ottenute sono state pubblicate e inserite su Bugzilla
Gcc, affinché i responsabili dello sviluppo del compilatore possano risolvere il problema e permettere l’utilizzo di Gcov anche sui sistemi
embedded con architettura ARM. Nell’ultimo mese del lavoro di tesi è
stata individuata una possibile soluzione al problema, che per motivi
di tempo, non è stata analizzata. Questa costituisce sicuramente un
approfondimento futuro del lavoro che può portare all’utilizzo di Gcov
sulle architetture ARM e quindi all’effettiva raccolta e interpretazione
dei dati sulla copertura.
135
CAPITOLO 9. Conclusioni
• Il Function Duration Tracer, tra tutti gli strumenti, ha una personalizzazione che è la più difficile. Il risultato ottenuto è stato soddisfacente.
Con i dati raccolti, infatti, abbiamo scoperto che la fase di boot del
BSP kernel, può essere ottimizzata escludendo in fase di produzione del
prodotto l’inizializzazione dei driver della porta seriale, un device che
è principalmente utilizzato in fase di produzione per sviluppo, testing
e debugging. Inoltre il lavoro è un contributo valido per l’integrazione
diretta dello strumento nel framework Ftrace nelle future versioni del
kernel e ha permesso di aggiungere al BSP il supporto per il function
tracing. Un approfondimento futuro del lavoro, può essere l’analisi
del peso, delle funzioni che stampano su console i trace di debug del
kernel sulla durata complessiva. Il peso è dato non dalla generazione
delle stringhe dei trace, quanto proprio dal fatto che queste stringhe
vengono inviate sulla seriale che è una periferica ‘’lenta” e genera un
numero di interrupt notevole rispetto alla quantità di dati che tratta.
Questi sono proprio gli interrupt che si notano nei risultati ottenuti dal
trace della porta seriale. Poiché, in fase di produzione la console viene
disabilitata, bisognerebbe catturare i trace al netto della console. Inoltre, si potrebbero analizzare altre funzioni pesanti come quelle legate
ad un altro bus ‘’lento” come I2C, usato per inizializzare in fase di boot
periferiche esterne tipo I2C, che richiede delle attese per soddisfare il
protocollo di comunicazione col device remoto. E infine, analizzare
il trace al boot con chiavetta USB inserita sin dall’inizio, per vedere
quanto il boot viene rallentato dalle transazioni fatte proprio dal driver
USB per gestire la discovery della chiavetta.
• Kmemleak per la rilevazione dei memory leakage nel kernel, non ha
avuto nessun problema per quanto riguarda la personalizzazione. Per
mostrare l’utilità dello strumento abbiamo studiato e individuato i
principali errori di programmazione che possono essere commessi nello
sviluppo di un driver e che causano difetti di memory leak. L’applicazione dello strumento si basa sulla rilevazione di memory leak causati
dagli errori identificati.
• Per Iozone, utile alla valutazione delle prestazioni di I/O, la personalizzazione è molto semplice. Lo strumento è stato utilizzato per verificare
il miglioramento di prestazioni dei device driver tra release successive
di sviluppo. In particolare è mostrata l’applicazione su una versione
migliorata del driver MMC (MultiMedia Card). L’infrastruttura di
test messa in piedi con IOzone è servita da un lato a confermare che il
kernel era configurato correttamente: la cache dati L1 del processore e
la cache del filesystem erano state correttamente abilitate. Dall’altro
lato è anche servita al programmatore del driver per testare le modifiche che di volta in volta venivano applicate al codice per tentare
136
CAPITOLO 9. Conclusioni
di migliorarne le prestazioni. Infatti il processo di ottimizzazione del
codice se in alcuni casi può essere eseguito in modo scientifico (algoritmi matematici, codec, ecc.) in molti altri casi è un processo che va
avanti per tentativi ragionati. Appunto sperimentando e verificando i
risultati con strumenti di misura come quello descritto. Un approfondimento futuro del lavoro, può essere la valutazione delle prestazioni di
I/O su altri dispositivi, USB ad esempio e inoltre la ricerca di dati
sulle prestazioni di dispositivi simili sul mercato, in modo da poter
eseguire un analisi con un termine di paragone preciso.
Il lavoro di tesi ha migliorato il processo di testing utilizzato nell’ambiente
sperimentale STM, aumentandone il perimetro, attraverso l’individuazione
di aree di qualità non ancora esplorate e introducendo, per la valutazione di
queste, strumenti di automazione che riduco i costi e i tempi del processo.
La tesi fornisce un contributo per l’identificazione di un approccio generale e per superare i problemi di specificità del testing in questo dominio,
attraverso la sperimentazione in una realtà industriale complessa e affermata, di tecniche e strumenti per il testing di caratteristiche funzionali e non
funzionali di software embedded.
137
A
Processo di boot del kernel Linux. Fase
platform-dependent
Il kernel esegue un processo di caricamento e inizializzazione prima di entrare nella sua modalità di lavoro standard. Questa processo è detto system startup o boot. Il processo di boot è fortemente dipendente dall’architettura hardware sottostante e questo capitolo descrive il processo
eseguito nel sistema embedded Cartesio, basato su architettura ARM, e
focalizza l’attenzione sull’inizializzazione della parte di sistema relativa ai
device driver.
A.1
Processo di boot del kernel Linux
Il processo di boot del kernel linux è diviso in tre fasi:
1. Caricamento del kernel in RAM e creazione di un ambiente minimo
di esecuzione. Il firmware e il bootloader sono responsabili di questo
passo.
2. Esecuzione del codice platform-dependent per l’inizializzazione dell’hardware specifico.
3. Esecuzione del codice platform-independent del kernel per l’inizializzazione di tutti i sottosistemi.
Dopo che il sistema embedded viene inizializzato dal firmware presenta
nella ROM, parte la prima fase in cui viene eseguito il Cartesio bootloader
residente o in una memoria esterna, come ad esempio una mmc card o una
memoria interna, come ad esempio una nand. In entrambi i casi bisogna
configurare degli switch sulla board per eseguire la lettura da una o dall’altra memoria. Il bootloader è il programma che inizializza la memoria
(MEM CTRL), copia l’immagine del kernel in RAM e scompatta il Linux
138
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
root file system il quale contiene le librerie run-time, file di configurazione e
applicazioni richieste dal kernel per completare la fase di boot e presentare
all’utente una shell interattiva. Questa prima fase può essere approfondita con documentazione relativa all’architettura specifica e al progetto del
sistema embedded.
Una volta in RAM il kernel inizia ad eseguire codice specifico della piattaforma. Ha inizio la seconda fase, sulla quale ci concentriamo in questo
capitolo, riferendoci all’ architettura Cartesio. Seguirà un paragrafo di
dettaglio su questa fase.
Una volta terminata l’esecuzione del codice relativo alla piattaforma
specifica, inizia la terza e ultima fase nella quale viene eseguito il codice
platform-independent. Descriviamo sommariamente questa fase. Per maggiori dettagli rimandiamo a [Mau08]. La prima funzione che viene chiamata è la start_kernel. L’insieme di operazioni eseguite dalla funzione è
mostrato in figura A.1 La start_kernel è responsabile di invocare le routine
per l’inizializzazione di tutti i sottosistemi del kernel. L’inizio di questa
fase può essere riconosciuta in quanto la prima operazione che la funzione
start_kernel esegue è la visualizzazione sul display del messaggio sulla versione del linux kernel. Il seguente è il messaggio visualizzato sul nostro
sistema.
Linux version 2.6.32.11-svn1618 (stm@linux-83xd)
(gcc version 4.4.1 (Sourcery G++ Lite 2009q3-67))
#2 PREEMPT Tue Apr 20 18:01:14 CEST 2010
Questo messaggio è memorizzato nella varibile globale linux_banner definita in init/version.c. La seconda operazione, riguarda il completamento dell’inizializzazione del codice per l’architettura specifica, ma non codice di
basso livello e soprattutto l’inizializzazione del framework per l’high level
memory management. Ma la maggior parte del lavoro di inizializzazione è
eseguito dall’operazione initialize core data structures of most subsystems,
dopo che gli argomenti della command-line passati al kernel sono stati interpretati (evaluate command-line arguments). L’operazione è molto complessa e coinvolge tutti i sottosistemi, per questo viene divisa in procedure
più piccole. In particolare vengono inizializzati i seguenti componenti del
kernel:
• il gestore della memoria, MMU che inizializza la tabella di traduzione
da indirizzi fisici a indirizzi virtuali;
• il gestore del tempo, TIMER per la temporizzazione del sistema;
• lo schedulatore per la gestione dei processi;
• il gestore degli eventi, VIC per reagire ai numerosi eventi;
• il gestore degli interrupt;
139
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
L’operazione finale è generare il processo idle del kernel. Viene inoltre
avviato il processo init con il PID 1 il quale esegue le routine di inizializzazione dei vari sottosistemi e dopo avvia /sbin/init come primo processo
user space. Conclude l’inizializzazione del kernel la comparsa su console di
un login prompt che indica all’utente che il linux kernel è stato avviato e
pronto a ricevere comandi. Sia il codice platform-independent che il codice
Figura A.1: Diagramma di flusso per la funzione start_kernel
platform-dependent sono implementati in C o assembly.
A.2
Fase platform-dependent del processo di boot del kernel
La fase platform-dependent è la seconda fase del processo di boot del kernel che ha come scopo la descrizione e l’inizializzazione dell’hardware sottostante e specifico della propria architettura attraverso l’esecuzione del
codice platform-dependent. Questa fase si divide in tre passi:
1. registrazione dei device
2. registrazione dei driver
3. associazione tra driver e device
Il codice di piattaforma per Cartesio è situato principalmente nella directory /arch/arm/mach-cartesio. In questa directory chiave del Linux base
port di Cartesio, si trovano:
• drivers specifici della piattaforma Cartesio (clcd, gpio, sga, time, ecc.);
• codice di inizializzazione del system on chip e della board (core-sta206x.c
e board-evb206x.c) contenente la definizione della struttura delle periferiche presenti sul core e sulla board.
140
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
Altro codice di piattaforma è situato nella directory /drivers, nella quale
sono presenti i driver delle periferiche di Cartesio più standardizzate e appartenenti ai vari sottosistemi di driver del kernel come USB, MMC, ecc.
A.2.1
Registrazione dei device
I file core-sta206x e board-evb206x sono molto importanti in quanto definiscono le periferiche (device) del system on chip Cartesio e della board. Nell’inizializzazione della parte platform-dependent del kernel, le prime funzioni che vengono chiamate sono situate proprio in questi file e sono la
cartesio_core_init in core-sta206x mostrata nel codice A.1,
Codice A.1: Funzione cartesio_core_init del file core-sta206x
1
2
3
4
5
6
7
8
9
10
11
12
13
static void __init cartesio_core_init ( void )
{
int i ;
/* Detect product ID and core revision */
c a r t e s i o _ d e t e c t _ c o r e ();
/* Register core - specific AMBA devices */
for ( i = 0; i < ARRAY_SIZE ( amba_devices ); i ++)
a m b a _ d e v i c e _ r e g i s t e r ( amba_devices [ i ] , & iomem_resource );
/* Register core - specific platform devices */
p l a t f o r m _ a d d _ d e v i c e s ( core_devices , ARRAY_SIZE ( core_devices ));
/* Following devices can wakeup the system */
device_init_wakeup (& rtc_device . dev , true );
}
e la cartesio_board_init in board-evb206x mostrata nel codice A.2.
Codice A.2: Funzione cartesio_board_init del file board-evb206x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int __init cartesio_board_init ( void )
{
/* Enable GPIO alternate interfaces to connect board devices */
WARN_ON ( g p i o _ a l t _ i n t e r f a c e _ e n a b l e ( " UART_0 " ));
WARN_ON ( g p i o _ a l t _ i n t e r f a c e _ e n a b l e ( " UART_1 " ));
WARN_ON ( g p i o _ a l t _ i n t e r f a c e _ e n a b l e ( " MSP_2 " ));
WARN_ON ( g p i o _ a l t _ i n t e r f a c e _ e n a b l e ( " MSP_0_A_TX " ));
WARN_ON ( g p i o _ a l t _ i n t e r f a c e _ e n a b l e ( " SSP_0_B " ));
WARN_ON ( g p i o _ a l t _ i n t e r f a c e _ e n a b l e ( " FSMC_A " ));
/* Enable alternate interface to STLC2500 Bluetooth device */
# ifdef CONFIG_BT
WARN_ON ( g p i o _ a l t _ i n t e r f a c e _ e n a b l e ( " UART_2_B " ));
# endif
/* Raise VDDIO to 3.3 V for ethernet adapter */
smc91x_board_init ();
/* Register board - specific platform devices */
p l a t f o r m _ a d d _ d e v i c e s ( board_devices , ARRAY_SIZE ( board_devices ));
return 0;
}
141
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
Le funzioni di init mostrate sopra, hanno l’obiettivo di descrivere i device
presenti sulla piattaforma attraverso la fase di registrazione dei device.
Facciamo una breve descrizione delle tipologie di device esistenti sulla
piattaforma Cartesio. Un sistema embedded come un normale sistema è
costituito da un processore e altre periferiche che comunicano con esso. Il
mezzo per comunicare è rappresentato dai bus. Esistono diverse tipologie di
bus, quali PCI, PCI-Express, ISA per le macchine più datate, VESA per la
grafica, ecc. In Cartesio i bus utilizzati sono AMBA bus e PERIPHERAL
bus. I device che utilizzano il primo bus sono detti AMBA device invece quelli
che utilizzano il secondo bus sono detti PERIPHERAL device. Gli AMBA
device sono presenti solo all’ interno del system on chip e quindi le definizioni
di queste periferiche si trovano soltanto nel file core-sta206x. Al contrario
nel file board-evb206x si trovano solo definizioni di PERIPHERAL device.
Inoltre, in quest’ultimo file sono descritti due device che rappresentano altri
due tipi di bus, I2C e SPI, utilizzati con dispositivi esterni che hanno requisiti
di velocità nel trasferimento dati inferiori rispetto ai device che utilizzano
l’AMBA bus o il PERIPHERAL bus che al contrario sono molto performanti
e ideati per far interagire la cpu con controllori molto veloci.
Come si può notare dal codice A.1 e dal codice A.2, le funzioni di init per
registrare entrambe le tipologie di device AMBA e PERIPHERAL chiamano
le funzioni amba_device_register e platform_device_register (incapsulata in
platform_add_devices) rispettivamente. Queste funzioni sono definite nelle
librerie dei driver dei bus. Entrambi sono dei bus molto semplici che non
richiedono particolari operazioni di gestione e quindi anche i loro driver
sono semplici. La complessità di un driver dipende dalla complessità della
periferica. Ad esempio il driver del bus AMBA è costituito da un insieme di
funzioni per registrare sia i device che i driver e per eseguire l’associazione
device-driver.
Per comprendere in cosa consiste la registrazione di un device prendiamo
in considerazione l’esempio del SGA (Smart Graphic Accelerator) device
di Cartesio. Questo è un AMBA device e quindi è definito nel file coresta206x attraverso il consueto modo di descrizione di una periferica, ovvero
utilizzando un tipo strutturato struct. Il codice A.3 definisce l’SGA device.
Codice A.3: Definizione SGA device
1
2
3
4
5
6
7
8
9
10
11
static struct amba_device sga_device = {
. dev = {
. init_name = " sga " ,
. coherent_dma_mask = DMA_BIT_MASK (32) ,
},
. res = {
. start = CARTESIO_SGA_BASE ,
. end
= CARTESIO_SGA_BASE + SZ_2K - 1 ,
. flags = IORESOURCE_MEM ,
},
. dma_mask
= DMA_BIT_MASK (32) ,
142
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
. periphid
. irq
12
13
14
= 0 x002D6202 ,
= { IRQ_SGA , NO_IRQ } ,
};
La struct contiene i campi che descrivono la periferica, tra cui .init_name
che corrisponde al nome del device.
Il nostro sistema embedded è composto da elementi hardware che possono essere raggruppati in tre aree principali: cpu, memoria e periferiche.
Semplificando, ad un livello molto basso possiamo dire che l’interfaccia di
comunicazione tra cpu e periferiche è rappresentata dai registri di periferica.
Questi registri sono tanti contenenti diverse informazioni e sono utilizzati
per descrivere lo stato della periferica. Per permettere la comunicazione tra
cpu e device uno dei metodi utilizzati è il mapping delle periferiche in memoria. In memoria però vengono mappate anche altre informazioni del sitema
embedded creando in questo modo un’ organizzazione della memoria peculiare di quel sistema. Cartesio definisce una mappa di memoria statica con
la quale tutte le periferiche vengono mappate ad indirizzi fisici ben precisi,
e tramite questo meccanismo la cpu riesce ad accedere ai registri dei device.
In particolare l’indirizzo di un registro è ottenuto con IndirizzoRegistro =
Base Address + Offset dove Base Address è contenuto nel campo .start della
struct del device e rappresenta l’indirizzo di partenza per la periferica. Tra
i diversi registri, è presente anche il registro di identificazione del device che
contiene un ID (Identifier) fondamentale come vedremo nella fase di associazione tra device e driver. L’ID, ricavato consultando il datasheet, viene
inserito nel campo .periphid.
La fase di creazione del device non ha bisogno di accedere ai registri
della periferica mappata in memoria. L’ID non viene letto dalla mappa di
memoria ma è soltanto un valore inserito manualmente nella struct.
La fase di registrazione si occupa di creare in memoria le struct di tutti
gli AMBA e PERIPHERAL device contenute nei file core-sta206x e boardevb206x invocando le funzioni cartesio_core_init e cartesio_board_init. Al
termine di questa fase, saranno presenti in memoria le tabelle dei device
registrati, una tabella per gli AMBA device, una per i PERIPHERAL device
e tabelle per altre categorie di device (I2C e SPI). Queste tabelle non sono
altro che tabelle di puntatori alle struct create in memoria e sono gestite
interamente dal kernel permettendo allo sviluppatore di driver di ignorare i
dettagli.
A.2.2
Registrazione dei driver
La fase di registrazione dei driver nel kernel è più complicata rispetto a quella
dei device. Il processo ha il suo inizio nella fase di compilazione coinvolgendo
il linker.
Un driver è un codice scritto in linguaggio C che permette di gestire tutti
i dettagli di basso livello della periferica a cui è associato. La moltitudine
143
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
di periferiche esistenti ha portato alla creazione di driver molto differenti
tra loro, con diversi gradi di complessità. Ma ciò nonostante è possibile
comunque delineare una struttura tipica di un driver. Per studiare questa
struttura, esaminiamo il codice del device driver SGA di Cartesio.
In un driver vengono definite due interfacce di comunicazione: un punto
di ingresso (entry-point) e un punto d’uscita (exit-point), mostrate nel codice
A.4.
Codice A.4: Entry-point e exit-point del driver SGA
1
2
3
4
5
module_init ( cartesio_sga_init );
module_exit ( cartesio_sga_exit );
MODULE_AUTHOR ( " STMicroelectronics " );
MODULE_DESCRIPTION ( " Cartesio Plus Smart Graphics Accelerator driver " );
MODULE_LICENSE ( " GPL " );
L’entry-point è rappresentato dalla funzione cartesio_sga_init e l’exit-point
dalla funzione cartesio_sga_exit. Dal codice A.4 si può notare come ogni
riga inizi con la parola module. Questa parola chiave indica che le righe
sono tutte tradotte in direttive al linker. Alcune forniscono informazioni
sul driver, come l’autore, il tipo di driver e la licenza. Le altre permettono
al linker di capire qual’ è l’entry-point e qual’ è l’exit-point del driver che
utilizza in fase di linking nella quale viene creato il file binario del kernel
costituito da un settore per il codice, uno per i dati, un settore INIT e un
settore EXIT. Questi ultimi due settori sono costituiti da:
• INIT: contiene la tabella di puntatori a funzioni nella quale il linker raggruppa tutti i puntatori alle funzioni che trova nelle direttive
module_init dei driver.
• EXIT: contiene la tabella di puntatori a funzioni nella quale il linker raggruppa tutti i puntatori alle funzioni che trova nelle direttive
module_exit dei driver.
Nella fase di boot del kernel le tabelle di puntatori di INIT e EXIT vengono lette e per ogni puntatore trovato viene chiamata la rispettiva funzione
di entry-point o exit-point. La funzione di entry-point del driver SGA di
Cartesio è mostrata nel codice A.5.
Codice A.5: Funzione di entry-point del driver SGA
1
2
3
4
static int __init cartesio_sga_init ( void )
{
return a m b a _ d r i v e r _ r e g i s t e r (& cartesio_sga_driver );
}
La cartesio_sga_init chiama la funzione di registrazione del driver amba_driver_register definita nella libreria di funzione del driver del bus AMBA.
144
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
Figura A.2: Struttura del file binario del kernel
La funzione di exit-point del driver SGA di Cartesio è mostrata nel codice
A.6.
Codice A.6: Funzione di exit-point del driver SGA
1
2
3
4
static void __exit cartesio_sga_exit ( void )
{
a m b a _ d r i v e r _ u n r e g i s t e r (& cartesio_sga_driver );
}
La cartesio_sga_exit chiama la funzione per eliminare la registrazione del
driver amba_driver_unregister definita nella libreria di funzione del driver
del bus AMBA.
Entrambe le funzioni amba_driver_register e amba_driver_unregister,
hanno come argomento il puntatore ad una struttura AMBA driver, mostrata nel codice A.7.
145
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
Codice A.7: Definizione del driver SGA
1
2
3
4
5
6
7
8
9
10
11
static struct amba_driver
. drv = {
. name
=
. owner =
},
. probe
=
. remove
=
. suspend
=
. resume
=
. id_table
=
}
cartesio_sga_driver = {
" cartesio - sga " ,
THIS_MODULE ,
cartesio_sga_probe ,
__devexit_p ( cartesio_sga_remove ) ,
cartesio_sga_suspend ,
cartesio_sga_resume ,
cartesio_sga_ids ,
Questa struct, come per i device, è la struttura che definisce un driver, in
questo caso un driver SGA di tipo AMBA. Le informazioni contenute in
questa descrizione minima del driver sono:
• il nome del driver: .name = ”cartesio-sga”;
• la funzione di init chiamata dopo l’associazione del driver con il device,
effettuata dal kernel: .probe = cartesio_sga_probe;
• la funzione di de-init: .remove = __devexit_p(cartesio_sga_remove);
• le funzioni per il power management del device associato al driver:
sospensione, .suspend = cartesio_sga_suspend e ritorno dalla sospensione, .resume = cartesio_sga_resume.
Una nota a parte merita la funzione associata a .id_table, ovvero cartesio_sga_ids, mostrata nel codice A.8.
Codice A.8: Struct ID del driver SGA
1
2
3
4
5
6
7
static struct amba_id cartesio_sga_ids [] __initdata = {
{
. id
= 0 x002D6202 ,
. mask
= 0 x00FFFFFF ,
},
{ 0, 0 },
}
In realtà essa non è una funzione ma una struttura che contiene gli identificatori (ID) dei device che possono essere associati al driver. La politica di
utilizzare una struttura separata nasce dal fatto che un driver può gestire
più device e quindi gli ID possono essere molteplici e l’unico modo per memorizzarli è un struttura a record. In questo modo il driver viene registrato
una sola volta nel kernel e può essere associato a diversi device, evitando
registrazione multiple per ogni periferica che può gestire. Naturalmente ogni
device anche se dello stesso tipo, ad esempio gpio, è diverso dagli altri per
146
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
via di alcuni dettagli quali indirizzo fisico, interrupt, ecc. e quindi il driver
viene personalizzato per ogni device con una struttura di stato.
Con la fase di registrazione dei driver, come nel caso dei device, vengono
create in memoria le tabelle per le diverse tipologie di driver, AMBA, PERIPHERAL, ecc. Queste tabelle sono tabelle di puntatori a strutture di
descrizione dei driver, come struct amba_driver cartesio_sga_driver,
presenti in memoria.
A.2.3
Associazione tra driver e device
Dopo le fasi di registrazione dei device e dei driver, il kernel ha in memoria
una tabella dei diversi tipi di device e una tabella dei diversi tipi di driver
del sistema. Consideriamo l’esempio delle tabelle di tutti gli AMBA device
e di tutti gli AMBA driver. A questo punto il kernel inizia a eseguire un
controllo incrociato tra le tabelle AMBA, con l’intento di associare i driver
ai device in modo da poter gestire la periferica con quel driver. Appena un
driver non associato viene rilevato vengono eseguiti i seguenti passi:
• Lettura ID device: scandisce la tabella dei device AMBA e per
ognuno legge il contenuto del campo .periphid della relativa struct,
• Lettura tabella ID device del driver: scandisce la tabella degli
ID dei device rappresentata dalla struct cartesio_sga_ids contenuta
nella struttura cartesio_sga_driver,
• Confronto e associazione: se viene identificata una corrispondenza
fra gli ID della lista e gli ID dei device inizia l’associazione:
– Creazione istanza device: il kernel alloca in memoria una
struttura di tipo device che rappresenta una generica periferica,
– Creazione istanza Amba device: alloca una struttura di tipo
Amba device, specializzazione di device, che rappresenta una generica periferica Amba,
– Inizializzazione Amba device: l’amba device creato viene inizializzato con le informazioni peculiari del particolare device (ID)
e con informazioni di carattere privato al kernel,
– Esecuzione funzione probe: esegue la funzione probe passando sia il puntatore all’AMBA device appena creato sia l’ID del
device. La probe inizializza il driver e il device: carica in memoria una struttura che rappresenta l’istanza del device che il driver
deve gestire. Questa struttura è detta device context. A questo
punto associa all’istanza device il device context finalizzando l’associazione.
147
CAPITOLO A. Processo di boot del kernel. Fase platform-dependent
La funzione probe inoltre resetta il device, inserendo nei registri dei valori
che definiscono uno stato noto, e abilita la periferica alla generazione degli
interrupt. Gli eventi che il device può generare vengono associati ad una
routine di interrupt. A questo punto il driver funziona in modo passivo,
restando in attesa di eventi dal device che quando si verificano attivano la
routine che chiama il driver per soddisfare le richieste.
Figura A.3: Associazione: confronto degli identificatori
148
Bibliografia
[Ber01] A. Berger. Embedded Systems Design: An Introduction to
Processes, Tools and Techniques. Elsevier Science, 2001.
[BF07] C. Brandolese and W. Fornaciari. Sistemi Embedded - sviluppo
hardware e software per sistemi dedicati. Pearson Paravia Bruno
Mondadori, 2007.
[Bir09] T. Bird. Measuring function duration with ftrace. In Proceedings
of Embedded Linux Conference, CELF09, 2009.
[BJJ+ 07] C. Baek, J. Jang, G. Jung, K. Choi, and S. Park. A case study of
black-box testing for embedded software using test automation
tool. In Journal of Computer Science, 2007.
[Bro02] B. Broekman. Testing Embedded Software. Addison Wesley
Longman Publishing, 2002.
[CCN+ 08] G. Carrozza, D. Cotroneo, R. Natella, A. Pecchia, and S. Russo.
An experiment in memory leak analysis with a mission-critical
middleware for air traffic control. In IEEE International Conference on Software Reliability Engineering Workshops, ISSRE08,
November 2008.
[CRKH05] J. Corbet, A. Rubini, and G. Kroah-Hartman. Linux Device
Drivers (Third Edition). O’Reilly, 2005.
[Dik00] J. Dike. A user-mode port of the linux kernel. In Proceedings of
the 4th annual Linux Showcase Conference, ALS00, 2000.
[EGW06] J. Engblom, G. Girard, and B. Werner. Testing embedded software using simulated hardware. In Embedded Real Time Software
and Systems Conference, ERTS06, 2006.
149
BIBLIOGRAFIA
[GR08] X. Guoqing and A. Rountev. Precise memory leak detection for
java software using container profiling. In ACM/IEEE 30th International Conference on Software Engineering, ICSE08, May
2008.
[Gra86] J. Gray. Why do computers stop and what can be done about
it? In 5th Symposium on Reliability in Distributed Software and
Database Systems, January 1986.
[GVVT98] S. Garg, A. Van Moorsel, K. Vaidyanathan, and K.S. Trivedi.
A methodology for detection and estimation of software aging.
In International Symposium on Software Reliability Engineering,
November 1998.
[HKKF95] Y. Huang, C. Kintala, N. Kolettis, and N.D. Fulton. Software
rejuvenation: analysis, module and applications. In TwentyFifth International Symposium on Fault-Tolerant Computing,
FTCS95, June 1995.
[KBE06] M. Karlesky, W. Bereza, and C. Erickson. Effective test driven development for embedded software. In IEEE International
Conference on Electro/information Technology, 2006.
[KKL05] B. Kang, Y. Kwon, and R. Lee. A design and test technique
for embedded software. In Proceedings of the Third ACIS Int’l
Conference on Software Engineering Research, Management and
Applications, SERA05. IEEE Computer Society, 2005.
[KL93] H. Koehnemann and T. Lindquist. Towards target-level testing
and debugging tools for embedded software. In Proceedings of
the conference on TRI-Ada93. ACM, 1993.
[Len01] A. Lennon. Embedded linux. In Arcom Control System, IEEE
REVIEW, May 2001.
[LHRF03] P. Larson, N. Hinds, R. Ravindran, and H. Franke. Improving
the linux test project with kernel code coverage analysis. In
Proceedings of the Ottawa Linux Symposium, July 2003.
[LHT09] H. Lyytinen, K. Haataja, and P. Toivanen. Designing and implementing an embedded linux for limited resource devices. In
Department of Computer Science, University of Kuopio, Finland,
IEEE DOI 10.1109/ICN.2009.27, 2009.
[Mau08] W. Mauerer. Professional Linux Kernel Architecture. Wiley
India Pvt, 2008.
150
BIBLIOGRAFIA
[PY08] M. Pezzè and M. Young. Software Testing and Analysis: Process,
Principles and Techniques. Wiley, 2008.
[QWS08] N. Qinqin, S. Weizhen, and M. Sen. Memory leak detection in
sun solaris os. In International Symposium on Computer Science
and Computational Technology, ISCSCT08, December 2008.
[QZ09] H. Qian and C. Zheng. A embedded software testing process model. In International Conference on Computational
Intelligence and Software Engineering, CiSE09, December 2009.
[SBL05] M. Swift, B. Bershad, and H. Levy. Improving the reliability of
commodity operating systems. In Proceedings of the nineteenth
ACM symposium on operating systems principles, 2005.
[SC02] A. Sung and B. Choi. An interaction testing technique between hardware and software in embedded systems. In Software
Engineering Conference, 2002.
[SKCL08] J. Seo, Y. Ki, B. Choi, and K. La. Which spot should i test
for effective embedded software testing? In Second International Conference on Secure System Integration and Reliability
Improvement, SSIRI08, 2008.
[TVG06] T. Tsai, K. Vaidyanathan, and K. Gross. Low-overhead run-time
memory leak detection and recovery. In Pacific Rim International Symposium on Dependable Computing, PRDC06, December
2006.
[VT01] K. Vaidyanathan and K.S. Trivedi. Extended classification of
software faults based on aging. In 12th International Symposium
on Software Reliability Engineering, November 2001.
[Wan04] L. Wang. Issues on software testing for safety-critical real-time
automation systems. In The 23rd Digital Avionics Systems
Conference, DASC04, 2004.
[Yag03] K. Yaghmour. Building Embedded Linux System. O’Reilly, 2003.
[YLW06] Q. Yang, J.J. Li, and D. Weiss. A survey of coverage based
testing tools. In Proceedings of the International Workshop on
Automation of Software Test, AST06, 2006.
151
Sitografia
[1] Coverity static analysis:
http://www.coverity.com/products/static-analysis.html
[2] IBM rational purify:
http://www-01.ibm.com/software/awdtools/purify/unix/
[3] Parasoft Insure++:
http://www.parasoft.com/jsp/products/insure.jsp?itemId=63
[4] Valgrind:
http://valgrind.org/
[5] LeakTracer:
http://www.andreasen.org/LeakTracer/
[6] Memwatch, mtrace, dmalloc:
http://www.linuxjournal.com/article/6059
[7] Kernel memory leak detector (kmemleak):
http://procode.org/kmemleak/
[8] Code Coverage Analysis:
http://www.bullseye.com/coverage.html
[9] Lcov Toolkit:
http://ltp.sourceforge.net/coverage/lcov.php
[10] Bullseye Coverage:
http://www.bullseye.com/index.html
[11] CodeTest:
http://www.metrowerks.com/MW/Develop/AMC/CodeTEST/default.htm
152
SITOGRAFIA
[12] Gcov User Space:
http://gcc.gnu.org/onlinedocs/gcc/Gcov.html
[13] Intel Code Coverage Tool:
http://www.intel.com/cd/software/products/asmo-na/eng/219794.htm
[14] Generic Coverage Tool:
http://www.cs.loyola.edu/~kbg/se-tools/gct.html
[15] Coverage Meter:
http://www.coveragemeter.com/
[16] BuildRoot:
http://buildroot.uclibc.org/
[17] Ftrace Function Graph ARM:
http://elinux.org/Ftrace_Function_Graph_ARM
[18] LTP - Linux Test Project:
http://ltp.sourceforge.net/
[19] CodeSourcery Sourcery G++:
http://www.codesourcery.com/sgpp
[20] Bonnie, Benchmark for Unix file systems, Copyright Tim Bray 19901996:
http://www.textuality.com/bonnie
[21] Bonnie++, Improved C++ Version of Bonnie, Copyright Russell Coker
2000:
http://www.coker.com.au/bonnie++
[22] IOzone File System Benchmark:
http://www.iozone.org/
[23] Tiobench, Threaded I/O benchmark, Copyright Mika Kuoppala 19992000:
http://sourceforge.net/projects/tiobench
153
Elenco delle figure
2.1
2.2
2.3
2.4
2.5
Incidenza dei sistemi embedded nel costo finale dei prodotti .
Mercato globale dei sistemi embedded in miliardi di dollari .
Suddivisione del fatturato del software embedded per categoria
Utilizzo recente dei sistemi operativi per sistemi embedded .
Architettura generale di un sistema Linux Embedded . . . . .
8
11
11
12
17
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
3.10
Sistema embedded: schema di riferimento . . .
Transizione dal sistema simulato a quello reale
Simulazione one-way . . . . . . . . . . . . . . .
Simulazione con feedback . . . . . . . . . . . .
Prototipazione rapida . . . . . . . . . . . . . .
V-Model . . . . . . . . . . . . . . . . . . . . . .
V-Model Prototipazione . . . . . . . . . . . . .
Livelli software di un sistema embedded . . . .
Device driver in Linux secondo [CRKH05] . . .
Diffusione dei bug nelle sezioni del kernel . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
21
22
22
23
23
27
28
33
37
41
4.1
4.2
4.3
4.4
4.5
Diagramma a blocchi Cartesio plus . . . . . . . . . . .
Processo di Testing adottato in STM per il BSP Linux
Esecuzione dei test per il touchpanel . . . . . . . . . .
Esecuzione dei test per la keyboard . . . . . . . . . . .
Esempio di report del testing del BSP Linux Cartesio
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
45
50
53
53
54
5.1
5.2
5.3
Configurazione di Gcov Kernel . . . . . . . . . . . . . . . . .
Esempio di risultati con gcov e lcov . . . . . . . . . . . . . . .
Esempio di risultati di dettaglio con gcov e lcov . . . . . . . .
69
73
74
6.1
Configurazione del file system debugfs . . . . . . . . . . . . .
79
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
154
ELENCO DELLE FIGURE
6.2
6.3
6.4
6.5
6.6
6.7
6.8
6.9
6.10
6.11
6.12
Instrumentazione con e senza l’opzione -pg . . . . . . . . .
Return trampoline per il tracing dell’exit-point . . . . . .
Function Graph Tracer nell’architettura ARM . . . . . . .
Applicazione modifiche per Function Duration Tracer . .
Applicazione modifiche per __gnu_mcount_mc . . . . .
Aggiunta dello strumento FDD . . . . . . . . . . . . . . .
Struttura del sottosistema di time management del kernel
Function Duration Tracer nel BSP Linux Cartesio . . . .
Risultati analisi FDD ordinati in base al campo Time . .
Risultati analisi FDD ordinati in base al campo Average .
Grafo delle chiamate della pl011_init . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
81
83
84
85
85
86
88
92
96
97
98
7.1
7.2
7.3
7.4
7.5
7.6
Classificazione dei difetti software . . . . . . . . . . . . . . . . 102
Configurazione kernel memory leak detector . . . . . . . . . . 110
Associazione tra device e driver . . . . . . . . . . . . . . . . . 113
Kmemleak: rilevazione errore deallocazione struttura di stato 118
Kmemleak: driver non compilato come modulo . . . . . . . . 119
Kmemleak: mancanza salvataggio handle della risorsa allocata 120
8.1
8.2
8.3
Prestazioni dell’operazione write . . . . . . . . . . . . . . . . 130
Prestazioni dell’operazione read . . . . . . . . . . . . . . . . . 132
Operazione read senza l’accelerazione delle cache . . . . . . . 134
A.1 Diagramma di flusso per la funzione start_kernel . . . . . . . 140
A.2 Struttura del file binario del kernel . . . . . . . . . . . . . . . 145
A.3 Associazione: confronto degli identificatori . . . . . . . . . . . 148
155
Elenco delle tabelle
3.1
3.2
Caratteristiche degli approcci al testing per sistemi embedded 26
Tecniche di testing nell’ambiente target . . . . . . . . . . . . 31
4.1
Dispositivi con cui il SoC Cartesio interagisce . . . . . . . . .
46
5.1
5.2
Confronto degli strumenti per l’analisi di code coverage . . .
Sezione ctors di vmlinux per l’architettura ARM e x86 . . . .
63
72
7.1
Confronto degli strumenti per la rilevazione di memory leak . 108
156
Elenco dei Codici
5.1
5.2
5.3
6.1
6.2
6.3
7.1
7.2
7.3
7.4
7.5
7.6
7.7
7.8
7.9
A.1
A.2
A.3
A.4
A.5
A.6
A.7
A.8
Debolezza dello statement coverage . . . . . . . . . . . . . . .
Debolezza del branch coverage . . . . . . . . . . . . . . . . .
Instrumentazione del codice per l’analisi di copertura . . . . .
Funzione sched_clock . . . . . . . . . . . . . . . . . . . . . .
Struttura clock source . . . . . . . . . . . . . . . . . . . . . .
API per la lettura delle informazioni temporali dall’hardware
Primo esempio codice C. . . . . . . . . . . . . . . . . . . . . .
Secondo esempio codice C. . . . . . . . . . . . . . . . . . . . .
Terzo esempio codice C. . . . . . . . . . . . . . . . . . . . . .
Quarto esempio codice C. . . . . . . . . . . . . . . . . . . . .
Struct device . . . . . . . . . . . . . . . . . . . . . . . . . . .
Struct amba device . . . . . . . . . . . . . . . . . . . . . . . .
Funzione probe . . . . . . . . . . . . . . . . . . . . . . . . . .
Struttura di stato driver Cartesio SGA . . . . . . . . . . . . .
Funzione remove . . . . . . . . . . . . . . . . . . . . . . . . .
Funzione cartesio_core_init del file core-sta206x . . . . . . .
Funzione cartesio_board_init del file board-evb206x . . . . .
Definizione SGA device . . . . . . . . . . . . . . . . . . . . .
Entry-point e exit-point del driver SGA . . . . . . . . . . . .
Funzione di entry-point del driver SGA . . . . . . . . . . . .
Funzione di exit-point del driver SGA . . . . . . . . . . . . .
Definizione del driver SGA . . . . . . . . . . . . . . . . . . . .
Struct ID del driver SGA . . . . . . . . . . . . . . . . . . . .
61
62
65
89
89
90
104
105
105
105
114
114
115
115
116
141
141
142
144
144
145
146
146
157