RTOS Mutex
A fejezet tartalma:Az előző fejezet végén bemutatott példaprogram három programszálat futtatott, ám ezek egymástól függetlenül végezték feladatukat, ugyanúgy, mintha három külön mikrovezérlő kártyán futottak volna. Az RTOS alkalmazások többsége azonban nem ilyen, a benne futó programszálak egy nagy feladat részfeladatait végzik, így alkalmanként szükség lehet rá, hogy tevékenységeiket egymáshoz szinkronizálják, a közös használatú erőforrásokon megosztozzanak, vagy adatot cseréljenek egymással. Az mbed-RTOS fejlett eszköztárat biztosít ezekhez a feladatokhoz (jelzőbitek, mutex, szemafor, levelesláda és üzenetek formájában), melyek közül most a megosztott erőforrások kezelésére használt mutex használatát tárgyaljuk.
Mutex (kölcsönös
kizárás)
A mutex név a kölcsönös kizárás angol
elnevezésének (mutual exclusion) rövidítése. Többfeladatos
programozásnál előfordul, hogy két vagy több programszál egyidejűleg
ugyanazt az erőforrást akarja használni, ami nem engedhető meg, mert
pl. összekeverednének a soros porton vagy az USB porton kiküldött
adatok. Ilyenkor versengés alakul ki, s az erőforráshoz hamarabb
forduló programszál egy mutex változó segítségével lefoglalja az
eszközt, majd használat után megszünteti a lefoglalást. A kritikus
szakaszhoz később érkező programszál pedig várakozásra kényszerül, amíg
az erőforrás fel nem szabadul. A programszálak tehát kölcsönösen
kizárják egymást a kritikus szakaszon, nem fordulhat elő, hogy
egyidejűleg vezérlik a közös használatú perifériát.1. ábra: Erőforrás megosztása mutex használatával
A mutex tulajdonképpen az általánosabb szemaforok speciális esete, amikor a szemafor csak két állapotot vehet fel: 0 és 1. Ezért bináris szemafornak is nevezik (szemben az ún. számláló szemaforral, amely többféle értéket is felvehet).
A Mutex objektumosztály tagfüggvényei
A kölcsönös kizárást biztosító mutexek létrehozása és kezelése a Mutex objektumosztály segítségével végezhető, tagfüggvényeit az alábbi táblázatban foglaltuk össze.Függvénynév |
Funkció |
---|---|
Mutex
név |
Létrehoz és inicializál egy
"név" nevű mutex objektumot |
lock(time) |
Várakozik, amíg a mutex
elérhetővé válik. Ha a mutex foglalt, a várakozás alapértelmezetten
korlátlan ideig tarthat, vagy a time
paraméterben ezredmásodpercekben megadott idő leteltekor időtúllépéssel
kilép a várakozásból. |
trylock() |
Megpróbálja lefoglalni a mutex
példányt, de nem várakozik, ha nem sikerül. |
unlock() |
Felszabadítja a korábban
lefoglalt mutex példányt. |
Megjegyzések és korlátozások:
- Az ARM C standard library már beépítetten tartalmaz mutexeket a stdio hozzáférés védelmére, ezért a Cortex-M3 mikrovezérlők (pl. LPC1768) mbed környezetben történő programozása esetén az alábbi mintapéldában bemutatott megoldás szükségtelen. Ezzel szemben az ARM microlib C programkönyvtár (amelyet a Cortex-M0 mikrovezérlők esetében használ az mbed) nincsenek beépített stdio mutexek, a hozzáférés védelméről tehát nekünk kell gondoskodnunk.
- Megszakítás szinten nem hívhatjuk meg a Mutex osztály tagfüggvényeit!
- Az ARM C standard library beépített mutexei miatt megszakítás szinten nem használhatjuk az stdio (printf, putc, getc, stb), malloc és new függvényhívásokat!
Mintapélda a mutex használatára
Az alábbi mintaprogram bemutatja a mutex használatát az stdio hozzáférés védelmére (printf() függvény használata). A program lefordításához importálni kell az mbed-RTOS programkönyvtárat.Hardver követelmények:
- FRDM-KL25z kártya
#include "mbed.h"
#include "rtos.h"
Mutex stdio_mutex;
void notify(const char* name, int state) {
stdio_mutex.lock();
printf("%s: %d\n\r", name, state);
stdio_mutex.unlock();
}
void test_thread(void const *args) {
while (true) {
notify((const char*)args, 0); Thread::wait(1000);
notify((const char*)args, 1); Thread::wait(1000);
}
}
int main() {
Thread t2(test_thread, (void *)"Th 2");
Thread t3(test_thread, (void *)"Th 3");
test_thread((void *)"Th 1");
}
A program több újdonságot tartalmaz,
amelyek magyarázatra szorulnak:- A harmadik sorban stdio_mutex névvel példányosítjuk a Mutex objektumosztályt. A notify() függvényben ezt a lock() és unlock() metódusokkal foglaljuk le, illetve szabadítjuk fel. A Mutex osztály példányosításakor az új objektum automatikusan szabad állásba kerül, így az inicializálással külön nem kell foglalkoznunk.
- A programban három programszálat hozunk létre, de ezek törzse ugyanaz az utasítás-sorozat lesz (a test_thread() függvény). Ilyen esetekben a programszál létrehozásakor, vagyis Thread példányosításakor a második paraméterben adhatunk át egy általános célú mutatót, amellyel a programszálat egyedivé tehetjük. Itt most egy-egy rövid nevet adunk át. A szövegkonstans a ROM-ban tárolódik, csak a szövegkonstansra mutató pointer kerül átadásra paraméterként.
- A programszálak végtelen ciklusban futnak (ezért nincs most a main() függvényben végtelen while ciklus, mivel a meghívott test_thread() függvény sohasem tér vissza) és másodpercenként meghívják a kiíratást végző nofity() függvényt, amellyel a saját nevükön kívül felváltva 0-t vagy 1-et íratnak ki. A közös erőforrás a standard output (esetünkben az UART0 soros port), amelyet a printf() függvény meghívásával veszünk igénybe. A stdio_mutex ennek a hozzáférését védi, hogy csak sorban, egyenként vehessék igénybe a programszálak.
2. ábra: A 10_rtos_mutex program futási eredménye
Mintapélda: Az étkező filozófusok problémája
Az étkező filozófusok esete (az angol szakirodalomban: Dining Philosopher Problem néven ismert) egy olyan probléma, ami könnyen és gyorsan megérthető, de felmerülő problémái valós informatikai problémákká növik ki magukat. A történet így szól: Egy tibeti kolostorban öt filozófus él. Minden idejüket egy asztal körül töltik. Mindegyikük előtt egy tányér, amelyből sohasem fogy ki a rizs. A tányér mellett jobb és bal oldalon is egy-egy pálcika található a 3. ábra szerinti elrendezésben. A filozófusok életüket az asztal melletti gondolkodással töltik. Amikor megéheznek, étkeznek, majd ismét gondolkodóba esnek a következő megéhezésig. És ez így megy az idők végezetéig. Az étkezéshez egy filozófusnak meg kell szereznie a tányérja melletti mindkét pálcikát. Ennek következtében amíg eszik, szomszédai nem ehetnek. Amikor befejezte az étkezést, leteszi a pálcikákat, amelyeket így szomszédai használhatnak.3. ábra: Étkező filozófusok
Elemezzük, milyen problémákkal szembesülhetnek a filozófusaink:
- Holtpont - előfordulhat-e olyan eset, amikor valamelyik filozófus nem eszik, és nem is gondolkodik?
- Kiéheztetés - előfordulhat-e olyan eset, hogy valamelyik filozófus éhen hal?
Könnyen belátható, hogy a holtpont elkerülése a programozó felelőssége. Ha például egyszerre minden filozófus azzal kezdi, hogy megragadja a jobb kezénél levő pálcikát, akkor máris holtpontra jutottunk! Olyan stratégiát kell tehát kidolgozni, amelyben szerepet kap a kooperativitás - azaz ha valamelyik filozófus nem tudja megszerezni mindkét pálcikát, akkor leteszi a kezéből az elsőnek felvettet is, hogy a szomszédai étkezni tudjanak. Ez önmagában még kevés a holtpont elkerüléséhez, mert ha mindegyik filozófus felveszi a jobb kezénél levő pálcikát, majd - intelligens emberek lévén - belátják, hogy ez holtpont, ezért leteszik a pálcikát, azután kezdik elölről, akkor nem haladtunk sokra! Közelebb jutunk a megoldáshoz, ha azt a módszert alkalmazzuk, amit a hálózati kártyák ütközés detektálásakor használnak: véletlenszerűen választott ideig várakoznak, majd újra próbálkoznak.
A i. programszál futásakor mutex lefoglalásánál most nem a végtelen várakozást választjuk, hanem a trylock() metódussal ellenőrizzük, hogy sikerült-e a jobboldali pálcikához tartozó i-1. mutexet lefoglalni. Ha sikerült, akkor megpróbáljuk lefoglalni a másikat (az i. mutexet) is. Ha ez is sikerült, akkor kezdődhet a falatozás, majd letesszük a pálcikákat és kezdődhet a gondolkodás. Ha viszont a második mutexet nem sikerült lezárni, akkor a kooperativitás jegyében feloldjuk az első mutexet is (nehogy holtpont alakuljon ki), majd véletlen hosszúságú ideig várunk, mielőtt újra próbálkozunk.
Hardver követelmények:
- FRDM-KL25z kártya
#include "mbed.h"
#include "rtos.h"
Mutex stdio_mutex;
Mutex chopstick[5]; //Array of mutexes representing the 5 chopsticks
void notify(int num, int state) {
stdio_mutex.lock();
if(state) {
printf("Philospher %d is EATING \n\r", num);
}
else {
printf("Philospher %d is thinking \n\r", num);
}
stdio_mutex.unlock();
}
void philosopher(void const *args) {
while (true) {
if(chopstick[(int)args-1].trylock()) {
if(chopstick[(int)args%5].trylock()) {
notify((int)args,1); //Start EATING
Thread::wait(1000+rand()%1000);
chopstick[(int)args%5].unlock(); //Release chopsticks
chopstick[(int)args-1].unlock();
notify((int)args,0); //Start Thinking
Thread::wait(2000+rand()%2000); //Get's hungry after this time...
}
else {
chopstick[(int)args-1].unlock();
Thread::wait(100+rand()%100); //Wait for random time if failed
}
}
else {
Thread::wait(100+rand()%100); //Wait for random time if failed
}
}
}
int main()
{
Thread t2(philosopher, (void *)2U);
Thread t3(philosopher, (void *)3U);
Thread t4(philosopher, (void *)4U);
Thread t5(philosopher, (void *)5U);
philosopher((void *)1U);
}
Megjegyzések:
- Állapotváltásoknál kiíratást végzünk (lásd notify() függvény). A stdio erőforrás hozzáférés védelmére is definiálnunk kellett egy mutexet (lásd stdio_mutex)!
- Az öt pálcikát reprezentáló mutexeket az indexelhetőség érdekében egy chopstick[] nevű tömbbe rendeztük.
- A notify() függvény első paramétere a programszál sorszáma (1-5), a második paraméter pedig a programszál által reprezentált filozófus "állapota" (0: gondolkodik, 1: étkezik)
- A main() függvényben a programszálak definiálásánál a programszál törzse (a végrehajtandó utasítássor) mindegyik szálra ugyanaz - a philosopher() függvény. Ennek paraméterként adjuk át a programszál sorszámát, mégpedig egy típus nélküli mutató formájában. Ezért hivatkozásnál típuskényszerítést kell végeznünk: (int)args lesz az aktuális programszál sorszáma.
- Az (int)args
sorszámú programszál (azaz filozófus) esetén a jobbra eső pálcikához
tartozó mutex indexe (int)args-1
lesz, a bal kéz felőli pálcikához tartozó mutex indexe pedig (int)args%5
lesz (a kifejezés végén a
%5, ami az
5-tel való osztás maradékát jelenti, azért kell, hogy az 5. filozófus
ne a nemlétező 6. pálcikát akarja felvenni, hanem az elsőt - azaz a
programszál a 0. indexű mutexet zárolja).
4. ábra: A 10_rtos_dpp_mutex program futási eredménye