Practic, orice aplicație încorporată utilizează întreruperi și multe - multitasking și multithreading. Aceste aplicații trebuie să fie puse în aplicare ținând cont de faptul că contextul de execuție se poate schimba în orice moment. De exemplu, o întrerupere care are loc face ca planificatorul să treacă contextul la o sarcină cu prioritate mai mare. Ce se întâmplă dacă sarcinile și funcțiile utilizează variabile comune? Cu un grad ridicat de probabilitate, putem presupune că va exista un dezastru - una dintre sarcinile va strica datele unei alte sarcini. Aproximativ Desigur, același lucru se poate spune despre variabilele comune de întrerupere și despre bucla principală în programele convenționale, cu o singură sarcină.
În lumea sistemelor încorporate, funcția de reentrant trebuie să îndeplinească următoarele reguli:
Regula 1. Funcția efectuează accesul atomic la variabilele globale sau este creată o variabilă pentru fiecare apel al funcției
Regula 2. Funcția nu generează funcții nereentante
Norma 3. Funcția utilizează accesul atomic la modulele periferice
În regulile 1 și 3, se găsește cuvântul "atomic", care se bazează pe cuvântul grecesc pentru "indivizibil". În programare, termenul "atomic" este folosit pentru operațiuni care nu pot fi întrerupte. Să luăm în considerare instrucțiunea:
Este atomic, deoarece nimic altceva decât o resetare poate întrerupe executarea acestei instrucțiuni - instrucțiunea este executată independent de alte sarcini sau întreruperi.
Prima parte a regulii 1 necesită utilizarea atomică a variabilelor globale. Lăsați cele două funcții să utilizeze foobarul global global global. Dacă funcția A conține codul
atunci nu este reentrant, deoarece accesul la foobar variabil nu este atomic, deoarece pentru a schimba foobar, trei acțiuni sunt folosite în loc de unul. Acest cod poate întrerupe o întrerupere, în care se numește funcția B, care efectuează și unele acțiuni cu foobar. După revenirea din întrerupere, este posibil ca valoarea temperaturii să nu corespundă valorii curente a foobarului.
Există un conflict, avionul este în scădere și sute de oameni strigă: "De ce nimeni nu la învățat pe acest nenorocit de programator să folosească funcții reentante". Cu toate acestea, vom presupune că funcția A pare diferită:
Acum operația este atomică, întreruperea nu va opri operația pe foobar în mijloc, conflictul nu va apărea, atunci această operație este reentantă.
Așteaptă ... Știi cu adevărat ce compilator C va genera pentru această operațiune? Pe un procesor x86, acest cod poate arăta astfel:
care, desigur, nu este un cadru atomic, deci expresia foobar + = 1; nu este reentrant. Versiunea atomică va arăta astfel:
Moralul este acesta: fii foarte atent în ipotezele legate de generarea codului atomic de către compilator. În caz contrar, puteți găsi reporterii programului "Maxim" sub ușa dvs. - "Cine este vinovat de eșecul frânelor motoretei Phillip Kirkorov?". În general, întotdeauna trebuie să porniți de la faptul că pentru astfel de expresii compilatorul generează întotdeauna un cod non-atomic
A doua parte a regulii 1 spune că, dacă funcția utilizează acces non-atomic, atunci datele trebuie create pentru fiecare apel al funcției. Prin "apel" ne referim la executarea imediată a unei funcții. În aplicațiile multitasking, funcția poate fi apelată simultan de mai multe sarcini.
Luați în considerare următorul cod:
foo este o variabilă globală al cărei domeniu de aplicare este în afara funcției some_function (). Chiar dacă nici o altă funcție nu folosește foo. datele pot fi corupte dacă some_function () este apelat din diferite sarcini (și, desigur, poate fi apelat simultan).
C și C ++ ne pot proteja de acest pericol. Utilizăm o variabilă locală. Ie definiți foo în interiorul funcției. În acest caz, fiecare some_function apel () va utiliza o variabilă alocată pe stivă (care este, desigur, nu se aplică PIC16 și PIC18. Cu acesta din urmă este adevărat, puteți utiliza compilator Microchip C18, care poate pune în aplicare o stivă de software și, astfel, reintrare. Dar, dacă este necesar este ea acolo?).
O altă opțiune este utilizarea alocării dinamice a memoriei. Este adevărat că trebuie să vă asigurați că funcțiile bibliotecii de alocare dinamică sunt reentante. De obicei nu. Ca și în cazul variabilelor locale, fiecare apel de memorie va fi generat pentru fiecare apel, rezolvând astfel problema fundamentală a reentranței.
Articolul 2 spune că funcția de apelare moșteneste absența reîntregirii în apel. Adică dacă datele sunt răsfăcute în funcția B care a fost apelată din funcția A, este inutil să vorbim despre reentranța lui A.
Folosind compilatoare de la limbi de nivel înalt, ne confruntăm cu o problemă dificilă. Sunteți sigur - într-adevăr sigur - că bibliotecile de compilatoare sunt reentante? Evident, șirul (la C acest lucru nu se aplică) și alte operațiuni complexe ale funcțiilor de apel din biblioteca compilatorului. Cele mai multe compilatoare generează apeluri de funcții ale bibliotecii pentru operații matematice, chiar și pentru multiplicarea trivială și împărțirea numerelor întregi.
Dacă codul dvs. care utilizează funcțiile bibliotecii trebuie să fie reentrant - consultați-le cu acestea. să sprijine producătorul compilatorului și să citească manualul - de obicei, scriu despre el. De asemenea, este necesară prudență atunci când se utilizează biblioteci terțe - stive de protocol, sisteme de fișiere etc.
Norma 3 se aplică numai sistemelor încorporate. Periferia poate fi privită ca o variabilă globală și dacă sunt necesare mai multe acțiuni pentru a menține modulul periferic, puteți obține o problemă de partajare.
Care este cel mai bun mod de a reînființa funcția? Desigur, evitați utilizarea variabilelor globale.
În general, variabilele globale complică foarte mult depanarea codului și provoacă bug-uri subtile. Încercați să utilizați variabile locale sau să alocați dinamic memoria.
Cu toate acestea, variabilele globale reprezintă cea mai rapidă modalitate de a transfera date. De regulă, este imposibil să scapi complet de variabilele globale în sistemele încorporate în timp real. Prin urmare, atunci când folosim resurse partajate (variabile globale sau periferice), trebuie să folosim diferite metode de asigurare a atomicității.
Cea mai obișnuită modalitate este de a dezactiva întreruperile în timpul executării codului non-reentrant. Dacă întreruperile sunt dezactivate, sistemul multitasking se transformă într-o singură sarcină. De fapt, acest lucru nu este chiar adevărat - cine previne după întreruperea unui context de întrerupere a serviciului RTOS de întrerupere? Dezactivați întreruperile, executați o parte nereentantă a codului, permiteți întreruperi. Destul de des se pare ca aceasta:
Cu toate acestea, acest cod este periculos! Dacă do_something () este o funcție globală care este apelată din diferite locuri, atunci la un moment dat poate fi apelată cu întreruperi interzise. Și înainte de întoarcere vor fi permise întreruperi, contextul va fi schimbat. O metodă foarte periculoasă care poate duce la probleme grave.
Și nu utilizați vechea scuză: "Da, dar când scriu codul, sunt foarte atent, eu numesc această funcție doar când sunt sigur că sunt permise întreruperi". Un programator care te (și cu astfel de scuze acest lucru se poate întâmpla foarte repede) va înlocui poate să nu fie conștient de acest limitări grave (mai ales că este puțin probabil să-l aibă în documentație).
Următoarea funcție arată mult mai bine:
Interzicerea întreruperilor mărește timpul de răspuns al sistemului la evenimentele externe (adesea pur și simplu inacceptabil). O modalitate mai ușoară de a asigura reîntoarcerea este să folosești semaphorele. indicând angajarea resurselor. Un semafor este un indicator binar simplu al tipului "on-off", acces la care se face atomic. Semaforul este folosit ca steagul de permisiune care păstrează sarcina în starea inactivă, în timp ce resursa necesară este ocupată.
Aproape toate tipurile comerciale de sincronizare sunt RTOS obiect „semafor“ (de multe ori pune în aplicare de protecție a semaforului mutex numit acces simultan; mutex are proprietăți suplimentare). Dacă utilizați RTOS, semafoarele reprezintă modalitatea de a vă asigura reîntregirea. Nu folosiți RTOS? Adesea văd programele care folosesc un flag-variabil pentru a proteja o resursă:
Arata simplu si elegant, dar acest cod este periculos!