2 MULTITASKING 2.1. Preliminarii Este deja foarte bine cunoscut faptul că o programare eficientă în domeniul sistemelor în timp real, în mod evident, vizând aici sistemele în timp real încorporate, este programarea multitasking. Dar la ce ne referim când folosim noţiunea de task? Un task este un program sau o parte a unui program care reunește activități independente sau autonome, apte de a fi rulate simultan cu altele. De exemplu, un program de procesare a textului și un program de calcul tabelar care rulează în același timp pe un sistem desktop sunt, fiecare dintre ele, task-uri. Acestea oferă posibilitatea de a face un calcul şi de a edita un text simultan. Dar procesorul de text poate, de exemplu, să permită editarea unui document şi tipărirea unui al doilea document, ambele acţiuni în acelaşi timp, posibil, al doilea în background. Deci, avem două tipuri de multitasking: multitasking noncooperativ, respectiv multitasking cooperativ. Multitasking-ul non-cooperativ desemnează situația în care task-urile sunt programe de execuţie separate. Task-urile care sunt programe de execuţie separate, sunt numite procese [GREH'98] și, în acest caz, multitasking-ul non-cooperativ se numeste multiprocessing [GREH'98]. 1
Multitasking-ul cooperativ desemnează situația în care task-urile sunt părți ale unui program sau, cu alte cuvinte, părți ale unui proces. Task-urile care sunt părți ale unui program sunt numite fire de execuţie, iar în acest caz, pentru multitasking-ul cooperativ se foloseşte noţiunea de multithreading. Evident, folosind această convenţie a termenilor, putem spune că există următoarele noţiuni: multiprocessing, multithreading, şi multiprocessing cu multithreading. În cele două cazuri de multitasking, nu doar denumirile sunt diferite, ci există şi diferenţe substanţiale - a se vedea următoarele paragrafe. 2.2. Despre multiprocessing Un proces se materializează printr-o stare CPU curentă și o colecție de zone de memorie alocată de sistemul de operare. Această colecție constă în următoarele: O zonă de cod care conţine codul executabil al procesului O zonă de stivă în care sunt implementate variabilele locale și argumentele funcțiilor și unde sunt făcute operaţiile de salvare O zonă de date unde sunt implementate variabilele globale O zonă heap dedicată alocărilor dinamice (alocarea dinamică = alocarea de memorie în timpul execuţiei programului, şi nu la compilare, când e vorba de alocarea statică) O zonă de sistem, dedicată sistemului de operare pentru procesul respectiv. Zona de sistem include informații cu privire la resursele alocate procesului respectiv, cum ar fi structurile de date pentru deschiderea fişierelor. Zona de sistem include, de asemenea, informații despre procesul în sine, cum ar fi localizarea regiunilor sale de memorie. 2
Starea CPU constă din toţi regiştrii CPU care pot fi modificaţi de un program: regiştrii generali, indicatorul de instrucţiuni, indicatorul de stivă, registrul flag-urilor, etc. Un sistem de operare multiprocessing execută mai multe procese prin comutarea succesivă de la contextul unui proces la contextul altuia sau, altfel spus, prin comutarea de context. Contextul unui proces constă în zonele sale de memorie şi în starea CPU. Sistemul de operare comută între procese prin suspendarea execuţiei procesului curent, salvarea contextului său, încărcarea contextului noului proces şi lansarea acestuia în execuţie. Încărcarea contextului noului proces constă în a face accesibile zonele sale de memorie și în încărcarea regiştrilor CPU cu informaţiile care au existat în ele când procesul a ieşit din rulare. Sistemul de operare permite fiecărui proces să ruleze de la un moment de timp până la un timp maxim specific, numit o felie de timp. Când felia de timp a unui proces expiră, sistemul de operare selectează un alt proces, folosind un algoritm de dispecerizare și trece la contextul acelui proces. Deși la un moment dat de timp, un singur proces se execută, toate procesele fac progrese dacă sunt privite într-un interval de timp suficient de lung. Deşi toate procesele se află în memoria sistemului în acelaşi timp, doar zonele de memorie deţinute de procesul curent sunt accesibile. În sistemele care dispun de o gestiune hardware a memoriei, sistemul de operare poate uşor să protejeze zonele de memorie ale proceselor inactive, astfel încât orice încercare de a le accesa să determine o eroare de protecţie a memoriei. În sistemele care nu dispun de un manangement hardware al memoriei, o variabilă pointer cu valoare eronată poate conduce la citiri sau scrieri accidentale în zona de memorie a unui alt proces. Nu există însă, nicio modalitate de a citi sau scrie în mod intenționat în zona de memorie a unui alt proces, pentru că nu se pot obține informații cu privire la localizarea altor procese în memorie. 3
Prin urmare, în cazul în care două procese trebuie să facă schimb de date, acest lucru este imposibil fără anumite mecanisme speciale, numite mecanisme de comunicare. 2.3. Despre multithreading După cum am văzut, un fir de execuţie este un flux autonom de execuţie în cadrul unui proces. Într-un sistem de operare multithreading, un proces este format din unul sau mai multe fire de execuţie. Toate firele de execuţie dintr-un proces partajează aceleaşi zone de memorie de cod, de date, heap şi de sistem ale procesului. Fiecare fir de execuţie are o stare CPU separată și o zonă de stivă separată. Pentru că toate firele de execuţie ale unui proces partajează aceleaşi zone de memorie pentru date si heap, toate datele globale din proces pot fi accesate de către oricare din firele de execuţie. Pe de altă parte, dat fiind că fiecare fir de execuţie are propria stivă, toate variabilele locale şi argumentele funcțiilor sunt private unui fir specific. Deoarece firele de execuţie partajează acelaşi cod şi aceleaşi date globale, firele sunt legate mult mai puternic între ele decât procesele şi tind să interacţioneze mult mai mult decât o fac procesele. Din acest motiv, sincronizarea între taskuri se utilizează mai mult în aplicaţiile de multithreading decât în aplicaţiile de multiprocessing. Modalitățile prin care problemele de sincronizare și de comunicare sunt rezolvate la nivelul proceselor și la nivelul firelor de execuţie, respectiv între procese și între fire sunt similare, aproape identice. Deoarce firele de execuţie partajează aceeaşi zonă de sistem, resursele pe care sistemul de operare le alocă pentru un proces sunt disponibile pentru toate firele de execuţie din cadrul procesului, aşa cum si toate datele globale sunt disponibile, de asemenea, pentru toate firele de execuţie. Aceasta înseamnă că, de exemplu, dacă Thread #1 va 4
deschide un fişier, Thread #2 va putea să acceseze acel fişier, fară să fie nevoit să îl mai deschidă. Aşa cum s-a spus, în aplicaţiile multithreading, datele globale sau statice sunt partajate de către toate firele de execuţie, iar datele locale sau temporare sunt private pentru fiecare fir de execuţie. Cele mai multe sisteme de operare multithreading implementeză un al treilea tip de date: date locale statice, care sunt private pentru un fir de execuţie. Acest tip de date sunt adesea numite date specifice unui fir de execuţie. Datele specifice unui fir de execuţie sunt utile deoarece furnizează o cale pentru ca firul de execuţie să aibă date private care sunt persistente și pot fi accesate de către orice funcție în cadrul procesului. Comutarea de context între firele de execuţie ale aceluiași proces implică pur și simplu salvarea stării CPU a firului de execuţie curent şi încărcarea stării CPU pentru noul fir. Deoarece regiştrii IP și SP sunt reîncărcaţi ca şi parte a stării CPU, după comutarea contextului, procesorul va executa codul de la locaţia asociată noului fir de execuţie în rulare și va utiliza stiva noului fir în rulare. 2.4. Dispecerizare Am spus că funcţionarea unui sistem de operare multitasking constă în executarea unui task și apoi trecerea în execuţie a unui alt task (termenii "task" și "multitasking" sunt aici, utilizaţi în sensul lor generic). Timpul în care un sistem de operare permite unui task să se execute numit felie de timp sau quantum or tick- este de o durată determinată. Sistemul de operare alocă o felie de timp pentru fiecare task. Când felia de timp pentru rularea unui task se termină, partea de sistem de operare numită dispecer determină carui task i se va aloca următoarea felie de timp pentru rulare. Un task poate fi în una din următoarele 3 stări: Rulare. În această stare, task-urile se execută. 5
6 Pregătit. Task-urile în starea de pregătit işi aşteaptă rândul de la CPU. Blocat. Task-urile blocate aşteaptă ca ceva să se întâmple. Un dispecer menține una sau mai multe liste interne pentru a urmări starea fiecărui task. Tipic, acesta are o listă de task-uri pregătite și separat, o listă de task-uri blocate pentru fiecare condiţie de sincronizare pe care task-urile o așteaptă. Task-ul din capul listei de task-uri în starea ready este următorul care va rula. Task-urile din oricare din listele de task-uri blocate sunt suspendate. Ele sunt în așteptare pentru un eveniment: o intrare de la un dispozitiv, un mesaj de la un alt task, schimbarea de stare a unui obiect de sincronizare etc. Ori de câte ori are loc un eveniment pe care un task de pe lista de task-uri blocate îl aşteaptă, acesta este şters de pe lista de taskuri blocate şi plasat în lista de task-uri pregatite, unde îşi așteaptă rândul pentru execuție. Când un task iese din rulare datorită blocării pe o condiţie de sincronizare sau la expirarea feliei de timp, sistemul de operare are nevoie de a efectua o schimbare de context pentru un alt task. Am rezumat acest fenomen mai devreme, dar să îl prezentăm pas cu pas. O schimbare de context implică următoarele: 1. Salvarea contextului task-ului curent. Dispecerul salvează starea CPU pe stiva acelui task şi îşi actualizează în zona sistem a memoriei informaţiile privind localizarea celorlalte zone de memorie ale respectivului task şi alte informaţii precum cele legate de lucrul cu fişierele. Apoi, task-ul curent este plasat pe lista de task-uri pregătite sau pe lista adecvată de taskuri blocate. 2. Selectarea noului task. Dispecerul determină care task va urma pentru execuţie şi îl şterge din lista de task-uri pregătite. 3. Încărcarea contextului noului task. Dacă task-ul este un proces, dispecerul face accesibile zonele de memorie corespunzătoare noului task şi apoi încarcă starea CPU salvată când acest task a ieşit din rulare.
4. Transferul de control al noului task. Ca şi pas final, sistemul de operare încarcă registrul indicator de instrucţiuni. Încărcarea indicatorului de instrucţiuni implică transferul controlului la următorul task. Task-ul işi reia execuţia din punctul în care a rămas (unde a fost făcut ultima oară inactiv). Deși precedent am explicat modul în care are loc schimbarea de context, rămâne o întrebare: Cum un dispecer determină ce task urmează să ruleze? Răspunsul depinde de algoritmul de dispecerizare folosit. Mulți algoritmi de dispecerizare sunt disponibili, cum ar fi: prin rotaţie, cel cu cea mai scurtă durată mai întâi, cel cu cel mai scurt timp de rulare rămas primul, după priorităţi, metoda ruletei, etc [TANE'97]. 7