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