Mecanisme Multitasking Si Aplicatii de Timp Real. Proiectare Si Implementare
Mecanisme multitasking și aplicații de timp real. Proiectare și implementare
CUPRINSUL
REZUMATUL PROIECTULUI
INTRODUCERE
1. EXECUTIVE DE TIMP REAL
1.1. TASK-URI, PROCESE ȘI THREAD-URI
1.1.1. Task-uri periodice și aperiodice
1.2. SISTEME DE OPERARE MULTITASKING
1.2.1. Planificatorul de task-uri
1.2.2. Starea unui task
1.2.3. Comutarea contextului, tabele de task-uri
1.3. ALGORITMI DE PLANIFICARE
1.4. ALGORITMI DE PLANIFICARE MONOPROCESOR
1.4.1. Algoritmul Round Robin
1.4.2. Algoritmi de planificare cu prioritate statică
1.4.3. Algoritmi de planificare cu prioritate dinamică
1.5. MECANISME DE SINCRONIZARE ȘI COMUNICARE ÎNTRE TASK-URI
1.5.1. Comunicarea între task-uri folosind o zonă de memorie partajată
1.5.2. Comunicarea între task-uri prin transmiterea de mesaje
1.5.3. Semnale
2. EXECUTIVUL DE TIMP REAL EXTR32
2.1. STRUCTURA EXECUTIVULUI
2.1.1. Mecanismul de implementare al funcțiilor de sistem
2.1.2. Implementarea planificatorului de procese
2.2. GESTIONAREA PROCESELOR
2.3. MECANISME DE COMUNICARE ÎNTRE PROCESE
2.4. UTILIZARE ȘI APLICAȚII
2.4.1. Primul program
2.4.2. Ilustrarea caracterului preemtiv al nucleului
2.4.3. Ilustrarea planificării Round Robin
2.4.4. Sincronizarea proceselor prin intermediul semafoarelor
2.4.5. Implementarea unui mecanism poducător-consumator
2.4.6. Utilizarea semnalelor
2.4.7. Implementarea unei arhitecturi client/server
3. APLICAȚIE DE CONTROL ÎN TIMP REAL ÎN REȚEA
3.1. DESCRIEREA PROBLEMEI
3.2. DISCRETIZAREA FUNCȚIILOR DE TRANSFER
3.3. ARHITECTURA APLICAȚIEI
3.4. PREZENTAREA INTERFEȚEI GRAFICE. EXEMPLU DE UTILIZARE
3.4.1. Prezentarea interfeței grafice.
3.4.2. Exemplu de utilizare
4. CONCLUZII
BIBLIOGRAFIE
LISTA FIGURILOR
Figura 1. Independența hardware prin utilizarea unui sistem de operare 16
Figura 2. Stările de planificare ale unui task 22
Figura 3. Mecanismul de comutare a contextului 23
Figura 4. Algoritmul de planificare Round Robin 24
Figura 5. Exemplu de execuție a task-urilor aplicând algritmul RM 26
Figura 6. Comunicație prin intermediul unei zone de memorie partajate 34
Figura 7. Exemplu de comunicare prin transmiterea de mesaje, cazul multiprocesor 35
Figura 8. Transferul mesajeor prin intermediul cozii de mesaje 36
Figura 9. Conținutul stivei la apelul funcției syscall 38
Figura 10. Consola executivului ExTR32 54
Figura 11. Execuția programului prog01 56
Figura 12. Execuția programului prog02. 58
Figura 13. Execuția programului prog03, Round Robin 61
Figura 14. Execuția programului prog04, cazul fără sincronizare 65
Figura 15. Execuția programului prog04, cazul cu sincronizare 65
Figura 16. Execuția programului prog05 71
Figura 17. Execuția programului prog07 75
Figura 18. Relația client-server 75
Figura 19. Execuția progrmului prog08 80
Figura 20. Regulator PID 81
Figura 21. Lege PID real implementată dintr-o lege I și o lege PD real 82
Figura 22. Schema de reglare a sistemului 84
Figura 23. Răspunsul sistemului, cazul continuu 85
Figura 24. Comanda aplicată la intrarea părții fixe, cazul continuu 85
Figura 25. Răspunsul sistemului, cazul discret 87
Figura 26. Comandă regulator, cazul discret 87
Figura 27. Arhitecutra aplicației de control distribuit în timp real 88
Figura 28. Diagrama simplificată de funcționare a aplicației 89
Figura 29. Diagrama UML a aplicației client 90
Figura 30. Diagrama UML a aplicației server 90
Figura 31. Fereastra aplicației server 93
Figura 32. Fereastra aplicației client 94
Figura 33. Setarea parametrilor conexiunii 95
Figura 34. Aplicația care simulează partea fixă a sistemului 96
Figura 35. Aplicația care simulează regulatorul 96LISTA TABELELOR
Tabelul 1. Exemplu de planificare RM, primul set de parametri 26
Tabelul 2. Exemplu de planificare RM, al doilea set de parametri 26
Tabelul 3. Exemplu de planificare EDF, parametrii task-urilor 29
Tabelul 4. Exemplu de planificare EDF, execuția task-urilor 31
Tabelul 5. Calculele exemplului de planificare PF Error! Bookmark not defined.
Introducere
Lucrarea de licența are ca punct de plecare utilizarea sporită a echipamentelor numerice de calcul în conducerea proceselor tehnologice. Acest lucru a fost posibil datorită introducerii în structura acestora a microprocesoarelor și a utilizării sistemelor de operare în timp real [Pet10], oferind astfel posibilitatea îndeplinirii constrângerilor de timp impuse de procesele tehnologice conduse. Acest lucru a constituit un pas remarcabil pe calea reducerii decalajului dintre rezultatele remarcabile oferite de teoria sistemelor automate și tehnicile aplicate în practica conducerii proceselor industriale.
În ultima perioadă, sistemele de calcul în timp real au cunoscut o dezvoltare exponențială, justificată prin faptul că oferă o soluție fiabilă și sigură, eficientă atât din punct de vedere economic, cât și tehnic în aplicații diverse. Astfel, se poate spune că aceste sisteme sunt critice pentru infrastructura unei țări industrializate, având aplicații în:
Conducerea proceselor de producție / automatizări industriale;
Sisteme de comutare feroviare;
Industria automotive;
Industria spațială și aerospațială;
Sisteme de achiziție și monitorizare a mediului înconjurător;
Sisteme de telecomunicații;
Sisteme militare;
Realitate virtuală.
În aplicațiile de timp real, corectitudinea unui calcul depinde nu numai de rezultatul obținut, ci, și de momentul în care acel rezultat a fost generat. Meritele unui sistem de operare în timp real includ [Sha93]:
Răspuns previzibil și rapid la evenimente urgente.
Grad înalt de planificabilitate. Planificabilitatea reprezintă gradul de utilizare a resurselor la care sau sub care pot fi asigurate cerințele de sincronizare a task-urilor.
Stabilitate la suprasarcină tranzitorie. Atunci când sistemul este supraîncărcat de evenimente și este imposibilă îndeplinirea tuturor termenelor limită, trebuie garantată în continuare îndeplinirea termenelor pentru task-urile critice selectate.
Unele dintre cele mai cunoscute și utilizate sisteme de operare în timp real sunt: LynxOS, OSE, QNX, RTLinux, VxWorks, Windows CE.
Sistemul LynxOS este un sistem de operare în timp real produs de compania LynuxWorks ce se comoportă într-o manieră similară sistemului Unix. Acest sistem oferă o conformitate completă cu standardele POSIX (Portable Operating System Interface) și, mai recent, compatibilitatea cu Linux.
Prin realizarea acestei lucrări se urmărește ilustrarea funcționalităților de bază ale unui sistem de operare în timp real și utilizarea acestora în realizarea unei aplicații de control distribuită, în timp real. Conținutul lucrării este structurat pe patru capitole, ultimul capitol fiind dedicat concluziilor.
Capitolul 1, denumit Executive de timp real, prezintă fundamentele teoretice ale sistemelor de operare în timp real. Sunt abordate aspecte cu privire la sistemele de operare multi-tasking, task-uri, algoritmi de planificare mono și multiprocesor, mecanisme de sincronizare a task-urilor și comunicare între acestea, secțiuni critice.
Capitolul 2, denumit Prezentarea nucleului ExTR32, ilustrează structura nucleului de timp real ExTR32, punerea în funcțiune și utilizarea acestuia. Folosind mediul Cygwin sunt rulate scripturile de compilare rezultând o imagine a unei dischete boot-abile ce conține nucleul de timp real ExTR32 și aplicațiile dezvoltate pe baza acestui sistem. Imaginea rezultată este încărcată în emulatorul PC open source Bochs IA 32 (x86), în care nucleul ExTR32 va fi lansat în execuție. Din linia de comandă a nucleului pot fi rulate aplicațiile dezvoltate. De asemenea în acest capitol sunt dezvoltate și discutate diverse apicații ce ilustrează caracteristicile nucleului ExTR32, cum ar fi algoritmii de planificare folosiți și mecanismele de sincronizare și comunicare între task-uri.
Capitolul 3, denumit Aplicație de control în timp real în rețea, prezintă dezvoltarea unei aplicații de control în rețea de tip „soft real time” folosind protocolul TCP/IP. În prima parte a capitolului sunt abordate modelul matematic al procesului tehnologic, discretizarea funcțiilor de transfer a sistemului condus și a regulatorului proiectat, precum și dezvoltarea unui algoritm numeric de reglare. În a doua parte a capitolului, folosind Microsoft Visual Studio Express Edition sunt dezvoltate două aplicații client și server ce vor rula pe mașini diferte conectate la o rețea ethernet. Aplicația client implementează sistemul condus, iar aplicația server regulatorul. Aceasta din urmă primește prin rețea valoarea mărimii reglate de la aplicația client, calculează valoarea mărimii de comandă în conformitate cu algoritmul de reglare proiectat anterior și o transmite prin rețea aplicației client care calculează răspunsul sistemului la comanda aplicată de regulator, realizându-se astfel circuitul închis al informațiilor. De asemenea, aplicația server permite modificarea on-line a referinței și a parametrilor regulatorului, precum și vizualizarea în timp real a comenzii date de regulator. Aplicația client permite modificarea on-line a parametrilor pății fixe și afișarea în timp real a răspunsului sistemului și/sau comanda regulatorului.
Executive de timp real
Sistemele de operare sunt medii software ce oferă o interfață între utilizator și nivelul hardware al sistemului de calcul. Ele oferă o interfață constantă și un set de utilitare care permit utilizatorilor să folosească sistemul rapid și eficient. Sistemele de operare permit ca aplicațiile software să fie mutate de la un sistem la altul și, prin urmare, fac posibil ca aceste aplicații să fie independte de plaforma hardware pe care vor rula [Hea03].
Figura . Independența hardware prin utilizarea unui sistem de operare
Obiectivele generale ale unui sistem de operare sunt:
automatizarea operațiilor standard în toate etapele de exploatare a sistemului de calcul;
minimizarea efortului uman pentru utilizarea sistemului de calcul;
optimizarea utilizării resurselor sistemului de calcul;
creșterea eficienței globale în utilizarea sistemului de calcul prin:
creșterea vitezei de execuție a prelucrărilor;
reducerea timpului de răspuns al sitemului;
creșterea gradului de utilizare a resurselor prin utilizarea lor la capacitate maximă;
Funcțiile prin intermediul cărora sistemul de operare realizează aceste obiective sunt:
funcția de instalare automată a unui nou sistem de operare pe un sistem de calcul;
funcția de încărcare în memoria internă a sistemului de operare, la pornirea sistemului de calcul;
funcția de configurare dinamică a sistemului de operare conform cu modificările intervenite în structura hardware sau cu necesitățile de exploatare a sistemului. Această funcție permite instalarea altor drivere de echipamente decât cele standard și definirea unor parametri de funcționare ai sistemului, permițând astfel modificarea, extinderea sau îmbunătățirea capacităților de funcționare ale sistemului de operare, în cadrul arhitecturii de bază a sistemului de calcul;
efectuarea operațiilor de intrare / ieșire la nivel fizic, pentru a permite utilizatorului tratarea echipamentelor periferice la nivel logic, adică independent de caracteristicile constructive ale lor. Această funcție permite degrevarea utilizatorului de sarcina tratării specifice a fiecărui tip de echipament periferic în parte;
oferirea unei interfețe cu utilizatorul, prin intermediul unui limbaj specific, numit limbajul de comandă al sistemului de operare. Prin intermediul acestui limbaj, utilizatorul transmite comenzi sistemului de operare; ele sunt traduse și lansate în execuție de programul interpretor de comenzi al sistemului de operare. În cele mai multe dintre sistemele de operare actuale, interfața cu utilizatorul este asigurată folosind metode grafice evoluate și principii noi de comunicare, rezultatul fiind o modalitate mult mai „prietenoasă” de dialog cu utilizatorul;
controlul execuției programelor: sistemul de operare încarcă programull de operare realizează aceste obiective sunt:
funcția de instalare automată a unui nou sistem de operare pe un sistem de calcul;
funcția de încărcare în memoria internă a sistemului de operare, la pornirea sistemului de calcul;
funcția de configurare dinamică a sistemului de operare conform cu modificările intervenite în structura hardware sau cu necesitățile de exploatare a sistemului. Această funcție permite instalarea altor drivere de echipamente decât cele standard și definirea unor parametri de funcționare ai sistemului, permițând astfel modificarea, extinderea sau îmbunătățirea capacităților de funcționare ale sistemului de operare, în cadrul arhitecturii de bază a sistemului de calcul;
efectuarea operațiilor de intrare / ieșire la nivel fizic, pentru a permite utilizatorului tratarea echipamentelor periferice la nivel logic, adică independent de caracteristicile constructive ale lor. Această funcție permite degrevarea utilizatorului de sarcina tratării specifice a fiecărui tip de echipament periferic în parte;
oferirea unei interfețe cu utilizatorul, prin intermediul unui limbaj specific, numit limbajul de comandă al sistemului de operare. Prin intermediul acestui limbaj, utilizatorul transmite comenzi sistemului de operare; ele sunt traduse și lansate în execuție de programul interpretor de comenzi al sistemului de operare. În cele mai multe dintre sistemele de operare actuale, interfața cu utilizatorul este asigurată folosind metode grafice evoluate și principii noi de comunicare, rezultatul fiind o modalitate mult mai „prietenoasă” de dialog cu utilizatorul;
controlul execuției programelor: sistemul de operare încarcă programul în memoria internă, pentru execuție, îl lansează în execuție, urmărește execuția în toate etapele sale și încheie execuția programului;
gestionarea alocării resurselor sistemului de calcul: sistemul de operare gestionează alocarea timpului de utilizare al procesorului, a memoriei interne, accesul la fișiere, accesul la echipamentele periferice, etc. pe toată durata execuției unui program, în scopul utilizării cât mai eficiente a acestor resurse. În cazul în care este posibilă executarea simultană a mai multor programe, sistemul de operare realizează alocarea resurselor între programe pe baza unor criterii de alocare, în scopul optimizării execuției programelor, conform obiectivelor de eficiență de mai sus;
asigurarea protecției între utilizatori, acolo unde sistemul de operare permite accesul concomitent al mai multor utilizatori (programe) la resursele sistemului de calcul, și asigurarea protecției între programe, fie că este vorba de programe utilizator sau programe ale sistemului de operare;
tratarea erorilor: sistemul de operare poate trata erori la nivelul mașinii fizice (de exemplu: erori de citire / scriere în memoria externă, erori de acces la un echipament periferic, lipsa din configurația sistemului de calcul a unui echipament, etc.) sau erori logice, care pot să apară în timpul executării unui program (de exemplu: operații interzise, ca împărțirea la 0, tentativa de acces în zone protejate ale memoriei interne, tentativa de execuție a unor instrucțiuni privilegiate, etc.).
Sistemele de timp real sunt acele sisteme pentru care comportarea lor corectă depinde nu doar de rezultatul logic al calculului, dar și de momentul de timp în care rezultatele sunt produse. De exemplu, în sistemele avionice, programul sistemului de control al zborului trebuie executat într-un interval de timp fix pentru a putea controla cu precizie aparatul de zbor. Sistemele elctronice din industria auto au constrângeri de timp strânse cu privire la managementul motorului sau sistemul de control al transmisiei, constrângeri ce derivă din sistemele mecanice pe care acestea le controlează. În funcție de precizia cu care sunt îndeplinite constrângerile de timp cu privire la furnizarea rezultatelor, sistemele de timp real se impart în trei categorii:
sisteme de tip „hard real time”: sunt acele sisteme în care orice depășire a unui termen limită poate conduce la defectarea întregului sistem. Astfel de sisteme sunt utilizate în aplicații critice cum ar fi, de exemplu, conducerea centralelor nucleare, sistemele de control din industria aerospațială sau industria automotive;
sisteme de tip „firm real time”: nerespectarea ocazională a termenelor limită este tolerabilă, dar poate degrada performanța sistemului. Rezultatele furnizate după termenul limită sunt inutile;
sisteme de tip „soft real time”: precizia cu care sunt îndeplinite constrângerile de timp este relativ scăzută, scopul fiind respectarea doar a anumitor termene limită în vederea optimizării unor criterii specifice aplicației.
Task-uri, procese și thread-uri
Modulele software executate de către un sistem de operare pot fi referite ca task-uri, procese sau thread-uri.
Task-ul reprezintă entitatea de lucru în cadrul unui sistem de operare, care are control asupra resusrselor resurselor de calcul. Acesta poate fi definit ca o secvență de instrucțiuni, cu rolul de a îndeplini o anumită sarcină a unei aplicații.
Un proces reprezintă o singură execuție a unui program. Dacă același program este rulat de două ori, au fost create două procese diferite. Fiecare proces are propria sa stare, care include nu doar starea regiștrilor procesorului, ci și starea zonei de memorie alocate [Wol08]. În general, procesele au alocate pentru excuție zone de memorie diferite. Procesele care împart aceeași zonă de memorie se numesc thread-uri.
Termenii de task, proces și thread sunt interschimbabili, dar, în practică, se pot referi la aspecte diferite în cadrul sistemului de operare. De exemplu, în cazul unui sistem de operare de complexitate redusă, nu există nici o diferență între un thread și un proces. În schimb, în cazul unui sistem multitasking, multiuser sunt definite trei nivele ierarhice: un proces este format din mai multe task-uri care la rândul lor sunt formate din mai multe thread-uri.
Task-uri periodice și aperiodice
O aplicație, referită ca un taskset notat τ, este alcătuită dintr-un set static de n task-uri (). Atunci când este utilizată planificarea cu pioritate fixă a task-urilor, numărul task-ului este, de asemenea, utilizat pentru a indica o prioritate unică i, de la 1 la n.
Majoritatea cercetărilor în domeniul sistemelor de timp real se concentrează pe două modele simple de task-uri: modelul task-ului periodic și modelul task-ului aperiodic. În ambele modele, task-urile pot conduce la o secvență infinită de apelări (sau sarcini). În modelul task-ului periodic, sarcinile de îndeplinit ale unui task apar strict periodic, separate de un interval de timp fix. În modelul task-ului aperiodic, fiecare sarcină de îndeplinit a unui task poate apărea la orice moment de timp după ce a trecut un interval minim de timp de la apariția anterioară a sarcinii pentru același task.
Fiecare task este caracterizat de:
timpul limită maxim (deadline) ;
timpul de execuție maxim ;
perioda de repetiție .
Utilizarea a taskului este dată de raportul . Utilizarea a unui taskset reprezintă suma tuturor utilizărilor task-urilor componente. Timpul maxim de răspuns al unui task se definește ca fiind cel mai lung timp de la apariția sarcinii de îndeplinit a task-ului până la execuția completă. Hiper-perioada a unui taskset se definește ca fiind cel mai mic multiplu comun al perioadelor task-urilor componente [Dav11].
Există trei nivele de constrângeri cu privire la deadline-urile task-urilor:
Deadline-uri implicite. Deadline-ul task-ului este egal cu perioda acestuia .
Deadline-uri constrânse. Deanline-ul task-ului este mai mic sau egal cu perioada acestuia .
Deadline-uri arbitrare. Deadline-ul task-ului poate fi mai mic decât, egal cu, sau mai mare decât perioada task-ului.
Sisteme de operare multitasking
Pentru majoritatea aplicațiilor de conducere, un sistem monotasking este foarte restrictiv. Ceea ce este necesar este un sistem de operare care poate rula mai multe aplicații simultan și pune la dispoziție un mecanism de sincronizare și comunicare între task-uri.
Principiul de funcționare al unui sistem multitasking se bazează pe împărțirea timpului de execuție procesor în intervale discrete de timp. Fiecare aplicație sau task necesită un anumit număr de intervale de timp pentru a-și finaliza execuția. Nucleul sistemului de operare decide cărui task i se poate atribui următorul interval, astfel încât, în loc ca un task să se execute continuu până la finalizare, execuția sa este intercalată cu cea a altor task-uri. Această împărțire a intervalelor de execuție pe procesor între mai multe task-uri, face posibilă rularea mai multor aplicații simultan.
În prezent există două tipuri de multitasking:
Multitasking cooperativ – task-urile cooperează între ele pentru a crea iluzia de multitasking. Acest lucru se realizează prin oferirea periodică altor task-uri a posibilității de a prelua controlul procesorului. Desavantajul acestui tip de multitasking este acela că sistemul poate fi distrus de către un singur task care acaparează întreaga putere de procesare. Această metodă poate fi acceptabilă pentru un calculator de uz personal, dar nu este destul de fiabilă pentru a putea fi utilizată în cadrul unui sistem de timp real.
Multitasking preemtiv – implică utilizarea unui mecanism de întrerupere care suspendă execuția task-ului curent și invocă un planificator pentru a determina următorul task care va fi lansat în execuție. Prin urmare, toate task-urile vor avea alocat la un moment dat un anumit interval de timp de execuție pe procesor. Nucleul sistemului de operare poate iniția o comutare de stare (context switch) pentru a îndeplini constrângerile de prioritate impuse de un algoritm de planificare. Atunci când task-ul curent, aflat în execuție, este întrerupt de către un alt task cu o prioritate mai mare, planificarea utilizată este cunoscută sub numele de planificare preemtivă a task-urilor.
Multitaking-ul preemtiv permite sistemelor informatice să garanteze îndeplinirea constrâgerilor de timp alocate tuturor task-urilor. De asemenea oferă posibiltatea sistemului, de a trata rapid evenimente externe importante, cum ar fi apariția unor date de intrare ce necesită atenția imediată a unui task.
La orice moment de timp, task-urile pot fi grupate în două categorii: task-uri care așteaptă date de intrare/iesire (referite ca „I/O bound”) și task-uri care utilizează în întregime procesorul. În primele sisteme, task-urile intrau adesea într-o stare „busy wait”, atunci când așteptau datele de intrare solicitate. În tot acest timp, task-ul nu efectua operații de calcul utile, dar deținea în continuare controlul complet al procesorului. Odată cu apariția întreruperilor și a multitasking-ului preemtiv, task-urile „I/O bound” au putut fi „blocate” până la apariția datelor solicitate, permițând altor task-uri utilizarea procesorului. Cum sosirea datelor solicitate genereză o întrerupere, este garantată întoarcerea în timp util în execuție a task-urilor blocate.
Chiar dacă tehinicle multitasking au fost dezvoltate inițial pentru a permite mai multor utilizatori să partajeze o singură mașină, a devenit imediat evidentă utilitatea multitasking-ului, indiferent de numărul de utilizatori. Multe sisteme de operare, de la mainframe-uri până la calculatoarele personale destinate unui singur uilizator și sistemele de control non-utilizator (cum ar fi cele utilizate la navele spațiale robotizate) au cunoscut utilitatea suportului multitasking dintr-o varietate de motive. Multitasking-ul permite ca un singur utilizator să poată rula mai multe aplicații în același timp sau de rula procese în „background” păstrând în același timp controlul sistemului.
Planificatorul de task-uri
Componenta sistemului de operare, care determină următorul task care va fi lansat în execuție se numește planificator de task-uri.
În cazul sistemelor de operare în timp real, obiectivul planificatorului de task-uri este acela de a garanta respectarea constrângerilor de timp ale tuturor task-urilor din sistem, în special în cazul sistemelor de tip hard real-time. În prezent, toate sistemele de timp real folosesc multitasking-ul preemtiv.
Starea unui task
Sistemul de operare consideră un task ca fiind într-una din următoarele stări de bază: waiting, ready sau run. La un moment dat există cel mult un singur task aflat în starea de excuție; în cazul în care nu sunt operații utile de efectuat, un task neoperativ poate fi tulizat pentru a efectua o operație nulă. Orice task care poate fi executat se află în starea ready; planificatorul de task-uri alege dintre task-urile aflate în starea ready următorul task care va intra în starea run. Un task nu poate fi întotdeauna în starea ready. De exemplu, un task poate aștepta date de la un dispozitiv de intreare/ieșire sau de la un alt task, ori este lansat în execuție de un timer care nu a expirat încă. Astfel de task-uri se află în starea waiting. Un task trece în starea waiting atunci când are nevoie de date care nu au fost încă primite sau și-a terminat de îndeplinit sarcina pentru perioada curentă. Un task trece în starea ready atunci când primește toate datele necesare sau trece într-o perioadă nouă. Un task trece în starea run atunci când are toate datele necesare, se află în starea ready, iar planificatorul de task-uri selectează acest task ca fiind următorul task ce trece în starea run [Wol08]. Figura 2, ilustrează trazițiile posibile între stări, disponibile pentru un task.
Figura . Stările de planificare ale unui task
Comutarea contextului, tabele de task-uri
Comutarea contextului unui task aflat în execuție este efectuată la apariția unei întreruperi hardware generată de un eveniment și constă în: întreruperea task-ului curent, salvarea regiștrilor procesorului într-o tabelă specială destinată acestui task și plasarea sa în lista de așteptare „ready”. Tablele speciale, adesea referite ca blocuri de control ale task-urilor, stochează toată informația necesară sistemului de operare cu privire la un task, de exemplu: memoria utilizată, nivelul de prioritate în sistem sau management-ul erorilor. Atunci când un task este înlocuit de un altul, informația de context este cea care se schimbă.
Lista de așteptare „ready” conține toate task-urile și starea acestora, și este folosită de planificatorul de task-uri pentru a decide care este următorul task care va fi lansat în execuție. Algoritmul de planificare determină secvența de execuție pe baza priorității și stării fiecărui task. De exemplu, dacă un task așteaptă finalizarea unui apel intrare/ieșire, atunci acesta va fi menținut în lista de așteptare „ready” până la finalizarea apelului.
O dată ce un task este selectat pentru a intra în execuție, conținutul regiștrilor și informația de stare la momentul în care a avut loc ultima comutare a contextului sunt încărcate înapoi în procesor. Noul task își continuă execuția ca și cum nimic nu s-ar fi întâmplat până la următoarea comutare a contextului. Aceasta este metoda de bază utilizată de către toate sistemele de operare multitasking [Hea03].
Figura . Mecanismul de comutare a contextului
Algoritmi de planificare
Algoritmii de planificare pot fi împarțiți în două categorii: algoritmi de planificare off-line și algoritmi de planificare on-line. În cazul algoritmilor de planificare off-line toate deciziile cu privire la planificarea execuției task-urilor sunt luate înainte de pornirea sistemului iar planificatorul are informații complete despre toate task-urile. În timpul rulării, task-urile snt executate într-o ordine prestabilită. Planificarea off-line este utilă în cazul sistemelor de tip hard real-time care au date complete cu privire la toate task-urile aplicației de conducere, deoarece se poate crea un algoritm de planificare care să asigurea îndeplinirea termenelor limită de execuție ale tuturor task-urile, dacă există un astfel de algoritm de planificare.
În cazul algoritmilor de planificare on-line, deciziile cu privire la planificarea execuției task-urilor sunt luate în timpul funcționării sistemului. Deciziile de planificare sunt luate pe baza priorităților task-urilor care sunt atribuite în mod static sau dinamic. În cazul algoritmilor de planificare cu prioritate statică, prioritățile task-urilor sunt atribuite înainte de punerea în funcțiune a sistemului. În cazul algoritmilor cu prioritate dinamică, prioritățile task-urilor sunt atribuite în timpul execuției.
În funcție de arhitectura sistemului, algoritmii de planificare pot fi de asemenea împărțiți în două categorii: algoritmi de planificare monprocesor și multiprocesor.
Algoritmi de planificare monoprocesor
Algoritmul Round Robin
Algoritmul Round Robin este un algoritm de planificare fără priorități în care fiecărui task îi este alocat pentru execuție un interval egal de timp numit cuantă de timp, toate task-urile în starea Ready fiind plasate într-o coadă circulară.
Pașii algoritmului Round Robin:
Planificatorul menține o coadă a task-urilor Ready și o coadă a task-urilor blocate de operații intrare/ieșire (aflate în starea Waiting).
Un task nou creat este adăugat în coada Ready. Blocul de control al unui task finalizat este eliminat din structura de date utilizată pentru planificare.
Planificatorul alege întotdeauna pentru execuție primul task din coada Ready.
Atunci când cuanta de timp este finalizată, task aflat în execuție este întrerupt și plasat în coada Ready. Toate task-urile sunt executate într-o manieră de tipul FIFO (First Come, First Served – Primul Venit, Primul Servit), dar acestea sunt întrerupte după trecerea unei perioade egală cu cuanta de timp asociată algoritmului. Astfel, un task își va finaliza execuția fie in intervalul unei cuante de timp, fie va fi plasat în coada Ready și va fi relansat în execuție la un moment ulterior de timp.
Gestiunea evenimentelor presupune următoarele acțiuni:
Atunci când un task emite o cerere pentru o operație intrare/ieșire, acesta este înlaturat din coada Ready și este adăugat în coada Waiting.
Atunci când o operație intrare/ieșire, așteptată de un task, este finalizată, task-ul respectiv este înlăturat din coada Waiting și adăugat în coada Ready.
Figura . Algoritmul de planificare Round Robin
În figura de mai sus este ilustrat algoritmul de planificare Round Robin pentru un set de opt task-uri notate cu , unde .
Avantaje
Algoritmul Round Robin asigură o distribuire egală a resurselor procesorului tuturor task-urilor. Este ușor de implementat, iar dacă se cunoaște numarul maxim de task-uri ce pot fi adăugate în coada Ready, se poate calcula timpul maxim de răspuns pentru un proces.
Dezavantaje
Oferind în mod egal fiecărui task acces la resursele procesorului nu este întotdeauna o soluție viabilă, performanța sistemului fiind influențată de dimensiunea cuantei de timp și numărul de task-uri aflate în coada Ready. Alegând o cuantă de timp prea mare, timpul de răspuns al task-urilor crește considerabil, iar acest lucru nu este tolerabil într-un mediu interactiv. Alegând o cuantă de timp prea mică, acest lucru determină apariția frecventă a unor comutări de context inutile, ceea ce conduce la scăderea eficienței algoritmului.
Algoritmi de planificare cu prioritate statică
Algoritmul de planificare Rate-Monotonic (RM)
Algoritmul Rate-Monotonic, introdus de Liu și Layland, este unul dintre primii algoritmi de planificare dezvoltați pentru sistemele de timp real, fiind utilizat în continuare pe scară largă.
Teoria care stă la baza algoritmului Rate-Monotonic este cunoscută sub numele de analiza Rate-Monotonic (Rate-Monotonic Analysis – RMA). Această teorie utilizează un model relativ simplu al sistemului [Wol08]:
Toate task-urile sunt executate periodic pe un singur procesor.
Timpul de comutare a contextului este neglijabil.
Nu există dependențe de date între task-uri.
Timpul de execuție al unui task este constant.
Pentru toate task-urile, termenul limită de execuție este egal cu perioada.
Task-ul cu prioritatea cea mai mare, aflat în starea Ready, este întotdeauna selectat pentru execuție.
În acest algoritm, prioritățile task-urilor sunt atribuite invers proporțional cu perioada acestora, astfel task-urile cu cea mai mică perioadă primesc cea mai mare prioritate. Acest criteriu de atribuire a priorităților este unul optim, prin faptul că oferă cea mai mare utilizare a procesorului, asigurând în același timp respectarea termenelor limită de execuție pentru toate task-urile [Wol08].
Ca exemplu se consideră un set simplu de task-uri împreună cu caracteristicile lor:
Tabelul 1. Exemplu de planificare RM, primul set de parametri
Fie , și prioritățile task-urilor , și, respectiv, . Aplicând principiile RMA acestui set, se obține . În Figura 5 este reprezentată execuția task-urilor pentru un interval de timp egal cu hiper-perioada, care este egală cu 12.
Figura . Exemplu de execuție a task-urilor aplicând algritmul RM
Cele trei perioade încep de la 0. Datele necesare pentru sosesc primele. Având în vedere că este task-ul cu cea mai mare prioritate, își poate începe execuția imediat. După o unitate de timp, se sfârșește și iese din starea Run până la începutul următoarei perioade. La momentul de timp 1, își începe execuția, fiind task-ul cu cea mai mare prioritate aflat în starea Ready. La momentul de timpt 3, se termină iar și începe execuția. Următoarea iterație a lui începe la momentul de timp 4, punct în care el întrerupe task-ul , care mai primește încă o unitate de timp la momentul execuției iterației doi a task-urilor și . Însă, task-ul nu reușește să își termine execuția decât după cea de-a treia iterație a task-ului .
În continuare se consideră următorul set de timpi de execuție, dar cu aceiași timpi limită:
Tabelul 2. Exemplu de planificare RM, al doilea set de parametri
În acest caz, se poate arăta că nu există nicio atribuire fezabilă a priorităților care să garanteze o bună planificare. Chiar dacă fiecare proces, privit în mod independent are un timp de execuție semnificativ mai mic decât perioada sa, combinarea tuturor task-urilor poate necesita mai mult de 100% din timpul de execuție disponibil pe procesor. Spre exemplu, într-un interval de 12 unități de timp, task-ul trebuie executat de trei ori, fiind necesare 6 unități din timpul de execuție disponibil pe procesor; task-ul trebuie executat de două ori , având nevoie de 6 unități de timp pe procesor, iar task-ul trbuie executat o singură dată, având nevoie de 3 unități de timp pe procesor. În total sun necesare 6 + 6 + 3 = 15 unități de timp de execuție pe CPU. Se observă că este mai mult decât cele 12 unități de timp disponible, ceea ce depășește în mod clar capacitatea de calcul a procesorului [Wol08].
Liu și Layland [Liu73] au dovedit că modul de atribuire a priorităților după algoritmul RMA este optimal utilizând analiza critical-instant. Momentul critic pentru un task este definit ca momentul din timpul execuției la care task-ul are cel mai mare timp de răspuns. Este ușor de dovedit ca momentul critic pentru orice task τ, sub un model RMA, apare atunci când este starea Ready și toate celelalte task-uri cu prioritate mai mare sunt și ele în starea Ready – dacă trecem orice task cu prioritate mai mare în starea Waiting, atunci timpul de răspuns al task-ului T nu poate decât să scadă.
Analiza moment critic (critical-instant) poate fi utilizată pentru a determina dacă există o planificare fezabilă pentru un set de task-uri. În exemplul anterior, pentru cel de-al doilea set de parametri ai task-urilor nu există nicio planificare fezabilă.
Presupunem că perioadele și duratele de execuție a două task-uri și sunt , și , cu . Putem să generalizăm rezultatul din exemplul anterior astfel încât să arătăm cerințele totale de timp pe procesor pentru cele două task-uri în două cazuri. În primul caz presupunem că are o prioritate mai mare. În cel mai rău caz este executat task-ul o dată în prioada sa, iar execuția task-ului are atâtea iterații câte încap în același interval. Având în vedere că sunt iterații ale lui în timpul unei singure perioade a task-ului , condiția care trebuie îndeplinită de timpii de execuție ai task-urilor astfel încât planificarea lor să fie fezabilă, neglijând timpul necesar comutării contextului este:
(1.4.2.1.1)
Dacă, pe de altă parte dăm oprioritate mai mare lui , atunci o analiză critical-instant ne va spune că, în cel mai rău caz, task-ul și task-ul sunt executate într-una din perioadele lui , ceea ce conduce la următoarea condiție de planificabilitate:
(1.4.2.1.2)
Există cazuri când prima condiție poate fi îndeplinită, relația (1.4.2.1.1), iar cea de-a doua nu, relația (1.4.2.1.2). Nu sunt cazuri în care ce-a de-a doua condiție poate fi îndeplinită iar prima nu. Prin inducție se poate demonstra că task-ul cu perioada mai scurtă ar trebui ca mereu să îi fie atribuită o prioritate mai mare, considerând seturi de task-uri de lungime arbitrară.
Din păcate, deși algoritmul de planificare RM este optim, acesta nu poate garanta o utilizare de 100% a procesorului. În cadrul algoritmului RM, utilizarea totală a procesorului pentru un set de n task-uri este dată de relația:
(1.4.2.1.3)
Raportul reprezintă fracțiunea de timp necesară procesorului pentru a executa task-ul i. Se poate arăta că pentru un set de două task-uri, folosind algoritmul de planificare RM, utilizarea procesorului U, nu va fi mai mare decât: . Cu alte cuvinte, procesorul va fi inactiv cel puțin 17% din timp. Acest timp de inactivitate este datorat faptului că prioritățile sunt atribuite static. Când sunt m task-uri cu priorități fixe, utilizarea maximă a procesorului nu va fi mai mare decât:
(1.4.2.1.4)
Pe masură ce m tinde la infinit, limita maximă a utilizarii procesorului este , procesorul fiind inactiv pentru 31% din timp. Asta nu inseamnă că procesorul nu va putea fi utilizat niciodată în proporție de 100%. Dacă perioadele task-urilor sunt dispuse corespunzător, atunci acestea pot fi planificate astfel încât să se obțină o utilizare de 100% a procesorului. Însă, ultima limită superioară de 69% ne demonstrează că algoritmul RMS poate, în unele cazuri, să ofere o utilizare a procesorului cu mult sub 100% [Wol08].
Algoritmi de planificare cu prioritate dinamică
Algoritmul de planificare Earliest Deadline First (EDF)
Earliest deadline first (EDF) este un alt algoritm de planificare bine-cunoscut care a fost, de asemenea, studiat de Liu și Layland. Prioritatea task-urilor este actualizată în timpul execuției. Ca și rezultat el poate obține o utilizare a procesorului mai mare decât algoritmul RM [Wol08].
Algoritmul EDF este de asemenea foarte simplu: prioritățile sunt atribuite în funcție de termenul limită. Task-ul cu prioritatea cea mai mare este cel care are termenul limită cel mai apropiat în timp, iar task-ul cu prioritatea cea mai mică este cel cu termenul limită cel mai târziu. În mod clar prioritățile trebuiesc recalculate la terminarea fiecărui task. Totuși, pasul final făcut de sistemul de operare în timpul procedurii de planificare este același ca și cel pentru algoritmul RM – task-ul aflat în starea Ready și cu prioritatea cea mai mare este ales pentru a fi executat.
Ca și exemplu de aplicare a lgoritmului EDF, se consideră următorul set de task-uri:
Tabelul 3. Exemplu de planificare EDF, parametrii task-urilor
Hiperperioada este 60. Pentru a putea observa întraga periodă, execuția task-urilor este reprezentată sub formă de tabel:
Tabelul 4. Exemplu de planificare EDF, execuția task-urilor
În Tabelul 4 se observă că a rămas doar un singur interval de inactivitate la t = 30, rezultând o utilizare de 59/60.
Liu și Layland au demonstrat că algoritmul EDF poate obține o utilizare de 100% a procesorului. Condiția ca planificarea unui set de task-uri să fie fezabilă este ca utilizarea procesorului pentru acel set (calculată în aceeași manieră ca și pentru RMA) să fie mai mică sau egală cu 1. De asemenea, ei au arătat că atunci când un sistem care utilizează algoritmul EDF este supraîncărcat și ratează termene limită, el va rula la 100% din capacitate pentru un timp, înainte ca termenul limită să fie depășit [Wol08].
Algoritmul Least Laxity First (LLF)
Algoritmul Least Laxity First (LLF), de asemena cunoscut și ca Minimum Laxity First (MLF), atribuie o prioritate mai mare task-ului care are cea mai mică laxitate. Laxitatea (întârzierea maximă) a unui task la momentul t, , este definită dupa cum urmează:
(1.4.3.2.1)
unde:
– momentul de timp la care task-ul trebuie să termine execuția;
– timpul necesar procesorului pentru a executa task-ul.
Laxitatea este o măsură a flexibilității disponibile pentru planificarea execuției unui task. O laxitate de înseamnă că task-ul poate fi întârziat cel mult unități de timp, fără a depăși termenul limită asociat [Sal07].
Un task cu laxitate zero trebuie trecut imediat în starea Run și executat fără preemțiune, altfel constrângerea de timp a acestui task nu va fi îndeplinită. O laxitate negativă indică faptul că task-ul va depăși termenul limită indiferent de momentul în care acesta va fi lansat în execuție.
O problemă majoră a algoritmului LLF apare atunci când task-uri diferite au laxități egale. Acest lucru conduce la comutări de context frecvente între task-urile corespunzătoare, determinând o degradare remarcabilă a performanței sistemului.
Similar cu EDF, algoritmul LLF asigură o margine de planificabilitate de 100%. Cu toate acestea, nu există nici o modalitate de a controla care task-uri își vor respecta termenele limită de execuție în timpul unei supraîncărcări tranzitorii [Sal07].
Algoritmul Maximum Urgency First (MUF)
Algoritmul Maximum Urgency First (MUF) rezolvă problema incertitudinii ce intervine în timpul unei supraîncărcări tranzitorii în cazul algoritmilor EDF si LLF. Acest algoritm este o combinație între planificarea după prioriteate fixă și planificarea după prioritate dinamică, fiind adesea cunoscut ca algoritmul de planificare după prioritate mixtă. În acest algoritm, fiecărui task îi este atribuit un nivel de urgență, care este definit ca o combinație a două priorități statice (prioritatea critică și prioritatea normală) și o proritate dinamică care este invers proporțională cu laxitatea. Prioritatea critică are întâietate față de prioritatea dinamică, iar cea din urmă are întâietate față de prioritatea normală [Sal07].
Algoritmul MUF atribuie prioritățile task-urilor în două etape. În prima etapă sunt atribuite prioritățile statice. Acestea sunt atribuite o singură dată și nu se schimbă după pornirea sistemului.
Prima etapă este formată din trei pași:
Task-urile sunt sortate crescător în funcție de perioadă. Se definește setul critic ca primele N task-uri pentru care factorul de încărcare al procesorului nu depășește 100%. Pentru aceste task-uri este garantată îndeplinirea constrângerilor de timp chiar și în timpul unei supraîncărcări tranzitorii.
Este atribuită prioritatea critică astfel: tuturor task-urilor din setul critic li se atribuie un nivel de criticitate ridicată. Celorlate task-uri li se atribuie un nivel de criticitate scăzută.
Opțional, fiecărui task din sistem i se atribuie o prioritate normală, unică.
În a doua etapă planificatorul MUF, urmărește un algoritm pentru a selecta următorul task care va intra în execuție. Acest algoritm este executat ori de câte ori un nou task este adăugat în coada Ready. Altoritmul este următorul:
Dacă există un singur task de criticitate ridictă acesta va fi lansat în execuție.
Dacă există mai multe task-uri de criticitate ridicată, va fi lansat în execuție cel cu prioritatea dinamică mai mare. În acest caz, task-ul cu laxitatea minimă are prioritate maximă.
Dacă există mai multe task-uri de criticitate ridicată, cu aceeași laxitate, va fi lansat în execuție cel cu prioritate normală mai mare [Sal07].
Mecanisme de sincronizare și comunicare între task-uri
Deoarece task-urile trebuie să interacționeze unul cu celălalt, există nevoia de sincronizare a diferitelor task-uri, precum și a schimbului de date între acestea. Mecanismele de sincronizare și comunicare între task-uri sunt oferite de către sistemul de operare ca parte a procesului de abstractizare.
În general, un task poate iniția o comunicare cu un alt task într-unul din cele două moduri: blocant sau nonblocant. După ce a inițiat o comunicare blocantă, task-ul trece în starea de așteptare (waiting) până când primește un răspuns. Comunicarea nonblocantă permite task-ului să iși continue execuția după inițierea comunicării. Ambele tipuri de comunicare sunt utile.
Există două categorii importante de mecanisme de comunicare între task-uri: utilizarea unei zone de memorie partajată și transmiterea mesajelor. Cele două mecanisme sunt logic echivalente, în sensul că pornind de la un mecanism se poate construi o interfață care implementează celălalt mecanism [Wol08].
Comunicarea între task-uri folosind o zonă de memorie partajată
Figura 6 ilustrează modul de funcționare al mecanismului de comunicare între task-uri folosind o zonă de memorie comună. Două componente ale sistemului, cum ar fi procesorul (CPU) și un dispozitiv de intrare/ieșire comunică prin intermediul unei zone de memorie comună. Programul care se execută pe procesor a fost conceput pentru a cunoaște adresa zonei de memorie partajate, care, de asemenea, a fost încărcată în registrul corespunzător al dispozitivului de intrare/ieșire. Dacă procesorul vrea să trimită date către dispozitivul de intrare/ieșire, le scrie în zona de memorie comună. După, dipozitivul de intrare/ieșire citește datele din această locație. Operațiile de citire și scriere sunt operații standard și pot fi încapsulate într-o interfață procedurală [Wol08].
Figura . Comunicație prin intermediul unei zone de memorie partajate
Pentru a explica modul de funcționare al acestui mecanism de comunicare se va folosi exemplul ilustrat in Figura 7. Trebuie să existe un flag care să indice indice dispozitivului de intrare/ieșire atunci când datele de la procesor sunt prezente și complete. Un flag este o locație suplimentară de date, partajată, care are valoarea 0 atunci când datele sunt incomplete și 1 atunci când sunt complete. De exemplu, procesorul scrie mai întâi datele în zona de memorie partajată și apoi setează flag-ul pe 1. Dacă flag-ul este utilizat doar de procesor, atunci acesta poate fi implementat prin intermediul unei operații standard de scriere în memorie. Dacă același flag este folosit pentru semnalizarea bidirecțională a disponibilității locației de memorie partajate între procesor și dispozitivul de intrare/ieșire, semnificația flag-ului este următoarea: 1 – locația de memorie partajată este disponibilă și poate fi accesată în vederea scrierii/citirii de date, 0 – locația partajată este utilizată de către un alt task și nu poate fi accesată. Se consideră următorul scenariu:
Procesorul citește valoarea flag-ului și observă că este 0.
Dispozitivul de intrare/ieșire citește valoarea flag-ului si observă că este 0.
Procesorul setează valoarea flag-ului la 1 și scrie date în locația partajată.
Dispozitivul de intrare/ieșire setează în mod eronat valoarea flagului la 1 și suprascrie datele adăugate de către procesor.
Scenariul de mai sus este cauzat de o așa numită „întrecere de sincronizare” („timing race”) între task-urile executate de către cele două componente ale sistemului. Pentru a evita astfel de probleme, magistrala microprocesorului trebuie să suporte o instrucțiune atomică numită test-and-set. Aceasta citește mai întâi valoarea locației de memorie, după care o setează la valoarea specificată și returnează rezultatul acestei operații. Dacă locația de memorie a fost deja setată la valoarea stabilită, atunci setarea acesteia din nou la aceeași valoare nu are nici un efect, iar instrucțiunea test-and-set va returna valoarea logică fals. Dacă locația de memorie nu a fost setată anterior, instrucțiunea va returna valoarea logică adevărat, valoarea locației de memorie fiind întradevăr setată de instrucțiunea test-and-set curentă. Această instrucțiune este atomică și nu poate fi întreruptă [Wol08].
Instrucțiunea test-and-set poate fi utilizată pentru a implementa un semafor, care este un element de sincronizare. Presupunem că sistemulde operare oferă un semafor pentru a controla accesul la un bloc de memorie protejată. Orice task care dorește să acceseze acest bloc de memorie trebuie să utilizeze semaforul pentru a se asigura că nu există nici un alt task care utilizează în mod activ această zonă de memorie. Așa cum este arătat mai jos, denumirile tradiționale ale funcțiilor de utilizare a semafoarelor sunt P() pentru a obține acces la blocul de memorie protejată și V() pentru al elibera.
/* operații care pot fi întrerupte */
P(); /* așteptare semafor */
/* operații protejate care nu pot fi întrerupte */
V(); /* eliberare semafor */
Funcția P() utilizează instrucțiunea test-and-set pentru a verifica în mod repetat o locație de memorie cu privire la starea resursei protejate: blocată sau nu de un alt task. Funcția P() nu este finalizată decât atunci când resursa este disponibilă, fiind apoi blocată automat de instrucțiunea test-and-set. Odată finalizată funcția P(), task-ul poate accesa blocul de memorie protejată. Funcția V() deblochează zona de memroie protejată, permițând accesul altor task-uri la această locație prin intermediul funcției P().
Comunicarea între task-uri prin transmiterea de mesaje
Comunicarea între task-uri prin transmiterea de mesaje complementează mecanismul de comunicare prin intermediul memoriei partajate. Așa cum este ilustrat în Figura 7, fiecare entitate a procesului de comunicare dispune de propria unitate de trimitere/primire a mesajelor. Mesajul nu este stocat pe legătura de comunicație, ci, mai degrabă, în punctele finale ale legăturii, la expeditor și destinatar. În contrast, mecanismul de comunicație prin intermediul memoriei partajate poate fi văzut ca un bloc de memorie utilizat ca un dispozitiv de comunicare, în care datele sunt stocate în legătura de comunicație [Wol08].
Figura . Exemplu de comunicare prin transmiterea de mesaje, cazul multiprocesor
Mecanismul de comunicare prin transmiterea de mesaje este utilizat de obicei în sistemele multiprocesor, în care un task care se execută pe un anumit procesor poate transmite un mesaj către un alt task care se execută fie pe același procesor, fie pe un altul. Un mesaj poate fi un număr întreg sau real, sau poate fi un șir de octeți de lungime fixă sau variabilă. Dacă mesajele pot fi transmise mult mai rapid decât acestea pot fi procesate de către destinatar, sistemul de operare pune la dispoziție o coadă pentru stocarea temporară a mesajelor până când acestea vor putea fi preluate de către task-ul destinatar, așa cum este ilutrat în Figura 8. Pentru a mări substanțial viteza de execuție, sistemul de operare copiază în coada de mesaje doar un pointer către mesajul transmis.
Figura . Transferul mesajeor prin intermediul cozii de mesaje
Acest mecanism de comunicare oferă primitivele send și receive pentru trimiterea și primirea mesajelor. Aceste primitive pot fi sincrone sau asincrone. Primitiva sincronă send va bloca task-ul expeditor până când task-ul destinatar va primi mesajul. Acest lucru permite expeditorului să știe dacă mesajul trimis a fost primit sau nu de către destinatar. Primitiva send asincronă doar plasează mesajul în coada de mesaje a task-ului destinatar fără a aștepta ca acesta să fie primit. Primitiva sincronă receive blochează task-ul destinatar până la primirea mesajului, în timp ce primitiva receive asincronă este finalizată imediat returnând fie mesajul primit, fie o valoare de retur care arată că nu a sosit nici un mesaj.
Semnale
Un alt mecanism de comunicare între task-uri este cel prin transmiterea de semnale, utilizat frecvent în sistemele de operare care au la bază sistemul Unix. Un semnal nu transmite alte date în afară de existența semnalului în sine [Wol08].
Semnalele sunt folosite pentru a indica apariția unor evenimente asincrone unuia sau a mai multor task-uri. Un semnal poate fi genereat de o întrerupere primită de la un dispozitiv de intrare/ieșire sau îndeplinirea unei condiții de eroare cum ar fi împărțirea la zero sau accesarea unei locații inexistente în memoria sistemului.
Executivul de timp real ExTR32
ExTR32 este un nucleu de timp real proiectat să funcționeze pe arhitecutra Intel IA-32. Acest nucleu permite dezvoltarea aplicațiilor multitasking, punând la dispoziție un planificator de task-uri împreună cu câteva din cele mai utilizate primitive de sincronizare și comunicație între task-uri.
Câteva dintre cele mai importante caracterisitici ale acestui nucleu sunt:
planificarea preemtivă a proceselor-urilor;
posibilitatea utilizării algoritmului de planificare Round-Robin;
sunt definite 18 nivele de prioritate, de la -1 la 16, cea mai maire prioritate fiind 16;
numărul maxim de procese care pot exista simultan în sistemul ExTR32 este limitat de dimensiunea tabelei de procese;
sunt disponibile trei mecanisme de comunicare și sincronizare a proceselor: semafoare, mesaje și semnale.
Structura executivului
Executivul de timp real ExTR32 are în structura sa două componente principale:
interfața funcțiilor de sistem, prin intermediul căreia programele utilizator pot solicita executivului diverse servicii cum ar fi: crearea unui proces, sau a unui semafor, trimiterea unui mesaj către un anumit proces, schimbarea prorității procesului curent sau setarea cuantei de timp utilizată de algoritmul de planificare Round Robin, etc;
planificatorul de procese, această componentă a executivului fiind responsbilă de planificarea execuției proceselor create.
Mecanismul de implementare al funcțiilor de sistem
Funcțiile de sistem asigură interfața dintre procesul utilizator și sistemul de operare. Cele mai multe operații care interacționează cu sistemul necesită permisiuni care nu sunt disponibile la nivelul procesului utilizator, cum ar fi de exemplu orice formă de comunicare cu alte procese, aceasta fiind posibilă doar prin intermediul funcțiilor de sistem.
Pentru a explica mecanismul de implementare al funcțiilor de sistem în cadrul executivului ExTR32, considerăm ca exemplu funcția setprio(), prin intermediul căreia este setată prioritatea procesului curent. Corpul funcției definit în interfața executivului este următorul:
int setprio(int prio)
{
return syscall(SYSCALL_SETPRIO,(void *)prio);
}
Se observă că în acestă funcție este apelat serviciul syscall, prototipul și semnificașia parametrilor fiind următoarele:
int syscall(int fnum, void *plist)
unde:
fnum – reprezintă indexul serviciului apelat din tabela funcțiilor de sistem a executivului. În acest caz, serviciul este apelat cu indexul SYSCALL_SETPRIO, care corespunde funcției de sistem _setprio();
plist – reprezintă parametrul sau lista de parametri a funcției de sistem apelate. În acest caz, avem un singur parametru, prio, care reprezită noua valoare la care va fi setată prioritatea procesului curent.
Serviciul syscall invocă prin intermediul întreruperii int 0x30 apelul funcției de sistem fnum cu parametrii furnizați la adresa plist. La apelul acestui serviciu, conținutul stivei este reprezentat în Figura 9, în care:
ESP – registru pe 32 de biți care inidică vârful stivei, Extended Stack Pointer;
EIP – registrul contor de program (pe 32 de biți), Extended Instruction Pointer.
Figura . Conținutul stivei la apelul funcției syscall
Serviciul syscall() copiază în registrul EAX indexul funcției de sistem fnum, iar în EBX adresa parametrilor plist, după care apelează întreruperea int 0x30. Codul funcției este următorul:
.globl syscall
syscall:
.globl _syscall
_syscall:
movl 4(%esp),%eax # indexul functiei de sistem este copiat
# in registrul %eax
pushl %ebx # este salvat pe stiva continul registrului %ebx
movl 12(%esp),%ebx # adresa parametrilor plist este copiata
# in registrul %ebx
int $0x30 # este executata functia de sistem
popl %ebx # este restaurat continutul registrului %ebx
ret
Tabela funcțiilor de sistem este definită astfel:
sftab: .long __conout # 0
.long __setvidpg # 1
.long __time # 2
.long __getpid # 3
.long __getpstate # 4
.long __getprio # 5
.long __setxlimit # 6
.long __getxlimit # 7
.long __poscur # 8
.long __setquantum # 9
.long __getquantum # 10
.long __setprio # 11
.long __newproca # 12
.long __exit # 13
.long __newsema # 14
.long __down # 15
.long __up # 16
.long __freesema # 17
.long __sleep # 18
.long __yield # 19
.long __kill # 20
.long __rdc # 21
.long __unmaskq # 22
.long __maskq # 23
.long __waitproc # 24
.long __wait # 25
.long __signal # 26
.long __send # 27
.long __receive # 28
.long __diskio # 29
.long __getsysparm # 30
.long __open # 31
.long __creat # 32
.long __mkdir # 33
.long __read # 34
.long __write # 35
.long __seek # 36
.long __close # 37
.long __remove # 38
.long __stat # 39
.long __fstat # 40
.long __opencon # 41
.long __setconmode # 42
.long __mptracc # 43
.long __rename # 44
.long __comput # 45
.long __comget # 46
.long __run # 47
.long __getprocinfo # 48
.long __brk # 49
.long __getcwd # 50
.long __chdir # 51
.long __getvpc # 52
.long __setvpc # 53
.long __vccaout # 54
.long __getcurpos # 55
.long __conattr # 56
.long __getrtc # 57
.long __wakeup # 58
.long __cancelwakeup # 59
.long __reboot # 60
.long __nicread # 61
.long __nicwrite # 62
MAXSYSCALL=62
În continuare, apelând întreruperea int 0x30, se verifică validitatea indexului funcției de sistem ce urmează a fi apelată; dacă este invalid se va returna un cod de eroare. Este salvat pe stivă conținutul regiștrilor serviciului apelant, după care sunt setați regiștrii de segment ai executivului. În continuare, este extrasă în registrul ECX adresa funcției de sistem, iar adresa listei de parametri, plist, este adăugată în stiva executivului. Flagul de replanificare a execuției proceselor, notat _rs, este resetat, fiind apoi apelată funcția de sistem specificată, în acest caz funcția _setprio(). La final, adresa listei de parametri este ștearsă din stivă, iar în registrul EAX este stocat rezultatul execuței funcției de sistem: succes sau eșec. Codul întreruperi int 0x30 este următorul:
.globl isr30
###################################################
# Este verificata validitatea indexului
# functiei de sistem.
###################################################
isr30: cmpl $MAXSYSCALL,%eax # indexul este valid ?
jbe isr30a # valid
movl $-1012,%eax # invalid, returnare cod eroare
iret
##########################################################
# Registrii [ss:esp], eflags, si cs:eip au fost deja
# salvati pe stiva executivului. Vor fi adaugati in stiva
# numarul intrerperii, registrii de segment gs, fs, es,
# ds si alti cativa registri de uz general.
# Dupa, vor fi setati registrii de segment ai
# executivului.
##########################################################
isr30a: pushl $0x30 # numarul intreruperii
pushl %gs # registrii serviciului apelant sunt salvati
pushl %fs # pe stiva executivului
pushl %es
pushl %ds
pushal
movw $SYS_DATA_SEL,%cx # sunt setati registrii de
movw %cx,%ds # segment ai executivului
movw %cx,%es
movw %cx,%fs
movw %cx,%gs
###################################################
# Apelarea functiei de sistem
###################################################
movl sftab(,%eax,4),%ecx # este extrasa adresa functiei de sistem
# din tabela de functii a executivului
pushl %ebx # este adaugata in stiva adresa
# parametrilor functiei
movl $0,__rs # este resetat flag-ul de replanificare a proceselor
call *%ecx # este apelata functia de sistem
##########################################################
# Punctul de retur al functiei
##########################################################
addl $4,%esp # adresa parametrilor este stearsa
# din stiva
movl %eax,28(%esp) # rezultatul functiei de sistem
# este salvat in registrul %eax
În acest exemplu funcția de sistem apelată este: int _setprio(int newprio). Această funcție verifică validitatea noii priorități primită prin parametrul newprio. Dacă este invalidă, va returna codul de eroare BADPRIO, altfel va seta prioritatea procesului curent la valoarea newprio. În continuare verifică dacă în lista Ready se află un proces a cărui prioritate este mai mare decât prioritatea procesului curent. Dacă există un astfel de proces, procesul curent, care se alfă în starea Run, este trecut în starea Ready, iar flagul de replanificare a proceselor este setat la valoarea 1. Codul funcției este următorul:
int _setprio(int newprio)
{
int prio;
/*–––––––––––––––––––––-*/
/* Este verificata valoarea prioritatii primita ca parametru. */
/*–––––––––––––––––––––-*/
if (newprio < LOWEST_PRIORITY || newprio > HIGHEST_PRIORITY)
return BADPRIO; /* prioritate invalida */
if (newprio == activeProcess->priority)
return NOERROR; /* valoarea prioritatii nu este schimbata */
/* Este setata noua prioritate */
activeProcess->priority = newprio;
/* Este parcursa lista proceselor aflate in starea Ready in
* cautarea unui proces a carui prioritate este mai mare decat
* prioritatea procesului curent. Daca exista alte procese
* de aceeasi prioritate, procesul curent isi continua
* executia. */
for (prio = HIGHEST_PRIORITY; prio > newprio; prio–)
if (readyList[prio+1].head != NULL) break;
/* Daca este necesara comutarea contextului, procesul curent
* este trecut in starea Ready, iar flagul de replanificare
* este setat la valoarea 1. */
if (prio > newprio) {
_addready(activeProcess);
_rs = 1;
}
return NOERROR;
}
Implementarea planificatorului de procese
Această componentă a executivului permite execuția preemtivă a proceselor, întotdeauna fiind lansat în execuție procesul alfat în starea Ready a cărui prioritate este cea mai mare. În cazul în care mai multe procese au aceeași prioritate, este evaluată cuanta de timp a algoritmului de planificare Round Robin. Dacă este mai mare strict ca zero, atunci execuția acestor procese va fi planificată folosind algoritmul Round Robin, altfel vor fi executate într-o manieră de tip FIFO (First In, First Out). În continuare va fi explicată implementarea acestui planificator.
După finalizarea apelului a oricărei funcții de sistem a executivului, planificatorul verifică starea flagului de replanificare a execuției proceselor, _rs, acesta putând avea următoarele valori:
_rs = 0 – planificatorul va returna controlul procesului apelant;
_rs = 1 – va fi efectuată o replanificare a execuției proceselor, fiind necesară o comutare a contextului;
_rs = 2 – este finalizată execuția procesului curent. În acest caz va fi selectat pentru execuție următorul proces aflat în starea Ready care are prioritatea cea mai mare.
Atunci când flagul _rs are valoarea 1, este salvat în tabela de procese pointer-ul stivei executivului pentru procesul curent aflat în execuție, după care este apelată procedura _xmain.
Procedura _xmain apelează funcția de sistem _getready() pentru a extrage procesul cu cea mai mare prioritate aflat în starea Ready și pentru a obține un pointer către datele sale din tabela de procese. În continuare este verificată valoarea cuantei de timp a algoritmului Round Robin; dacă este mai mare strict ca 0, atunci acest proces va fi executat pentru un interval de timp egal cu această cuantă, atfel procesul va fi executat până când va fi întrerupt de un proces cu o prioritate mai mare. În continuare acest proces este setat ca proces activ și va fi trecut în starea Run. Conținutul registrului ESP este setat la valorarea corespuzătoare procesului respectiv salvată în tabela de procese. Este selectată pagina de memorie utilizată de acest proces, iar înainte de a fi lansat în execuție este restabilit conținutul regiștrilor DS, ES, FS și GS. Codul plnificatorului este următorul:
########################################################################
# PLANIFICATORUL DE PROCESE #
########################################################################
# Este verificata starea flag-ului _rs:
# _rs == 0 -> return catre apelant
# _rs == 1 -> replanificare
# _rs == 2 -> terminare proces
########################################################################
kreturn:
movl __rs,%eax # switch pe _rs
orl %eax,%eax
jz isr30z # rs = 0 return catre apelant
cmpl $1,%eax
je do_sched # rs = 1 efectuare replanificare
#######################################################
# _rs = 2, are loc terminarea procesului curent
#######################################################
_xterm:
movl $kstack,%esp # este selectata stiva temporara
# a executivului
call __xcleanup # este apelata functia de sistem
# _xcleanup pentru restituirea resurselor
jmp _xmain
############################################################
# _rs = 1, este necesara replanificarea proceselor si
# comutarea contextului. Pointer-ul stivei executivului
# este salvat in tabela de procese in pozitia
# corespunzatoare procesului curent. In continuare
# este apelata procedura _xmain.
############################################################
do_sched:
movl _activeProcess,%eax
movl %esp,PROCKSP(%eax)
#######################################################
# Este apelata functia de sistem _getready pentru a
# obtine un pointer catre iregistrarea din tabela
# de procese corespunzatoare urmatorului proces care
# va fi lansat in executie.
#######################################################
.globl _xmain
_xmain: call __getready # (rezulatul returnat in %eax, este
# intotdeauna nenul)
movl PROCKBASE(%eax),%ebx # este setat pointer-ul stivei executivului
movl %ebx,tss_esp0
################################################################
# Daca este utilizata planificarea Round Robin, este setata
# variabila _ticks_left – timpul ramas pana la comutarea
# contextului
################################################################
movl __quantum,%ebx # este utilizata planificarea Round Robin?
orl %ebx,%ebx
jz isr30d # nu
movl __ticks_left,%ebx # da; a expirat cuanta de timp?
jz isr30c # nu
cmpl %eax,_activeProcess # da; acelasi proces?
jne isr30d # nu
isr30c: movl __quantum,%ebx
movl %ebx,__ticks_left # este alocat un intreg interval
# de timp egal cu cuanta
###########################################################
# Este setat activeProcess, iar starea procesului este
# trecuta in RUNNING
###########################################################
isr30d: movl %eax,_activeProcess
movl PROCSTATE(%eax),%ebx # este verificata starea procesului
orl %ebx,%ebx
isr30e: jz isr30e # diferit de ready –> eroare;
movl $2,PROCSTATE(%eax) # starea procesului = running
###################################################
# Registrul esp este setat la valoarea salvata
# in tabela de procese
###################################################
isr30y:
pushl %eax
call __verify # este apelata functia de sistem _verify
popl %eax
movl PROCKSP(%eax),%esp
#####################################
# Este selectata pagina de memorie
# corespunzatoare procesului
#####################################
movl UPTADDR(%eax),%ebx # este extrasa adresa tabelei
# a paginii de memorie
orl $7,%ebx
movl $USERVMA,%edx
shrl $20,%edx
movl %ebx,pagedir(%edx)
# Este invalidat registrul cr3
movl %cr3,%eax
movl %eax,%cr3
########################################
# Este restabilit continut registrilor
# segment si este returnat controlul
# procesului aflat in starea Run
########################################
isr30z: popal
popl %ds
popl %es
popl %fs
popl %gs
addl $4,%esp
iret
Gestionarea proceselor
Thread-urile și procesele sunt obiectele asociate cu activitate computațională din sistemul ExTR32. Doar unul dintre aceste obiecte utilizează procesorul la un moment dat. Două astfel de obiecte sunt create când sistemul este inițializat. Thread-ul inactiv se execută când toate celelalte procese sunt întârziate (sau blocate). Thread-ul Main este cel cu care programele create de utilizator își încep execuția.
Numărul maxim de thread-uri și procese care pot exista în această versiune a sistemului ExTR32 este limitat de mărimea tabelei de procese. Fiecare thread sau process utilizează o intrare în tabelă, incluzând și thread-ul inactiv. Numărul de intrări din tabelă este fixat și este specificat prin valoarea constantei NPROC din h/sysparm.h. Intrarea din tabela proceselor este eliberată odată cu încetarea thread-ului sau procesului care a folosit-o.
Thread-urile și procesele sunt similare în ceea ce privește executia lor, însă diferă în mod evident prin modul în care memoria este alocată pentru ele. Toate apelurile de sistem pot fi utilizate de thread-uri sau procese cu excepția unor apeluri care se ocupă cu management-ul memoriei care sunt rezervate pentru procese.
Un thread (altul decât thread-ul inactiv) este creat în timpul inițializării sistemului. Acest thread, în mod comun numit thread-ul Main, își începe execuția cu funcția denumită Main. Toate celelalte thread-uri și procese trebuie create de acest thread sau de thread-uri sau procese create de acesta.
Orice thread sau proces are un set de cozi asociat. Fiecare coadă este identificată printr-un număr în intervalul 0 și NQUEUE-1 (NQUEUE este definit în h/sysparm/h). Fiecare coadă înregistrează semnalale și mesajele neprocesate care sunt destinate thread-ului sau procesului corespunzător.
Fiecare proces sau thread are asociată o stare. Un proces sau thread poate utiliza în mod activ procesorul (fiind în starea numită PROCESS_CURRENT), gata pentru a utiliza procesorul (fiind în starea PROCESS_READY), sau este indisponibil de a utiliza procesorul deoarece așteaptă un eveniment sau o resursă. Când singurul proces rămas în sistem este procesul inactiv, atunci sistemul se va închide cu mesajul “Sistem oprit”.
Procesele sau thread-urile au asignate o prioritate, care este un întreg cuprins între -1 și HIGHEST_PRIORITY. Thread-ul sau procesul care utilizează procesorul este printre setul de procese sau thread-uri gata de execuție cu cea mai mare prioritae. Thread-ul idle este singurul thread sau proces cu prioritate -1, ceea ce garantează că va utiliza procesorul doar când celelalte procese sau thread-uri nu sunt capabile să utilizeze procesorul ca urmare a unei întârzieri cauzate de așteptarea după o resursă sau după un eveniment.
Când un proces care a fost întârziat devine gata de execuție (datorită resursei sau evenimentului după care așteptă devine disponibil) sau un nou thread sau proces este creat, prioritatea acelui thread este comparată cu pioritatea thread-ului care utilizează în prezent procesorul. Dacă este mai mare (noul thread sau thread-ul trezit are o prioritate numeric mai mare decât cea thread-ului curent), procesul curent este trecut în starea gata de execuție și este plasat la sfârșitul cozii tuturor proceselor sau cea a proceselor cu aceeași prioritate. Sistemul selectează dintre procesele gata de execuție pe cel din vârful cozii cu prioritatea cea mai mare; acest thread este trecut apoi în starea rulează și începe utilizarea procesorului.
În mod implicit un thread sau proces al sistemului ExTR32 își va continua execuția până când este întârziat așteptând după o resursă sau eveniment care nu este imediat valabil sau eliberează în mod void procesorului, sau își termină execuția. Acest algoritm de planificare este intitulat run-to-complete. Sistemul poate opera și utilizând algoritmul Round Robin. În acest mod (care este controlat prin utlizarea apelului de sistem setquantum) unui thread sau proces îi este permis să utilizeze procesorul un interval cel mult egal cu perioada specificată prin parametrul quantum. Dacă întreaga perioadă este utilizată de un thread, acesta este plasat apoi la sfârșitul cozii proceselor gata de execuție cu aceeași prioritate, iar procesul din capul cozii proceselor cu aceeași prioritate cu acesta este selectat pentru a utiliza procesorul. Dacă nu sunt alte thread-uri sau procese cu aceeași prioritate, procesul va fi readus în execuție.
Funcțiile care se ocupă cu managementul proceselor precum și parametrii acestora împreună cu erorile pe care le pot returna sunt prezentate în cele ce urmează.
Funcția newproca
pid_t newproca (int (*root)(int,char **), int prio, unsigned stksize, unsigned nargs, unsigned char **args)
Apelul acestei funcții are ca urmare crearea unui nou task care inițial va rula cu prioritatea prio. Task-ul va avea dimensiunea stivei egală cu stksize * 4 Kocteți și își va începe execuția cu funcția identificată prin root. Funcției rădăcină îi vor fi pasate două argumente. Primul are valoare specificată prin nargs iar cel de-al doilea este matricea de caractere args. Identificatorul unic asociat task-ului este returnat utilizând newproc. Task-ul se finalizează când funcția rădăcină execută return sau folosește apelul de sistem exi(). Întregul returnat de funcția rădăcină (sau specificat ca și parametru pentru apelul procedurii exit()) este pus la dispoziție fiecărui task care așteaptă finalizarea acestui task. Această funcție poate returna următoarele coduri de eroare:
NOSTACKSPACE – memoria rămasă este insuficientă pentru a aloca stiva;
BADPRIO – prioritate invalidă;
PROCTBLFULL – spațiu insuficient în tabela de procese.
Funcția run
pid_t run(char *path, struct runinf *ri, unsigned nargs, unsigned char **args)
Aplelul are ca urmare crearea unui nou task care va rula fișierul executabil dat de parametrul path, cu un număr de argumente egal cu nargs specificate în variabila args. Dacă ri este diferit de NULL, atunci se vor utiliza informațiile specificate prin structura de informații de rulare la care aceasta pointează pentru a determina prioritatea inițială a task-ului, environment-ul, directorul de lucru, descriptorul fișierului, etc. Altfel vor fi folosite informațiile default. Dacă ri este NULL, atunci noul task va avea aceeași prioritate, mecanisme standard de intrare/ieșire/descriptor al erorilor fișierelor, environment-ul și directorul de lucru ca și task-ul care l-a creat.
Structura runinf are următorii membri:
prio – prioritatea inițială pentru noul task;
flags – fanioanele care afectează noul task;
fd – un tablou cu indicii descriptorilor task-ului;
environ – un pointer către un string care conține environment-ul task-ului;
workdir – calea către directorul task-ului nou creat;
Dacă task-ul a fost creat cu succes, ID-ul task-ului nou creat va fi returnat (mereu mai mare ca 0). Task-ul va rula într-o nouă zonă de memorie. Această funcție poate returna următoarele coduri de eroare:
NOSUCHFILE – fișierul identificat prin path nu există;
BADPATH – un director din path nu este valid;
NOMEM – nu există suficientă memorie pentru a rula programul;
BADPATHNAME – o componentă din path este invalidă;
BADEXEC – path-ul nu este executabil sau este malformat;
NOFILESYSTEM – niciun sistem de fișiere nu a fost găsit pe disk;
NODISK – nu s-a găsit niciun disk;
PROCTBLFULL – nu există spațiu în tabela task-urilor pentru noul task;
BADPRIO – prioritate invalidă;
Funcția exit
void exit(int status)
Forțează terminarea task-ului curent. Echivalent cu „return status” executat în funcția rădăcină a task-ului.
Funcția getpid
pid_t getpid(void)
Returnează identificatorul unic al task-ului curent.
Funcția getpstate
int getpstate(pid_t proc)
Returnează starea task-ului identificat prin proc. Următoarele valori pot fi returnate. Valorile numerice (prezentate în paranteze) sunt cele definite în header-ul extr32.h.
PROCESS_READY (1) – Task-ul este gata de execuție, dar nu poate fi executat deoarece un task cu prioritate mai mare utilizează CPU-ul.
PROCESS_CURRENT (2) – Task-ul este executat în prezent.
PROCESS_BLOCKED (3) – Task-ul așteaptă finalizarea unei operații down asupra unui semafor.
QUEUE_BLOCKED (4) – Task-ul așteapta un semnal sau mesaj.
SLEEP_BLOCKED (5) – Task-ul așteaptă un timeout.
SEM_TIMED_BLOCKED (6) – Task-ul așteaptă finalizarea unei operații down asupra unui semafor sau ca un timeout să apară.
QUEUE_TIMED_BLOCKED (7) – Task-ul așteaptă un semnal , un mesaj sau un timeout.
PROCESS_WAIT (8) – Task-ul așteaptă finalizarea unui alt task.
PROCESS_TIMED_WAIT (9) – Task-ul așteaptă terminarea altui task sau expirarea unui timer.
DISK_BLOCKED (10) – Task-ul așteaptă finalizarea unei operații de I/O asupra hard disk-ului.
NOSUCHPROCESS – Niciun task cu ID-ul respectiv nu există.
Imediat după crearea task-ului acesta va fi în starea PROCESS_READY. Dacă prioritatea sa este mai mare decât a procesului părinte, atunci acesta va fi selectat pentru execuție și procesul părinte va fi trecut în starea PROCESS_READY.
Funcția kill
int kill(pid_t pid)
Termină în mod forțat task-ul cu pid-ul dat, revendicând toate resursele pe care task-ul le poate utiliza (atât spațiul din stivă cât și mesajele necitite). Un task terminat va returna în mod efectiv valoarea PROCESS_KILLED tuturor task-urilor care așteptau terminarea acestuia. Un task se poate termina singur, caz în care apelul kill nu va returna. Această funcție poate returna următoarele coduri de eroare:
NOSUCHPROCESS – task-ul cu pid-ul specificat nu există.
Funcția waitproc
int waitproc(pid_t pid, int *exitstatus, int timeout)
Așteaptă ca task-ul specificat să se finalizeze. Returnează statusul în pointerul exitstatus. În mod normal returnează NOERROR, doar dacă task-ul nu s-a terminat înainte ca waitproc să fie apelată, în acest caz NOSUCHPROCESS este returnat. Dacă task-ul a fost oprit forțat, atunci statusul de ieșire va fi PROCESS_KILLED.
Dacă parametrul timeout este INFINITE, atunci apelul la waitproc nu se va termina niciodată printr-un timeout. Dacă parametrul timeout este pozitiv, atunci apelul waitproc se va termina și va returna TIMEOUT dacă finalizarea task-ului pid nu a fost observată pe durata a timeout impulsuri de tact. Această funcție poate returna următoarele coduri de eroare:
NOSUCHPROCESS – task-ul pid nu există.
TIMEOUT – perioada timeout a expirat.
Funcția quit
void quit(char *msg)
Această funcție are ca efect terminarea execuției sistemului ExTR32. Dacă mesajul nu este null, string-ul la care pointează va fi afișat ca și motiv de oprire. Acest lucru nu este destinat pentru a fi utilizat de aplicații ci mai degrabă pentru uz intern de către kernel. Totuși, dacă o aplicație consideră că sistemul trebuie să fie oprit imediat, atunci această funcție poate fi utilizată.
Funcția getprio
int getprio(void)
Returnează prioritatea task-ului apelant.
Funcția setprio
void setprio(int newprio)
Setează prioritatea task-ului apelant cu newprio. Această funcție poate returna următoarele coduri de eroare:
BADPRIO – parametrul newprio este incorect.
Funcția yield
int yield(void)
Cedează CPU-ul task-ului din capul cozii de task-urile ready pentru task-uri cu aceeași prioritate ca task-ul apelant. Dacă această coadă este liberă (adică nu există task-uri cu aceeași prioritate ca și task-ul apelant, task-ul curent își continuă execuția).
Funcția setquantum
int setquantum (unsigned int nquantum)
Setează dimensiunea cuantei asociată cu toate task-urile. Dacă nquantum este 0, atunci toate task-urile vor rula folosind planificarea FIFO. Adică, atunci când un task este selectat să ruleze, va face acel lucru până când un task cu prioritatea mai mare este pregătit să ruleze. Dacă nquantum este diferit de zero, atunci procedura Round Robin este folosită pentru toate task-urile. Un task se va executa până când un task cu prioritatea mai mare este gata să ruleze sau până când când a trecut un interval de timp egal cu nquantum impulsuri de ceas. În cazul în care un task părăsește starea de funcționare fără a se încheia, se va întoarce la sfârșitul listei pentru prioritatea curentă. Dimensiunea quantum este zero atunci când task-ul își începe execuția.
Mecanisme de comunicare între procese
Sistemul ExTR32 pune la dispoziție trei mecanisme principale de comunicare între procese și sincronizare a acestora: semafoare, semnale și mesaje.
Un semafor este o structură de date care are două componente: un set conținând idetificatorul procesului care așteaptă semaforul, și un unsigned int count. Două operații sau metode sunt utilizate de procese în mod normal: down și up și implementate în ExTR32 cu apeluri de sistem având aceleași nume. Când un proces execută comanda down asupra unui semafor variabila count este examinată. Dacă este diferită de zero (pozitivă), este decrementată cu unu iar procesul își continuă execuția. Dacă este zero, atunci procesul este blocat și identificatorul său este adăugat în setul de procese în așteptare. Aceste operații sunt făcute în mod automat.
Când o operație up este efectuată asupra unui semafor, setul este examinat pentru a se stabili dacă sunt procese care așteaptă. Dacă există, unul dintre acestea este eliminat din set și este făcut gata de execuție. Dacă nu sunt procese care așteaptă, variabila count este incrementată cu unu. Ca și în cazul funcției down aceste operații sunt executate automat.
Există un număr finit de semafoare care pot fi utilizate de thread-uri sub ExTR32. Numărul de semafoare este dat de simbolul NSEMA din h/sysparm.h. Un proces care dorește să utilizeze un semafor trebuie mai intâi să aloce unul și să-i inițializeze variabila count utilizând apelul de sistem newsema. În momentul în care utilizarea acestui semafor s-a încheiat, el trebuie eliberat utilizând apelul freesema.
În sistemul ExTR32 un semnal este o notificare anonimă sincronă livrată către una din cozile asociate cu un thread sau proces. Fiecare proces are NQUEUE cozi, identificate prin întregi în intervalul 0 și NQUEUE-1. Valoarea lui NQUEUE este definită în h/sysparm.h.
Un semnal este trimis utilizând apelul de sistem signal.Unei cozi pot să ii fie trimise mai multe semnal, iar cele care nu au fost încă recunoscute de procesul țintă sunt reținute. Un semnal poate fi așteptat de un proces utilizând apelurile wait sau receive. La sosirea unui semnal într-o coadă a unui proces, acest eveniment nu va cauza o acțiune asicronă a procesului. Pocesul trebuie să aștepte sau să interogheze semnalul. Semnalele sunt considerate anonime deoarece nu au identificatori cu privire la procesul care le-a trimis.
ExTR32 utilizează mesaje care sunt alcătuite din trei componente: o valoare arbitrară cu dimensiunea 32 biți, care reprezintă conținutul mesajului, identificatorul expeditorului și o coadă cu specificațiile de confirmare. Fiecare mesaj este livrat unui thread sau mesaj într-un nod de meaje și există un număr finit de astfel de noduri de mesaje în sistem. Fiecare mesaj care nu a fost primit în mod explicit (utilizând apelul de sistem receive) este păstrat în coada în care a fost transmis; tipul cozii este FIFO. După ce un mesaj a fost primit, nodul de mesaje folosit pentru livrare este făcut disponibil pentr a fi utilizat de alte mesaje.
Identitatea expeditorului este inclusă în fiecare mesaj prin utilizarea identificatorului procesului (returnat de run sau newproca).
Dacă procesele care utilizează mesaje aleg să implementeze un protocol în care receptorul răspunde sau confirmă mesaje, identitatea cozii în care răspunsul sau confirmarea ar trebui să fie trimise pot fi obținute utilizând ce-a de-a treia componentă a mesajului. Numărul cozii de confirmare poate fi specificat ca NO_ACK pentru a se indica că nu se așteaptă confirmare sau răspuns. Sistemul ExTR32 nu trimite automat mesaje de confirmare, această responsabilitate revine aplicației dacă se dorește acest lucru.
Un proces poate aștepta sau interoga primirea unui mesaj prin utilizarea apelului de sistem receive. Acest apel mai întâi recunoaște un semnal dacă se așteaptă livrarea unuia. Dacă nu există semnale, atunci mesajele primite sunt recunoscute și primite.
Dacă există semnalale sau mesaje în câteva cozi asociate unui thread sau proces, coada cu numărul cel mai mic este selectată utilizând apelurile de sistem wait și receive. Dacă nu există semnale sau mesaje care așteaptă, procesul care încearcă să primească un semnal sau mesaj este întârziat până un mesaj ajunge într-o coadă nemascată sau intervalul de așteptare a expirat.
Fiecare coadă poate fi mascată sau nemascată. Doar semnalele sau mesajele din cozi nemascate pot fi recunoscute de apelurile de sistem wait și receive. Masca unei cozi poate fi manipulată prin utilizarea apelurilor de sistem maskq și unmaskq. Semnalele și mesajele dintr-o coadă mascată rămân în așteptare și pot fi recunoscute mai târziu dacă coada este nemascată mai tarziu.
Pentru a face posibilă comunicarea dintre procese un set de funcții specializate au fost create. Acestea împreună cu parametrii și erorile returnate vor fi prezentate în continuare.
Funcția signal
int signal(pid_t proc, int qnum)
Trimite semnalul în coada qnum task-ului cu ID-ul proc. Task-ul care trimite semnalul nu se va bloca ca rezultat al acestui apel, dar dacă task-ul care primește mesajul are o prioritate mai mare și este blocat așteptând un semnal în coada nemascată qnum (adică este în una din stările QUEUE_BLOCKED sau QUEUE_TIMED_BLOCKED), atunci el va deveni gata de execuție, iar task-ul care a trimis semnalul va fi plasat la finalul cozii task-urilor gata de execuție cu prioritatea curentă. Această funcție poate returna următoarele coduri de eroare:
NOSUCHPROCESS – parametrul proc este incorect.
NOSUCHQUEUE – parametrul qnum este incorect.
Funcția wait
int wait(int timeout)
Așteaptă ca un semnal să fie primit în una din cozile nemascate de evenimente, sau pentru ca timeout-ul să aibă loc. Durata de timp până ca un timeout să aibă loc este specificată de parametrul timeout. Dacă valoarea este INFINITE, atunci timeout-ul nu va avea loc niciodată. Dacă valoarea este 0, atunci timeout-ul va apărea imediat ce se determină că nu este niciun semnal pregătit să fie primit. Altfel, timeout-ul va apărea după un interval de timp egal cu timeout impulsuri în care nu s-a primit un semnal. Dacă apare un timeout, funcția va returna TIMEOUT. În mod normal această funcție returnează numărul cozii în care s-a primit semnalul. Pot fi returnate următoarele coduri de eroare:
TIMEOUT – nu s-a primit un semnal în timpul specificat
ALLQMASK – toate cozile asociate task-ului sunt mascate.
Funcția send
int send(pid_t proc, int qnum, void *msg, int ackqnum)
Trimite un mesaj de mărimea unui pointer, msg, la task-ul din coada specificată prin qnum. Mesajul este acompaniat de identitatea task-ului care l-a trimis și de numărul cozii în care se așteaptă mesajul de confirmare a recepției, ackqnum. Dacă ackqnum este NO_ACK, atunci nu se așteaptă nicio confirmare. Task-ul care trimite mesajul nu se va bloca ca urmare al acestui apel, dar dacă task-ul destinație are prioritate mai mare și este blocat așteptând un mesaj în coada nemascată qnum, atunci el va deveni gata de execuție, iar task-ul sursă va fi plasat la finalul cozii de task-uri gata de execuție cu prioritatea egală cu prioritatea acestuia. Această funcție poate returna următoarele coduri de eroare:
NOSUCHPROCESS – parametrul proc este incorect;
NOSUCHQUEUE – parametrul qnum sau ackqnum este incorect;
NOMSGNODES – nu există noduri de mesaje libere în sistem.
Funcția receive
int receive(void **msg, pid_t *sender, int *qnum, int timeout)
Primește un mesaj sau un semnal în coada de evenimente nemascate cu cea mai înaltă prioritate. Această funcție poate returna următoarele coduri de eroare:
ALLQMASK – toate cozile mascate și timeout-ul este infinit;
TIMEOUT – nu s-a primit un semnal sau mesaj în timpul specificat.
Funcția maskq
int maskq(int qnum)
Maschează o coadă de evenimente astfel încât semnalul și mesajul sosit vor fi ignorate. Sosirea mesajului sau semnalului într-o coadă mascată va fi înregistrată dar nu și detectată de funcțiile wait sau receive. Dacă operația reușește, maskq returnează starea anterioară a cozii (MASKED sau UNMASKED). Această funcție poate returna următoarele coduri de eroare:
NOSUCHQUEUE – parametrul qnum este incorect.
Funcția unmask
int unmaskq(int qnum)
Elimină masca aplicată unei cozi de evenimente astfel încât semnalul și mesajul sosit să fie detectat. Dacă operația reușește, unmaskq returnează starea anterioară a cozii (MASKED sau UNMASKED). Această funcție poate returna următoarele coduri de eroare:
NOSUCHQUEUE – parametrul qnum este incorect.
Funcția newsema
sem_t newsema(int count)
Alocă un semafor nefolosit, dacă este disponibil și setează variabila lui inițială count cu parametrul count. În cazul în care operația reușește, un ID al semaforului este returnat care poate fi folosit în apelurile up, down și freesema. Această funcție poate returna următoarele coduri de eroare:
BADSEMCOUNT – parametrul count este negativ;
SEMTBLFULL – nu sunt disponibile semafoare nefolosite;
Funcția up
int up(sem_t sema)
Orice task care este blocat ca rezultat al apelului down asupra semaforului specificat prin sema, va fi trecut în starea Ready. Dacă acest task are prioritate mai mare decât task-ul care a făcut apelul up, task-ul curent este plasat la finalul cozii task-urilor gata de execuție și este rulat task-ul debocat de semafor. Dacă nu sunt task-uri blocate, atunci numărătorul din semafor este incrementat cu unu. Up returnează NOERROR când este executată cu succes. Pot fi returnate următoarele coduri de eroare:
BADSEMA – nu există semaforul specificat prin parametrul sema;
Funcția down
int down(sem_t sema, int timeout)
Dacă numărătorul din semaforul specificat este mai mare ca zero, atunci este decrementat cu unu iar apelul funcției down returnează NOERROR. Altfel, comportamentul depinde de valoarea parametrului timeout. Dacă acesta este negativ, task-ul apelant este blocat cât timp nu este deblocat explicit prin apelul funcției up de către alt task. Dacă parametrul timeout este zero, atunci funcția va returna imediat TIMEOUT. Altfel, task-ul este blocat până cel puțin un număr de impulsuri de tact egale cu timeout au avut loc, sau alt task face un apel al funcției up iar acest task este plasat la începutul listei de task-uri blocate de acest semafor. Pot fi returnate următoarele coduri de eroare:
BADSEMA – nu există acest semafor, sau semaforul a fost deblocat cât timp task-ul era blocat;
TIMEOUT – intervalul specificat prin parametrul timeout a expirat.
Funcția freesema
int freesema(sem_t sema)
Eliberează semaforul specificat în zona semafoarelor nefolosite. Această funcție returnează NOERROR dacă s-a executat cu succes. Orice task care este blocat ca rezultat al unei operații de tip down, în momentul apelului freesema, aceste task-uri vor fi deblocate și apelul down va returna BADSEMA. Această funcție poate returna următoarele coduri de eroare:
BADSEMA – nu există semaforul specificat prin sema.
Utilizare și aplicații
Pentru a putea compila și pune în funcțiune nucleul ExTR32 este necesar un pachet de programe. Acest pachet este format din Cygwin, Bochs și o arhivă a nucleului ExTR32.
Cygwin este o suită de programe și dll-uri pentru sistemul de operare Windows care mapează apelurile de sistem din Linux în apeluri de sistem Windows, cât și o colecție de aplicații Linux care au fost portate pentru mediul Cygwin. În cele mai multe cazuri, modul de operare al Cygwin este identic cu un sistem Linux. Pentru a fi utilizat la instalarea și dezvoltarea nucleului ExTR32, este necesar să se instaleze următoarele pachete pentru Cygwin:
binutils (necesar pentru linker, asamblor, etc);
coreutils (necesar pentru dd, cât și comenzi de bază din /usr/bin);
gcc (pentru compilator);
tar (pentru a executa comanda tar);
Bochs este un emulator portabil pentru IA-32, care emulează procesoare din familia Intel x86, dispozitive comune de intrare/ieșire și un BIOS customizat. Acesta constituie un mijloc facil în încercările de testare a modificărilor făcute asupra sistemului ExTR32, deoarece elimină necesitatea utilizării unei mașini reale pentru boot-area sistemului.
După ce au fost instalate utilitarele Cygwin și Bosch pentru a putea testa și dezvolta programe sub nucleul ExTR32 sunt necesari o serie de pași. Aceștia vor fi descriși în continuare.
Concomitent cu rularea pentru prima dată a ferestrei terminal Cygwin, în directorul /cygwin/home/ este creat în mod automat un alt director cu numele utilizatorului curent. În acest folder trebuie plasată arhiva extr32.tgz. Această arhivă conține structura nucleului precum și scripturile de configurarea ale acestuia.
Pentru a compila și rula nucleul ExTR32, în fereastra de comandă Cygwin trebuie rulate următoarele comenzi (finalizare cu tasta enter):
tar zxf extr32.tgz
cd extr32
export TEMPOROOT=`pwd`
make
cd exec
../run
Aceste comenzi vor avea ca efect compilarea, construirea unei imagini boot-abile a nucleului ExTR32 și încărcarea acestuia în emulatorul Bochs.
În cazul în care aceste operații s-au efectuat cu succes se ajunge într-o fereastră principală a sistemului de operare din care se pot selecta programele existente sau programele create de utilizator. Imagine de mai jos are rolul de a exemplifica o rulare cu succes a comenzilor descrise anterior.
Figura . Consola executivului ExTR32
Primul program
Orice program dezvoltat pe baza nucleului ExTR32 are în structura sa o funcție principală al cărei nume trebuie să fie unic, acesta reprezentând numele programului. De asemenea, funcția principală este singurul element din program vizibil extern, declararea tuturor celorlalte variabile globale sau funcții fiind precedată de cuvântul cheie „static” pentru a fi vizibile doar programului propriu.
Mai jos este prezentat un exemplu simplu de program în care sunt apelate câteva funcții de sistem:
/*************************************************/
/* prog01.c – Primul program, exemple de apelare */
/* a functiilor de sistem */
/*************************************************/
#include "../../h/tempo.h"
/****************************************************/
/* Functia principala a programului */
/****************************************************/
int prog01(int argc, char *argv[])
{
printf("Program prog01\n");
printf("Acesta este un proces cu id-ul %d\n", getpid());
printf("Starea procesului este :\n");
process_state(getpstate(getpid()));
printf("Prioritatea sa este %d\n", getprio());
setprio(10);
printf("Acum, prioritatea sa este %d\n", getprio());
printf("Cuanta de timp a algoritmului de planificare Round Robin este "\
"%d\n", getquantum());
setquantum(10);
printf("Acum, cuanta de timp este %d\n", getquantum());
printf("Timpul sistemului este %d\n", time());
return 0;
}
În acest program este exemplificată utilizarea următoarelor funcții de sistem:
getpid() – este returnat identificatorul procesului curent;
getpstate() – este returnată starea procesului curent;
getprio() – este returnată prioritatea procesului curent;
setprio(10) – prioritatea procesului curent este setată la 10;
getquantum() – este returnată cuanta de timp a algoritmului de planificare Round Robin, în mod implicit, aceasta este 0;
setquantum(10) – cuanta de timp a algoritmului de planificare Round Robin este setată la 10 impulsuri de ceas;
time() – returneză timpul sistemului în impulsuri de ceas.
În urma execuției programului se obține:
Figura . Execuția programului prog01
Atunci când un program este lansat în execuție, executivul ExTR32 creează un proces care execută funcția sa principală. După cum se poate observa în Figura 11, identificatorul acestui proces este 2, iar prioritatea sa este 0.
Ilustrarea caracterului preemtiv al nucleului
În acest exemplu se urmărește ilustrarea caracterului preemtiv al executivului ExTR32. În acest sens, prin intermediul funcției de sistem newproc(), sunt create trei procese de priorități diferite, astfel:
procesul_1() (notat P1) are prioritatea 3;
procesul_2() (notat P2) are prioritatea 2;
procesul_3() (notat P3) are prioritatea 1.
Fiecare proces afișează câte șase mesaje pe ecran. În timpul execuției, procesele P1 și P2 vor trece pe rând în starea Sleep pentru intervale de timp diferite. Acest lucru va permite lansarea în execuție a procesului P3. Ulterior procesele P2 și P3 vor trece succesiv în starea Ready, determinând astfel apariția evenimentelor de preemțiune. Codul programului este următorul:
/***************************************************************/
/* prog02.c – Ilustrarea caracterului preemtiv al executivului */
/***************************************************************/
#include "../../h/tempo.h"
#include "../../h/syserrs.h"
/* Functiile radacina ale proceselor create */
static int procesul_1(int, char**);
static int procesul_2(int, char**);
static int procesul_3(int, char**);
int prog02(void)
{
pid_t proces1, proces2, proces3;
printf("Program prog02\n");
/* Prioritatea procesului curent este setata la 10 */
setprio(10);
/* Este creat primul proces cu prioritatea 3 */
proces1 = newproc(procesul_1, 3, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces1 < 0) {
errout((int)proces1);
return 0;
}
printf("Prog02: A fost creat primul proces, al carui id este %d.\n",proces1);
proces2 = newproc(procesul_2, 2, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces2 < 0) {
errout((int)proces2);
/* Este distrus primul proces */
kill(proces1);
return 0;
}
printf("Prog02: A fost creat al doilea proces,"\
"al carui id este %d.\n",proces2);
proces3 = newproc(procesul_3, 1, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces3 < 0) {
errout((int)proces3);
/* Este distrus al doilea proces */
kill(proces2);
/* Este distrus primul proces */
kill(proces1);
return 0;
}
printf("Prog02: A fost creat al trilea proces,"\
"al carui id este %d.\n",proces3);
printf("Este finalizata functia principala\n");
return 0;
}
/* Functia radacina a primului proces */
static int procesul_1 (int argc, char *argv[])
{
int i;
for (i = 0; i <= 5; i++) {
CS(printf("Procesul 1: mesajul cu numarul %d\n",i););
if(i==2){
/* La iteratia 2, procesul trece in starea sleep
pentru un interval de timp egal cu 10 impulsuri de ceas */
sleep(10);
}
}
return 0;
}
/* Functia radacina a celui de-al doilea proces proces */
static int procesul_2 (int argc, char *argv[])
{
int i;
for (i = 0; i <= 5; i++) {
CS(printf("Procesul 2: mesajul cu numarul %d\n",i););
if(i==1){
/* La iteratia 1, procesul trece in starea sleep
pentru un interval de timp egal cu 4 impulsuri de ceas */
sleep(4);
}
}
return 0;
}
/* Functia radacina a celui de-al doilea proces proces */
static int procesul_3 (int argc, char *argv[])
{
int i;
for (i = 0; i <= 5; i++) {
CS(printf("Procesul 3: mesajul cu numarul %d\n",i););
}
return 0;
}
Execuția programului este următoarea:
Figura . Execuția programului prog02.
La începutul execuție programului, prioritatea procesului care execută funcția principală este setată la valoarea 10 pentru a evita întreruperea acestesteia în timpul creării proceselor P1, P2 și P3. După finalizarea funcției principale, este lansat în execuție procesul P1, acesta având prioritatea cea mai mare. După afișarea mesajului cu numărul 2, procesul P1 trece in starea Sleep pentru un interval de timp egal cu 10 impulsuri de ceas. În acest moment este lansat în execuție procesul P2, acesta fiind următorul proces cu prioritatea cea mai mare aflat în starea Ready. După afișarea celui de-al doilea mesaj, procesul P2 trece la rândul său în starea Sleep pentru un interval de timp egal cu 4 impulsuri de ceas; astfel este lansat în execuție procesul P3. În continuare, procesul P2 trece în starea Ready, care având prioritate mai mare întrerupe procesul P3 după ce acesta afișeză esajul cu numărul 3. Procesul P1 trece în starea Ready, care având prioritate mai mare întrerupe procesul P2 după ce acesta afișeză mesajul cu numărul 3. În continuare procesul P1 își finalizează execuția, afișând mesajele rămase, după care este lansat în execuție procesul P2. La final este lansat în execuție procesul P3, acesta având cea mai mică prioritate.
Ilustrarea planificării Round Robin
În acest exemplu se urmărește ilustrarea modului în care executivul ExTR32 utilizează algoritmul Round Robin pentru planificarea a unui grup de procese. În acest sens sunt create trei procese notate cu P1, P2, P3 ale căror funcții rădăcină sunt procesul_1(), procesul_2(), procesul_3(). În acest exemplu, procesele P1 și P2 au ambele prioritatea 2, iar procesul P3 are prioritatea 1.
Ca și în exemplul precedent, fiecare proces afișează câte șase mesaje pe ecran, însă nici un unul dintre acestea nu mai este trecut în mod intenționat în starea Sleep prin intermediul funcției de sistem sleep(int). Codul programului este următorul:
/***************************************************************/
/* prog03.c – Exemplu de utilizare a planificarii Round Robin */
/***************************************************************/
#include "../../h/tempo.h"
#include "../../h/syserrs.h"
/* Functiile radacina ale proceselor create */
static int procesul_1(int, char**);
static int procesul_2(int, char**);
static int procesul_3(int, char**);
/****************************************************/
/* Functia principala a programului */
/****************************************************/
int prog03(void)
{
/* Id-urile proceselor */
pid_t proces1, proces2, proces3;
/* Quanta de timp a planificatorului de task-uri */
int q;
/* Valoarea de retur a functiilor de sistem */
int status;
/* Buffer pentru introducerea datelor de la tastatura */
char buff[10];
/* Cuanta de timp este setata la 0, va fi folosit algoritmul d planificare
Fixed Priority */
status = setquantum(0);
printf("Program prog03\n");
/* Prioritatea procesului curent este setata la 10 */
setprio(10);
printf("Introduceti o valoare pentru cuanta de timp a planificatorului:\n");
printf("0 – FP sau >0 – Round Robin: ");
status = read(0,buff,10);
if (status <= 1) exit(0); /* no input or end of file */
q = atol(buff);
/* Este creat primul proces cu prioritatea 2 */
proces1 = newproc(procesul_1, 2, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces1 < 0) {
errout((int)proces1);
return 0;
}
proces2 = newproc(procesul_2, 2, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces2 < 0) {
errout((int)proces2);
/* Este distrus primul proces */
kill(proces1);
return 0;
}
proces3 = newproc(procesul_3, 1, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces3 < 0) {
errout((int)proces3);
/* Este distrus al doilea proces */
kill(proces2);
/* Este distrus primul proces */
kill(proces1);
return 0;
}
printf("Este finalizata functia main\n");
if (q < 0) exit(0); /* valoare negativa */
/* Pentru q > 0, se va folosi algoritmul de panificare Round-Robin */
status = setquantum(q);
return 0;
}
/* Functia radacina a primului proces */
static int procesul_1 (int argc, char *argv[])
{
int i;
for (i = 0; i <= 5; i++) {
CS(printf("Procesul 1: mesajul cu numarul %d\n",i););
}
return 0;
}
/* Functia radacina a celui de-al doilea proces proces */
static int procesul_2 (int argc, char *argv[])
{
int i;
for (i = 0; i <= 5; i++) {
CS(printf("Procesul 2: mesajul cu numarul %d\n",i););
}
return 0;
}
/* Functia radacina a celui de-al doilea proces proces */
static int procesul_3 (int argc, char *argv[])
{
int i;
for (i = 0; i <= 5; i++) {
CS(printf("Procesul 3: mesajul cu numarul %d\n",i););
}
return 0;
}
Execuția programului este următoarea:
Figura . Execuția programului prog03, Round Robin
Pentru cuanta de timp a algoritmului de planificare Round Robin, a fost introdusă de la tastatură valoarea 10, aceasta fiind interpretată de către executiv în impulsuri de ceas. În figura de mai sus se observă că proceselor P1 și P2 sunt executate alternativ, acestea afișând, pe rând, câte un mesaj.
Se observă că pentru execuția proceselor P1 și P2 a fost utilizată planificarea Round Robin, acestea având aceeași prioritate. Procesul P3, având o prioritate mai mică decât procesele P1 și P2, acesta este executat ultimul. În cazul în care pentru cuanta de timp a algoritmului Round Robin este introdusă valoarea 0, procesele de aceași prioritate vor fi planificate într-o manieră de tip FIFO (First In, First Out). Astfel, ordinea de execuție a proceselor va fi în acest caz: P1, P2, P3.
Sincronizarea proceselor prin intermediul semafoarelor
În acest exemplu se urmărește ilustrarea sincronizării proceselor prin intermediul semafoarelor. În aceste sens sunt create două procese, referite în continuare ca P1 și P2, ale căror funcții rădăcină sunt procesul_1() și procesul_2(), fiecare funcție afișând câte zece mesaje.
În acest exemplu procesul P1 are prioritatea 2, iar procesul P2 are prioritatea 1. Procesul P1 trece în starea Sleep de fiecare dată după ce a afișat un mesaj, astfel cele două procese sun executate concurent.
Pentru a face vizibil efectul sincronizării proceselor prin intermediul semafoarelor, pentru afișarea mesajelor este utilizată direct funcția printf fără macro-ul CS. Funcția printf nu este o fucție de sistem atomică, prin urmare procesul curent poate fi întrerupt de către un altul în momentul afișării unui mesaj pe ecran. Pentru a evita acest lucru, în funcția principală a programului este creat un semafor s cu numărătorul inițializat pe 1. În fiecare proces din cele două, se apelează funcția de sistem down(s,INIFINITE), având ca parametru semaforul s și constanta INIFINITE ca timeout, înainte de apelul funcției printf. De exemplu, în momentul în care procesul P1 apelează funcția down(), iar numărătorul semaforului este 1, acesta este decrementat, procesul P1 blocând semaforul. Din acest moment orice alt proces care apelează funcția de sistem down() va trece în starea Waiting până în momentul în care procesul P1 va debloca semforul s printr-un apel la funcția de sistem up(s). Astfel, utilizând funcțiile de sistem down() și up() se poate crea în fiecare proces câte o secțiune critică pentru apelul funcției printf. În acest mod, nici un proces nu va mai fi întrerupt de un altul în momentul în care afișează un mesaj pe ecran. Codul programului este următorul:
/***************************************************************/
/* prog04.c – Ilustrarea sincronizarii proceselor prin */
/* intermediul semafoarelor */
/***************************************************************/
#include "../../h/tempo.h"
#include "../../h/syserrs.h"
/* Functiile radacina ale proceselor create */
static int procesul_1(int, char**);
static int procesul_2(int, char**);
/* Flag utilizat pentru indica utilizarea semafoarelor */
/* 0 = nu sunt utilizate semafoare,
1 = sunt utilizate semafoare */
static int usesema;
/* Semafor utilizat pentru sincronizare */
static sem_t s;
/****************************************************/
/* Functia principala a programului */
/****************************************************/
int prog04(void)
{
/* Id-urile proceselor */
pid_t proces1, proces2;
char raspuns;
printf("Program prog04\n");
/* Prioritatea procesului curent este setata la 10 */
setprio(10);
printf("Utilizati semafoare pentru sincronizare ? (D/d) ");
read(0,&raspuns,1);
usesema = raspuns == 'D' || raspuns == 'd';
if (usesema)
/* Este creat semaforul s cu counter-ul initializat la 1 */
s = newsema(1);
/* Este creat primul proces cu prioritatea 1 */
proces1 = newproc(procesul_1, 2, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces1 < 0) {
errout((int)proces1);
return 0;
}
printf("prog04: A fost creat primul proces, al carui id este %d.\n",proces1);
proces2 = newproc(procesul_2, 1, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces2 < 0) {
errout((int)proces2);
/* Este distrus primul proces */
kill(proces1);
return 0;
}
printf("prog04: A fost creat al doilea proces,"\
"al carui id este %d.\n",proces2);
printf("Este finalizata functia principala\n");
setprio(0);
sleep(5000);
if (usesema)
freesema(s);
return 0;
}
/* Functia radacina a primului proces */
static int procesul_1 (int argc, char *argv[])
{
int i;
/* Timpul de inactivitate pentru primul proces */
int NUM_TICKS = 1;
for (i = 0; i <= 9; i++) {
if (usesema)
/* Testare si blocare semafor */
down(s,INFINITE);
/* Sectiunea critica a procesului */
printf("Procesul 1: mesajul cu numarul %d\n",i);
/* Procesul 1 va trece in starea Sleep pentru un interval de timp egal cu
NUM_TICKS impulsuri de ceas */
sleep(NUM_TICKS);
if (usesema)
/* Deblocare semafor */
up(s);
}
return 0;
}
/* Functia radacina a celui de-al doilea proces */
static int procesul_2 (int argc, char *argv[])
{
int i;
for (i = 0; i <= 9; i++) {
if (usesema)
/* Testare si blocare semafor */
down(s,INFINITE);
/* Sectiunea critica a procesului */
printf(" ");
printf("PROCESUL 2: MESAJUL CU NUMARUL %d\n",i);
if (usesema)
/* Deblocare semafor */
up(s);
}
return 0;
}
Atunci când nu sunt utilizate semafoare, execuția programului este următoarea:
Figura . Execuția programului prog04, cazul fără sincronizare
Într-adervăr, în Figura 14 se observă că execuția proceselor nu este sincronizată în vederea afisării corecte a mesajelor, textul de pe ecran nefiind unul lizibil. În continuare este afișată execuția programului în cazul în care execuția proceselor a fost sincronizată prin intermediul semafoarelor. Se observă că mesajele afișate pe ecran sunt cele corecte.
Figura . Execuția programului prog04, cazul cu sincronizare
Implementarea unui mecanism poducător-consumator
În acest exemplu se urmărește crearea unui mecanism de partajare a unei resurse comune între două procese, în care un proces are rolul de producător, iar celălalt de consumator. Accesul la această resursă este sincronizat prin intermediul semafoarelor. Procesul producător este responsabil de generarea datelor, iar procesul consumator este responsabil de procesarea lor.
În vederea implementării acestui mecanism, în funcția principală sunt create două procese, ambele cu prioritatea 0, unul producător, iar celălalt consumator având funcțiile rădăcină produce(), respectiv consuma(). Procesul producător generează 10 numere întregi pe care le va scrie în resursa partajată. Procesul consumator va citi aceste numere din resursa partajată și le va afișa pe ecran.
Resursa partajată de către cele două procese este un buffer de tip inel în care pot fi stocate cel mult cinci elemente. Pentru a scrie sau citi un element din buffer au fost create două funcții scrie_element(), respectiv citeste_element(). Accesul la buffer al acestor funcții este restricționat prin intermediul unui semafor, notat în program buffer_mutex, al cărui numărător este inițializat la 1. Înainte de a accesa buffer-ul, fiecare funcție efectuează un apel la funcția de sistem down(buffer_mutex,INFINITE) cu parametrii buffer_mutex și INFINITE. Dacă numărătorul semaforului buffer_mutex esete 1, atunci funcția respectivă va bloca acest semafor până în momentul în care va apela funcția de sistem up(buffer_mutex) cu parametrul buffer_mutex. Astfel în fiecare din funcțiile scrie_element(), respectiv citeste_element() este creată câte o zonă critică pentru accesul în scriere sau citire a zonei de memorie partajate.
Tot în funcția principală au fost create două semafoare liber și ocupat. Numărătorul semaforului liber este inițializat cu valoarea 5 (numărul de elemente care pot fi stocate în buffer), iar numărătorul semaforului ocupat este inițializat cu valoarea 0; la început buffer-ul fiind gol. Procesul consumator așteaptă ca datele generate la pasul anterior să fie consumate, fapt semnalat de semaforul [Pet13] liber. Apoi va genera noul buffer, după care eliberează semaforul [Pet13] ocupat. La adăugarea unui element în buffer procesul producător incrementează numărătorul semaforului ocupat și decrementeză numărătorul semaforului liber.
Consumatorul va aștepta umplerea buffer-ului de către producător, fapt indicat de semaforul ocupat. După ce a fost posibilă blocarea acestui semafor, consumatorul afișează elementele din buffer și va elibera semaforul liber. La citirea unui element din buffer procesul consumator incrementează numărătorul semaforului liber și decrementeză numărătorul semaforului ocupat. Codul programului este următorul:
/****************************************************/
/* prog05.c – Implementeare unui mecanism simplu de */
/* tip producator/consumator */
/****************************************************/
#include "../../h/tempo.h"
#include "../../h/syserrs.h"
/******************************************/
/* Dimensiune buffer */
/******************************************/
#define SIZE 5
static void scrie_element(int element);
static int citeste_element(void);
static void init_buffer(void);
static int produce(int, char **);
static int consuma(int, char **);
/* Semafoare pentru sabilirea pozitiilor libere in buffer */
static sem_t ocupat, liber;
/* Semafor pentru sincronizarea functiei printf */
static sem_t s;
/* Semafor pentru sincronizarea accesului la buffer */
static sem_t buffer_mutex;
/*****************************************************/
/* Resursa partajata de catre producaor si consumator
consta intr-un buffer de tip inel */
/*****************************************************/
static int buffer[SIZE];
static int in_index, out_index, count;
/*****************************************************/
/* Functie generica pentru scrierea unui element
in buffer. */
/*****************************************************/
static void scrie_element(int item)
{
int status;
status = down(buffer_mutex,INFINITE);
if (status < 0) {
errout(status);
return;
}
buffer[in_index] = item;
in_index = (in_index + 1) % SIZE;
count++;
status = up(buffer_mutex);
if (status < 0) {
errout(status);
return;
}
}
/*****************************************************/
/* Functie generica pentru citirea unui element
dinn buffer. */
/*****************************************************/
static int citeste_element(void)
{
int item, status;
status = down(buffer_mutex,INFINITE);
if (status < 0) {
errout(status);
return 0;
}
item = buffer[out_index];
out_index = (out_index + 1) % SIZE;
count–;
status = up(buffer_mutex);
if (status < 0) {
errout(status);
return 0;
}
return item;
}
/********************************/
/* Initialize the buffer object */
/********************************/
static void init_buffer(void)
{
/* Index pentru stocarea urmatorului element */
in_index = 0;
/* Index pentru citirea urmatorului element */
out_index = 0;
/* Numarul de elemente din buffer */
count = 0;
/* Este creat semaforul pentru protectia accesului in buffer */
buffer_mutex = newsema(1);
if ((int)buffer_mutex < 0) {
errout((int)buffer_mutex);
return;
}
}
/****************************************************/
/* Functia principala a programului */
/****************************************************/
int prog05(void)
{
pid_t producator, consumator;
/* Prioritatea procesului curent este setata la 10 */
setprio(10);
s = newsema(1);
down(s,INFINITE);
printf("Program ex09\n");
up(s);
/* Counter-ul semaforului reprezinta numarul de pozitii ocupate in buffer */
ocupat = newsema(0);
if ((int)ocupat < 0) {
errout(ocupat);
freesema(s);
return 0;
}
/* Counter-ul semaforului reprezinta numarul de pozitii libere in buffer */
liber = newsema(SIZE);
if ((int)liber < 0) {
errout(liber);
freesema(s);
freesema(ocupat);
return 0;
}
/* Initializare indecsi buffer */
init_buffer();
/* Crearea procesului producator */
producator = newproc(produce, 0, 0);
if ((int)producator < 0) {
errout(producator);
freesema(s);
freesema(ocupat);
freesema(liber);
freesema(buffer_mutex);
return 0;
}
/* Crearea procesului consumator */
consumator = newproc(consuma, 0, 0);
if ((int)consumator < 0) {
errout(consumator);
kill(producator);
freesema(s);
freesema(ocupat);
freesema(liber);
freesema(buffer_mutex);
return 0;
}
/* Prioritatea procesului curent este setata la 0 */
setprio(0);
sleep(5000);
freesema(s);
freesema(ocupat);
freesema(liber);
freesema(buffer_mutex);
return 0;
}
/****************************************************/
/* Functia radacina a procesului producator */
/****************************************************/
static int produce (int argc, char *argv[])
{
int i, status;
for (i = 0; i < 9; i++) {
/* Se asteapta eliberarea unei pozitii in buffer */
status = down (liber, INFINITE);
if (status < 0) {
errout(status);
return 0;
}
down(s,INFINITE);
printf("PRODUCATOR: a fost produs elementul %d\n",i);
up(s);
/* Se scrie elemetul i in buffer */
scrie_element(i);
/* Este incrementat semaforul pentru poziitiile ocupate din buffer */
status = up(ocupat);
if (status < 0) {
errout(status);
return 0;
}
}
return 0;
}
/****************************************************/
/* Functia radacina a procesului consumator */
/****************************************************/
static int consuma (int argc, char *argv[])
{
int i, element, status;
for (i = 0; i < 9; i++) {
/* Se asteapta scrierea unui element in buffer */
status = down(ocupat, INFINITE);
if (status < 0) {
errout(status);
return 0;
}
/* Este citit elementul curent */
element = citeste_element();
down(s,INFINITE);
printf("CONSUMATOR: a fost consumat elementul %d\n",element);
up(s);
/* Este incrementat semaforul pentru poziitiile libere din buffer */
status = up(liber);
if (status < 0) {
errout(status);
return 0;
}
}
return 0;
}
Execuția programului este urmatoarea:
Figura . Execuția programului prog05
În figura de mai sus se observă o alternanță a proceselor consumator și producător, întotdeauna procesul producător fiind executat înaintea procesului consumator.
Utilizarea semnalelor
În acest program se urmărește crearea unui exemplu de comunicare între procese prin intermediul semnalelor. În general, semnalele sunt folosite la indicarea producerii unor evenimente.
În funcția principală sunt create două procese ale căror funcții rădăcină sunt transmite_semnal() și receptioneaza_semnal(), ale căror priorități sunt 1, respectiv 2. Procesul transmite_semnal() afișează cinci mesaje pe ecran și semnalizează procesului receptioneaza_semnal() producerea următoarelor evenimente:
afișarea unui mesaj cu număr par prin transmiterea unui semnal în coada cu numărul 3 a procesului receptioneaza_semnal().
finalizarea execuției prin transmiterea unui semnal în coada cu numărul 1 a procesului receptioneaza_semnal().
Pentru transmiterea semnalelor este apelată funcția signal() având ca parametri identificatorul procesului receptioneaza_semnal() (proces2) și indexul cozii de mesaje și semnale în care acestea vor fi plasate (coada 1, respectiv coada 3).
Procesul receptioneaza_semnal(), având prioritate mai mare, este lansat primul în execuție. Prin apelul funcției de sistem wait() cu parametrul INFINITE, acesta va trece în starea Queue_Blocked pâna la primirea unui semnal într-una din cozile de mesaje și semnale asociate. În acest mod, procesul receptioneaza_semnal() va fi blocat până la primirea unui semnal de la procesul transmite_semnal(). După primirea unui semnal, procesul receptioneaza_semnal() va trece din starea Queue_Blocked în starea Ready. Având prioritate mai mare, va în trerupe procesul transmite_semnal() și va procesa semnalul primit. În continuare, procesul receptioneaza_semnal() va trece din nou starea Queue_Blocked până la primirea unui nou semnal de la procesul transmite_semnal().
La primirea unui semnal în coada 3, procesul receptioneaza_semnal() va afișa pe ecran mesajul:
„receptioneaza_semnal: a fost primit un semnal in coada 3
procesul transmite_semnal a afisat un mesaj par”
La primirea unui semnal în coada 1, procesul receptioneaza_semnal() își va finaliza excuția după ce va afișa pe ecran următorul mesaj:
„receptioneaza_semnal: a fost primit un semnal in coada 1
procesul transmite_semnal si-a finalizat executia”
Codul programului este următorul:
/***************************************************************/
/* prog07.c – Utilizarea semnaelor */
/***************************************************************/
#include "../../h/tempo.h"
#include "../../h/syserrs.h"
/* Functiile radacina ale proceselor create */
static int transmite_semnal(int, char**);
static int receptioneaza_semnal(int, char**);
/* Id-urile proceselor transmite_semnal si distinatar */
static pid_t proces1, proces2;
/* Semafor utilizat pentru sincronizare printf */
static sem_t s;
/****************************************************/
/* Functia principala a programului */
/****************************************************/
int prog07(void)
{
printf("Program prog07\n");
/* Prioritatea procesului curent este setata la 10 */
setprio(10);
s = newsema(1);
/* Este creat procesul transmite_semnal cu prioritatea 1 */
proces1 = newproc(transmite_semnal, 1, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces1 < 0) {
errout((int)proces1);
freesema(s);
return 0;
}
/* Este creat procesul receptioneaza_semnal cu prioritatea 2 */
proces2 = newproc(receptioneaza_semnal, 2, 0);
/* Daca a intervenit o eroare, executia programului este oprita */
if ((int)proces2 < 0) {
errout((int)proces2);
/* Este distrus primul proces */
kill(proces1);
freesema(s);
return 0;
}
printf("Este finalizata functia principala\n\n");
/* Prioritatea procesului curent este setata la 0 */
setprio(0);
sleep(5000);
freesema(s);
return 0;
}
/****************************************************/
/* Functia radacina a procesului transmite_semnal */
/****************************************************/
static int transmite_semnal (int argc, char *argv[])
{
int i, status;
for (i = 1; i <= 5; i++) {
down(s, INFINITE);
printf("transmite_semnal: mesajul %d\n\n", i);
up(s);
/* Daca este afisat un mesaj cu numar par */
if((i%2) == 0){
/* Este trimis un semnal procesului receptioneaza_semnal in coada 3,
pentru semnalarea cestui eveniment */
status = signal(proces2,3);
if (status < 0) {
down(s, INFINITE);
printf("transmite_semnal: eroare la trimiterea semnalului\r\n");
up(s);
errout(status);
kill(proces2);
return 0;
}
}
}
/* Dupa afisarea tuturor mesajelor, procesul transmite_semnal isi
semnalizaeaza finalizarea executiei procesului receptioneaza_semnal,
prin trimiterea unui semnal catre acesta in coada 1 */
status = signal(proces2,1);
if (status < 0) {
down(s, INFINITE);
printf("transmite_semnal: eroare la trimiterea semnalului\r\n");
up(s);
errout(status);
kill(proces2);
return 0;
}
return 0;
}
/******************************************************/
/* Functia radacina a procesului receptioneaza_semnal */
/******************************************************/
static int receptioneaza_semnal (int argc, char *argv[])
{
int k, executa;
executa = 1;
while(executa) {
/* Este asteptata primirea semnal cu timeout infinit */
k = wait(INFINITE);
if (k < 0) {
down(s, INFINITE);
printf("receptioneaza_semnal: eroare la primirea semnalului\r\n");
up(s);
errout(k);
kill(proces1);
return 0;
}
if(k == 3){
down(s, INFINITE);
printf("receptioneaza_semnal: a fost primit un semnal in coada 3\n");
printf(" procesul transmite_semnal a afisat un mesaj par\n\n");
up(s);
}
if(k == 1){
down(s, INFINITE);
printf("receptioneaza_semnal: a fost primit un semnal in coada 1\n");
printf(" procesul transmite_semnal si-a finalizat executia\n\n");
up(s);
/* Este finalizata executia procesului receptioneaza_semnal */
executa = 0;
}
}
down(s, INFINITE);
printf("procesul receptioneaza_semnal isi incheie executia\n");
up(s);
return 0;
}
Execuția programului este următoarea:
Figura . Execuția programului prog07
Implementarea unei arhitecturi client/server
Modelul client-server reprezintă o relație între două sau mai multe procese bazată pe schimbul de mesaje între acestea [Pet13]. Procesul server pune la dispoziție o funcție sau un serviciu unia sau a mai multor procese client, care trimit cereri pentru un astfel de serviciu. Schematic, relația client-server între două procese este reprezentată în figura de mai jos:
Figura . Relația client-server
În funcția principală este creat un sigur proces cu prioritatea 1, acesta având rolul de server. În acest exemplu, funcția principală joacă rolul de proces client. Clientul trimite NMSG mesaje de cerere către server în coada INQ, așteptând confirmarea primirii mesajelor în coada ACKQ. Trimiterea mesajelor se realizează prin următorul apel al funcției de sistem send():
status = send(server_id, INQ, (void *)i, ACKQ);
unde:
status – valoarea de retur a funcției send();
server_id – identificatorul procesului server;
INQ – indexul cozii pe care server-ul va primi mesajele și semnale de la client;
i – mesajul de cerere transmis;
ACKQ – indexul cozii în care procesul client așteaptă confirmarea primirii mesajului.
După ce au fost trimise și răspunse toate cele NMSG mesaje de crere, clientul trimite server-ului un semnal în coada INQ pentru ca acesta să își încheie execuția.
Înainte de primirea oricărui mesaj, procesul server maschează toate cozile de mesaje și semnale cu excepți cozii INQ prin intermediul funcțiilor de sistem mask() și unmask() după cu urmează:
for (i=0;i<NQUEUE;i++) maskq(i);
unmaskq(INQ);
Astfel, toate mesajele și semanlele primite pe altă coadă de mesaje decât cea cu index-ul INQ vor fi ignorate. Server-ul așteaptă primirea mesajelor de la client prin apelarea funcției de sistem recive():
k = receive(&data, &pid, &ack_q, INFINITE);
unde:
k – coada pe care a fost primit mesajul/semanlul de la client;
data – mesajul primit;
pid – identificatorul procesului client;
ack_q – indexul cozii de mesaje și semnale în care procesul client așteaptă confirmarea primirii mesajului;
INFINITE – timeout pentru primirea mesajelor. În acest caz, procesul server va fi blocat până la primirea unui mesaj de la client.
Este important de menționat că atunci când idetificatorul procesului client, pid, returnat de funcția receive(), are o valoare diferită de -1, înseamnă că server-ul a primit un mesaj de la client, care urmează a fi procesat. Dacă această valoare este egală cu -1, atunci server-ul a primit un semnal de la client. În urma primirii acestui semnal, procesul server își va finaliza execuția, nemaifiind lte mesaje de procesat.
Fiecare mesaj conține câte un număr întreg. Procesul server calculează pătratul acestui număr și returnează rezultatul procesului client. Acesta din urmă, verifică rezutatul primit, îl afișează pe ecran și va trimite apoi un alt mesaj. Codul acestui program este următorul:
/***********************************************************/
/* prog08.c – Implementarea unei arhitecturi client-server */
/***********************************************************/
#include "../../h/tempo.h"
#include "../../h/syserrs.h"
/* Numarul de mesaje trimise de client */
#define NMSG 5
/* Canalul de intrare al server-ului */
#define INQ 3
/* Canal pentru confirmarea primirii mesajelor */
#define ACKQ 7
static int server(int, char **);
/* Id procese client si server */
static pid_t server_id, client_id;
/* Semafor utilizat pentru sincronizare printf */
static sem_t s;
/****************************************************/
/* Functia principala a programului */
/****************************************************/
int prog08(void)
{
int status, ack_q;
unsigned int i, j;
pid_t pid;
/* Prioritatea procesului curent este setata la 10 */
setprio(10);
printf("Program prog08\n");
s = newsema(1); /* printf mutex */
client_id = getpid();
server_id = newproc (server, 1, 1);
if ((int)server_id < 0) {
printf("Server process not created:\r\n");
errout((int)server_id);
freesema(s);
return 0;
}
for (i=1;i<=NMSG;i++) {
/* Trimitere mesaj catre server */
status = send(server_id, INQ, (void *)i, ACKQ);
if (status < 0) {
down(s,INFINITE);
printf("Erooare la trimiterea mesajului catre server:\r\n");
errout(status);
up(s);
kill(server_id);
freesema(s);
return 0;
}
/* Primire raspuns mesaj de la server */
status = receive((void **)&j, &pid, &ack_q, 5);
if (status == TIMEOUT) {
down(s,INFINITE);
printf("TIMEOUT la primirea mesajului de raspuns!\n");
up(s);
kill(server_id);
freesema(s);
return 0;
} else if (status < 0) {
down(s,INFINITE);
printf("Eroare la primirea mesajului de raspuns!\r\n");
errout(status);
up(s);
kill(server_id);
freesema(s);
return 0;
}
if (i * i != j) {
down(s,INFINITE);
printf("CLIENT: a fost primit %d, in loc de %d\r\n",
j, i * i);
up(s);
kill(server_id);
freesema(s);
return 0;
} else{
down(s,INFINITE);
printf("CLIENT: a fost primit rezultatul %d\r\n", j);
up(s);
}
}
/* Trimitere semnal pentru oprirea server-ului */
status = signal(server_id,INQ);
if (status < 0) {
down(s,INFINITE);
printf("Eroare la trimiterea semnalului de oprire a server-ului\r\n");
up(s);
errout(status);
kill(server_id);
}
down(s,INFINITE);
printf("Client: au fost trimise %d mesaje\r\n",NMSG);
up(s);
freesema(s);
return 0;
}
/**********/
/* Server */
/**********/
int server(int argc, char *argv[])
{
unsigned int i, j;
int k;
unsigned int nwork;
pid_t pid;
void *data;
int ack_q;
int status;
int executa;
executa = 1;
/* Sunt mascate toate cozile de mesaje, cu exceptia cozii INQ */
for (i=0;i<NQUEUE;i++) maskq(i);
unmaskq(INQ);
nwork = 0;
while(executa) {
/* Se asteapta primirea unui mesaj de la client */
k = receive(&data, &pid, &ack_q, INFINITE);
if (k < 0) {
down(s,INFINITE);
printf("Server: eroare la primire\n");
up(s);
errout(k);
return 0;
}
/* Daca id-ul procesului destinatar este diferit de -1 */
if (pid != -1) {
/* A fost primit un mesaj */
i = (int)data;
down(s,INFINITE);
printf("SERVER: valoare de intrare %d\n", i);
up(s);
j = i * i;
/* Este trimis raspunsul catre client */
status = send(pid, ack_q, (void *)j, NO_ACK);
if (status < 0) {
down(s,INFINITE);
printf("Server: eroare la trimiterea mesajului de raspuns\n");
up(s);
errout(status);
return 0;
}
nwork++;
} else
/* A fost primit semnalul de oprire */
executa = 0;
}
down(s,INFINITE);
printf("Server: au fost procesate %d mesaje.\n", nwork);
up(s);
return 0;
}
Execuția programului este următoarea:
Figura . Execuția progrmului prog08
Se observă că toate cele cinci mesaje trimise de către client au fost procesate de către server, rezultatele primite fiinde cele corecte.
Aplicație de control în timp real în rețea
Descrierea Problemei
În acest capitol va fi exemplificată utilizarea unui nucleu de timp real pentru a se asigura reglarea unei instalații tehnologice la distanță utilizând comunicare într-o rețea ethernet. Comanda furnizată de regulator va fi calculată la momente de timp egale cu durata perioadei de eșantionare apoi va fi trimisă părții fixate.
Pentru a satisface aceste cerințe, sistemul de operare trebuie să fie capabil să ofere următoarele facilități:
Comunicare în rețea prin intermediul unui protocol sigur;
Multitasking pentru aplicațiile lansate în execuție;
Planificare preemtivă a task-urilor aflate în execuție;
Robustețe și siguranță în utilizare îndelungată;
În scopul prezentării acestui sistem de reglare se va recurge la metoda de simulare Hardware-in-the-loop (HIL) pentru a se testa performanțele legii de reglare implementate. Acest principiu presupune reprezentarea sistemului de reglare sub forma unor ecuații matematice care descriu cât mai aproape de realitate funcționarea instalației tehnologice reale. Avantajele unei astfel de abordări sunt: utilizarea unei reprezentări matematice ceea ce elimină costurile și riscurile utilizării unei instalații reale, durată redusă de dezvoltare, modificări rapide asupra parametrilor părții fixe.
Sistemul de conducere va fi constituit din două aplicații: server, care va simula regulatorul și legea de reglare discretizată a acestuia și client care va simula instalația tehnologică și funcția de transfer discretizată corespunzatoare acesteia. În cadrul interfeței grafice a celor două aplicații este prezentat un grafic care se va actualiza în timp real reprezentând comanda aplicată sistemului și ieșirea părții fixe. Aplicațiile vor rula pe două calculatoare conectate în rețea care vor rula o versiune a sistemul de operare Microsoft Windows.
Începând cu versiunea 9 (Windows95), sistemul de operare Microsoft Windows a introdus planificarea preemtivă a task-urilor, ceea ce l-a transformat într-un sistem de operare multitasking performant. Datorită acestui lucru putem utiliza sistemul de operare Microsoft Windows pentru a prezenta cu success facilitățile sistemelor de operare în timp real în aplicația ce se dorește să se prezinte în continuare.
Protocolul de comunicare folosit de cele două aplicații este Trasmission Control Protocol (TCP) datorită faptului că acesta este un protocol sigur, orientat pe conexiune care permite ca un flux de octeți trimiși de o masină să ajungă fără erori pe orice altă mașina din rețea.
Limbajul de programare ales pentru realizarea aplicațiilor a fost C#, care este un limbaj de programare multi paradigmă, orientat de obiecte, imperativ, generic și cu tipuri de date sigure. Acest limbaj de programare este dezvoltat de Microsoft și rulează utilizând framework-ul .NET. Alegerea limbajului de programare s-a bazat pe gradul de abstractizare al conceptelor ce stau la baza aplicațiilor (comunicarea în rețea, management-ul memoriei, thread-uri, posibilitatea de vizualizarea a datelor de interes în timp real).
Discretizarea funcțiilor de transfer
În scopul utilizării metodei de simulare HIL, partea fixă este reprezentată cu ajutorul unui model matematic care să o descrie cât mai bine. Regulatorul asociat sistemului de reglare este unul de tipul PID. Sistemul astfel obținut trebuie adus la o formă discretă pentru a putea fi folosit în cadrul unui sistem numeric.
Un regulator PID poate fi privit ca suma dintre un element integrator și un element proporțional-derivator:
Figura . Regulator PID
Pentru a discretiza funcțiile de transfer s-a utilizat metoda discretizării ecuațiilor de stare. Funcția de transfer pentru un regulator de tip PID fiind:
(3.2.1)
Din relația de mai sus se obține:
(3.2.2)
Diagrama corespunzătoare ecuației de mai sus este:
Figura . Lege PID real implementată dintr-o lege I și o lege PD real
Din schema de mai sus se obține direct că:
(3.2.3)
Aplicând transformarea Laplace inversă, în timp se obține:
(3.2.4)
Sub forma matriceal-vectorială, ecuațiile de stare sunt:
(3.2.5)
unde este vectorul de stare cu dimensiunea (2 x 2) iar elementele de descriere sunt date de : A = ; b = ; ; d =.
La momentul , unde T>0 reprezintă intervalul de discretizare sau perioada de eșantionare, iar , relațiile de mai sus devin:
(3.2.6)
Renuțând în aceaste ecuații la variabila T din paranteze, forma discretă a algoritmului PID, reprezentat prin ecuațiile sale de stare, este dat relațiile:
, , precizat (3.2.7)
, , precizat (3.2.8)
(3.2.9)
Sub formă compactă, matriceal-vectorială, ecuațiile de mai sus se transcriu sub forma:
(3.2.10)
unde este vectorul de stare cu dimensiunea (2), iar elementele de descriere sunt date de:
; ; ; .
În cazul regulatorului implementat în sistemul de conducere, funcția de transfer asociată reprezentată în domeniul continuu este:
(3.2.11)
Funcția de transfer în domeniul continuu asociată părții fixe a sistemului este:
(3.2.12)
Schema de reglare implementată utilizând Symulink este următoarea:
Figura . Schema de reglare a sistemului
Răspunsul sistemului precum și comanda aplicată la intrarea părții fixe, pentru cazul continuu, sunt reprezentate în figurile de mai jos:
Figura . Răspunsul sistemului, cazul continuu
Figura . Comanda aplicată la intrarea părții fixe, cazul continuu
După discretizare, ecuațiile de stare recursive sunt de forma:
(3.2.13)
, cu (perioada de eșantionare) dată (3.2.14)
(3.2.15)
Programul Matlab aferent este următorul:
% perioada de eșantionare:
Te = 0.3;
% t inițial
Ti = 0;
% t final
Tf = 60;
% vector timp
t = Ti:Te:Tf;
% parametri regulator:
Kr = 10;
Ti = 30;
Td = 4;
Tg = 1;
% parametri funcție de transfer
Kv = 1;
Ts = 30;
a0 = 1 – Te/Ts;
b0 = Kv * (Te/Ts);
% step time
step_t = 10;
% indice step time
its = step_t/Te;
% step final value
step_final_val = 1;
% inițializare parametri
x1(1) = 0;
x2(1) = 0;
y(1) = 0;
% calcul ieșire sistem:
for k = 1 : Tf/Te+1
% calcul mărime de referință
if k <= its
v(k) = 0;
else
v(k) = step_final_val;
end
% calcul eroare
e(k) = v(k) – y(k);
% calcul comandă regulator:
% lege I:
x1(k+1) = x1(k) + Kr * (Te/Ti) * e(k);
% lege PD real:
x2(k+1) = (1 – Te/Tg) * x2(k) + Kr * (Te/Tg) * (1 – Td/Tg) * e(k);
% comandă:
u(k) = x1(k) + x2(k) + Kr * (Td/Tg) * e(k);
% calcul ieșire sistem
y(k+1) = a0 * y(k) + b0 * u(k);
yd(k) = y(k);
end
În urma discretizării celor două funcții de transfer, răspunsul sistemului și comanda aplicată la intrarea părții fixe sunt afișate în graficele următoare:
Figura . Răspunsul sistemului, cazul discret
Figura . Comandă regulator, cazul discret
Se observă că performanțele obținute utilizând discretizarea după stare a legii de reglare sunt satisfăcătoare, putându-se astfel trece la implementarea algoritmului numeric de reglare pe baza ecuațiilor recursive determinate.
Arhitectura aplicației
Principiul de funcționare din spatele celor două aplicații este următorul:
Figura . Arhitecutra aplicației de control distribuit în timp real
Aplicația server rulează în fundal așteptând ca un client să se conecteze. Utilizatorul din spatele aplicației server poate să modifice înainte de pornirea aplicației parametrii regulatorului PID implementat pe aceasta. După pornirea aplicației aceștia poti fi editați în modul on-line, adică în timp real, moficările putând fi observate în aplicația client.
Aplicația client este pornită iar aceasta se conectează la aplicația server utilizând protocolul de comunicație TCP/IP. Imediat după realizarea cu succes a conexiunii dintre cele două calculatoare, începe schimbul de informații dintre acestea. Astfel aplicația server va furniza comanda sistemului, în timp ce aplicația client va furniza ieșirea calculată pe baza comenzii primite de la aplicația server.
Toate aceste operații se vor realiza într-o buclă infinită, buclă care va rula într-un thread de sine stătător pentru a minimiza consumul intens de resurse. Pentru a asigura performanțele impuse, operațiile de transmisie / recepție a datelor în rețea se vor realiza sincronizate în timp, la un interval egal cu perioada de eșantionare furnizată de utilizator.
Conceptual aplicația poate fi privită ca doua task-uri care comunică prin protocolul TCP/IP. Aceste task-uri (thread-uri C#) sunt sincronizate prin întârzierea transmisiei comenzii de către task-ul server cu o perioadă de timp egală cu perioada de eșantionare.
Modul de execuție al acestor task-uri este reprezentat în figura de mai jos:
Figura . Diagrama simplificată de funcționare a aplicației
Aplicațiile se bazează pe framework-ul .NET, versiunea 3.5 pentru a realiza comunicarea în rețea și interfața grafică aferentă aplicației. Pentru o mai bună înțelegere a structurii aplicației în continuare sunt prezentate diagramele UML asociate aplicației client și respectiv server.
Figura . Diagrama UML a aplicației client
Figura . Diagrama UML a aplicației server
Aplicația server are la bază clasa TcpListener pentru a face posibilă comunicarea în rețea utilizând protocolul TCP/IP. Clasa TcpListener pune la dispoziție metode care așteaptă și preaiau cereri de conexiune în mod sincron cu blocare. Pentru a putea realiza o conexiune cu clasa TcpListener se poate utiliza fie un obiect TcpClient fie un obiect al clasei Socket. Pentru a începe procesul de așteptare de conexiuni este necesar să se stabilească o adresa IP și un port în care se așteaptă conexiunile, sau doar portul dacă se dorește ca toate adresele IP asociate mașinii să fie „ascultate”, în cazul unui computer cu mai mult de o placă de rețea.
Pentru a se ușura și mai mult utilizarea clasei TcpListener aceasta a fost încapsulată în clasa TCPServer care pune la dispoziție utilizatorului o versiune simplificată a facilităților oferite de clasa TcpListener. Un obiect nou creat al clasei va avea nevoie de portul pe care se vor primi conexiuni de la clienți. Apoi prin metodele start și stop se va controla starea în care se află serverul, pornit sau oprit. Proprietatea NetStream pune la dispoziție o cale de acces la streamul de comunicare în care vor fi scrise informațiile ce se trimit sau se recepționează între cele server și client.
Proprietatea NoDelay are ca rol activarea sau dezactivarea utilizarii algoritmului Nagle. Algoritmul Nagle este proiectat pentru a minimiza traficul în rețea în anumite circumstanțe prin crearea de către socket a unor buffer-e de pachete cu dimensiuni reduse care sunt apoi transmise. Un pachet TCP este compus din 40 de octeți care reprezintă header-ul și mesajul transmis. Când sunt transmise pachete cu dimensiune redusă utilizând protocolul TCP, header-ul TCP necesar pentru acestea poate reprezenta o parte importantă din traficul din acea rețea. În cazul în care în rețeaua curentă se realizează operații intense ca și trafic, congestia realizată de aceste pachete mici, poate duce la cazuri în care datagramele sunt pierdute, sau are loc retransmisia pachetelor. Algoritmul Nagle are ca scop stoparea transmiterii de noi segmente TCP când se primesc date de la utilizator, iar datele transmise anterior nu au fost primite de client.
Pentru aplicațiile de timp real, algoritmul Neagle poate avea unele rezultate nedorite în termeni de performanță. Astfel, în cazul sistemelor în care se dorește ca răspunsul să fie transmis imediat, algoritmul întârzie în mod deliberat transmisia pachetelor, mărind astfel eficiența cu care se utilizează lățimea de bandă, dar mărind latenta în rețea. Așadar pentru sistemle în care timpul de răspuns este foarte important este recomandat să se utilizeze opțiunea NO_DELAY.
Un aspect foarte important îl constituie impactul asupra performanțelor calculatorului care rulează aceste aplicații. Pentru minimizarea utilizării resurselor calculatorului în cauză s-a recurs la utilizarea de thread-uri atât pentru partea de comunicație cât și pentru partea de afișare a graficelor în cele două aplicații. Un thread in C# este cea mai mică unitate de procesare ce poate fi programată spre execuție de către sistemul de operare. Punerea în execuție a unui thread se realizează utilizând metoda Start(), acest apel având ca urmare trecerea thread-ului din starea UNSTARTED în starea RUNNING. Acest thread își va continua execuția până la apelul metodei Abort(), moment în care starea thread-ului devine ABORTREQUESTED, care va informa sistemul de operare că se dorește încetarea activității acelui thread. În momentul în care sistemul de operare începe procesul de terminare a thread-ului starea acestuia devine ABORTED.
Pentru realizarea calculului comenzii și stocarea informațiilor referitoare la parametrii regulatorului PID s-a decis crearea unei clase denumite PID care conține proprietăți răspunzătoare de stocarea parametrilor legii de reglare și o metodă:
double computeCommand(double error, double sampleTime)
care va avea ca scop calcularea comenzii date de regulator pe baza erorii și a perioadei de eșantionare utilizând algoritmul numeric de reglare prezentat în subcapitolul anterior.
Sincronizarea transmisiei / recepției datelor la un interval de timp egal cu perioada de eșantionare se realizează utilizând metoda void Thread.Sleep(Int32 miliseconds). În funcție de valoarea parametrului miliseconds comportamentul poate fi următoarul:
dacă valoarea este mai mare ca 0, execuția threadului curent va fi suspendată cu durata specificată;
dacă valoare este 0, atunci threadul va ceda porțiunea sa de timp de procesor ca orice thread de prioritate egală cu a sa care este gata de execuție. În cazul în care nu sunt alte thread-uri gata de execuție, execuția thread-ului curent nu se va suspenda.
Aplicația a fost creată utilizând tehnologia Winforms pentru partea de interfață grafică, iar pentru afișarea în timp real a datelor de interes s-a optat pentru utilizarea librariei open-source ZedGraph.
ZedGraph este un set de clase, scris în C#, pentru a crea linii 2D și grafice tip bară pentru seturi de date arbitrare. Clasele care sunt furnizate posedă un grad mare de flexibilitate, astfel încât fiecare aspect al graficului putând fi modificat de către utilizator. În același timp folosirea claselor este păstrată simplă prin folosirea de valori default pentru toate atributelor graficului. Clasele conțin cod pentru alegerea intervalului scalelor so pasului de masurare bazându-se pe valorile datelor care sunt afișate. ZedGraph include și interfața de tipul UserControl, care permite editare de tipul drag&dropî in Visual Studio și în plus acces din alte limbaje cum ar fi C++ și Visual Basic .NET.
Prezentarea interfeței grafice. Exemplu de utilizare
Prezentarea interfeței grafice.
În continuare vor fi prezentate interfețele grafice pentru aplicația client cât și pentru aplicația server.
Figura . Fereastra aplicației server
Interfața aplicației server cuprinde controale necesare modificărilor parametrilor regulatorului, stării serverului cât și posibilitate de vizualizare în timp real a evoluției comenzii. Conform numerotării din figura anterioară comenzile disponibile pentru utilizator sunt:
1 – Modalitate de editare a parametrilor corespunzători legii de reglare de tip PID implementate în aplicație;
2 – Update în timp real al parametrilor;
3 – Modificări referitoare la conexiune (setarea portului pe care server-ul să-l asculte, setarea opțiunii NoDelay);
4 – Selector a marimii de afișat (comandă și / sau eroare);
5 – Buton pentru pornirea server-ului;
6 – Buton pentru oprirea server-ului;
7 – Statusul conexiunii (client conectat / client neconectat);
8 – Fereastra de afișare a graficului mărimii de comandă și / sau erorii în funcție de timp, actualizat în timp real;
Figura . Fereastra aplicației client
Fereastra aplicației client dispune de controalele necesare modificării parametrilor funcției de transfer discretizate a părții fixe a sistemului, opțiuni de afișare a mărimilor de interes, opțiuni cu privire la conexiune.
Conform numerotării din figura de mai sus, controalele puse la dispoziția utilizatorului sunt:
1 – Meniu comenzi asociate aplicatiei (Minimize / Exit / Configurare conexiune / Selectare ordin sistem simulat (I/II) );
2 – Modalitate de editare a parametrilor funcției de transfer;
3 – Buton care asigură posibilitatea actualizarii parametrilor în timp real;
4 – Diverse opțiuni de afișare a mărimilor de interes precum și butoane cu rolul de conectare sau deconectare de la aplicația server;
5 – Fereastra de afișare a graficului marimilor de interes conform cu opțiunile selectate;
Figura . Setarea parametrilor conexiunii
Prin accesarea meniului Options -> Connection din fereastra principală se vor putea edita parametrii conexiunii astfel:
1 – Portul prin care se comunică cu aplicația server;
2 – Adresa IP a mașinii pe care rulează aplicația server;
3 – Buton pentru salvarea opțiunilor alese;
După cum se poate observa toate controalele au valori implicite, cele cu care s-au testat aplicația și care sunt oferite pentru a nu surveni erori în rularea acestora.
Exemplu de utilizare
Pașii de urmat pentru a rula aplicația client sunt următorii:
Plasarea într-un director de lucru a programului executabil PlantSimulator.exe cât și a fișierului ZedGraph.dll;
Rularea fișierului executabil PlantSimulator.exe;
Accesarea meniului Options -> Connection și alegerea portului și respectiv adresi IP asociată mașinii pe care rulează aplicația server;
Setarea parametrilor funcției de transfer;
Conectarea la aplicația server prin apăsarea butonului Connect;
Pașii de urmat pentru a rula aplicația server sunt următorii:
Plasarea într-un director de lucru a programului executabil RegulatorSimulator.exe cât și a fișierului ZedGraph.dll;
Rularea fișierului executabil RegulatorSimulator.exe;
Setarea portului pe care se dorește efectuarea comunicației;
Setarea parametrilor legii de reglare;
Pornirea server-ului prin apăsarea butonului Start;
În cazul în care aplicația server se rulează în cadrul unei rețele aflate în spatele unui router sau firewall atunci trebuie facută posibilă comunicare cu exteriorul prin crearea unei excepții pentru această aplicație în fișierul de configurarea al firewall-ului sau prin realizarea de portforward în interfața router-ului.
Cu scopul de a exemplifica modul de funcționare al celor două aplicații, în continuare sunt prezentate capturi de ecran din timpul rulării acestora.
Figura . Aplicația care simulează partea fixă a sistemului
Figura . Aplicația care simulează regulatorul
Comanda și respectiv ieșirea afișată au fost obținute prin setarea referinței la valorile 0, 1, 3, 10. Treptele astfel aplicate au fost menținute până la atingerea valorii de regim staționar.
Concluzii
Nucleul prezentat pe parcursul acestei lucrări este un excelent punct de plecare pentru dezvoltarea și testarea de aplicații în timp real. Acesta poate fi instalat de sine stătător pe o mașină care operează cu un procesor din familia Intel x86 ceea ce deschide posibilitatea de realizare a unui sistem capabil să poată rula aplicații de reglare a unor procese tehnologice complexe cu rezultate deosebite.
Ca și simulator educațional al unui nucleu de timp real, ExTR32, și-ar putea găsi de asemenea o întrebuințare destul de bună. Pe acesta se pot dezvolta atât softuri de timp real cât și testa și de ce nu implementa algoritmi de planificare a task-urilor și mecanisme de comunicare între procese. Posibile direcții de dezvoltare includ:
implementarea unui portocol de comunicare în rețea (TCP/IP – UDP) care să asigure comunicații de mare viteza cu exteriorul;
crearea de driver-e pentru periferice des întâlnite (plăci de rețea / porturi seriale);
adăugarea de suport pentru dispozitive periferice de tip USB;
Versiunea actuală a sistemului ExTR32 pune la îndemâna oricui un nucleu de timp real multi-scop cu ajutorul căruia se pot desluși concepte fundamentale despre modul de lucru al sistemelor de operare în timp real.
Aplicația de control în rețea, în timp real, prezentată în ultimul capitol, poate fi folosită cu succes în scopul realizării unor simulări de tip HIL (Hardware-In-the-Loop). Acest tip de simulare poate salva ore de teste practice pe instalații fizice și costuri ridicate prin simpla utilizare a modelului matematic al instalației de interes, însă oferind în continuare posibilitatea testării performanței regulatorului luând în calcul și intervalele de timp necesare transmiterii comenzii și primirii răspunsului de la partea partea fixă a sistemului la fel ca în cazul utilizării instalației tehnologice reale.
Bibliografie
[Dav11] – Robert I. Davis, Alan Burns, A survey of hard real-time scheduling for multiprocessor systems, ACM Comput. Surv. 43, 4, Articol 35, 2011
[Hea03] – Steve Heath, Embedded Systems Design, Second Edition, Newnes, Oxford, 2003
[Liu73] – C. L. Liu, James W. Layland, Scheduling algorithms for multiprogramming in a Hard-Real-Time Environment, Journal of the ACM 20, 1973
[McN10] – Dan McNulty, Lena Olson, Markus Peloquin, A Comparison of Scheduling Algorithms for Multiprocessors, 2010
[Pet10] – Petre Emil, Sisteme de Operare și Limbaje în Timp Real, Note de curs, 2010
[Pet13] – Petre Emil, Structuri Software pentru Aplicații în Timp Real, Note de curs, 2013
[Sal07] – V. Slamani, S. Zargar, M. Naghibzadeh, A Modified Maximum Urgency First Scheduling Algorithm for Real-Time Tasks, International Journal of Computer, Information Science and Engineering Vol:1 No:9, 2007
[Sha93] – Lui Sha, Shirish S. Sathaye, Distributed Real-Time System Design: Theoretical Concepts and Applications, Carnegie Mellon University Pittsburgh, Pennsylvania, 1993
[Wol08] – Wayne Hendrix Wolf, Computers as components: principles of embedded computing system design, Second Edition, Morgan Kaufmann, Burlington, 2008
Bibliografie
[Dav11] – Robert I. Davis, Alan Burns, A survey of hard real-time scheduling for multiprocessor systems, ACM Comput. Surv. 43, 4, Articol 35, 2011
[Hea03] – Steve Heath, Embedded Systems Design, Second Edition, Newnes, Oxford, 2003
[Liu73] – C. L. Liu, James W. Layland, Scheduling algorithms for multiprogramming in a Hard-Real-Time Environment, Journal of the ACM 20, 1973
[McN10] – Dan McNulty, Lena Olson, Markus Peloquin, A Comparison of Scheduling Algorithms for Multiprocessors, 2010
[Pet10] – Petre Emil, Sisteme de Operare și Limbaje în Timp Real, Note de curs, 2010
[Pet13] – Petre Emil, Structuri Software pentru Aplicații în Timp Real, Note de curs, 2013
[Sal07] – V. Slamani, S. Zargar, M. Naghibzadeh, A Modified Maximum Urgency First Scheduling Algorithm for Real-Time Tasks, International Journal of Computer, Information Science and Engineering Vol:1 No:9, 2007
[Sha93] – Lui Sha, Shirish S. Sathaye, Distributed Real-Time System Design: Theoretical Concepts and Applications, Carnegie Mellon University Pittsburgh, Pennsylvania, 1993
[Wol08] – Wayne Hendrix Wolf, Computers as components: principles of embedded computing system design, Second Edition, Morgan Kaufmann, Burlington, 2008
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Mecanisme Multitasking Si Aplicatii de Timp Real. Proiectare Si Implementare (ID: 150019)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
