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:
- A stdio (pl. printf) védelme megosztott használat esetén:: Többszálú futtatásnál a korábbi Mbed Online Compiler (Keil ARM C Library) használatakor a Cortex-M3/M4 mikrovezérlőknél a C könyvtár beépített mutexei automatikusan védték a stdio kimenetet. A GCC toolchain (gcc4mbed / Newlib C könyvtár) használatára való áttéréssel azonban ez az automatikus védelem megszűnt. Így GCC környezetben – architektúrától függetlenül – nekünk kell Mutex-szel védenünk a párhuzamosan futó szálakból történő printf, puts vagy putc hívásokat, megakadályozva a kimenetek összekeveredését. A Cortex-M0/M0+ mikrovezérlőknél azonban korábban sem volt automatikus védelem, így a KL25Z-hez készült régi mintaprogramjaink változatlanul használhatók gcc4mbed környezetben is.
- A Mutex megszakításokban nem használható: Megszakításkiszolgáló rutinból (ISR) szigorúan tiltott a Mutex osztály tagfüggvényeinek hívása! A mutex zárolása blokkolhatná a hívót, az ISR pedig nem kerülhet várakozó/felfüggesztett állapotba.
- A printf, malloc és new korlátozása megszakításokban: A stdio függvények (printf, putc, getc stb.), valamint a dinamikus memóriakezelő hívások (malloc, free, new, delete) sem használhatók programmegszakításokban. Ezek a C/C++ könyvtár belső állapotainak kezelési módja miatt nem reentránsak, és nem megszakításbiztosak.
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 10_rtos_mutex projekt Makefile állománya:
PROJECT := 10_rtos_mutex
DEVICES := KL25Z
GCC4MBED_DIR := ../..
GCC4MBED_TYPE := Release
MBED_OS_ENABLE := 1
NEWLIB_NANO := 1
NO_FLOAT_SCANF := 1
NO_FLOAT_PRINTF := 1
include $(GCC4MBED_DIR)/build/gcc4mbed.mk
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 The 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);
}
A 10_rtos_dpp_mutex projekt Makefile állománya:
PROJECT := 10_rtos_dpp_mutex
DEVICES := KL25Z
GCC4MBED_DIR := ../..
GCC4MBED_TYPE := Release
MBED_OS_ENABLE := 1
NEWLIB_NANO := 1
NO_FLOAT_SCANF := 1
NO_FLOAT_PRINTF := 1
include $(GCC4MBED_DIR)/build/gcc4mbed.mk
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