Soluzioni per lo sviluppo di applicazioni real time su reti di sensori

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