RTOS alapok
A fejezet tartalma:A többfeladatos rendszerek kihívásai
Egy hagyományos számítógép program általában egyszerre csak egy dologgal foglalkozik: beolvas egy állományt, ezután végez valamilyen számolást a beolvasott adatokkal, végül kiírja az eredményeket. Egy beágyazott rendszer mikrovezérlője ezzel szemben számos dolgot végez egyidejűleg: például leolvas egy hőmérőt, működtet egy szelepet, gomblenyomásra figyel, ellenőrzi az eltelt időt, számlálja az elhaladó üvegeket egy palackozóban stb. A mikrovezérlőnek ez a képessége, hogy (látszólag) egyidejűleg több feladattal is tud foglalkozni az, amiről többfeladatos (multitasking) programozás szól.Egy mikrovezérlő program természetesen egyszerre mindig csak egy dologgal tud foglalkozni. De ha mindezt nagyon gyorsan teszi (többmillió művelet másodpercenként), akkor megoldható, hogy a feladatok között gyorsan váltogasson, azt az illúziót keltve, mintha számos dolgot tényleg egyidejűleg végezne. A kérdés csak az, hogy
hogyan írjuk meg a programot, hogy az megossza a figyelmét több feladat között, de ne zavarodjunk bele sem mi, sem a mikrovezérlő a folyamat bonyodalmaiba. Az alábbiakban felsorolunk néhány szokásos módszert:
Szuperhurok
A szuperhurok lényegében egy végtelen ciklusból álló programszerkezet, amely az összes végrehajtandó feladatot tartalmazza. Legegyszerűbb esetben a programszerkezet így néz ki:int main()
{
initialisation(); //Kezdeti beállítások
while (true) { //Végtelen ciklus
Feladat_1(); //Feladatok
Feladat_2();
}
}
A szuperhurok módszer jellemzői:- Hagyományos technológia, egyszerű alkalmazásokra jól megfelel
- A program fix sorrendű funkciók egymásutáni sorozata
- Időkritikus feladatok kiszolgálása csak megszakításokkal
valósítható meg
- Időzítések implementálása nehézkes, a változtatások kihatnak az egész hurokra
Véges állapotgép módszer
A gyakorlati programozás során sokszor élünk szoftver absztrakcióval, ami abban segít bennünket, hogy egy komplex problémát néhány egyszerű alapelv segítségével fogalmazzunk meg. A véges állapotgép esetében például ezeket az absztrakt alapelemeket használjuk: bemenetek, kimenetek, állapotok, átmenetek egyik állapotból a másikba.A véges állapotgép ( Finite State Machine, FSM) egy olyan absztrakció, amely a probléma megoldását az algoritmusokhoz hasonló módon fogalmazza meg. Van azonban egy lényeges különbség: egy algoritmus azt adja meg, hogy milyen lépések elvégzése során jutunk el a megoldáshoz, a véges állapotgép pedig egy rendszer viselkedését írja le , mint egy automatát, amely a bemenetek hatására váltogatja az állapotait (a megoldást itt az jelenti, ha sikerül a modell viselkedését s programunkban realizálni).
Egy véges állapotgép az alábbi öt alapvető dologgal jellemezhető:
- A lehetséges állapotok véges halmaza. Ezen állapotok egyike a kiindulási állapot.
- A bejövő adatok véges halmaza.
- A rendszer által generált kimenő adatok véges halmaza
- Az állapotok közötti átmenetek explicit megadása. Az átmenet a pillanatnyi állapot és a bemenetek függvénye.
- A kimenő adatok előállításának szabályai (a kimenet a pillanatnyi állapot és a bemenetek függvénye).
Az alábbiakban egy nagyon egyszerű (talán ez a lehető legegyszerűbb) mintapéldát mutatunk be, amelyben egy LED-et kapcsolgatunk egy nyomógomb segítségével. Az SW1 nyomógombot a korábbi nyomógombos mintapéldához hasonlóan itt is a D3 (más néven PTA12) bemenet és a GND közé kell kötni. Az állapotgép a nyomógombbal kapcsolatban két állapotot tartalmaz:
- Nyomógomb lenyomásra várunk (hogy magas szintről alacsonyra váltson az SW1 bemenet).
- Nyomógomb felengedésre várunk (hogy alacsony szintről magas szintre váltson az SW1 bemenet).
- a pergésmentesítést
- a LED csak új lenyomáskor vált állapotot
- a nyomógomb állapotváltásainak figyelése nem blokkoló
várakozásokban történik, tehát a program könnyen bővíthető további
feladatokkal
1.
ábra: Az állapotgép megfogalmazása irányított gráffal
1. táblázat: Az állapotgép megadása
táblázattalPillanatnyi
állapot |
Következő
állapot |
Állapotváltás
feltétele |
Tevékenység |
---|---|---|---|
Lenyomásra
vár |
Felengedésre vár | SW1 == 0 |
LED1 = ! LED1 |
Felengedésre
vár |
Lenyomásra vár | SW1 == 1 |
#include "mbed.h"
DigitalIn SW1(D3,PullUp);
DigitalOut LED_1(LED_RED);
typedef enum { //Set of possible states
STATE_WAIT_FOR_PRESS,
STATE_WAIT_FOR_RELEASE
} state_t;
int main()
{
//Define initial state
state_t mystate = STATE_WAIT_FOR_PRESS;
LED_1 = 1; //LED off at start
while (true) {
switch(mystate) {
case STATE_WAIT_FOR_PRESS:
if (SW1==0) { //if button pressed
LED_1 = !LED_1; //toggle LED_1
mystate = STATE_WAIT_FOR_RELEASE;
}
break;
case STATE_WAIT_FOR_RELEASE:
if (SW1==1) { //if button released
mystate = STATE_WAIT_FOR_PRESS;
}
}
wait_ms(20); //debounce delay
}
}
Egyszerű körkörös ütemezés
Az egyszerű szuperhurok módszert átalakíthatjuk úgy, hogy a főprogramban csak definiáljuk a feladatokat (task), majd végül átadjuk a vezérlést egy ütemezőnek, mely legegyszerűbb esetben körkörös ütemezés (round robin scheduling) valósít meg. Felmerülhet a kérdés: mit nyerünk ezzel? Nos, egyrészt azt, hogy az ütemező megírható újrafelhasználható kódként, másrészt továbbfejleszthető különféle szolgáltatásokkal, vagy ütemezési stratégiákkal, megtéve ezzel az első lépést az operációs rendszerek felé. Az operációs rendszerek kezdete a feladatütemező (scheduler)!Kooperatív többfeladatú rendszer
Az egyszerű körkörös ütemezőt bővíthetjük például azzal, hogy megjegyzi, hogy az egyes feladatok hol tartották, amikor visszaadták a vezérlést az ütemezőnek, valamint lehetőség ad arra, hogy a feladatok megadhassák, hogy mennyi idő múlva akarják folytatni a tevékenységüket. Ilyen lehetőséget kínál Adam Dunkels: Protothreads (protoszálak) programkönyvtára, amely minimális erőforrás felhasználású program protoszálak megvalósítását támogatja. A protoszál itt azt jelenti, hogy "könnyűsúlyú", azaz saját veremtárral nem rendelkező programszálakról van szó. Az ütemező ezért protoszálanként csak egy visszatérési (pontosabban folytatási) címet és egy időzítési értéket kell, hogy megjegyezzen. A közös veremhasználat miatt azonban a protoszálak nem tárolhatnak lokális változókat a veremtárban.Fontos továbbá, hogy a protoszálak záros határidőn belül visszaadják a vezérlést az ütemezőnek. Az ilyen többfeladatú rendszert kooperatív működésűnek nevezzük, mivel a működés a feladatok együttműködésén alapul. Ha valamelyik programszál önző módon magánál tartja a vezérlést, a többi programszál nem tud futni, mint ahogy a Windows 3.1 rendszerben is gyakran előfordult, hogy egy-egy rosszul megírt alkalmazás "lefogta" az egész rendszert. Egy rosszmájúan vicces megjegyzés szerint a korai Windows verzióknál a többfeladatúság (multitasking) abban merül ki, hogy "bármely alkalmazás képes együtt futni a homokórával, ami megakadályozza további alkalmazások elindítását".
Megszakításos ütemezés
A prioritási szintek bevezetése és a megszakításos ütemezés (preemptive scheduling) a következő lépés a valós idejű operációs rendszerek (Real-Time Operating System, RTOS) megvalósítása felé. A programszálak megszakíthatósága azt jelenti, hogy ha egy magasabb prioritású programszál futtatható állapotba került (egy esemény bekövetkezte miatt), akkor az éppen futó, alacsonyabb prioritású programszál futása félbeszakad, s csak akkor folytatódik, ha a magasabb prioritású programszál befejezte a tevékenységét (újabb eseményre vagy időzítésre várakozó állapotba kerül). A megszakíthatóság azt is eredményezi, hogy - a kooperatív működéssel szemben - az ütemező akkor is megszakíthatja az éppen futó programszálat, ha egy vele azonos prioritású feladat már régóta futásra vár. Az azonos prioritású programszálak időszeleteléses alapon, körkörös ütemezéssel (round robin scheduling) futnak. Az RTOS kezdeti konfigurálásától függ, hogy egy-egy időszelet milyen időtartamú (általában 10 - 50 ms).A kooperatív működés és a megszakításos ütemezés közötti különbséget az alábbi ábra szemlélteti. Az ábra felső részében (A) a kooperatív működés látható, ahol kezdetben egy alacsonyabb prioritású feladat fut (1a). Ezt megszakítja egy esemény (2a), s tegyük fel, hogy a megszakítás kiszolgálása során olyan eseményt észlelünk, amely futtathatóvá teszi a korábban erre az eseményre váró magasabb prioritású feladatot. A megszakításból azonban a vezérlés a megszakított programszálhoz kerül vissza (4a), s a kooperatív működési módnak megfelelően mindaddig az fut, amíg blokkoló várakozásra, vagy explicit kilépésre (visszaadjuk a vezérlést sz ütemezőnek) nem kerül sor (5a). Az ütemező (kernel) kiértékeli a helyzetet, s átadja a vezérlést a futtatható állapotba került magasabb prioritású programszálnak(6a).
2.
ábra: A kooperatív működés és a megszakításos ütemezés különbségének
szemléltetése
Az ábra alsó részében (B) a megszakításos ütemezés látható, ahol kezdetben egy alacsonyabb prioritású feladat fut (1b). Ezt megszakítja egy megszakítási kérelem (2b), s tegyük fel, hogy a megszakítás kiszolgálása során olyan eseményt észlelünk, amely futtathatóvá teszi a korábban erre az eseményre váró magasabb prioritású feladatot. A megszakítás kiszolgálása után a vezérlés az ütemezőhöz kerül (4b), s a megszakításos ütemezésnek megfelelően mindig a legmagasabb prioritású futtatható programszál kapja meg a vezérlést (5b), az alacsonyabb prioritású programszál futása pedig felfüggesztésre kerül mindaddig, amíg a magas prioritású programszál be nem fejezi a tevékenységét (6b) és az ütemező nem talál futtatható állapotú magasabb prioritású programszálat (7b).
A programszálak megszakíthatósága miatt - mivel az bármikor bekövetkezhet - szükséges azonban, hogy a programszálak folytatásához minden szükséges információt elmentsünk (CPU regiszterek, visszatérési cím, lokális változók). Emiatt a megszakításos ütemezést használó operációs rendszerek erőforrásigénye jóval nagyobb a kooperatív ütemezéshez képest, s a feladatváltás is több időt vesz igénybe. Minden programszál saját veremterülettel és a taszkleíró táblában egy-egy bejegyzéssel kell, hogy rendelkezzen (ez utóbbira az ütemezőnek van szüksége, hogy a programszálak állapotát nyilvántartsa).
3. ábra: Programszálanként külön verem és taszkleíró táblabejegyzés szükséges
Az RTOS rendszerek fontos feladata az ütemezésen kívül az is, hogy a programszálak szinkronizálásához, az osztott használatú erőforrások ideiglenes lefoglalásához, valamint a programszálak közötti kommunikációhoz rendszerszintű szolgáltatásokat nyújtson. Jelzők, mutexek, szemaforok, várakozási sorok, üzenetküldés, memóriakészlet, időzítők jelentik a szokásos eszköztárat.
Kellemetlen következmények: prioritás inverzió, holtpont
Az előző bekezdésben említettük, hogy a programszálak a közös használatú erőforrásokat ideiglenesen lefoglalhatják (pl. mutex vagy szemafor segítségével), hogy amíg ezeket használják (pl. kiíratás soros porton, vagy adatbeolvasás memóriakártyáról), más programszál ne avatkozhasson közbe. Ez a lefoglalás azonban olyan mellékhatásokhoz vezethet, mint például a prioritás inverzió, amit az alábbi ábrán szemléltetünk.4. ábra: Prioritás inverzió szemléltetése
Az ábra magyarázata:
- Az alacsony prioritású L programszál megszerez egy erőforrást (pl mutex)
- A magas prioritású H programszál félbeszakítja L futását (t1)
- A H programszál futása megszakad, ha az L által lefoglalt erőforrásra kell várnia (t2)
- Ha egy közepes prioritású M programszál ekkor megszerzi a futási jogot, akkor a prioritás megfordul, mert bármeddig futhat, blokkolva H és L működését.
A címben említett másik probléma a holtpont (deadlock) akkor következik be, ha két programszál kölcsönösen lefoglal olyan erőforrást, amelyre a másik programszál várakozik. Ennek elkerülése megfontolt programtervezést kíván. Elvileg az ütemező is figyelhetné a holtpont kialakulását, ám ennek megvalósítása túl sok erőforrást (memória és CPU idő) venne igénybe.
Valós idejű rendszerek jellemzői
Az általános célú operációs rendszerekkel (OS) szemben a beágyazott rendszerek többnyire valósidejű (Real-Time) követelményeket támasztanak, ami azt jelenti, hogy egy-egy esemény bekövetkezésére adott időkorláton belül reagálni kell. Az autó fékrendszerének csúszásgátló elektronikája vagy a légzsák esetében tizedmásodperces késedelmeken is emberéletek múlhatnak. A követelmények szempontjából nézve a valósidejű operációs rendszereket (RTOS) három nagy csoportra szokták osztani:- Hard RTOS: Ezeknél követelmény a feladatokhoz tartozó határidők szigorú betartása. A határidők elmulasztása katasztrófához vezethet. A fentebb említett légzsák esete is ebbe a kategóriába tartozik.
- Firm RTOS: Ezeknél szintén követelmény a határidők betartása, bár a határidő lekésése nem vezet katasztrófához, de nemkívánatos következménnyel járhat, ami lerontja a termék minőségét.
- Soft RTOS: Ennél a típusú RTOS-oknál megengedett az időkorlátok alkalmankénti túllépése.
- A programszálak közötti váltás (context switching) lehetőleg gyors legyen. A gyors váltás érdekében a megszakítható ütemezés esetén minden programszál saját veremtárral rendelkezik, s a CPU regiszterek közül csak a legfontosabbakat mentjük el alapértelmezetten.
- A megszakított program utolsó utasítása és a programmegszakítást kiszolgáló eljárás első utasításának végrehajtása között eltelt idő, az úgynevezett interrupt késedelem, minél rövidebb és kiszámítható legyen. A Programmegszakítások c. fejezetben már említettük a programmegszakítási késedelem (interrupt latency) fogalmát és mértékét. Ezen RTOS rendszer rövidíteni nem, csupán rontani tud, például akkor, ha az ütemező kritikus programszakaszain letiltja a programmegszakításokat. Azokat az RTOS termékeket, amelyekben a programmegszakítása nélkül tudját megoldani a kritikus szakaszok hibátlan működését (például atom műveletekkel), azokat "zero interrupt latency" jelzővel hirdetik. Ez természetesen nem azt jelenti, hogy ne lenne interrupt késedelem, hanem azt, hogy ehhez az ütemező nem ad hozzá, menet közben sehol sem tiltja le a programmegszakítási rendszert.
- A programmegszakítás kiszolgálása és a soron következő programszál indítása között eltelt idő (interrupt dispatch latency) is rövid és kiszámítható legyen. Itt arról van szó, hogy a megszakítható ütemezésnél a programmegszakítás végén az ütemezőnek ki kell értékelnie a helyzetet, s a legmagasabb prioritású programszálnak kell átadni a vezérlést (lásd 2. ábra B része, a 6b és 7b között eltelt szakasz).
- Megbízható és időhöz kötött
mechanizmusokat kell bevezetni a folyamatok egymás közötti
kommunikációjának megvalósításához.
- Az RTOS támogassa a többfeladatú
programfutást (multitasking) és a megszakításos ütemezést (preemptive
scheduling).
mbed-RTOS
Az mbed programkönyvtár 2012 óta RTOS támogatást is nyújt, amely a CMSIS 3.0-ban bevezetett RTOS API-n és a nyílt forrásúvá tett ARM/Keil RTX valósidejű operációs rendszeren alapul. A következő oldalakon az mbed-RTOS használatával fogunk megismerkedni.Ízelítőül jöjjön egy egyszerű példa, amelyben a FRDM-KL25Z kártya RGB LED színkomponenseit más-más frekvenciával villogtatjuk. A feladatot természetesen egy egyszerű szuperhurokkal is megoldhattuk volna...
2. lista: A 09_rtos_basic/main.cpp program listája
#include "mbed.h"
#include "rtos.h"
DigitalOut led1(LED1);
DigitalOut led2(LED2);
DigitalOut led3(LED3);
void led2_thread(void const *args) {
while (true) {
led2 = !led2;
Thread::wait(2000);
}
}
void led3_thread(void const *args) {
while (true) {
led3 = !led3;
Thread::wait(4000);
}
}
int main() {
Thread thread2(led2_thread);
Thread thread3(led3_thread);
while (true) {
led1 = !led1;
Thread::wait(1000);
}
}
Az első szembetűnő különbség a korábbi programokhoz képest, hogy az rtos.h fejléc állományt is be kell csatolni. A main függvény maga az első programszál, s ennek elején még két másik programszálat indítunk (Thread objektumok példányosításával). A programszálakhoz egy-egy függvényt rendelünk, amelyek más-más frekvenciával villogtatnak egy-egy LED-et.
Fontos eltérés az is, hogy a korábban használt blokkoló várakozások (wait(), wait_ms(), wait_us() függvények) helyett most a Thread::wait() tagfüggvényt használjuk, amely úgy várakoztatja a programszálat, hogy a többi futását nem gátolja meg.