Universitatea Politehnica Bucureşti Facultatea de Automatică şi Calculatoare Departamentul de Automatică şi Ingineria Sistemelor LUCRARE DE LICENŢĂ Algoritmi de planificare a resurselor in sisteme de operare de timp real Coordonator: Prof. dr. Ing. Daniela Saru Absolvent: Levent Menadil Bucureşti, 2013
CUPRINS 1 Introducere... 4 2 Aspecte teoretice... 7 2.1. Algoritmi de planificare... 8 2.1.1. Criterii de performanţă... 10 2.1.2. Algoritmul FCFS... 12 2.1.3. Algoritmul SJF... 14 2.1.4. Algoritmi bazaţi pe priorităţi... 16 2.1.5. Algoritmi de tip Round-robin... 17 2.1.6. Algoritmi de tip coada multinivel... 19 2.2. Compararea algoritmilor... 21 2.3. Condiţiile de timp real... 26 3 Implementarea Microkernelului... 28 3.1. De ce un microkernel?... 28 3.2. Componenta Hardware... 30 3.2.1. Arhitectura Cortex-M3... 33 3.3. Componenta Software... 36 3.3.1. Schimbarea contextului... 36 3.3.2. Iniţializarea task-urilor... 39 3.3.3. Mecanisme de sincronizare... 41 3.4. Modul de utilizare... 43 4 Comparaţie cu alte soluţii existente... 45 5 Studiu de caz: algoritm PID... 49 5.1. Obţinerea ecuaţiei cu diferenţe... 49 5.2. Structura task-ului... 52 5.3. Interpretarea rezultatelor... 54 6 Concluzii si dezvoltări ulterioare... 57 7 Anexe... 58 8 Bibliografie:... 77 2
Listă de figuri si tabele: Figură 2.1. Regimul de funcţionare al procesorului... 9 Figură 2.2. Diagrama de stări a task-urilor...10 Figură 2.3.Diagrama simplificată de stări...12 Figură 2.4. Ordinea de execuţie cu algoritmul FCFS...13 Figură 2.5. Simularea algoritmului FCFS...14 Figură 2.6. Ordinea de execuţie cu algoritmul SJF...15 Figură 2.7. Simularea algoritmului SJF...15 Figură 2.8. Ordinea de execuţie cu algoritmul bazat pe priorităţi...16 Figură 2.9. Simularea algoritmului bazat pe priorităţi...17 Figură 2.10. Simularea algoritmului Round-robin...18 Figură 2.11. Simularea algoritmului Lottery Round-robin...19 Figură 2.12. Algoritmul de tip coadă multinivel...20 Figură 2.13. Algoritmul de tip coadă multinivel cu feedback...20 Figură 2.14. Funcţia de probabilitate a tt pentru cei trei algoritmi...23 Figură 2.15. Funcţia de probabilitate a tr pentru cei trei algoritmi...24 Figură 2.16. Funcţia de probabilitate a tt pentru RR, respectiv Lottery RR...25 Figură 2.17. Funcţia de probabilitate a tr pentru RR si pentru Lottery RR...25 Figură 2.18. Constrângerile de timp real...26 Figură 3.1. Sistem de operare monolitic...29 Figură 3.2. Sistem de operare de tip microkernel...29 Figură 3.3. Sistem de operare de tip hibrid...30 Figură 3.4. Plăcuţa de dezvoltare STM32-H107...31 Figură 3.5. Programatorul/Depanatorul ST/Link v2....32 Figură 3.6. Stagiile pipeline pentru instrucţiunile ARMv7....34 Figură 3.7. Regiştrii procesorului Cortex-M3...35 Figură 3.8. Schimbarea contextului in procesorul Cortex-M3...37 Figură 5.1. Schema de reglare automată pentru un regulator discret...52 Figură 5.2. Execuţia task-urilor in sistemul proiectat...54 Tabel 2.1. Timpurile medii de terminare pentru algoritmii de planificare...21 Tabel 2.2. Timpurile medii de răspuns pentru algoritmii de planificare...21 Tabel 2.3. Media si abaterea medie pătratică a măsurătorilor...22 Tabel 2.4. Măsurătorile algoritmilor FCFS, SJF si RR...22 Tabel 3.1. Modurile de pornire pentru microcontrolerul STM32-F107...32 Tabel 4.1. Evaluarea criteriilor de performanţă a soluţiei propuse...47 Tabel 4.2. Evaluarea criteriilor de performanţă pentru microkernelul ThreadX...47 Tabel 4.3. Evaluarea criteriilor de performanţă pentru microkernelul FreeRTOS...48 Tabel 4.4. Evaluarea criteriilor de performanţă pentru microkernelul Neutrino...48 3
1 Introducere În ultimele decade am fost martori ai unei explozii a sistemelor informatice. Progresul lor s-a datorat în mare măsură utilităţii în viaţa de zi cu zi, începând de la ajutorul cotidian de la locul de muncă, ajungând la sistemul medical şi terminând cu vastele sisteme informatice care sunt folosite pentru asigurarea securităţii. Un al exemplu îl reprezintă sistemele integrate ce sunt prezente acum în aproape toate aparatele electronice. Modelul clasic al unui sistem informatic este clădit pe interacţiunea dintre componenta hardware; partea fizică (procesor, memorie şi restul circuitelor auxiliare) şi componenta software; definită ca partea logică. Pentru a permite utilizatorului să interacţioneze eficient cu resursele hardware, sistemele de calcul implementează un nivel separat de software, numit nivelul sistemului de operare. Acesta este, din punct de vedere structural, un program care gestionează resursele hardware, iar din punct de vedere funcţional, duce la o abstractizare a resurselor hardware pentru utilizatori. Sistemele de operare prezintă utilizatorului o interfaţă mai plăcută comparativ cu interfaţa directă cu hardware-ul, ducând la performanţe sporite. Realizând această cerinţă, dificultatea utilizării elementelor de bază ale sistemului de calcul (sau simpla cunoaştere a acestora) se transferă proiectantului sistemului de operare. Printre cerinţele sistemelor de operare putem aminti: asigurarea accesului mai multor utilizatori la un sistem de calcul, rularea mai multor programe în paralel, asigurarea protecţiei programelor din memorie, precum şi nemodificarea majoră a performanţelor sistemului de calcul prin implementarea sistemului de operare, iar sistemele specializate pot avea cerinţe mai variate. Spre exemplu, în aplicaţii ce implică stocarea de date pe un sistem distribuit, un accent mai mare este pus pe asigurarea siguranţei sistemului iar în aplicaţii industriale sau ştiinţifice apar constrângeri de timp real. De aici apare nevoia dezvoltării unor sisteme de operare ce pot satisface aceste cerinţe. Una din cele mai frecvente cerinţe este execuţia în paralel a mai multor aplicaţii. Fie că este vorba de execuţia a 20-30 de aplicaţii pe un PC sau de achiziţia, prelucrarea şi 4
trimiterea prin o reţea a unor măsurători într-o aplicaţie industrială, multitasking-ul este esenţial. Întrucât sistemele de calcul au un număr redus de nuclee de execuţie (cel mai adesea unul singur), şi prin urmare, un număr redus de fire de execuţie fizice, apar probleme în realizarea, sau mai degrabă, simularea paralelismului. Obţinerea paralelismului prin maparea fiecărui proces logic pe un proces fizic este realizat de sistemul de operare, mai precis de o componentă a sa, numită planificator de thread-uri sau, mai simplu, planificator (engl: scheduler). Aceasta componentă trebuie să ţină cont de numeroşi parametrii şi constrângeri ducând la dificultăţi majore în proiectarea sa. Din cauza dificultăţii proiectării unui algoritm de planificare care să satisfacă toate cerinţele date şi să minimizeze toate criteriile de performanţă se recomandă utilizarea algoritmilor în funcţie de cerinţele sistemului. În sisteme interactive, o importanţă sporită o are timpul de răspuns, adică timpul de la crearea task-ului până la lansarea sa în execuţie iar în sistemele de timp real, timpul de terminare, definit ca timpul de la crearea task-ului până la execuţia sa în întregime, este vital. Lucrarea de faţă îşi propune studierea algoritmilor de planificare şi a criteriilor acestora de performanţă în Capitolul 2, ţinându-se cont de constrângerile de timp real. În Capitolul 3 am proiectat şi am implementat nucleul unui Sistem de Operare în Timp Real (SOTR) cu scopul de a testa şi a evalua aceşti algoritmi. Nucleul, de tip microkernel, este implementat pe un microcontroler STM32-F107, dar va putea fi portat cu uşurinţă către alte microcontrolere cu acelaşi procesor, Cortex-M3, sau cu versiuni mai noi de procesoare Cortex-M. Acesta va avea un grad ridicat de scalabilitate, având implementate semafoare binare şi mutexi că mecanisme de sincronizare şi putând să i se adauge la revizii ulterioare un sistem de fişiere, drivere pentru periferice, şi un sistem de protecţie a memoriei, toate utile în aplicaţiile uzuale. În Capitolul 4, am relizat o comparaţie a microkernelului cu alte soluţii existente de pe piaţă, iar în Capitolul 5, am implementat pe acesta un algoritm de reglare, de tip PID, sub forma unui task pentru a putea studia criteriile de performanţă definite în Capitolul 2 şi a evidenţia funcţionalitatea acestui task în timp real. Scopul urmărit a fost aprofundarea domeniului sistemelor de operare şi a proiectării lor precum şi a domeniului programării microcontrolerelor. Conexiunea dintre aceste domenii, aparent neintuitivă, este vitală în sectoarele bazate pe sisteme integrate. Însăşi proiectarea sistemelor de operare este deseori considerată de mulţi ca fiind un domeniu 5
periculos, destinat unui număr restrâns de oameni, preferându-se utilizarea acestora din perspectiva de utilizatori, iar lucrarea de faţă încearcă, oferind un exemplu relativ simplu şi intuitiv, să arate contrariul. 6
2 Aspecte teoretice Pentru a înţelege conceptul de execuţie în paralel a firelor de execuţie trebuie să fie definim clar termenul de fir de execuţie (engl. thread). Acesta este cea mai mică unitate de procesare ce poate fi programată spre execuţie de către sistemul de operare. Nu trebuie făcută confuzia între program şi fir de execuţie, cel din urmă fiind o instanţiere a unui program în execuţie şi prezentând parametrii de stare. În cadrul sistemelor de operare, mai există conceptul de proces pentru a defini o zonă clară de memorie care poate conţine unul sau mai multe fire de execuţie. Acest concept ne permite evitarea efectelor nefaste ale utilizării aceluiaşi spaţiu de către mai multe fire de execuţie şi a alterării codului sau a datelor unui fir de execuţie de către alt fir de execuţie. Diferenţa dintre proces şi fir de execuţie constă, aşadar în utilizarea unei zone comune de memorie de către mai multe thread-uri şi utilizarea unor zone separate în cazul proceselor. Din perspectiva sistemului de operare, schimbarea de la un proces la altul implică saltul la o zonă de memorie îndepărtată, trecerea la altă stivă, la alt "heap", precum şi schimbarea completă a parametrilor de stare. Însă trecerea de la un fir de execuţie la altul implica cel mai adesea încărcarea altui set de regiştrii ai procesorului şi trecerea la altă stivă. Din această cauză, schimbarea de la un proces la altul se realizează mai lent, iar schimbarea între fire de execuţie mai rapid, ducând la denumirea de "light-weight process" pentru fire de execuţie. În mod ideal, firele de execuţie sunt folosite pentru programe ce au nevoie de un paralelism perfect controlabil. De exemplu, dacă o problemă poate fi împărţită în mai multe probleme, cu sarcini aproape identice, firele de execuţie ar putea fi o alegere bună, iar dacă nu este nevoie de un paralelism atât de controlabil, ar trebui utilizate procese [1]. Pentru a obţine performanţe maxime în sisteme în care se doreşte o paralelizare semnificativă (ex. Serverele WEB sau sistemele de tip PC) se folosesc procesele iar în cadrul acestora, firele de execuţie. Pentru a realiza o abstractizare a ambilor termeni când ne referim la îndeplinirea unui scop, se foloseşte termenul de task. Acest termen, împreună cu cel de fir de execuţie şi de proces va fi utilizat în continuare în această lucrare, deşi există o distincţie clară între ultimele două. 7
Sistemele de operare cu suport pentru multitasking au de îndeplinit mai multe cerinţe. Cele mai elementare sunt alocarea unui spaţiu în memorie pentru variabilele de stare şi pentru codul programului precum şi asigurarea schimbării între task-uri fără pierderi de informaţii (doar în cod reentrant) şi relativ rapid [1] iar printre cerinţele opţionale putem enumera comunicarea şi sincronizarea între task-uri. Tot sistemului de operare îi revine sarcina asigurării că programele nu se suprascriu şi că nu se ajunge la pierderi de date. Ideal, într-un astfel de sistem, fiecare task creat de utilizator ar trebui privit ca având monopol asupra procesorului, a regiştrilor săi de uz general şi a contorului de program, precum şi a unei zone de memorie. Alegerea programului ce se va executa în continuare este realizată de o componentă a sistemului de operare, din cadrul kernelului, numit planificator de task-uri (engl: task scheduler) iar schimbarea efectivă a regiştrilor procesorului şi sărirea la altă linie de program este datoria dispacherului, tot din cadrul kernelului [2]. Acesta poate rula ca task separat pentru a decide task-ul care va urma, sau poate fi apelat de către o întrerupere pentru a aplica pe moment algoritmul de planificare. Ultima variantă, prezintă avantaje clare de flexibilitate, kernelul fiind apelat mai des şi ducând la o eficienţă mai bună a algoritmului folosit, însă are ca dezavantaj schimbarea mai greoaie între task-uri în cazul algoritmilor complecşi şi cu o durată nedeterministă. 2.1. Algoritmi de planificare Descrierea algoritmilor de planificare se va face, în continuare cu următoarele premize: procesorul are un singur nucleu de execuţie, aşadar în orice moment de timp se poate executa fizic un singur task, şi nu se face distincţia între fire de execuţie şi procese, sistemul considerându-se că are un singur tip de task implementat. Pentru a înţelege condiţiile în care se poate schimba contextul şi, implicit task-urile, trebuie să fie făcute clare condiţiile în care funcţionează majoritatea sistemelor de calcul. În prezent, date fiind creşterea frecvenţei procesorului şi îmbunătăţirea microarhitecturii acestora, puterea de calcul este rareori o problemă. În schimb, acestea sunt cel mai adesea 8
limitate de puterea utilizatorului (prin care o să ne referim la orice instanţă situată în afara calculatorului) de a introduce date. Spre exemplu, motoraşul dintr-un hard disk rulează de câteva ordine de mărime mai încet decât viteza procesorului. În cazuri concrete, şi luând în considerare un singur task, acesta din urmă va alterna deseori între perioade lungi în care va rula pe procesor şi perioade lungi în care va aştepta să primească date din exterior. Acest lucru reprezintă un avantaj pentru proiectantul sistemului de operare permiţându-i acestuia să folosească timpul în care task-ul aşteaptă să primească date, pentru a rula alt task. În figura de mai jos, este reprezentată grafic alternanţa perioadelor în care procesorul rulează cu programul task-ului (numite aici CPU burst-uri) şi a perioadelor în care acesta aşteaptă date din exterior (numite I/O burst-uri). Figură 2.1. Regimul de funcţionare al procesorului Sursa imaginii este [15] De aici rezultă o primă categorie de algoritmi de planificare care duc la schimbarea task-ului curent, şi anume algoritmii non-preemptivi sau cooperativi, (Silberschantz, et al., 2005) aceştia invocând kernelul doar în situaţia în care task-ul curent trece în starea de aşteptare pentru a întâmpina un semnal din exterior şi, evident, în situaţia în care task-ul 9
curent se termină de executat. Termenul de cooperativi mai este folosit deoarece, în lipsa unei întreruperi din exterior pentru a schimba task-ului curent, ei îşi pot ceda între ei, voluntar, accesul la procesor. A doua categorie a algoritmilor de planificare o reprezintă algoritmii preemptivi care pot fi schimbaţi din execuţie de către o întrerupere sau la terminarea unui I/O burst. Deşi algoritmii non-preemptivi pot fi folosiţi şi pe sisteme ce nu au anumite componente hardware (de exemplu, un timer) se pot vedea clar beneficiile algoritmilor de tip preemptiv, aceştia permiţând o flexibilitate sporită, nebazându-se pe evenimente semistocastice cum ar fi cererea unor date de I/O. În sistemele de operare actuale, sunt în folosinţă algoritmi preemptivi încăpând cu Windows 95 şi cu Mac OS X. În figura 2.2 este prezentată diagrama corespunzătoare task-urilor organizate de un algoritm de planificare. În cazul algoritmilor non-preemptivi, dispatcherul nu poate trimite spre rulare un alt task decât după blocarea task-ului actual. Figură 2.2. Diagrama de stări a task-urilor 2.1.1. Criterii de performanţă Alegerea unui algoritm de planificare în defavoarea altuia se face raportat la nişte criterii de performanţă. Aceştia, la rândul lor, pot fi sau nu semnificativi, în funcţie de context şi sunt [2]: 10
1. Utilizarea CPU-ului. Reprezintă procentual raportul dintre timpul folosit pentru a executa cod util şi timpul total care trece. Ideal se doreşte utilizarea procesorului într-o proporţie de 100%, dar în sistemele reale, utilizarea variază între 40% şi 90%. 2. Rata de terminare a task-urilor. Pentru a măsura eficienţa algoritmului raportat la cantitatea de muncă efectuată se mai poate lua drept criteriu de performanţa rata în care se termină task-urile. Pentru task-uri lungi, poate fi de un task / oră iar în cazul task-urilor scurte, poate fi de 10 task-uri / secundă. 3. Timpul de terminare. Din punctul de vedere al unui task, acesta este caracterizat prin timpul total scurs de la creare până la terminare. Acest timp este alcătuit din suma timpilor în care task-ul aşteaptă în lista de aşteptare, execută pe CPU şi realizează transferuri I/O. 4. Timpul de aşteptare. Acesta reprezintă timpul total pe care task-ul îl petrece în lista de aşteptare şi este direct afectat de acţiunile luate de planificator 5. Timpul de răspuns. În sistemele interactive se pune accent pe timpul mediu de la crearea task-ului până când acesta începe să ofere rezultate. Acest timp este în general limitat de viteza echipamentului periferic de I/O, dar pentru simplificare putem considera acest timp ca fiind timpul de aşteptare de la crearea task-ului până la lansarea sa în execuţie pentru prima dată. Pentru optimizarea algoritmilor de planificare ar trebui să se urmărească mărirea utilizării CPU-ului şi a ratei de terminare a task-urilor şi să se micşoreze timpul de terminare, timpul de aşteptare şi timpul de răspuns. Cel mai adesea se doreşte optimizarea valorii medii, dar în multe situaţii se doreşte obţinerea unui minim sau unui maxim garantat pentru cel puţin un criteriu de performanţă. În unele situaţii se poate dori obţinerea unei variante mici între valoarea medie şi valorile de minim sau de maxim. În continuare vor fi prezentaţi şi exemplificaţi mai mulţi algoritmi de planificare. Aceştia au fost simulaţi în C++ sub mediul de dezvoltare Devcpp 5.3 cu compilatorul TDM- GCC pe 64 de biţi. Pentru realizarea diagramelor am folosit programul Gnuplot 4.6. şi un program realizat în Python 3.3, pentru a translata datele dintr-un format.txt într-un format.gpl, util programului de plotare. 11
Ca ipoteza se consideră 5 task-uri cu momentul de pornire un timp aleator cuprins între 0 şi 399 unităţi de timp, cu o durată (care se consideră cunoscută) cuprinsă între 150 şi 500 u.t. şi cu prioritatea între 0 şi 16, din 4 în 4, 16 considerându-se prioritatea minimă, iar 0 prioritatea maximă. De asemenea, cuanta de timp se consideră de 50 u.t. şi se neglijează, pentru moment, timpul utilizat de planificator şi de dispatcher pentru a schimba între task-uri. În alegerea acestor mărimi s-a ţinut cont, în defavoarea unor studii şi referinţe pentru sisteme de operare de pe piaţă (acestea fiind deseori valabile doar în situaţii specifice), doar de respectarea condiţiilor tipice ce apar în execuţia task-urilor. Ca exemple în acest sens sunt numărul relativ mic de priorităţi diferite, durata task-urilor comparabilă cu momentul creării acestora şi mărimea cuantei de timp considerabil mai mică decât durata task-ului. O altă notă importantă este faptul că nu se în calcul mărimea CPU burst-ului, ci mai degrabă mărimea task-ului. Aşadar, sistemului simplificat îi lipseşte starea de blocat pentru I/O rezultând diagrama de stări prezentată în figura 2.3. Figură 2.3.Diagrama simplificată de stări Criteriile de performanţă luate în considerare au fost timpul mediu de terminare şi timpul mediu de răspuns. Procesorul se consideră ocupat în proporţie de 100%, neputându-se lua nivelul de ocupare a CPU-ului drept criteriu iar, deoarece unitatea de timp aleasa este pur conceptuală, nu se poate lua în calcul nici criteriul ratei de terminare a task-urilor. 2.1.2. Algoritmul FCFS Unul dintre cei mai simplii algoritmi de planificare este algoritmul Primul Venit Primul Servit, (engl: First Come, First Served - FCFS). Acesta este de tip non-preemptiv şi implică executarea task-urilor în ordinea în care acestea sunt create. Kernelul ţine o coadă de 12
tip FIFO în care sunt trecute task-urile pe măsură ce sunt create, pe o parte, şi din care sunt şterse task-uri, pe măsură ce intră în execuţie, pe cealaltă parte. Luăm exemplul a patru task-uri: A, B, C, D, de lungime 5, 1, 3, 2 ms care sunt create în această ordine la momentul 0. În acest caz, procesele se vor executa precum se vede în figura 2.4: Figură 2.4. Ordinea de execuţie cu algoritmul FCFS În acest caz, timpul de terminare pentru procesul A este de 5 ms, pentru procesul B, 6 ms, pentru procesul C, 9 ms iar pentru procesul D, 11 ms. Aşadar, timpul mediu de terminare este (5 + 6 + 9 + 11) / 4 = 7.75 ms, iar timpul mediu de răspuns este (0 + 5 + 6 + 9) / 4 = 5 ms. Simularea acestui algoritm a rezultat, conform aşteptărilor, într-un timp mediu de terminare şi un timp mediu de răspuns relativ mari, 1047 u.t. respectiv, 650 u.t. Rezultatele acestui algoritm depind foarte mult de ordinea în care sunt create task-urile şi durata lor, iar optimizarea lui este practic imposibilă, considerându-i caracterul non-preemptiv. Simularea acestui algoritm apare în figura 2.5. 13
Figură 2.5. Simularea algoritmului FCFS 2.1.3. Algoritmul SJF Pentru a încerca scăderea timpului mediu de terminare şi a timpului mediu de răspuns, s-a încercat prioritizarea task-urilor scurte, punându-le pe acestea înaintea task-urilor create anterior, dar mai lungi. Acesta este principiul din spatele algoritmului Shortest Job First (SJF) de tip preemptiv. Algoritmul verifică la fiecare creare de task, dacă acesta are cea mai scurtă durată, comparându-i durata cu cea a task-ului curent, iar dacă are durata mai mică, se trece la task-ului nou creat (de unde rezultă caracterul lui preemptiv). Deoarece, deseori, nu cunoaştem durata task-ului, problema la acest algoritm e dată de estimarea acestui timp. Aşadar se poate ajunge la pierderea unei părţi semnificative din puterea de procesare al algoritmului de planificare, care este în sine greu de proiectat. De asemenea, acest algoritm nu se recomandă aplicaţiilor de timp real, care impun constrângeri dure din cauza neglijării taskurilor cu durată mare. Pentru a funcţiona corespunzător, algoritmul mai necesită din partea kernelului un vector în care sunt ţinute task-urile cu un câmp adiţional în care sunt trecute durata lor estimată (sau dată). 14
Considerând acelaşi exemplu teoretic de mai devreme cu 4 task-uri, luate în ordinea sosirii, A, B, C, D de durata 5, 1, 3, 2 şi utilizând algoritmul SJF, ele se vor executa în ordinea B, D, C, A, cum se vede şi în figura 2.6. Figură 2.6. Ordinea de execuţie cu algoritmul SJF Timpul de terminare devine de 1 ms pentru procesul B, de 3 ms pentru D, respectiv 6 şi 11 ms pentru procesele C şi A. Prin urmare, timpul mediu de terminare devine (1 + 3 + 6 + 11) / 4 = 5.25 ms iar timpul mediu de aşteptare este (0 + 1 + 3 + 6) / 4 = 2.5 ms. Implementarea acestui algoritm a rezultat într-un timp mediu de terminare de 940 u.t. şi într-un timp mediu de aşteptare de 515 u.t., ambele fiind mai mici decât echivalentele lor în algoritmul FCFS. Optimizarea acestui algoritm se realizează aproape exclusiv părţii responsabile de predicţia duratei task-ului sau a duratei CPU burst-ului în cazul proceselor dependente de I/O. Ordinea de execuţie se poate vedea în simularea din figura 2.7. Figură 2.7. Simularea algoritmului SJF 15
Cum se vede şi din diagramă, sunt executate prioritar task-urile cu o durată mică. Ce nu se poate observa, însă, este apelarea planificatorului la fiecare iniţializare de task, acesta fiind chemat şi la momentul 334 odată cu iniţializarea task-ului 4 precum şi la momentul 369, cu crearea task-ului 5. Asta duce la un număr dublu de apeluri ale planificatorului faţă de algoritmul FCFS. 2.1.4. Algoritmi bazaţi pe priorităţi Problema planificării se poate complica dacă este să luăm în calcul prioritatea explicită a task-urilor. Algoritmii trataţi anterior aveau prioritatea egală, în cazul algoritmului FCFS şi o prioritate implicită, dată de durată task-ului, în cazul algoritmului SJF. Planificarea bazată pe priorităţi poate fi atât de tip non-preemptivă cât şi de tip preemptivă, în prima situaţie, schimbarea cu task-ul cel mai prioritar din coadă de aşteptare având loc doar la încheierea task-ului curent. În cazul preemptiv, schimbarea poate avea loc la o întrerupere dată de expirarea unui timer sau, mai simplu, la crearea noului task (dacă acesta este mai prioritar decât task-ul curent). La algoritmii bazaţi pe priorităţi poate apărea problema "înfometării" task-urilor cu o prioritate scăzută, aceştia ajungând să nu se execute decât după toate task-urile cu o prioritate crescută. Această problemă poate fi rezolvată modificând dinamic prioritatea taskului, crescându-i prioritatea pe parcurs, când acesta nu se execută (procedeu cunoscut drept îmbătrânire ) sau modificând algoritmul prin introducerea conceptul de cuanta de timp. Considerând aceleaşi task-uri, cu durata de 5, 1, 3 şi 2 ms, le vom asigna priorităţile 2, 3, 1, 4 şi vom verifica priorităţiile doar la momentul creării task-urilor. Aşadar, ele se vor executa în ordinea C, A, B, D potrivit priorităţii, cum reiese şi din figura 2.8. Figură 2.8. Ordinea de execuţie cu algoritmul bazat pe priorităţi 16
Simulând algoritmul cu priorităţi, obţinem timpul mediu de aşteptare de 1167 u.t. şi timpul mediu de răspuns de 770 u.t., lucru ilustrat şi în figura 2.9. Figură 2.9. Simularea algoritmului bazat pe priorităţi În acest exemplu priorităţile task-urilor de la 1 la 5 au fost 12, 8, 4, 4, 12. Deşi se remarcă un timp mediu de aşteptare şi un timp mediu de răspuns mai mare decât în ultimii algoritmi studiaţi, acest algoritm are printre cele mai bune performanţe în sistemele de timp real, garantând un timp minim de răspuns pentru anumite task-uri critice. 2.1.5. Algoritmi de tip Round-robin O clasă de algoritmi foarte des utilizaţi în prezent este cea de tip Round-robin. Aceştia sunt algoritmi preemptivi ce implică schimbul între task-uri la un interval de timp stabilit. Astfel, procesorul executa cuante din fiecare task din coadă, aflat în starea ready, pe rând. Fiind folosiţi preferenţiabil în sisteme interactive, aceşti algoritmi au mai multe variaţiuni care ţin sau nu cont de prioritatea task-urilor. 17
timp de 50 u.t. În figura 2.10 se poate observa succesiunea task-urilor executate pentru o cuantă de Figură 2.10. Simularea algoritmului Round-robin Timpul de aşteptare în acest caz este mult mai mare, fiind proporţional cu numărul de task-uri înmulţit cu durata medie a unui task, însă timpul de răspuns este mult mai mic, fiind limitat la numarul de task-uri înmulţit cu mărimea unei cuante de timp. Algoritmii de tip Round-robin pot fi optimizaţi pentru a corespunde cerinţelor sistemului variând mărimea cuantei de timp pentru a obţine rezultate mai bune. Aceşti algoritmi pot avea caracteristici de timp real, introducând conceptul de prioritate. Aşadar se poate ajunge la creşterea dimensiunii cuantei de timp sau a numărului acestora pentru task-uri cu o prioritate ridicată. Un astfel de algoritm bazat pe priorităţi şi pe cuante de timp este cel de Lottery scheduling, care oferă un număr mai mare de cuante de timp task-urilor prioritare. Acest algoritm nu duce decât la performanţe sporite în medie pentru task-urile cu prioritate mare, nefiind recomandat sistemelor de timp real. Totuşi, avem garanţia executării cel puţin timp de o cuantă a tuturor proceselor într-un ciclu complet, rezolvând problema "înfometării". 18
În simularea acestui algoritm am ales acordarea unei cuante de timp task-urilor cu prioritate 12, a doua cuante pentru prioritatea 8, şi a 4 şi 8 cuante pentru task-uri cu priorităţile 4 şi 0. Ordinea de execuţie este vizibilă în figura 2.11. Figură 2.11. Simularea algoritmului Lottery Round-robin 2.1.6. Algoritmi de tip coada multinivel Algoritmii studiaţi până acum au constant în menţinerea de către sistemul de operare a unei singure cozi sau a unui vector care să conţină informaţii despre toate procesele din memorie. O altă abordare implica împărţirea task-urilor în mai multe cozi şi executarea lor într-o manieră ierarhică. Acest lucru este facilitat de separarea relativ facilă a task-urilor în mai multe categorii. Cea mai simplă categorisire ar fi împărţirea în task-uri interactive ce rulează în foreground şi în task-uri ne-interactive ce rulează în background. Modalitatea de execuţie este simplă, existând un algoritm de planificare între cozi. Cel mai adesea acesta este bazat pe priorităţi fixe, aşadar se execută procese din coadă cea mai prioritară, abia la finalizarea tuturor acestora putând rula procese din coadă imediat 19
următoare, s.a.m.d. O altă variantă este executarea după o manieră de tip Round-Robin cu priorităţi între cozi, spre exemplu acordarea primei cozi a 80% din timpul pe procesor, iar celei de-a două cozi, a restului de 20% într-o structură cu două cozi. În cadrul fiecărei cozi se poate implementa câte un alt algoritm de planificare, rezultate bune fiind obţinute cu algoritmi de tip Round-Robin pentru primele cozi şi cu algoritmul FCFS pentru ultimele cozi. În figura 2.12 apar reprezentate grafic cele 3 cozi ale kernelului în care se vor păstra date despre procese. Figură 2.12. Algoritmul de tip coadă multinivel Pentru a acorda o flexibilitate mai mare algoritmului coada multinivel, se permite schimbarea dinamică a task-urilor între cozi. Acest lucru poate avea loc după o perioadă de timp, când unui task îi creşte prioritatea datorită îmbătrânirii sau explicit, printr-un apel de sistem din interiorul task-ului. Algoritmul rezultat poartă denumirea de algoritm coada multinivel cu reacţie (engl: multilevel feedback-queue scheduling algoritm). Figură 2.13. Algoritmul de tip coadă multinivel cu feedback 20
2.2. Compararea algoritmilor Deoarece avem un singur set de valori simulat pe algoritmi cu intrări aleatoare, alegem să formulăm o concluzie pe o analiză statistică. Aşadar, pentru a obţine un set de măsurători cu nivel ridicat de încredere, formulam problema cu 10 task-uri şi efectuăm măsurătorile de 10 de ori pentru fiecare din cei 5 algoritmi. Măsurând timpul mediu de terminare, t t, obţinem setul de date din tabelul 2.1 Algoritm Test 1 Test 2 Test 3 Test 4 Test 5 Test 6 Test 7 Test 8 Test 9 Test 10 FCFS 1803 1787 1741 1497 1395 1697 2031 1485 1412 1720 SJF 1348 1543 1261 1346 1455 1351 1105 1420 1144 1255 Priority Based 1813 1911 1723 1251 1572 1392 2054 1492 1566 1639 Round-Robin 2445 2460 2315 2577 2699 2265 2735 1992 2188 3506 Lottery RR 1760 2131 2071 2428 2601 2231 2860 2157 2129 2561 t t Tabel 2.1. Timpurile medii de terminare pentru algoritmii de planificare Iar pentru timpul mediu de răspuns, t r, avem valorile: Algoritm Test 1 Test 2 Test 3 Test 4 Test 5 Test 6 Test 7 Test 8 Test 9 Test 10 FCFS 1465 1445 1404 1217 1127 1338 1636 1154 1117 1403 SJF 974 1204 969 973 1135 1038 814 1078 727 936 Priority Based 1297 1421 1245 953 1227 1044 1464 1085 908 1296 Round-Robin 108 145 113 128 115 87 53 155 97 77 Lottery RR 316 182 351 263 341 314 157 527 277 366 t r Tabel 2.2. Timpurile medii de răspuns pentru algoritmii de planificare 21
Efectuând media tuturor testelor şi calculând abaterea medie pătratica pentru fiecare set de 10 teste cu ecuaţia 2.1 obţinem datele din tabelul 2.3. Abaterea medie pătratică, în acest context, are rolul de a stabili nivelul de dispersie a valorilor, permiţându-ne să facem afirmaţii clare pe baza unor seturi de valori. σ(x) = 1 ( n (X X ) 2 n i=1 (2.1) În ecuaţia 2.1, σ(x) este abaterea medie pătratică a variabilei X, acesta fiind, pe rând, t t si t r, iar X reprezintă media celor n măsurători. Algoritm t t σ(t t ) t r σ(t r ) FCFS 1656,8 193.5 1330,6 163.2 SJF 1322,8 128.5 984,8 134,4 Priority Based 1641,3 229.4 1194 179,8 Round-Robin 2518,2 394.8 107,8 29.3 Lottery RR 2292,9 302.5 309,4 98 Tabel 2.3. Media si abaterea medie pătratică a măsurătorilor Pentru a formula concluzii din acest tabel alegem sa luăm in calcul doar algoritmii care nu ţin cont de prioritatea explicită a task-urilor. Deoarece aceasta nu apare în formulele pentru a calcula cei doi indici de performanţă, raportat la timpul mediu de terminare şi la timpul mediu de răspuns, vom avea rezultate mai slabe. Algoritm t t σ(t t ) t r σ(t r ) FCFS 1656,8 193.5 1330,6 163.2 SJF 1322,8 128.5 984,8 134,4 Round-robin 2518,2 394.8 107,8 29.3 Tabel 2.4. Măsurătorile algoritmilor FCFS, SJF si RR Din acest ultim tabel, se poate observa că algoritmul FCFS are atât timpul mediu de terminare cât şi timpul mediu de răspuns mai mari decât în cazul algoritmul SJF. De asemenea, algoritmul Round-robin duce la un timp mediu de aşteptare mai mare decât ceilalţi 22
doi algoritmi, dar la un timp mediu de răspuns mult mai mic. Aceste concluzii ţin cont de media ± abaterea medie pătratică pentru cele trei seturi de valori. Pentru a ilustra ordonarea timpului mediu de aşteptare pentru cei trei algoritmi am plasat pe acelaşi grafic funcţia densitate de probabilitate pentru cele trei seturi de valori. Figură 2.14. Funcţia de probabilitate a t t pentru cei trei algoritmi 23
Figură 2.15. Funcţia de probabilitate a t r pentru cei trei algoritmi Graficele au fost realizate folosind metoda neparametrică kernel density estimation (KDE) pentru a obţine densitatea de probabilitate pentru o serie de variabile aleatoare. O altă observaţie este că algoritmul bazat pe cuante de timp fără priorităţi (Roundrobin) are timpul mediu de răspuns mult mai mic decât primii doi algoritmi. Acest lucru este datorat limitării timpului de răspuns la numărul de taskuri înmulţit cu mărimea cuantei de timp, cel din urmă fiind mic relativ la durata unui task. Tot despre algoritmul Round-robin se poate spune că are cel mai mare timp de terminare dintre cei cinci algoritmi studiaţi. Realizând o comparative între cei doi algoritmi bazaţi pe cuante de timp, anume Round-robin şi Lottery Round-robin, observăm că media timpilor de terminare este puţin mai mare în cazul algoritmului Round-robin, cea ce era de aşteptat, având în vedere preferinţa algoritmului Lotterry RR pentru anumite task-uri. 24
Figură 2.16. Funcţia de probabilitate a t t pentru RR, respectiv Lottery RR În cazul timpului mediu de răspuns, pentru algoritmii Lottery RR respectiv RR, se obţin rezultate mai bune pentru cel din urmă. Figură 2.17. Funcţia de probabilitate a t r pentru RR si pentru Lottery RR 25
2.3. Condiţiile de timp real Sistemele de timp real se caracterizează prin necesitatea obţinerii unui rezultat într-un interval stabilit. Pentru ele, respectarea acestui timp minim impus este la fel de importantă ca şi corectitudinea rezultatelor [3]. Ele se împart în sisteme cu constrângeri dure si sisteme cu constrângeri lejere. În sistemele cu constrângeri dure (engl: Hard Real-time) ratarea termenului de predare a rezultatelor poate duce la situaţii catastrofale iar prin proiectarea lor trebuie să se prevină astfel de situaţii. Exemple în acest sens sunt sistemul de control al unei centrale nucleare sau sistemul de deschidere a airbag-urilor într-o maşină. În sistemele cu constrângeri lejere (engl: Soft Real-time) pe de altă parte, se permite depăşirea termenului, ocazional. Acest lucru ducând doar la scăderea calităţii serviciului oferit. Printre exemplele de sisteme de timp real cu restricţii lejere putem enumera monitoarele de supraveghere video sau telecomunicaţiile. În figura 2.18 apare executarea unui task periodic având constrângeri de timp real. S-a notat cu t a timpul de aşteptare pentru apariţia evenimentului, cu C timpul de execuţie sau timpul de calcul iar cu D timpul limită maxim (engl: deadline). Condiţia pentru respectarea cerinţei de timp real este ca t a + C < D. În figura 2.18 apare executarea unui task periodic având constrângeri de timp real. S-a notat cu t a timpul de aşteptare pentru apariţia evenimentului, cu C timpul de execuţie sau timpul de calcul iar cu D timpul limită maxim (engl: deadline). Condiţia pentru respectarea cerinţei de timp real este ca t a + C < D. Figură 2.18. Constrângerile de timp real 26
În sisteme cu astfel de constrângeri, sistemul de operare, numit şi RTOS (Real Time Operating System) trebuie să ne asigure facilităţi pentru garantarea nevoilor de timp real. Mai precis ele trebuie să aibă un sistem de priorităţi care să asigure un timp minim de execuţie task-urilor prioritare. În majoritatea microcontrolerelor este prevăzut un astfel de sistem, numit sistem de întreruperi, care permite întreruperea asincronă a programului principal şi schimbarea fluxului de control către un alt program. Aceste întreruperi funcţionează după o regulă absolută de priorităţi: orice întrerupere cu o prioritate mai mare se poate executa peste o întrerupere cu o prioritate mai mică. Luând în calcul proiectarea pe un microcontroler cu un sistem de întreruperi, proiectantul sistemului de operare trebuie să implementeze un sistem echitabil de planificare pentru task-uri cu aceeaşi gamă de priorităţi (de regulă scăzută). Dintre algoritmii studiaţi, FCFS şi SJF pot duce la executarea întârziată a unor taskuri prioritare, iar Lottery Round-robin este nedetermist. Totuşi se poate implementa cu succes algoritmul bazat pe priorităţi pentru task-uri cu priorităţi diferite şi algoritmul Round-robin pentru task-uri cu aceeaşi prioritate. 27
3 Implementarea Microkernelului În ciclul de viaţă al unui proiect trebuie urmărite o serie de etape bine stabilite pentru a asigura îndeplinirea obiectivelor cu succes. Aceste etape sunt de regulă de analiză, proiectare, implementare şi de testare. Însă ciclu de viaţă nu se termină odată cu implementarea efectivă a soluţiei, ci se continua cu etapa de mentenanţă post-implementare. Aceasta constă în îmbunătăţirea facilitaţilor deja existente, corectarea unor erori imposibil de prevăzut în momentul proiectării sau dezvoltarea ulterioară a proiectului. În cadrul implementării microkernelului am folosit tehnica proiectării în spirală. Aceasta a constat în realizarea mai multor prototipuri unul după altul, fiecare fiind realizat prin cei patru paşi definiţi anterior. 3.1. De ce un microkernel? Actualmente, datorită abundenţei sistemelor de operare şi a utilizării lor într-un mod generalizat pe sisteme cu microprocesoare, producătorii de chipuri au implementat suport direct pe procesor pentru funcţiile acestora. Un exemplu concret este capacitatea de a scrie cod protejat sau neprotejat prin modificarea unui bit dintr-un registru intern. Pentru a coordona resursele unui calculator, sistemele de operare trebuie să aibă acces de scriere/citire asupra programelor utilizator, dar pentru a fi protejate de erori umane, trebuie să nu poată fi modificate de acestea. Acest lucru duce la o primă clasificare a sistemelor de operare între sisteme de operare scrise în întregime în cod privilegiat, aşa numitele kernele monolitice (dintr-o singură bucată) şi sisteme de operare scrise parţial în cod protejat. Pentru ultima categorie, se face distincţia clară între kernel (numit microkernel datorită dimensiunii reduse) şi restul sistemului de operare, scris în cod neprotejat. A treia categorie reprezintă o combinaţie a celor două abordări, având componentele esenţiale, dar şi o parte a elementelor neesenţiale ale sistemului scrise în cod protejat. 28
În figurile 3.1, 3.2 şi 3.3 sunt reprezentate grafic cele trei mari categorii de sisteme de operare, sursa bibliografică utilizată fiind [19]. În zona colorată cu roşu apar componentele sistemului de operare scrise în cod protejat iar în zona colorată cu galben sunt trecute elementele scrise în cod utilizator sau neprotejat. Figură 3.1. Sistem de operare monolitic Figură 3.2. Sistem de operare de tip microkernel 29
Figură 3.3. Sistem de operare de tip hibrid Datorită dimensiunilor reduse şi a modularităţii microkernelelor, acest tip de sistem de operare este cel mai adesea folosit în sistemele integrate. Printre versiunile cunoscute de microkernele putem enumera QNX, MINIX, Symbian, ThreadX şi FreeRTOS. Pentru o arhitectură dată, acestea pot varia ca mărime între câteva zeci de KB până la câteva sute KB. Din cauza cerinţelor diferite ce pot să apară în proiectarea sistemele integrate, o practică des întâlnită este conceperea separată a sistemului de operare, în defavoarea utilizării unei versiuni existente de pe piaţă. Proiectantul sistemului de operare, poate alege, aşadar, metoda de implementare a task-urilor, de alocare a memoriei, algoritmul de planificare a taskurilor precum şi eventuale componente din afara microkernelului (spre exemplu un sistem de fişiere). 3.2. Componenta Hardware Microcontrolerul ales pentru a implementa sistemul de operare este STM32-F107 şi este integrat pe o plăcuţă de dezvoltare STM32-H107, amândouă produse de Olimex. Acest microcontroler utilizează un procesor de tip Cortex-M3 interfaţat cu o memorie de program de 256 KB de tip Flash şi cu 64 KB de memorie de date de tip SRAM. Sursele primare de ceas constau dintr-un oscilator de cuarţ extern cu frecvenţa de 25MHz si din un 30
oscilator intern RC cu frecvenţa de 8 Mhz. Printre alte periferice putem enumera 10 timere din care 4 sunt de uz general, 2 CAN-uri pe 12 biţi ce pot analiza până la 16 canale externe, 2 DAC-uri pe 12 biţi şi un senzor de temperatură. Comunicarea cu exteriorul se poate realiza prin trei porturi SPI, două porturi I²C, şi printr-un port MII ce ne permite accesul pe Ethernet. Pinii de I/O sunt configuraţi pe 5 porturi numerotate de la A la E cu 8 biţi fiecare. Pentru a vedea uşor parametrii de stare ai microcontrolerului, plăcuţa mai are 2 LED-uri de culori diferite. Tensiunea de utilizare este de 2-3.6V (3.3V in mod nominal) pentru alimentare şi nivelul logic "1". Aceasta tensiune este asigurată printr-un transformator alimentat prin USB. În starea nealimentata, microcontrolerul poate asigura tensiune unui număr de 41 de regiştrii pe 16 biţi şi unui oscilator 32.768 Khz utilizând o baterie internă. Frecvenţa utilizată în cadrul proiectului a fost de 37.5 Mhz, obţinută folosind un multiplicator de frecvenţă, de la oscilatorul extern de 25 Mhz. Figură 3.4. Plăcuţa de dezvoltare STM32-H107. Sursa pozei este [20] 31
O altă componentă hardware folosită a fost debugger-ul pe JTAG ST-Link/v2 produs tot de Olimex. Acesta se conectează la portul JTAG al microcontrolerului pentru a avea control şi acces nemijlocit asupra resurselor microcontrolerului utilizând magistrala internă a procesorului. Prezentată în figura 3.5, aceasta componentă e folosită pentru programarea microcontrolerului şi mai asigură depanarea programului prin funcţiile de executare pas cu pas şi prin introducerea breakpoint-urilor. Figură 3.5. Programatorul/Depanatorul ST/Link v2. Sursa imaginii este [21] Pentru a facilita programarea sa, microcontrolerul mai este prevăzut cu un bootloader în ultimii 2 KB (numită şi zona de memorie de sistem) ai memoriei Flash. Acesta iniţializează regiştrii interni ai procesorului precum şi regiştrii interfeţei seriale de comunicaţie USART pentru a permite comunicare cu PC-ul. Tabelul 3.1, luat din [12] ne prezintă zone de memorie la care putem sării la alimentarea microcontrolerului, setând pinii B0 şi B1 prin intermediul unor jumperi. Boot mode selection pins BOOT1 (B1) BOOT0 (B0) Boot mode X 0 User Flash memory 0 1 System memory 1 1 Embedded SRAM Tabel 3.1. Modurile de pornire pentru microcontrolerul STM32-F107 32
Deoarece nu vom folosi interfaţa serială USART pentru programare şi dorim executarea programului din memoria Flash pinul B0 va fi setat pe 0. 3.2.1. Arhitectura Cortex-M3 Cel mai important element al microcontrolerului este procesorul Cortex-M3. Acesta implementează arhitectura ARMv7 de tip RISC pe 32 de biţi. Spaţiul de adrese este tot pe 32 de biţi, ducând la o zonă adresabilă de 4GB de la 0x00000000 la 0xFFFFFFFF pentru cele două memorii. Acestea sunt adresate prin magistrale diferite datorită arhitecturii ARMv7 de tip Harvard. În cazul microcontrolerului STM32-F107, memoria Flash ocupă zona de la 0x08000000 până la 0x0803FFFF iar memoria SRAM de la 0x20000000 până la 0x2000FFFF. Printre elementele strâns legate de procesor, putem enumera NVIC-ul (Nested Vectored Interrupt Controller) tabela ROM precum şi mai multe componente cu scopul de a ajuta in depanarea programului. Dintre acestea, NVIC-ul reprezintă elementul însărcinat cu prioritizarea şi trimiterea întreruperilor către procesor, acesta putând configura intre 1 si 240 de întreruperi externe. Trecând scurt în revistă setul de instrucţiuni ARMv7-M, acesta suporta atât instrucţiuni pe 16 biţi (Thumb şi Thumb2) utile pentru a mării densitatea codului cât şi pe 32 de biţi. Cât despre microarhitectură, elementele semnificative ale acesteia sunt: specularea branch-urilor şi pipeline-ul cu 3 stagii: Instruction Fetch, Instruction Decode şi Instruction Execute. Cunoaşterea acesteia din urmă este importantă pentru măsurarea corectă a duratei unor instrucţiuni cu restricţii legate de pipeline. Etapele pipeline sunt reprezentate în figura 3.6. 33
Figură 3.6. Stagiile pipeline pentru instrucţiunile ARMv7. Sursa pentru imagine este [11] Microcontrolerele cu procesoare ce implementează setul de instrucţiuni ARMv7-M prezintă o serie de avantaje faţă de alte microcontrolere de pe piaţă, cum ar fi un raport putere de calcul/energie consumată foarte bun precum şi documentaţie şi suport amplu în compilatoare. În cazul de faţă, a fost ales un microcontroler cu un procesor Cortex-M3 din cauza suportului nativ al acestuia pentru sistemele de operare. În mod concret acesta prezintă, asemenea arhitecturilor ARM anterioare, două niveluri de protecţie a codului. Nivelul protejat aparţine exclusiv funcţiilor de întrerupere şi a altor tipuri de excepţii, el fiind propice scrierii codului sistemului de operare, iar nivelul neprotejat aparţine restului aplicaţiilor, el putând fi executat şi de funcţiile de întrerupere. Alt aspect important sunt întreruperile SysTick şi PendsSV implementate. Ele fac parte din grupul predefinit de întreruperi, care ocupa primele 15 priorităţi din totalul celor 255 de întreruperi posibile. Întreruperea Systick este generată când timer-ul pe 24 de biţi numit tot SysTick ajunge la 0. Acestui timer îi se poate configura durata precum şi sursa de ceas. Pentru 34
a ţine microcontrolerul funcţional într-o stare latentă în lipsa alimentării sau pentru a avea o cuantă de timp bazată pe multiplii/submultiplii de o secundă, putem utiliza RTC-ul (Real Time Clock-ul) integrat pe cip ca sursă de ceas pentru SysTick. Cealaltă întrerupere, numită PendSV poate fi utilizată de programator pentru a apela "manual" scheduler-ul. Aceste două întreruperi au priorităţile -1 respectiv -2, fiind executate înaintea tuturor întreruperilor configurabile de utilizator. Procesorul Cortex-M3 mai prezintă suport pentru sistemele de operare sub forma a doi pointeri pentru stivă: Main Stack Pointer (MSP) şi Process Stack Pointer (PSP). Aceştia sunt amândoi adresabili în zona registrului Stack Pointer (SP), cum se vede şi în figura 3.7, luată din documentul [14] Figură 3.7. Regiştrii procesorului Cortex-M3 După un restart, procesorul trece automat la utilizarea MSP-ului, acesta putând fi folosit atât din cod privilegiat, cât şi din cod neprotejat. PSP-ul, în schimb, poate fi utilizat doar din cod neprotejat, acesta fiind recomandat în executarea proceselor. Deoarece nu am utilizat în mod separat un thread pentru sistemul de operare, acesta fiind scris în zona funcţiilor de excepţie, se va folosi în toate cazurile, stiva principală (MSP). 35
3.3. Componenta Software În proiectarea microkernelului, se va folosi întreruperea SysTick pentru implementarea unui algoritm de tip Round-robin, urmând ca la reviziile ulterioare să se poată implementa alţi algoritmi preemptivi, utilizând întreruperea PendSV. Algoritmul Round-robin a fost ales datorită caracterului său determinist, al timpului mic de schimbare între task-uri precum şi datorită caracteristicilor sale de timp-real. Proiectul a fost scris în C++ şi în limbaj de asamblare şi compilat în mediul de dezvoltare CooCox. Acesta a mai ajutat şi la programarea respectiv depanarea pas cu pas a programului, împreună cu programatorul ST-Link/v2. Limbajul predominant folosit a fost C++, iar pentru salvarea şi încărcarea regiştrilor s-a folosit limbajul de asamblare specific arhitecturii ARMv7. O notă importantă aici este necesitatea dezactivării opţiunii de optimizare a codului produs de compilator pentru a evita situaţii neprevăzute. 3.3.1. Schimbarea contextului Într-un microkernel, planificatorul de thread-uri este componenta ce aloca pe procesor task-urile după un algoritm dat. Pentru realizarea acestui ţel, el se foloseşte de o altă componentă, numită dispatcher pentru a salva starea task-ului curent şi a încărca starea taskului următor. Contextul, sau starea procesului, e păstrat în cei 16 + 1 regiştrii ai procesorului. Dispatcherul trebuie să asigure, aşadar, salvarea conţinutului regiştrilor R1-R13 precum şi a regiştrilor LR (Link Register), PC (Program Counter), LR (Link Register) PSR (Program Status Register), şi să încarce aceiaşi regiştri ai următorului thread în procesor. Pe de altă parte, planificatorul, trebuie să asigure alegerea corectă a următorului task pentru execuţie precum şi actualizarea tabelei de procese cu noua stare a task-urilor. Aceste operaţii trebuie realizate utilizând cât mai puţine instrucţiuni pentru a asigura fluenţa rulării programelor şi dimensiunea redusă a microkernelului. Sistemul de operare va implementa un tabel central pentru a ajuta cunoaşterea numărului şi stărilor task-urilor. Acesta este definit ca o structură având câmpurile 36
stack_pointer şi flag. Rolul câmpului stack_pointer este intuitiv, acesta păstrând un pointer către stiva unui task, iar câmpul de flag-uri ne spune dacă task-ul respectiv este gata sau nu de execuţie, în funcţie de ultimul bit. Trecerea la următorul thread se concretizează prin următorii paşi: a. Salvarea regiştrilor pe stivă principală b. Trecerea în tabela de procese a nivelului stivei procesului curent c. Căutarea următorului proces liber de execuţie în tabela de procese d. Trecerea în registrul de stivă al procesorului a nivelului noii stive e. Încărcarea restului regiştrilor în procesor În prima etapă, precum şi în ultima, trebuie să fie încărcaţi explicit doar regiştrii de la R5 la R11 inclusiv. De restul regiştrilor se ocupă automat procesorul la executarea întreruperii SysTick. Acest lucru este reprezentat în figura 3.8, luată şi modificată din [22]. Figură 3.8. Schimbarea contextului in procesorul Cortex-M3 37