Scuola Politecnica e delle Scienze di Base Corso di Laurea in Ingegneria Informatica Elaborato finale in Sistemi Operativi Real Time. Soluzioni per lo sviluppo di applicazioni real time su reti di sensori senza filo. Anno Accademico 2014/2015 Candidato: Salvatore Barone matr. N46000121 Indice. Introduzione 1 Capitolo1: Generalità 1.1. Sorgenti di imprevedibilità 1.2. I nodi della rete; 1.3. Sviluppo di un RTOS. 2 2 5 8 Capitolo 2: Sistemi operativi per reti si sensori senza filo. 2.1. TinyOs. 2.1.1. Architettura del sistema. 2.1.2. Modello di programmazione. 2.1.3. Modello esecutivo e concorrenza. 2.1.4. Supporto ad applicazioni real-time. 11 11 11 12 15 16 2.2 Contiki 2.2.1 Modello esecutivo. 2.2.2. Protothreads e multithreading. 2.2.3. Supporto Real-Time. 16 17 19 23 2.3 Nano-RK. 2.3.1. Architettura del sistema. 2.3.2. Esecuzione di task Real-Time. 2.3.3. Networking. 24 25 26 27 Capitolo 3: Aggiungere prevedibilità ai sistemi non real-time. 3.1. Aggiungere supporto real-time a TinyOS. 29 29 Capitolo 4: Conclusioni. 34 Riferimenti. 35 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. Introduzione. Nel presente lavoro verranno introdotti i concetti relativi alle reti di sensori senza filo ed ai sistemi operativi utilizzati per la loro gestione. L' elaborato è diviso in quattro capitoli. Nel primo capitolo verranno illustrate alcune generalità riguardo le reti di sensori senza fili ed i sistemi operativi impiegati per la loro gestione. Verranno evidenziate le motivazioni che possono portare all'utilizzo di una rete di sensori senza fili piuttosto che una rete di sensori tradizionali, i vantaggi e gli svantaggi del loro utilizzo, le problematiche che conducono alla necessità di un sistema operativo specifico per reti di sensori wireless. Nel secondo capitolo verranno analizzati i sistemi operativi per reti di sensori senza fili più importanti tra quelli attualmente disponibili. Verrà posta particolare attenzione sulla loro architettura, sul modello di programmazione, sul modello di esecuzione, sulla loro capacità di poter essere utilizzati in applicazioni real-time. Nel terzo capitolo verranno proposte delle tecniche e delle metodologie di progettazione di applicazioni volte ad ottenere un comportamento prevedibile del sistema laddove il sistema operativo non presenti nativamente il supporto ad applicazioni di tipo real-time. Infine, nel quarto capitolo, verranno tratte le conclusioni a cui è possibile giungere dopo aver analizzato i sistemi operativi per reti di sensori senza fili più importanti tra quelli attualmente disponibili. 1 Salvatore Barone Capitolo 1: Generalità. Nell'accezione più generica del termine, un Sistema Operativo è il software che gestisce le risorse hardware di un sistema di calcolo. All'utente del sistema, che sia esso un umano o un' altro sistema elettronico, le attività del Sistema Operativo possono essere totalmente nascoste. Il sistema operativo svolge, dunque, il ruolo dell'intermediario tra l'utilizzatore del calcolatore ed il calcolatore stesso. Quando si parla di sistema real-time, si intende un sistema per il quale la correttezza del funzionamento non dipende solamente dalla correttezza dei risultati di elaborazione, ma anche dal tempo impiegato per la loro produzione. In un sistema real-time, infatti, ad ogni flusso elaborativo è associata una scadenza temporale entro la quale il task deve essere completato. Questa definizione, molto semplificata, ci consente di porre l'accendo su uno degli aspetti fondamentali di un sistema real-time, ossia la prevedibilità. Per prevedibilità di un sistema real-time si intende la capacità di poter determinare in anticipo, ad esempio nell'istante di creazione, se uno o più task potranno essere completati rispettando i vincoli temporali. Se gli effetti di uno sforamento dei vincoli temporali si traducono in effetti catastrofici sul sistema si parla di sistema hard real-time. Se, invece, gli effetti di uno sforamento dei vincoli temporali si traducono semplicemente nel degrado delle prestazioni del sistema, si parla di sistema soft real-time. Analizzando quanto appena detto saremmo portati a pensare che quanto più il sistema sia veloce tanto più sia semplice soddisfare i requisiti temporali. Il compito di un sistema operativo real-time, però, non è quello di soddisfare i requisiti temporali di un singolo processo, ma quello di soddisfare i requisiti individuali di ogni singolo processo e i requisiti complessivi di un insieme di processi. 1.1. Sorgenti di imprevedibilità. Le cause di imprevedibilità in un sistema sono molteplici e possono avere sia natura hardware che software. Le fonti di imprevedibilità hardware sono il meccanismo del DMA e l'utilizzo della cache memory mentre quelli di natura 2 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. software comprendono il meccanismo di gestione delle interruzioni, la gestione della memoria ed il modello di esecuzione e concorrenza dei task. Il DMA (Direct Memory Access) è un meccanismo che permette di trasferire dati da un dispositivo alla memoria centrale senza gravare sul processore. Essendo il bus di memoria condiviso tra DMA e processore, quest'ultimo potrebbe restare bloccato a causa di operazioni DMA, per cui per ottenere prevedibilità si utilizza un accesso alla memoria a multiplazione di tempo: un ciclo di accesso alla memoria è diviso in due parti di cui una assegnata al processore e l'altra assegnata al DMA. In questo modo gli accessi sono temporalmente disgiunti e non vi è possibilità di bloccaggi. La cache memory è una memoria veloce che si frappone tra processore e memoria centrale. L'origine della sua introduzione sta nel fatto che l'impronta di un processo è, spesso, allocata in regioni di memoria contigue. Per ridurre il numero di accessi alla memoria centrale, dunque, si effettua una copia dell'immagine del processo nella cache al fine di velocizzare l'esecuzione. Le dimensioni della cache sono, tuttavia, di diversi ordini di grandezza inferiori rispetto alla memoria centrale per cui l'immagine di un processo potrebbe sforare la sua dimensione. Ogni volta che il processore cerca di accedere alla cache per ottenere una parte di programma che in realtà non vi risiede si verifica un cache-fault. Il cache-fault è l'origine dell'aleatorietà introdotta dall'utilizzo della cache memory. Per aggirare il problema ed introdurre prevedibilità le strade percorribili sono due: supporre che si verifichi un cache-fault ad ogni accesso in cache oppure lavorare con processori con cache disabilitata o senza cache. Una fonte importante di non determinismo di natura software è il meccanismo di gestione delle interruzioni. Le interruzioni generate da dispositivi periferici rappresentano un grosso problema in quanto potrebbero creare ritardi indeterminati nell'esecuzione di processi real-time. Per ovviare a tale problema esistono tre diverse soluzioni: 1. polling a livello applicativo: prevede di disabilitare tutte le interruzioni ad eccezione di quella del timer – necessaria al funzionamento del sistema – e gestire le interruzioni generate dai dispositivi attraverso dei task che effettuano polling direttamente sui registri hardware dei dispositivi. Questo approccio ha una bassa efficienza dovuta al fatto che i processi dediti alla gestione dei dispositivi rimangono in attesa attiva. 3 Salvatore Barone 2. polling a livello di sistema: rispetto alla soluzione precedente, prevede sempre di eseguire ad interruzioni disabilitate, eccezion fatta per l'interruzione generata dal timer, e di affidare la gestione degli interrupt esterni a routine di sistema che interrogano periodicamente direttamente i registri hardware dei dispositivi. Questa soluzione è leggermente più efficiente della precedente ma presenta lo svantaggio di dover riscrivere parte del kernel qualora uno dei dispositivi sia modificato. 3. driver minimale avviato dall'interrupt handler: è la più efficiente, è eseguire il sistema ad interruzioni abilitate ed associare a ciascun interrupt una routine che altro non fa che richiamare un task per gestire l'interrupt che si è manifestato. Il driver che parte in corrispondenza di un evento, quindi, non gestisce direttamente l'evento ma attiva solamente un processo applicativo o di sistema dedicato alla sua gestione. Per quanto concerne la gestione della memoria le tecniche di paginazione a domanda non sono assolutamente adatte ad un sistema real-time in quanto introducono ritardi in corrispondenza dei page-fault che minano la prevedibilità del sistema. Tipicamente viene adottata la segmentazione o il partizionamento statico della memoria in modo da introdurre prevedibilità nel sistema poiché, in generale, la preallocazione aumenta le prestazioni del sistema anche se ne riduce la flessibilità. In merito al modello esecutivo dei task e la concorrenza, i fattori che incidono negativamente sulla prevedibilità del sistema sono legati soprattutto all'utilizzo di risorse condivise. I meccanismi di sincronizzazione classici, come semafori e monitor, possono provocare un fenomeno detto inversione di priorità. Questo fenomeno si manifesta quando un processo ad alta priorità viene bloccato da un processo a bassa priorità con cui condivide risorse. Il non-determinismo, in questi casi, è molto difficile da evitare se non con l'implementazione, nel sistema operativo, di un protocollo per l'accesso a risorse condivise che sia in grado di prevedere i tempi di bloccaggio dei processi. Protocolli normalmente messi a disposizione dai sistemi operativi più evoluti sono priority inheritance e priority ceiling. Per una loro descrizione formale si rimanda a [1]. 4 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. È chiaro che senza una adeguata metodologia di sviluppo ed un'attenta analisi è impossibile determinare la prevedibilità di un sistema real-time basandosi solamente sulle capacità elaborative del sistema. Una rete di sensori senza filo è composta da un numero di sensori spesso molto elevato rispetto ad una rete di sensori tradizionali. Le ragioni per cui si adoperati una rete di sensori senza fili possono essere molteplici, tra cui vi sono l'impossibilità di cablare una rete di sensori tradizionali a causa del numero dei sensori stessi, continue variazioni della topologia della rete, caratteristiche ambientali che potrebbero rendere difficoltoso il posizionamento dei sensori (aree colpite da eventi catastrofici, campi di battaglia). Sia per le situazioni ambientali difficili che per l'esaurirsi della batteria, la rete di sensori tende a perdere molto rapidamente nodi. Il fault-tolerance assume, quindi, un ruolo importante nell'ambito delle reti di sensori wireless. In [3] è stato stimato che la solidità della rete rispetto ai guasti e alla perdita di nodi può essere modellata usando la distribuzione di Poisson. La funzione Rk(t) esprime la probabilità di non avere guasti nell'intervallo di tempo (0, t) −λ k t Rk (t ) = e In tale espressione λk rappresenta il tasso di errore del nodo k nel periodo (0, t). 1.2. Nodi della rete. I sensori di una rete, anche detti motes, costituiscono la rete di acquisizione delle informazioni. Tipicamente i motes si occupano del campionamento di una o più grandezze di interesse attraverso dei sensori, di effettuare piccole elaborazioni sui dati raccolti, di eseguire algoritmi di routing per costituire l'infrastruttura di rete con la quale i nodi potranno partecipare all'elaborazione distribuita dei dati. Molte di queste operazioni possono avere dei vincoli di tipo real-time. 5 Salvatore Barone Lo schema a blocchi di un mote è il seguente Esso è, essenzialmente composto da una batteria, un microcontrollore, una memoria esterna (non sempre presente), un blocco rice-trasmettitore, un convertitore A/D ed uno o più sensori. Tutto l'apparato elettrico ed elettronico del mote è alimentato da una Power Source che, il più delle volte, è una semplice batteria. In alcuni scenari applicativi la sostituzione della Power Source, o la sua ricarica, è impossibile, per cui diventa importante tenere in considerazione l'efficienza energetica degli apparati elettronici del mote, il che si traduce in molti vincoli che riguardano sia l' hardware che il software del mote: un hardware efficiente dal punto di vista dell'energia è, spesso, poco dotato in termini di prestazioni, il che richiede una attenta progettazione delle componenti software. Il microcontrollore controlla il funzionamento delle restanti parti del mote, preleva i dati provenienti dai sensori, effettua piccole elaborazioni su di esse, cura la comunicazione con altri nodi della rete. I dispositivi usati sono molto poco dotati in capacità di calcolo e memoria rispetto ai microcontrollori general-purpose, questo perché si tende a preferire economicità di esercizio ed efficienza energetica alle prestazioni. Spesso al microcontrollore viene affiancato un banco di memoria di pochi kilobyte dove poter immagazzinare i dati campionati o i risultati delle piccole elaborazioni eseguite qualora la memoria interna dello stesso non dovesse essere sufficiente. Diverse sono le alternative per quanto riguarda la tecnica trasmissiva utilizzata dai motes: si va dall'utilizzo di segnali ottici o infrarossi, al Bluetooth o alla rice6 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. trasmissione RF su banda ISM. Essendo che la trasmissione ottica o infrarossa richiede l'esistenza di un canale fisico sgombro da ostacoli tra trasmettitore e ricevitore, la maggioranza dei motes utilizza segnali radio nelle bande di frequenza ISM. Per ridurre i consumi energetici dovuti alla necessità di trasmettere a lunga distanza, i nodi utilizzano una comunicazione di natura multi-hop, scambiandosi informazioni tra loro per realizzare, in modo automatico ed autonomo, una rete attraverso la quale ciascun nodo possa raggiungere il “nodo principale”, detta anche base-station o stazione base, od un qualsiasi altro nodo della rete. La trasmissione multi-hop tra nodi richiede che i nodi siano a conoscenza del loro stato energetico. Si pensi ad un nodo la cui batteria sia quasi scarica: esso informerà i nodi vicini in modo che essi non gli inviino dati da reindirizzare verso altri nodi. Molti dei motes di ultima generazione, chiamati WMSN (Wireless Multimedia Sensor Network), includono sensori multimediali come videocamere o microfoni che li rendono capaci di catturare e trasmettere stream audio e video. Essi rimangono, tuttavia, poco dotati per quanto riguarda la capacità computazionale e la dotazione di memoria per cui il protocollo di trasmissione riveste una particolare importanza per quanto riguarda la funzionalità della rete. La scarsità di risorse e la natura wireless del collegamento esistente tra i nodi della rete, rendono lo stack TCP/IP tradizionale inadatto all'utilizzo nell'ambito delle reti di sensori senza fili. La complessità del sistema presentato richiede un software che gestisca ciascuno dei mote della rete di sensori in modo che i vari task (costruzione dell'infrastruttura di rete ed aggiornamento, acquisizione dei dati, elaborazione, trasmissione dei risultati e routing di dati provenienti da altri mote) possano essere eseguiti in sicurezza rispettando eventuali scadenze temporali. Diventa, così, necessario l'utilizzo di un sistema operativo specificamente progettato per poter essere utilizzato nell'ambito delle reti di sensori wireless. Un tale sistema viene indicato con l'acronimo WSN-OS. 7 Salvatore Barone 1.3. Sviluppo di un RTOS per WSN. Ricapitolando, i fattori che incidono maggiormente sullo sviluppo di un sistema operativo per reti di sensori senza fili sono: • topologia di rete molto dinamica a causa della “morte” di alcuni dei sensori dovuta alle condizioni ambientali o all'esaurimento delle batterie; • impossibilità di rimpiazzare i nodi morti; • risorse computazionali scarse. Per questi motivi lo sviluppo di un sistema operativo per reti di sensori senza filo si differenzia in maniera netta dallo sviluppo di un sistema operativo tradizionale. Le seguenti caratteristiche di un sistema operativo sono quelle che, nell'ambito delle reti di sensori senza fili, richiedono maggiore attenzione: • Architettura: l'organizzazione del sistema incide sulla dimensione in memoria dell'immagine del sistema operativo. Un'architettura monolitica, in cui i diversi servizi forniti dal sistema operativo vengono implementati separatamente a livello kernel e forniti alle applicazioni attraverso delle interfacce, consente di ottenere immagini di memoria molto piccole. Lo svantaggio principale può essere legato alla scarsa manutenibilità del sistema. Un'architettura a microkernel, in cui solo un insieme base di servizi viene fornito a livello kernel – ad esempio scheduling, primitive di sincronizzazione e comunicazione – mentre la restante parte viene fornita attraverso server che eseguono in user space, consente di ottenere un sistema più robusto rispetto ai guasti (il crash di un server non causa il crash di tutto il sistema), più manutenibile e personalizzabile in base alle specifiche applicazioni. Lo svantaggio principale di questo tipo di architettura sta nei continui cambi di contesto tra kernel-space e user-space, i quali potrebbero causare un decadimento delle prestazioni generali del sistema. Va detto che, nell'ambito dei sistemi per reti di sensori, i cambi di contesto potrebbero essere in numero molto ridotto nell'unità di tempo. • Modello di programmazione. Il modello multithread, con il quale la maggior parte dei programmatori ha più familiarità, prevede che diversi task siano eseguiti contemporaneamente utilizzando in maniera concorrente l'unità elaborativa. Il modello ad eventi, invece, prevede che il sistema resti in attesa 8 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. del verificarsi di qualche evento, al cui manifestarsi è associata una routine di gestione dell'evento stesso. Il modello di programmazione ad eventi è più efficiente rispetto a quello multithread se il sistema possiede risorse scarse in quanto può essere implementato in modo semplice mediante una routine di gestione degli interrupt. • Gestione e protezione della memoria. Una gestione statica della memoria, la quale prevede l'allocazione di tutte le risorse di cui un task ha bisogno all'atto del suo avvio, è molto semplice da realizzare ma rende il sistema molto poco flessibile. Una gestione dinamica della memoria, che consenta alle applicazioni di allocare e deallocare memoria quando occorre, è molto più flessibile. Il più delle volte, essendo che su un mote potrebbe essere in esecuzione un solo task, la protezione della memoria potrebbe non essere necessaria. • Comunicazione. La comunicazione è una delle questioni fondamentali perché, come abbiamo sintetizzato in precedenza, la rete di sensori è un sistema distribuito in cui, potenzialmente, vi è eterogeneità nei nodi della rete. Il sistema operativo deve, pertanto, implementare un protocollo di comunicazione che tenga conto dell'eterogeneità dei nodi. Per i motivi illustrati in precedenza, lo stack TCP/IP così com'è implementato normalmente non può essere tenuto in considerazione. • Supporto per applicazioni real-time. Nel caso in cui la rete di sensori debba essere utilizzata per applicazioni mission-critical, con deadline associate ad i vari task, il sistema operativo deve prevedere un algoritmo di scheduling realtime. Il modello di programmazione e di esecuzione dei task deve prevedere la possibilità di determinare il WCET (Worst Case Execution Time) dei task, in modo da poter determinare la fattibilità di un insieme di task su cui siano espressi vincoli di tipo real-time. Ciò vuol dire essere in grado di determinare il flusso esecutivo dei task e la durata delle eventuali sezioni critiche. Un ruolo molto importante è rivestito dall'algoritmo di scheduling implementato dal sistema operativo. Un algoritmo di scheduling su base prioritaria che preveda la possibilità di preemption è molto importante per poter realizzare un sistema hard real time. Se il sistema operativo non prevede un algoritmo 9 Salvatore Barone preemptive non si può considerare hard real-time. Esistono due algoritmi di scheduling particolarmente importanti ed utilizzati dalla maggiorparte dei sistemi real time e sono Rate Monotonic (RM) e Earliest Deadline First (EDF). La specifica dettagliata dei due algoritmi può essere trovata in [1]. In parole povere si tratta di due algoritmi di scheduling su base prioritaria: RM è un algoritmo di scheduling a priorità fissa che prevede che ai task venga assegnata una priorità tanto più alta quanto minore è il loro periodo, mentre EDF è un algoritmo a priorità dinamiche che assegna priorità maggiore a quel task la cui deadline è più la prossima nel sistema. In [1] viene mostrato che entrambi sono algoritmi ottimi, anche se in senso diverso, e viene fornito un limite superiore al fattore di utilizzazione del processore per ciascuno dei due algoritmi. È, inoltre, necessario integrare i normali protocolli di comunicazione con protocolli di comunicazione real-time che siano prevedibili, per i quali, cioè, sia possibile determinare i WCET di trasmissione dei pacchetti. Le estensioni real-time del protocollo TCP/IP non possono essere tenute in considerazione per le stesse ragioni espresse precedentemente. 10 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. Capitolo 2: Sistemi operativi per reti si sensori senza filo. 2.1. TinyOs. TinyOS non è un sistema operativo nel senso stretto del termine. È un framework attraverso il quale assemblare componenti riusabili per costruire un sistema operativo specifico, adatto ad una particolare applicazione. Il sistema che si ottiene non contiene tutte le funzionalità normalmente fornite da un sistema operativo ma solo il sottoinsieme di funzioni veramente necessario alla particolare applicazione considerata. Un' immagine basilare del sistema, contenente solo i componenti per lo scheduling, ha un' impronta di memoria di circa 400 byte. Il sistema è scritto in NesC, un dialetto del linguaggio C, nato con TinyOs e in evoluzione assieme ad esso. Un sorgente NesC, prima di diventare l'immagine binaria di un'applicazione, affronta due passaggi di compilazione. Nel primo passaggio, il sorgente NesC subisce numerosi test volti ad ottimizzare il codice dell'applicazione per ridurne l'occupazione di spazio, evitare race-condition e prevenire errori di memoria. Il risultato di questo primo passo di compilazione è del codice sorgente scritto in linguaggio C. L'immagine dell'applicazione viene ottenuta dalla compilazione del suddetto sorgente ottenuto dopo il primo passo di compilazione. 2.1.1. Architettura del sistema. L'architettura del sistema è monolitica e l'approccio utilizzato per la programmazione è un approccio component-based: un programma TinyOS può essere organizzato come un grafo in cui i nodi sono le componenti del sistema le quali sono entità computazionalmente indipendenti le une dalle altre. Le componenti del sistema definiscono una serie di variabili di stato e buffer private, alle quali solo il componente può avere accesso, alle quali viene garantito accesso attraverso delle interfacce specifiche. Le interfacce, dunque, permettono un'interazione tra i diversi componenti. 11 Salvatore Barone La comunicazione inter-componente e la concorrenza intra-componente vengono modellate attraverso tre astrazioni: commands, events e tasks. I commands modellano la richiesta di fruizione di un servizio offerto dal componente, ad esempio la lettura di un sensore. Gli events sono delle notifiche associate ad eventi che si manifestano nel sistema. Gli eventi possono essere asincroni, ad esempio associati ad un interrupt hardware oppure sincroni, che si manifestano al completamento delle operazioni richieste da un command. I task costituiscono un meccanismo che permette l'esecuzione differita di insiemi di operazioni più onerose. Essi eseguono fino al completamento, vale a dire che vengono schedulati con un algoritmo di scheduling che non prevede preemption. Lo scheduler standard di TinyOS è uno scheduler che opera con strategia FIFO. Questa scelta, che può sembrare una forte limitazione, in realtà è motivata da alcune necessità che saranno chiarite più avanti. Nelle ultime versioni di TinyOS è stato introdotto uno scheduler che implementa l'algoritmo EDF ma, comunque, si tratta di uno scheduler molto semplice che non prevede possibilità di preemption. I task sono entità esplicite presenti nel linguaggio NesC e vengono creati esplicitamente da un “command” tramite l'operatore “post”. Il modello esecutivo dei task è “split-phase” ossia la richiesta di esecuzione e l'esecuzione del task sono disaccoppiate il che permette all'applicazione di posticipare l'esecuzione del task e di rimanere reattiva rispetto ad eventi asincroni. Quando il task viene schedulato e completato viene “sparato” un event che segnala il completamento della richiesta creata in precedenza. Anche gli “events” sono eseguiti fino al completamento e possono esercitare preemption sui task qualora siano associati ad interrupt hardware. 2.1.2. Modello di programmazione. Come accennato, NesC consente di creare applicazioni TinyOs seguendo un approccio component-based, incentrato sul concetto di componente visto come unità computazionalmente indipendente, con buffer e variabili di stato private con le quali è possibile interagire mediante delle interfacce. Il concetto è molto simile a quello di oggetto nella programmazione object-oriented: un oggetto possiede dei metodi che disciplinano l'accesso ai membri privati. 12 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. Ciascun componente possiede due tipologie di interfacce: le interfacce che fornisce (provides) e le interfacce che usa (uses). Attraverso di esse è possibile organizzare in maniera gerarchica il grafo che rappresenta il codice dell'applicazione. Si consideri, a titolo di esempio, il seguente componente, che fornisce le funzionalità di timer sfruttando l'astrazione di un clock hardware. module TimerM { provides { StdControl; Timer; } uses { } HWClock; } A sinistra vi è uno schema del componente, con le interfacce StdControl e Timer fornite e l'interfaccia HWClock usata dal componente. Nello schema sono indicati anche i commands, le frecce nere orientate verso il basso, e gli event, le frecce bianche orientate verso l'alto. A destra, invece, vi è il codice NesC che definisce l'interfaccia del modulo. Un componente può usare o fornire più volte la stessa interfaccia a patto che ciascuna istanza abbia un nome diverso. Le interfacce di un componente contengono sia i commands che gli events. Facendo riferimento allo schema precedente, le interfacce del componente TimerM possono essere definite dal codice NesC seguente. interface StdControl { command result_t init(); command result_t start(); command result_t stop(); } interface Timer { command result_t start(); command result_t stop(); event result_t fired(); } interface HWClock { command result_t set_rate(char interval, uint32_t scale); event result_t file(); } 13 Salvatore Barone Quelle funzioni che nell'interfaccia di un componente sono classificate come “command” devono essere implementate dal componente stesso mentre le funzioni classificate come “event” dovranno essere implementate dal quel componente che userà il componente in questione. Se il componente A utilizza l'interfaccia di un componente B allora sarà A a definire le operazioni da eseguire in corrispondenza di uno degli eventi appartenenti all'interfaccia del componente B. Oltre ai “modules”, ossia il codice contenente la logica dell'applicazione, NesC prevede di specificare una “configuration” per ciascun componente. Esse vengono usate per interconnettere tra loro i componenti attraverso le interfacce esposte. Le “configuration” permettono di legare tra loro diversi componenti, connettendo le interfacce fornite dagli uni con quelle usate dagli altri, per costituire dei “supercomponenti” che forniscono funzionalità più complesse. Il componente che fornisce le funzionalità di networking, ad esempio, è composto da 21 “modules” collegati facendo uso di 10 diverse “configuration”. Si consideri, a titolo di esempio, il componente TimerC, schematizzato di seguito, e il relativo codice “configuration” attraverso il quale può essere ottenuto. configuration TimerC { provides { StdControl; Timer; } } implementation { Components TimerM, HWClock; StdContol = TimerM.StdControl; Timer = TimerM.Timer; TimerM.Clock → HWClock.Clock; } Il linguaggio NesC, inoltre, impone diverse restrizioni al linguaggio C nell'utilizzo di alcuni dei suoi costrutti. Innanzitutto non è permesso utilizzare puntatori a funzione, quindi il call-graph dell'applicazione può essere totalmente determinato a tempo di compilazione in modo tale da permettere il “pruning” del codice non eseguito ed evitando cambi di contesto o chiamate a funzioni tra moduli diversi. Un'altra importante limitazione sta nell'impossibilità di allocare dinamicamente la 14 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. memoria, obbligando le applicazioni a dichiarare tutta la memoria di cui hanno bisogno. Questo permette di evitare frammentazioni dell'immagine dell'applicazione ed evitare errori dovuti ad accessi di memoria ad indirizzi deallocati o non accessibili. 2.1.3. Modello esecutivo e concorrenza. Il modello esecutivo in TinyOS è basato sui task, che rappresentano il carico elaborativo, che eseguono fino al loro completamento e uno o più interrupt handler stimolati da eventi asincroni di origine hardware. I task eseguono fino al loro completamento senza poter effettuare preemption su altri task (quindi i task sono atomici tra loro) ma possono subire preemption da routine associate all'interrupt handler. A questo punto è chiaro che race-condition tra task sono impossibili – è uno dei pochi vantaggi di avere uno scheduler non-preemptive - mentre è possibile avere race-condition tra task e routine dell'interrupt handler. Per assicurare atomicità di sezioni critiche esistenti tra task e interrupt handler TinyOS prevede la possibilità di poter usare delle “sezioni atomiche”. L'attuale implementazione delle sezioni atomiche in TinyOS prevede l'esecuzione di codice ad interruzioni disabilitate così che il task non possa essere interrotto a causa di eventi asincroni. Questa implementazione potrebbe avere pesantissime ripercussioni sulla prevedibilità di una applicazione per cui sono previste numerose restrizioni riguardo l'utilizzo delle sezioni atomiche. In molti sistemi a particolari eventi asincroni vengono associate delle routine su cui sono definiti dei vincoli di tipo real-time per cui se un task blocca una routine di questo tipo i vincoli temporali definiti su di essa potrebbero non poter essere rispettati in quanto la routine potrebbe rimanere bloccata per un tempo indefinito. NesC, come accennato, prevede numerose restrizioni riguardo l'utilizzo di sezioni atomiche: non è ammesso che, all'interno di una sezione atomica, un task esegua cicli, sia direttamente che indirettamente, che chiami commands o che “spari” events. In questo modo è possibile porre un tetto massimo al tempo per la quale una routine può risultare bloccata perché un task stia eseguendo sezioni atomiche. 15 Salvatore Barone 2.1.4. Supporto ad applicazioni real-time. TinyOS non fornisce nessun meccanismo per specificare esplicitamente vincoli temporali sui task o sulle interfacce di un componente, non fornisce nessun meccanismo per calcolare con precisione il WCET di un task, ne meccanismi per valutare schedulabilità dei task. Come sarà discusso nel seguito, comunque, esistono diverse tecniche di progettazione e realizzazione di componenti per TinyOS che consentono di ottenere predicibilità nel sistema. Tali soluzioni saranno discusse nel paragrafo 3.1. 2.2 Contiki Contiki è un sistema operativo per sistemi embedded completo e scalabile: implementa, ad esempio, lo stack TCP/IP in maniera completa con supporto ad IPv4 ed IPv6. Presenta anche una interessante funzionalità: i programmi possono essere compilati assieme al sistema operativo ed inclusi in un'unica immagine di memoria oppure caricati a run-time da una memoria o dalla rete e, inoltre, sfruttando il meccanismo di dynamic-linkage, possono essere sostituiti durante l'esecuzione. L'intero sistema in esecuzione, in genere, comprende il kernel del sistema, il loader delle applicazioni ed un insieme di processi e servizi. Un servizio, o “service”, implementa una particolare funzionalità a cui le applicazioni possono accedere tramite delle specifiche interfacce. Inizialmente previsti come entità esplicite del sistema, con la versioni 2.0 sono stati sostituiti da normali applicazioni ed hanno smesso di essere entità esplicite. In Contiki i processi condividono sia lo spazio degli indirizzi che lo stack, non esiste protezione della memoria e la comunicazione inter-processo viene supportata attraverso specifiche primitive kernel. L'intera immagine del sistema è divisa in due parti: la parte “core”, che comprende il kernel, il loader e le librerie software più utilizzate, e la parte “loaded” che comprende le librerie di livello applicazione utilizzate dalle specifiche applicazioni, oltre, ovviamente, alle applicazioni stesse. La parte core molto spesso è quella che viene “impressa” sui mote prima che 16 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. vengano dispiegati nell'ambiente da monitorare, mentre la parte loaded è quella che viene caricata a run-time. Come già accennato, Contiki offre una funzionalità molto importante che è quella di poter caricare programmi effettuandone il download dalla rete. Sfruttantdo in maniera intensiva il meccanismo del linking dinamico è possibile ridurre al minimo le dimensioni delle immagini dei programmi per cui diffonderli attraverso la rete diventa molto semplice. Il kernel utilizza una stringa per identificare i vari processi e fornisce delle primitive specifiche che permettono di gestire il meccanismo di caricamento e sostituzione delle immagini dei programmi, anche se essi sono in esecuzione. 2.2.1 Modello esecutivo. Il modello esecutivo di Contiki è un modello event-trigger a livello kernel ma, attraverso una libreria di livello applicazione, è implementato un meccanismo che permette il multithreading, sebbene in maniera limitata. La libreria che implementa il multithreading può essere linkata dalle applicazioni che richiedono tale tipo di supporto. Il kernel fornisce soltanto un insieme basilare di funzionalità tra cui un eventdispatcer ed un polling-handler. Il resto delle funzionalità viene implementato da librerie di livello applicazione le quali possono essere dinamicamente linkate alle applicazioni. I programmi devono, quindi, essere linkati al codice di livello kernel staticamente e, se necessario, linkati dinamicamente alle librerie addizionali. Il kernel supporta due tipologie di eventi: sincroni ed asincroni. Gli eventi asincroni costituiscono una metodologia per l'esecuzione differita di task mentre gli eventi sincroni causano l'esecuzione immediata di un processo. Il kernel non fornisce nessun HAL (Hardware Abstraction Layer) ma consente alle applicazioni di interagire direttamente con i registri dei dispositivi hardware. Ovviamente questo significa che, al variare del sistema hardware su cui viene eseguito Contiki, sarà necessario riscrivere parte delle applicazioni affinché possano essere in grado di interfacciarsi correttamente con l'hardware. In aggiunta al meccanismo degli event e degli event-handler, in Contiki è implementato un meccanismo detto “polling-event” che viene usato dalle applicazioni per interagire direttamente con l'hardware. Esso può essere visto come 17 Salvatore Barone un evento ad alta priorità schedulato in concomitanza con il manifestarsi di un evento asincrono. Il codice di un sistema Contiki può essere diviso in due parti: • “cooperative”: esegue in maniera sequenziale fino al completamento senza interferire con altro codice cooperative; • “preemptive”: codice eseguito da un event-handler, da un interrupt-handler o da un processo real-time, può esercitare preemption su codice cooperative ma non su altro codice preemption. Tutti gli eventi in Contiki hanno lo stesso livello di priorità e l'event-handler associato ad un particolare evento non può essere interrotto, eseguendo fino al suo completamento. Ciascun processo contiki è un thread (come sarà chiarito più in avanti, in realtà, è un protothread) ed è diviso in due parti: il PCB – Process Control Block – ed il process-thread. Il PCB contiene tutte le informazioni riguardo l'esecuzione del processo, come il nome, l'identificativo, lo stato, il puntatore alla funzione che implementa il task, ed è memorizzato in memoria RAM. Il process-thread è la funzione che implementa il task ed è, invece, memorizzata nella memoria programma. Il process-thread non dovrebbe mai accedere al suo PCB se non attraverso le funzioni fornite dal kernel, inoltre il PCB di un processo dovrebbe sempre essere allocato staticamente. 18 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. 2.2.2. Protothreads e multithreading. Nei sistemi il cui modello di programmazione è event-based le applicazioni sono implementate come event-handler e chiamate in risposta al manifestarsi di determinati eventi. Gli event-handler, tipicamernte, eseguono fino al completamento quindi non possono eseguire funzioni “conditional-blocking”, cioè funzioni che risulterebbero bloccanti nel caso in cui si verifichi una certa condizione. In TinyOS il problema viene risolto tramite il concetto di “split-phase” operation, che abbiamo già ampiamente discusso precedentemente, il quale consente di disaccoppiare la richiesta di fruizione di una particolare funzionalità dalla sua esecuzione. Inoltre, utilizzando un approccio event-driven, è molto difficile implementare applicazioni complesse in quanto è necessario progettare una macchina a stati finiti che evolva in modo da risolvere il problema. Si consideri il caso in cui si debba realizzare una trasmissione radio e la si debba gestire con un event-handler, la procedura si articolerebbe attorno ai seguenti punti: 1. abilita la circuiteria radio; 2. aspetta per Tawake millisecondi affinchè la circuiteria sia pronta; 3. se la comunicazione non è terminata aspetta la sua terminazione; 4. se tutti i trasferimenti sono stati completati spegli la circuiteria radio; 5. aspetta per Tsleep millisecondi. Se la radio non può essere spenta a causa di trasmissioni ancora in corso non spegnere la radio; Il codice potrebbe essere quello riportato di seguito. enum {ON, WAITING, OFF} state; void radio_wake_eventhandler() { switch(state) { case OFF: if(timer_expired(&timer)) { radio_on(); state = ON; timer_set(&timer, T_AWAKE); } break; case ON: if(timer_expired(&timer)) { timer_set(&timer, T_SLEEP); 19 Salvatore Barone if(!communication_complete()) state = WAITING; } else { radio_off(); state = OFF; } break; case WAITING: if(communication_complete() || timer_expired(&timer)) { state = ON; timer_set(&timer, T_AWAKE); } else { radio_off(); state = OFF; } break; } } Per ridurre la complessità di programmazione, in [10, 11] è stato introdotto il concetto di protothread. Un protothread è una normale funzione C scritta in modo tale da comportarsi come un thread. Il modo in cui viene scritta la funzione che implementa un protothread rende possibile interromperne l''esecuzione qualora il protothread debba attendere il manifestarsi di un evento e riprenderla da dove sia stata interrotta al manifestarsi dello stesso. I protothread sono progettati per operare con severe restrizioni per quanto riguarda l'utilizzo della memoria. Non possiedono stack (o meglio, lo stack viene condiviso tra tutti i protothread), per cui non è possibile utilizzare variabili automatiche e ciascun protothread deve essere confinato all'interno di un'unica funzione. I protothread consentono l'implementazione di funzionalità complesse con un approccio di programmazione sequenziale, rendendo la programmazione molto più semplice senza richiedere la progettazione di macchine a stati finiti Uno dei vantaggi maggiori dell'utilizzo dei protothread rispetto ai thread tradizionali risiede nella leggerezza: non essendo usato stack, per la gestione di un protothread occorre lo stesso spazio utilizzato per la memorizzazione di un unsigned integer. La problematica relativa al non poter utilizzare variabili automatiche può essere aggirata utilizzando variabili statiche – che vengono allocate in area dati - oppure 20 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. uno spazio apposito dove lo stato delle variabili può essere salvato prima di ogni cambio di contesto. A tale spazio, in genere allocato staticamente, ci si riferisce con l'acronimo TCB, ossia protoThread Control Block. L'implementazione della funzione di gestione della comunicazione radio vista in precedenza può essere implementata mediante l'utilizzo di protothread utilizzando, ad esempio, il seguente codice. PT_THREAD(radio_wake_thread(struct pt *pt)) { PT_BEGIN(pt); while(1) { radio_on(); timer_set(&timer,PT_WAIT_UNTIL(pt,Tawake); timer_expired(&timer)); timer_set(&timer, Tsleep); if(!communication_complete()) { PT_WAIT_UNTIL(pt, communication_complete() || timer_expired(&timer)); } if(!timer_expired(&timer)) { radio_off(); PT_WAIT_UNTIL(pt, timer_expired(&timer)); } } PT_END(pt); } I protothread vengono implementati in una maniera molto semplice sfruttando un meccanismo detto “local-continuation”. Questo meccanismo prevede che quando un protothread debba eseguire una funzione che possa risultare bloccante, salvi il suo stato di esecuzione. Non essendo disponibile uno stack dove memorizzare lo stato di esecuzione, viene salvato un riferimento alla linea alla quale l'esecuzione si è interrotta. Sfruttando il costrutto switch del linguaggio C lo stato di esecuzione verrà ripristinato alla successiva chiamata alla funzione che implementa il protothread. Definendo, ad esempio, le seguenti macro #define PT_BEGIN(pt) switch(pt->line) {\ case 0: #define PT_WAIT_UNTIL(pt, func) pt->line = __LINE__;\ case __LINE__:\ if(!(func)) return; #define PT_END(pt) } 21 Salvatore Barone il codice sorgente di cui sopra viene espanso come segue void radio_wake_thread(struct pt *pt) { switch(pt->lc) { case 0: while(1) { radio_on(); timer_set(&timer,T_AWAKE); pt->lc = 8; case 8: if(!timer_expired(&timer)) return; timer_set(&timer, T_SLEEP); if(!communication_complete()) { pt->lc = 13; case 13: If(!( communication_complete() || timer_expired(&timer))) return; } if(!timer_expired(&timer)) radio_off(); pt->lc = 18; case 18: if(!timer_expired(&timer)) return; } } } Anche se, apparentemente, il codice può sembrare scorretto, il linguaggio C non pone nessun vincolo riguardo la posizione dei “case” rispetto agli scope, per cui essi possono essere usati per “spostarsi” fino al punto in cui l'esecuzione è stata interrotta precedentemente. Quando viene eseguito il codice della macro PT_WAIT_UNTIL viene salvata la linea precedente l'esecuzione della funzione contenente il “conditional-blocking”, ossia l'attesa di un certo evento. Se la funzione restituisce “false”, indicando che il processo dovrà attendere il manifestarsi dell'evento, viene chiamato un “return” esplicitamente in modo da interrompere l'esecuzione del protothread per fare in modo che ne venga schedulato un'altro. Quando l'evento si manifesta viene richiamata la sua funzione precedentemente interrotta, la quale comincerà la sua esecuzione dalla riga precedente l'interruzione. Le macro definite precedentemente costituiscono solo un esempio. In Contiki il meccanismo che prevede la possibilità di creare e gestire protothread è offerto da una libreria di livello applicazione che giace sopra lo “strato” event-driven del kernel. Tale libreria è divisa in due parti. La parte “application-indipendent” mette a disposizione le primitive per la creazione, la terminazione e lo scheduling dei 22 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. protothread. La parte “application-specific” deve essere implementata dal programmatore a seconda della specifica applicazione e deve curare il salvataggio ed il ripristino dello stato di esecuzione dei protothread. La documentazione dettagliata riguardo al funzionamento dei protothread e multithreading può essere consultata in [13, 15] mentre la documentazione delle librerie può essere consultata in [14, 16]. 2.2.3. Supporto Real-Time. Contiki non fornisce supporto real-time a livello kernel: un insieme ristretto di funzionalità real-time vengono fornite da una libreria di livello applicazione. Contiki include una serie di librerie per l'utilizzo di timer ad elevata precisione, utilizzate anche dal sistema operativo per la schedulazione dei task, che possono essere utilizzate dalle applicazioni per implementare alcune funzionalità real-time. Le librerie messe a disposizione da Contiki offrono, tra le altre, funzioni per “sparare” eventi temporizzati, verificare se sia trascorso un determinato periodo di tempo, “svegliare” il sistema in maniera temporizzata e supportare l'esecuzione di task a scadenza prefissata con notevole precisione. La libreria “rtimer” fornisce funzionalità di esecuzione e scheduling di task real time. La libreria, a differenza delle altre librerie che implementano le funzionalità di timer, utilizza un modulo clock che utilizza direttamente il clock di sistema affinché possa essere ottenuta una misurazione del tempo nella risoluzione migliore possibile. La macro RTIME_NOW può essere utilizzata per ottenere il tempo corrente espresso in “tick” mentre la macro RTIME_SECOND esprime il numero di “tick” al secondo. Come già accennato precedentemente, il codice di un task real-time rientra nella categoria del codice “preemptive” per cui un task real-time può esercitare preemption su un altro task che sia in esecuzione. Un task real-time è rappresentato dalla struttura seguente. struct rtimer { rtimer_clock_t time; rtimer_callback_t func; void *ptr; }; Lo scheduler real-time implementato in Contiki è uno scheduler che utilizza un 23 Salvatore Barone algoritmo a priorità fissa la quale viene assegnata al task non in base alla deadline ma in base all'istante in cui il task deve essere eseguito. Esso va inizializzato chiamando la funzione void rtime_init(void); chiamandola prima di qualsiasi altra funzione. rtime_init inizializza lo scheduler ed la libreria rtime in modo tale le funzionalità real-time siano disponibili all'applicazione. Per creare un task real-time è possibile utilizzare la funzione int rtime_set( struct rtime* task, rtimer_clock_t start, rtimer_clock_t duration, rtimer_callback_t function, void* args); essa inizializza “task” con i restanti parametri e inserisce il descrittore del task nella coda dello scheduler real-time. La funzione restituisce “1” se il task potrà essere schedulato, “0” altrimenti. Bisogna precisare che la funzione non effettua nessun test di scheduling e che il valore restituito da essa dipende unicamente dal parametro “start”. La macro RTIMER_TIME(struct rtime* t) può essere utilizzata per ottenere il tempo di esecuzione dell'ultima istanza di un processo real-time “t”. Per schedulare il task real-time successivo, qualora ve ne fosse uno, si può usare la funzione void rtimer_run_next(void); 2.3 Nano-RK. Nano-RK è un sistema operativo per sistemi embedded che fornisce nativamente funzionalità real-time sia per quanto riguarda lo scheduling e l'esecuzione di task che per quanto riguarda i protocolli di rete. Il sistema, così come le applicazioni, è scritto in C e fornisce la classica astrazione multitasking adottata dai sistemi operativi più moderni e complessi, rendendo la programmazione più semplice ed immediata anche per i programmatori con poca esperienza. In nano-RK ad ogni task è associata una priorità, fissa per tutta la vita del task, in 24 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. base alla quale viene effettuato lo scheduling. La priorità può essere determinata in base al periodo o assegnata esplicitamente. Nano-RK supporta sia task periodici che a-periodici. Dal momento che lo scheduling implementa l'algoritmo RM, i task aperiodici devono essere serviti attraverso un server aperiodico. L'implementazione del server è a discrezione del programmatore. Non è previsto nessun meccanismo per la verifica della schedulabilità per cui essa va effettuata offline. Come per i sistemi operativi precedentemente illustrati, all'atto della compilazione viene creata un'unica immagine di memoria contenente l'applicazione e le funzionalità del sistema operativo necessarie al funzionamento. Le dimensioni di un' immagine basilare del sistema operativo sono di circa 2KB mentre un'immagine completa dello stack per il routing ed il networking arriva a circa 8KB. 2.3.1. Architettura del sistema. Come precedentemente accennato, nano-RK supporta il meccanismo di programmazione multitasking: ciascun task è implementato come un thread e viene schedulato tenendo conto della sua priorità, in accordo con l'algoritmo RateMonotonic. Non esiste protezione della memoria ed i diversi thread condividono lo stesso spazio di indirizzamento. A ciascuno di essi, però, è riservata una porzione di stack. La gestione dinamica della memoria è disponibile ma viene fortemente disincentivata dagli stessi programmatori di nano-RK in quanto potrebbe compromettere la prevedibilità del sistema. Il TCB (Task Control Block) di ogni singolo task deve essere allocato staticamente ed inizializzato all'inizio dell'esecuzione dell'applicazione. Ad esempio nrk_task_type Task1; Task1.task = Sound_Task; Task1.Ptos = (void *) &Stack1[STACKSIZE - 1]; Task1.TaskID = 1; Task1.priority = 3; Task1.Period = 10; Task1.set_cpu_reserve = 5; Task1.set_network_reserve = 3; Task1.set_sensor_reserve = 3; nrk_activate_task (Task1); Il seguente esempio implementa un task che acquisisce dati da un microfono e li 25 Salvatore Barone invia sulla rete periodicamente. void Sound_Task() { int i, status, sound,prev_sound; char tx_buff[2]; nrk_port_des my_port; port_des = nrk_port(tx_buff, 2, 0); nrk_connect(port_des, 0xFFFF); } while (1) { sound = read_sensor(MIC); printf("T1 Sound = %d\r",sound); tx_buff[0] = sound; if (sound_change(sound,prev_sound)) { nrk_port_send(my_port); wait_until_sent(); printf("T1 Sent Packet\r"); } prev_sound = sound; nrk_suspend_task(); } 2.3.2. Esecuzione di task Real-Time. Per garantire un comportamento real-time, nano-RK utilizza il meccanismo della resource-reservation: ciascun task, in un singolo periodo, può utilizzare una specifica risorsa rispettando una “quota” massima assegnata. Se la resource-reservation è di tipo hard allora un task che supera la quota massima di utilizzo di una risorsa subisce preemption ed eseguirà al prossimo periodo. Se la resource-reservation è di tipo soft allora un task che supera la quota massima di utilizzo di una risorsa potrà continuare ad usarla soltanto se essa non è usata da nessun'altro task. In nano-RK la resource-reservation viene usata per la gestione della CPU, per la gestione della rete e per la gestione dei sensori. Per quanto riguarda la cpureservation nano-RK utilizza un approccio hard, vale a dire che superata la quota di cpu disponibile il task viene sospeso fino al prossimo periodo. Nel TCB di ciascuno dei task è conservata la quota massima di cpu utilizzabile dal task in un periodo e la quota residua. Per quanto riguarda la sensor-reservation, ossia il numero massimo di letture di dati da sensori effettuabili in un periodo, in ciascun TCB è presente la quota di letture e le letture residue. Per quanto riguarda la networking-reservation viene effettuato un distinguo sulla tipologia di utilizzo che si fa della rete e sono previste due quote diverse, una per la ricezione ed una per la trasmissione, che esprimono il 26 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. numero di byte massimi trasferibili su rete durante un periodo. Sia per la sensorreservation che per la networking-reservation viene adottato un approccio hard resource-reservation. In questo modo viene massimizzato il tempo di inattività del processore. Considerando che lo scheduler manda il processore il sleep-mode quando non vi sono task da schedulare, questa scelta aiuta ad aumentare le prestazioni in termini di efficienza energetica. Il modello esecutivo multitask permette cooperazione e concorrenza tra task. L'uso concorrente di risorse da parte di più thread fa si che nasca l'esigenza di meccanismi di sincronizzazione per permettere l'utilizzo in mutua esclusione di risorse condivise. Nano-RK fornisce l'astrazione dei mutex e le primitive lock(), try_lock() e unlock() per permettere l'esecuzione in mutua esclusione di sezioni critiche. Per mantenere uniformità con un modello di esecuzione real-time, i mutex sono implementati con il protocollo di priority-ceiling, per i cui dettagli rimandiamo a [1], in modo da porre un “tetto” al tempo per cui un task possa rimanere in attesa su un semaforo. 2.3.3. Networking. Nano-RK implementa uno stack per il networking completo e leggero dal punto di vista della memoria occupata. Essendo concepito per una comunicazione su banda ISM, lo stack implementato non è complesso come lo standard TCP/IP comune ma ne imita diverse caratteristiche. Dal momento che i protocolli di routing possono essere molto diversi a seconda delle esigenze che si incontrano per una particolare applicazione, nano-RK implementa protocolli MAC e di routing basilari in modo da permettere al programmatore di implementare i protocolli necessari. Il protocollo di routing implementato è un semplice protocollo basato su tabelle di routing statiche utilizzate per inoltrare messaggi destinati ad altri nodi della rete. È implementato un protocollo MAC che sfrutta CSMA-CA. L'implementazione dello stack ricorda un po' le socket di Berkeley. I dati da trasmettere sono incapsulati in un pacchetto formato da un header e da un payload. L' 27 Salvatore Barone header contiene l'indirizzo del nodo di destinazione e un “numero di porto”, o porta di destinazione, che identifica un buffer a cui i dati sono diretti. Tutti i pacchetti in uscita da un nodo sono gestiti da un unico task periodico che serve tutti gli altri task che necessitano di utilizzare i servizi di rete. I buffer di ricezione sono dimensionati ed inizializzati dalle applicazioni ma sono gestiti da un interrupt-handler, implementato nello stack, che cura la ricezione e serve gli altri task. I pacchetti in ricezione vengono salvati dall'handler direttamente nei buffer e la corretta ricezione viene segnalata tramite un flag. Questo tipo di gestione permette di evitare inutili copie salvando memoria e tempo esecutivo. A differenza delle socket di Berkeley in nano-RK è consentito a più applicazioni di utilizzare lo stesso buffer, quindi la stessa porta di comunicazione. Sorge il problema della sincronizzazione tra l'handler e i vari task che utilizzano un buffer. Nano-RK prevede che tutte le applicazioni che utilizzino un buffer comunichino l'avvenuta lettura tramite la chiamata ad una primitiva specifica. Solo quando tutti i task che utilizzino un buffer ne abbiano confermato la lettura o lo abbiano rilasciato l'handler potrà sovrascrivere il buffer con altri dati. 28 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. Capitolo 3: Aggiungere prevedibilità ai sistemi non real-time. In un sistema operativo real-time la correttezza del comportamento di un'applicazione dipende sia dalla correttezza dei risultati prodotti sia dal tempo impiegato per ottenerli. Ciò non vuol dire che un sistema real-time debba essere il più veloce possibile ma che il sistema debba prevedere una serie di metodologie atte a calcolare il tempo di esecuzione dei task nel caso peggiore per valutare se i vincoli temporali di un insieme di task possano essere rispettati e, al contempo, valutare le condizioni nelle quali essi non possano essere rispettati. In altre parole tali meccanismi devono consentire di effettuare previsioni circa le condizioni che possono portare il sistema in una situazione di errore. 3.1. Aggiungere supporto real-time a TinyOS. In TinyOS gli ostacoli da superare affinchè sia possibile determinare il tempo di esecuzione di un task sono insiti nell'architettura stessa del sistema. Un task, infatti, è spezzettato su un insieme di componenti ed event-handler diversi eseguiti in risposta al manifestarsi di eventi asincroni. Anche se le interfacce dei componenti in TinyOS non prevedono la possibilità di specificare vincoli temporali è possibile adottare metodologie di progettazione e realizzazione di componenti che consentano un calcolo agevole del worst-case execution time di un processo. Un altro importante ostacolo è costituito dal modello non-preemptive di esecuzione dei task secondo il quale un task esegue fino al suo completamento senza interruzioni da parte di altri task. Ciò vuol dire che un task a bassa priorità può bloccare l'esecuzione di un task ad alta priorità fin quando non giungerà a completamento. Questo problema può essere aggirato implementando i processi ad alta priorità direttamente nell'interrupt handler in modo che esso possa esercitare preemption su un eventuale task a bassa priorità. Il sistema che si ottiene, comunque, non può essere ritenuto un sistema hard real-time. 29 Salvatore Barone In [8] viene proposto un modello di programmazione che consente di calcolare i tempo di esecuzione di un task nel caso peggiore. Tale modello si basa sul concetto di componente real-time che incorpora un componente standard di TinyOS o un altro componente real-time. Il componente real-time fornirà le stesse interfacce del componente TinyOS incapsulato ma, in più, consentirà di definire dei vincoli temporali che dovranno essere rispettati. Un generico componente real-time può essere visto come una “scatola nera” che fornisce determinate funzionalità alle quali è possibile accedere tramite una “finestra” che accetta eventi in ingresso solo se sopraggiungono con un periodo superiore ad un “periodo minimo di interarrivo” prestabilito e caratteristico dell'evento, altrimenti l'evento viene rigettato. Tale periodo minimo di interarrivo è, in sostanza, il periodo di tempo minimo tra due manifestazioni successive dello stesso evento. Ciascuna interfaccia Ti del componente che definisca una operazione eseguita come split-phase (si veda il modello esecutivo in TinyOS al paragrafo 2.1.1) viene descritta attraverso la tripletta di parametri (ei, ri, xi) che rappresentano, rispettivamente, tempo di calcolo parziale, tempo di risposta parziale e periodo minimo di interarrivo. Affinché sia possibile calcolare il WCET vanno fatte le alcune ipotesi semplificative: bisogna supporre che ciascuna delle operazioni eseguite in modo “split-phase” sia invocata una sola volta per ciascuna occorrenza di un certo evento e che le deadline dei task coincidano con il periodo minimo di interarrivo. Consideriamo un task τ che invoca un'interfaccia T1(e1, r1, x1) che, a sua volta, chiama le iterfacce T2(e2, r2, x2) e T3(e3, r3, x3). Il wcet del task τ, Eτ dovrà essere calcolato tenendo in considerazione non solo il tempo di calcolo necessario ad eseguire le funzioni dell'interfaccia T1, ma anche quello necessario all'esecuzione di T2 e T3. Se ciascuna delle interfacce che eseguono con il modello split-phase viene invocata una sola volta per ciascuna occorrenza di un certo evento allora detto I τ l'insieme delle interfacce invocate direttamente dal task τ ed E i il wcet di Ti ∈ Iτ interfaccia invocata direttamente dal task τ, possiamo calcolare il wcet del task τ ed il suo tempo di risposta con le seguenti 30 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. Eτ =∑ Ei (1) R τ = ∑ Ri (2) i∈ I τ i∈ I τ Le quantità Ei ed Ri sono i wcet ed i tempi di risposta delle interfacce T i direttamente invocate dal task. Per il loro calcolo andranno tenute in considerazione i tempi di calcolo ed i tempi di risposta delle interfacce T j ∈ Ii direttamente invocate da Ti. ∀ Ti ∈ Iτ Ei=ei + ∑ E j (3) Ri=r i + ∑ R j (4) j∈I i j∈ I i dove Ii rappresenta l'insieme delle interfacce direttamente invocate da Ti. Una volta determinato il WCET di un'interfaccia T i è possibile determinare il periodo minimo Pi con la quale viene invocata e verificare che esso sia maggiore del periodo minimo di interarrivo xi. Per il calcolo di Pi va tenuto in conto che l'interfaccia Ti può essere chiamata da più interfacce anche se, come imposto precedentemente, è possibile richiamarla una sola volta al manifestarsi di un certo evento. Detto Qi il set delle interfacce che invocano T i si può calcolare Pi con la seguente relazione: Pi = { min {P j /T j ∈Qi} ∣Qi∣ xi Qi ≠∅ (5) Qi=∅ In effetti, quando Qi = si suppone, per comodità, che il tempo di interarrivo coincida con il tempo di interarrivo minimo. In questo modo ci si mette nella condizione peggiore. 31 Salvatore Barone Esempio. Si consideri il seguente grafo di invocazione. Ti ei ri xi Qi Ii 1 5 5 20 {T3} 2 10 10 40 {T3, T4} 3 1 3 11 {T1, T2} 4 6 6 15 {T2} In tabella sono indicati anche gli insiemi Q i e Ii che, per un'interfaccia T i, indicano rispettivamente l'insieme delle interfacce che invocano T i e l'insieme delle interfacce invocate direttamente da Ti. Attraverso le espressioni (1) – (4) è possibile calcolare il WCET dei task ed il loro tempo di risposta in modo molto semplice. Il risultato è riportato nella tabella di seguito. Ti Ei Ri 1 5+1=6 5+3=8 2 10+1+6=17 10+3+6=19 3 1 3 4 6 6 Una volta determinati tempi di esecuzione e tempi di risposta si può determinare il periodo minimo col quale le interfacce vengono invocate utilizzando la relazione (5). P1 = x 1 = 20 P2 = x 2 = 40 32 P3 = min {P j / T j ∈Q3 } = 10 < x3 ∣Q3∣ P4 = min {P j / T j ∈Q 4 } = 40 > x 4 ∣Q4∣ Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. Essendo P3, periodo con il quale viene invocata l'interfaccia T 3, inferiore al periodo minimo di interarrivo x3, l'interfaccia è sovraccarica nel senso che viene invocata più spesso di quanto è consentito. 33 Salvatore Barone Capitolo 4: Conclusioni. In questo breve lavoro sono stati analizzati i sistemi operativi per sistemi embedded più utilizzati, quali TinyOs, Contiki e Nano-RK, e la loro attitudine ad essere utilizzati per applicazioni real-time. Per quanto riguarda TinyOS è stato mostrato che la struttura di programmazione, la quale prevede che un task possa essere implementato da componenti diversi le cui interfacce non consentono di specificare esplicitamente vincoli di natura temporale, ed il modello esecutivo event-triggered, con task che non prevedono la possibilità di subire preemption, non consentono la realizzazione di applicazioni hard real-time. È stato illustrato uno dei metodi proposti per introdurre prevedibilità nel sistema introducendo il concetto di componente real-time, che consente di specificare vincoli di natura temporale in modo esplicito. Sono state, inoltre, presentate delle ipotesi semplificative che consentono di stimare il WCET di un task. Per quanto riguarda Contiki è stato mostrato come il meccanismo di programmazione ed esecuzione consenta l'utilizzo, seppur in maniera limitata, di un insieme ristretto di funzionalità real-time/ , come l'esecuzione di task real-time su scadenza temporale. È stata illustrata la libreria rtimer, la quale permette di monitorare il trascorrere del tempo con notevole precisione e programmare l'esecuzione dei task. L'assenza di un test di schedulabilità, però, rende questo sistema inutilizzabile qualora l'applicazione abbia requisiti hard-real time. Per quanto riguarda Nano-RK è stato mostrato che si tratta dell'unico sistema operativo tra quelli analizzati a possedere funzionalità real-time più evolute. L'implementazione di uno scheduler Rate-Monotinic, la possibilità di soddisfare richieste aperiodiche implementando un server aperiodico, la possibilità di poter usare semafori implementati con il protocollo di priority ceiling ed il meccanismo della resource reservation permettono di stimare correttamente il WCET dei task e valutarne la schedulabilità. È possibile, dunque, realizzare applicazioni hard real time. L'assenza di un test di schedulabilità automatico e l'assenza di un algoritmo per la gestione dei sovraccarichi sono allo stesso tempo pecche e nuovi spunti per sviluppi futuri di questo sistema operativo. 34 Soluzioni per lo sviluppo di applicazioni Real-Time su reti di sensori senza filo. Riferimenti. 1. G. Buttazzo, Sistemi in Tempo Reale, Editrice Pitagora, ISBN: 8837116403. 2. I. F. Akyildiz, W. Su, Y. Sankarasubramaniam, and E. Cayirci. A Survey on Sensor Networks, Georgia Institute of Technology. 3. M. Staroswiecki. Fault tolerant estimation in sensor networks, Ecole Polytechnique Universitaire de Lille, France; 4. M. O. Farooq, T. Kunz. Operating Systems for Wireless Sensor Networks: A Survey , Department of Systems and Computer Engineering, Carleton University Ottawa, Canada; 5. P. Levis, S. Madden, J. Polastre, R. Szewczyk, K. Whitehouse, A. Woo, D. Gay, J. Hill, M. Welsh, E. Brewer, D. Culler. TinyOS: An Operating System for Sensor Networks; 6. N. Cooprider, W. Archer, E. Eide, D. Gay, J Regehr. 7. A Survey On Tiny Os A Real Time Operating System For Wireless Sensor Network, consultabile on-line all'indirizzo http://www.ukessays.com/essays/computer-science/a-survey-on-tiny-os-a-realtime-operating-system-for-wireless-sensor-network-computer-science-essay.php#ixzz3VK72LDBP (visitato il 20 marzo 2015) 8. C. Duffy, J. Herbert. Achieving Real-Time Operation in TinyOS, Computer Science Dept., University College Cork, Ireland. 9. A. Dunkels, B. Gronval, T. Voigt Contiki – a lightweight and flexible operating system for tiny networked sensor, Swedish Institute of Computer Science. 10. A. Dunkels, O. Schmidt, T. Voigt Using protothreads for sensor node programming Swedish Institute of Computer Science. 11. A. Dunkels, B. Gronval, T. Voigt Protothreads: semplifing event-driven programming of memory constrained embedded system, Swedish Institute of Computer Science. 12. D. Willmann Contiki – a memory-efficient operating system for embedded smart object. 13. The Contiki Documentation Group, Protothread Library, consultabile online https://github.com/contiki-os/contiki/wiki/Processes#Protothreads (visitato il 28 marzo 2015) 14. The Contiki Documentation Group, Protothread Library Reference, consultabile online all'indirizzo http://contiki.sourceforge.net/docs/2.6/a01802.html (visitato il 28 marzo 2015) 15. The Contiki Documentation Group, Multi-threading Library, consultabile online https://github.com/contiki-os/contiki/wiki/Multithreading (visitato il 28 marzo 2015) 16. The Contiki Documentation Group, Multi-threading Library Reference, consultabile online all'indirizzo http://contiki.sourceforge.net/docs/2.6/a01669.html (visitato il 28 marzo 2015) 17. The Contiki Documentation Group, Real-Time task scheduling Library, consultabile online all'indirizzo https://github.com/contiki-os/contiki/wiki/Timers (visitato il 28 marzo 2015) 18. The Contiki Documentation Group, Real-Time task scheduling Library Reference, consultabile online all'indirizzo http://contiki.sourceforge.net/docs/2.6/a01673.html (visitato il 28 marzo 2015) 35 Salvatore Barone 19. A. Eswaran1, A. Rowe1, R. Rajkumar, Nano-RK: an Energy-aware Resource-centric RTOS for Sensor Networks, Electrical and Computer Engineering Department, School of Computer Science Carnegie Mellon University, Pittsburgh, PA, USA 36