Documente noi - cercetari, esee, comentariu, compunere, document
Documente categorii

Nucleul de timp real minimal

Nucleul de timp real minimal


Obiectivele lucrarii


Se aprofundeaza cunostintele dobindite la curs, prin implementarea efectiva a primitivelor de baza ale oricarui nucleu multitasking, in varianta sa cea mai simplificata.


Aspecte teoretice


Pentru a evidentia mecanismele esentiale dintr‑un nucleu multitasking, vom realiza un nucleu simplificat. Primitivele de baza permit ale acestuia vor fi:


- crearea unui task

- activarea unui task

- executia unui task

- terminarea unui task




Programul va trebui sa contina cel putin un task activ, deoarece in acest nucleu nu se prevede gestionarea inactivitatii procesorului. Din motive de simplitate, planificarea taskurilor se face folosind o coada circulara cu un singur nivel de prioritate. Acest tip de planificare nu este utilizata in sistemele de timp real, dar tratarea cozilor facindu‑se independent, ea poate fi usor modificata.


A. Contextul unui task


Intr‑un nucleu, contextul unui task este mai redus decit in cazul unui sistem de operare, continind insa obligatoriu urmatoarele informatii:


- adresa de inceput a taskului

- adresa stivei

- starea curenta a taskului

- valoarea registrelor CPU


│ Pointer │ │ Pointer │ Context │

│ inceput │ Stare │ inceput │ CPU │

│ de task │ │ de stiva │ (registre) │

Contextul unui task


Toate aceste informatii pot fi grupate intr‑o singura variabila structurata - s‑o numim CONTEXT. Se poate crea o legatura intre aceste contexte, iar ordinea de executie a taskurilor este functie de politica de ordonare aleasa.


/*Definitii tipuri de date*/


#define TASK void far

typedef TASK (*TASK_ADR )( void );

typedef void far interrupt ( *INT_FUNC )( void );

#define word unsigned short

#define byte unsigned char


/*Contextul unui task */


typedef struct

CONTEXT;


TASK este un tip utilizat pentru a defini un task. El se utilizeaza pentru a putea distinge rapid o functie de un task. Acest tip va fi redenumit dupa cum urmeaza.

Tipul TASK_ADR se refera la un pointer de functie fara parametri si care nu returneaza ceva. El va servi pentru a defini variabila task_adr, ce contine adresa de inceput a taskului. Aceasta variabila serveste la lansarea in executie a taskului.

Tipul INT_FUNC refera un pointer de functie de tratare a intreruperii si va fi folosit pentru a defini vectorul de intrerupere catre planificatorul de tip 'preemptiv'.

Contextele pot fi organizate:


- in liste inlantuite;

- in masive (arrays).


Organizarea aleasa in mininucleu este cea de masiv (tablou), caci permite desolidarizarea politicii de ordonare in raport cu organizarea contextelor. Gestiunea cozii de taskuri consta deci in gestiunea unei cozi de intregi, care se face separat. Numarul taskului, obtinut in urma apelului functiei next_t(), serveste la indexarea tabloului de contexte.

Dimensiunea unui element de tablou trebuie aleasa o putere a lui 2, in ideea de a obtine o adresare mai eficace. Realizind acest lucru, accesul la un element de tablou se poate face in acest caz prin deplasarea indicelui si nu prin inmultire (daca se foloseste un compilator evoluat).


B. Codificarea starilor unui task


Starea fiecarui task se codifica printr‑o constanta simbolica. Aceste constante sint arbitrare. Un exemplu de codificare:


#define NCRE 0                         starea NECREAT

#define CRE 0x80 starea CREAT

#define RDY 0x90 starea GATA (READY)

#define SUS 0xA0 starea SUSPENDAT

#define RUN 0xC0 starea IN LUCRU (RUN)


C. Variabile globale in nucleu


EXTERN CONTEXT _context[MAX_TASKS];

EXTERN volatile word              _task_c;

EXTERN word                             _tos;


Prima variabila este tabloul de contexte deja descris. A doua memoreaza numarul taskului in lucru. Ultima refera valoarea virfului de stiva pentru urmatorul task ce va fi creat.

Variabilele de mai jos vor fi referite ulterior.


EXTERN jmp_buf                      _context_ret;

EXTERN volatile                         _itlog;

EXTERN INT_FUNC _v0x8;

extern word                               _stklen;

extern word                               _psp;



D. Comutarea contextelor


Comutarea taskurilor este mecanismul prin care un task este lansat, reluat, suspendat sau terminat. Modul de realizare a comutarii depinde de tipul de nucleu realizat. Intr‑un nucleu fara rechizitia procesorului, contextul unui task trebuie conservat (salvat) intre executiile a 2 linii de program adiacente.

Salvarea registrelor esentiale dupa executia unei linii de program se poate realiza prin intermediul functiilor din biblioteca C standard setjmp si longjmp. Existenta acestor functii permite realizarea de nuclee portabile la nivel de program sursa.

Intr-un nucleu cu rechizitie, contextul unui task trebuie conservat (salvat) intre executiile a doua instructiuni de procesor. In acest caz devine esentiala salvarea tuturor registrelor procesorului. Aceasta salvare este mult facilitata de compilatoarele C ce permit declararea functiilor de tip interrupt. Modificatorul interrupt forteaza salvarea registrelor in stiva la intrarea in functie si restaurarea lor la iesirea din functie.


E. Nucleu cu rechizitie


Ca si in nucleul fara rechizitia procesorului, planificatorul poate fi apelat in mod explicit (executia unei primitive), sau la intervale regulate printr‑o intrerupere de ceas. Intreruperea survenind in mod asincron, planificatorul trebuie sa salveze si registrele nesalvate de functia setjmp. Aceasta secventa se face in general in limbaj de asamblare.

Pentru compilatoarele care recunosc modificatorul interrupt, toate registrele sint salvate la intrarea in functie. Ramine ca obligatorie salvarea indicatorului de stiva (_SP) in variabila _context.



Comutarea taskurilor prezentata anterior poate fi optimizata, prin accesarea variabilei _context[_task_c] prin intermediul unui pointer, ca variabila de tip register (nu auto!). Daca compilatorul nu permite alocarea de pointeri in registrele procesorului, atunci pointerul trebuie declarat prin alocare statica (atributul static).

Cu un procesor rapid aceasta comutare se poate face in 30..60 microsecunde (masurata pentru un microprocesor I80286 16Mz, cu gestiunea cozii de taskuri in asamblare). Acest planificator poate reprezenta punctul de plecare pentru un executiv de timp real industrial.

In cazul nucleelor cu rechizitie (preemptive), comutarea poate avea loc in orice zona de cod intreruptibila. Daca nucleul este testat pe un sistem de operare in care taskurile sint nereentrante (cazul MS‑DOS‑ului), va rezulta in general o pierdere de control asupra masinii. Devine astfel necesar controlul contextului in care are loc comutarea. Modul cel mai simplu de a verifica ce sectiune de cod este in curs de comutare, este de a analiza continutul stivei. Daca adresa de retur plasata in stiva taskului apartine sistemului de operare, comutarea trebuie terminata fara a efectua schimbarea de context.

Accesul la continutul stivei se face declarind o variabila structurata transmisa ca parametru comutatorului. Structura acestei variabile nu poate fi portabila; de asemeni nici adresele sistemului de exploatare ce trebuiesc controlate. Totusi principiul ramine acelasi oricare ar fi procesorul utilizat.

Pentru un microprocesor din familia i80x86, executia unei intreruperi logice sau materiale interzice intreruperile, apoi provoaca salvarea in stiva a indicatorilor (FLAGS), a registrului de segment CS si a pointerului de instructiuni IP. Daca se utilizeaza compilatoare de la firmele Borland sau Microsoft, codul generat la intrarea intr-o functie de tip interrupt este urmatorul:


_scheduler proc far

push ax

push bx

push cx

push dx

push es

push ds

push si

push di

push bp

mov bp,DGROUP



Ansamblu acestor registre (inclusiv FLAGS, CS si IP) poate fi accesat prin declararea unui parametru de procedura de tip REGS:


typedef struct

co;

} adr;

word flags;

} REGS;


Procedura scheduler() este apelata printr‑o intrerupere materiala la fiecare 55 milisecunde (intreruperea de ceas, cu numarul 8). Ea include si apelul rutinei de intrerupere de ceas originala, salvata anterior intr-un pointer la functie (*_v0x8)(). Deoarece comutatorul poate fi apelat atit prin intreruperi software cit si hardware, se poate utiliza un indicator global (_itlog), ce trebuie pozitionat pe 1 inaintea apelului nucleului printr-o primitiva.


/*Planificator: asigura comutarea taskurilor la fiecare 55 ms

(si implicit comutarea contextelor) conform algoritmului de

planificare ales (baleiere circulara simpla, fara priorita-

te). Planificatorul este lansat implicit de fiecare intreru-

pere de ceas (vectorul 8) daca secventa intrerupta nu a fost

o functie DOS sau alt cod nereentrant. Comutarea se bazeaza

pe analiza stivei.*/


void interrupt scheduler( REGS regs )


else/* daca nu era in READY era RUN */

_SP = p->sp;/* comuta pe stiva noului task */

} /* la iesire se face comutarea */

} /*prpriu-zisa !!!*/



F. Primitivele nucleului minimal



F1. Crearea unui task


Intr-o aplicatie de timp real, taskurile sint in general cunoscute dupa lansarea aplicatiei. Crearea taskurilor va putea fi deci realizata cu ajutorul unui program numit configurator. Acest program interactiv permite descrierea caracteristicilor taskurilor (nume, dimensiunea stivei, prioritate,). Iesirea acestui program este un program sursa in asamblare sau in C ce contine un ansamblu de declaratii de variabile structurate legate de aplicatie in timpul fazei de compilare.

Crearea unui task poate fi facuta si in mod dinamic. In acest caz codul taskului este adus din memorie de pe un dispozitiv de stocare (disc). Aceasta solutie, foarte lenta, nu poate fi luata

in considerare intr-o aplicatie de timp real.

In nucleul minimal prezentat aici, crearea taskului se face in timpul fazei de initializare. Crearea unui task consta in actualizarea variabilei de stare a taskului (NCRE -> CRE), rezervarea de stiva si memorarea adresei de inceput a taskului in variabila CONTEXT a acestuia. Sectiunile critice sint realizate prin intermediul functiilor _lock_() si _unlock_(). Primitiva de creare returneaza un identificator al taskului sub forma unui numar intreg. In continuare orice referire a taskului se face prin intermediul

acestui intreg.


/*Crearea unui task: asocierea unui context, alocare de stiva,

memorarea adresei de inceput a taskului, trecerea din starea

NCRE in starea CRE. */


word creat( TASK_ADR adr_task )



F2. Activarea unui task


Activarea unui task consta in pozitionarea variabilei de stare a taskului la valoarea RDY, apoi plasarea taskului in lista taskurilor gata. Cererile de activare a taskului pot fi memorate sau nu.

Intr‑un sistem care nu realizeaza memorarea cererilor de activare, acestea vor fi pierdute daca taskul nu este pregatit sa le primeasca (se afla intr-o stare diferita de CRE). In caz contrar, cererile pot surveni in orice moment, si vor fi luate in considerare in momentul in care taskul revine in starea CRE.

Intr‑un sistem fara rechizitia procesorului, in general, o data cu executia acestei primitive se realizeaza si lansarea planificatorului (ordonatorului). Intr-un nucleu cu rechizitie, apelul planificatorului este facultativ, comutarea taskurilor active avind loc obligatoriu intr-un timp finit. In toate situatiile



insa, activarea unui task nu este sinonima cu lansarea lui in executie imediat, momentul lansarii depinzind de tipul de planificator utilizat.

Primitiva activ din mininucleu nu realizeaza memorarea cererilor de activare.


/*Activare task: trecerea in starea READY si plasarea in lista

taskurilor gata. */


void activ( word task )



F3. Terminarea unui task


Aceasta primitiva trebuie obligatoriu apelata la terminarea unui task.


/*Sfirsit de task: trecerea in starea CREAT si apoi distruge-

rea sa prin extragerea sa din coada de taskuri. */


void endtask( void )



F4. Initializarea sistemului (nucleului)


Initializarea nucleului multitasking trebuie realizata inainte de lansarea aplicatiei.

Dupa ce s‑au initializat variabilele interne si s-a pregatit iesirea din program (normala sau in caz de eroare), sistemul apeleaza o functie declarata ca punct de intrare in aplicatie.

Intr-un sistem in care taskurile pot fi create in mod dinamic, aceasta functie este incarcata de la crearea si pina la lansarea taskurilor initiale. Minisistemul prezentat aici procedeaza (lucreaza) intr‑un mod similar: el executa un task al aplicatiei al carui nume este dat ca parametru. Acest task este folosit ca task de initializare. In timpul fazei de initializare, sistemul se afla deci in mod multitasking. Acest fapt poate prezenta inconveniente, daca taskurile sint lansate dezordonat. Remedierea se poate face fie atribuind acestui task prioritatea maxima, fie realizindu‑l neintreruptibil.


/*Initializarea nucleului: pregateste toate zonele de date pt.

salvarea contextelor, se pun toate taskurile potentiale in

starea NECREAT, se goleste coada de taskuri, seteaza handle-

rul <Ctrl/BREAK>, corupe vectorul 8 cu adresa planificatoru-

lui, formeaza contextul de revenire in DOS, si defineste

punctul de iesire din monitor.*/


void start( TASK_ADR adr_task )



F5. Gestiunea cozii de taskuri


Acestea permit manevrarea taskurilor conform strategiei de ordonare aleasa. Operatiile tipice sint:


-introducerea unui task in coada;

-scoaterea unui task (sau a celui curent) din coada;

-returnarea identificatorului urmatorului task din coada.


Functiile de gestiune a cozilor sint utilizate de catre primitivele nucleului si planificator intr‑un mod transparent pentru utilizator. Codul sursa al acestor functii va fi prezentat in lucrarea urmatoare.


Consideratii practice


G. Aplicatie cu 4 taskuri


Aplicatia descrisa mai jos, pune in evidenta utilizarea executivului minimal pentru controlul a patru taskuri simple.


#include <stdio.h>

#include 'MTR.H'


/*Prototipuri pentru taskuri */



TASK task_A( void );

TASK task_B( void );

TASK task_C( void );

TASK task_D( void );


/*Taskul de initializare: scrie un mesaj de start si activeaza

celelalte trei taskuri. La sfirsit isi incheie executia. */


TASK task_A( void )



/*Taskul B: la inceput executa o afisare a unui mesaj de start

si apoi, din timp in timp, afiseaza cite un mesaj de control

pentru a marca trecerea prin task. */


TASK task_B( void )


/*Taskul C: la inceput executa o afisare a unui mesaj de start

si apoi, din timp in timp, afiseaza cite un mesaj de control

pentru a marca trecerea prin task. */


TASK task_C( void )


/*Taskul D: la inceput executa o afisare a unui mesaj de start

si apoi, din timp in timp, afiseaza cite un mesaj de control

pentru a marca trecerea prin task. Dupa 50 de treceri prin

acest task se forteaza iesirea din monitorul de timp real. */


TASK task_D( void )


/*Program principal. Starteaza aplicatia.*/



void main()



H. Realizarea aplicatiilor de timp real bazate pe monitorul (nucleul) minimal


Codul sursa al monitorului prezentat se gaseste in intregime (in mod normal), in directorul X:STR (impreuna cu lucrarile de laborator):


MTRCONST.H header constante si definitii;

MTRVAR.H header variabile globale;

MTRPROTO.H header prototipuri functii;

MTR.H header general pentru aplicatii;

MTR.C primitive si planificator;

MTRFILE.C gestiunea cozilor;

TEST.C aplicatie de test.


Exista mai multe cai de a dezvolta o aplicatie cu acest nucleu. Cea mai simpla consta in realizarea unui proiect (sub mediul TCPP de exemplu) cuprinzind urmatoarele fisiere:


X:STRTEST.C (se inlocuieste cu orice nume, dupa caz)

X:STRMTR.C

X:STRMTRFILE.C


F. Primitivele nucleului minimal





F1. Crearea unui task


Intr-o aplicatie de timp real, taskurile sint in general cunoscute dupa lansarea aplicatiei. Crearea taskurilor va putea fi deci realizata cu ajutorul unui program numit configurator. Acest program interactiv permite descrierea caracteristicilor taskurilor (nume, dimensiunea stivei, prioritate,). Iesirea acestui program este un program sursa in asamblare sau in C ce contine un ansamblu de declaratii de variabile structurate legate de aplicatie in timpul fazei de compilare.

Crearea unui task poate fi facuta si in mod dinamic. In acest caz codul taskului este adus din memorie de pe un dispozitiv de stocare (disc). Aceasta solutie, foarte lenta, nu poate fi luata in considerare intr-o aplicatie de timp real.

In nucleul minimal prezentat aici, crearea taskului se face in timpul fazei de initializare. Crearea unui task consta in actualizarea variabilei de stare a taskului (NCRE -> CRE), rezervarea de stiva si memorarea adresei de inceput a taskului in variabila CONTEXT a acestuia. Sectiunile critice sint realizate prin intermediul functiilor _lock_() si _unlock_(). Primitiva de creare returneaza un identificator al taskului sub forma unui numar intreg. In continuare orice referire a taskului se face prin intermediul

acestui intreg.


/*Crearea unui task: asocierea unui context, alocare de stiva,

memorarea adresei de inceput a taskului, trecerea din starea

NCRE in starea CRE. */


word creat( TASK_ADR adr_task )



F2. Activarea unui task


Activarea unui task consta in pozitionarea variabilei de stare a taskului la valoarea RDY, apoi plasarea taskului in lista taskurilor gata. Cererile de activare a taskului pot fi memorate sau nu.

Intr‑un sistem care nu realizeaza memorarea cererilor de activare, acestea vor fi pierdute daca taskul nu este pregatit sa le primeasca (se afla intr-o stare diferita de CRE). In caz contrar, cererile pot surveni in orice moment, si vor fi luate in considerare in momentul in care taskul revine in starea CRE.

Intr‑un sistem fara rechizitia procesorului, in general, o data cu executia acestei primitive se realizeaza si lansarea planificatorului (ordonatorului). Intr-un nucleu cu rechizitie, apelul planificatorului este facultativ, comutarea taskurilor active avind loc obligatoriu intr-un timp finit. In toate situatiile insa, activarea unui task nu este sinonima cu lansarea lui in executie imediat, momentul lansarii depinzind de tipul de planificator utilizat.

Primitiva activ din mininucleu nu realizeaza memorarea cererilor de activare.


/*Activare task: trecerea in starea READY si plasarea in lista

taskurilor gata. */


void activ( word task )


_unlock_(); /* sfirsit sectiune critica */



F3. Terminarea unui task


Aceasta primitiva trebuie obligatoriu apelata la terminarea unui task.


/*Sfirsit de task: trecerea in starea CREAT si apoi distruge-

rea sa prin extragerea sa din coada de taskuri. */


void endtask( void )



F4. Initializarea sistemului (nucleului)


Initializarea nucleului multitasking trebuie realizata inainte de lansarea aplicatiei.

Dupa ce s‑au initializat variabilele interne si s-a pregatit iesirea din program (normala sau in caz de eroare), sistemul apeleaza o functie declarata ca punct de intrare in aplicatie.

Intr-un sistem in care taskurile pot fi create in mod dinamic, aceasta functie este incarcata de la crearea si pina la lansarea taskurilor initiale. Minisistemul prezentat aici procedeaza (lucreaza) intr‑un mod similar: el executa un task al aplicatiei al carui nume este dat ca parametru. Acest task este folosit ca task de initializare. In timpul fazei de initializare, sistemul se afla deci in mod multitasking. Acest fapt poate prezenta inconveniente, daca taskurile sint lansate dezordonat. Remedierea se poate face fie atribuind acestui task prioritatea maxima, fie realizindu‑l neintreruptibil.


/*Initializarea nucleului: pregateste toate zonele de date pt.

salvarea contextelor, se pun toate taskurile potentiale in

starea NECREAT, se goleste coada de taskuri, seteaza handle-

rul <Ctrl/BREAK>, corupe vectorul 8 cu adresa planificatoru-

lui, formeaza contextul de revenire in DOS, si defineste

punctul de iesire din monitor.*/


void start( TASK_ADR adr_task )



F5. Gestiunea cozii de taskuri


Acestea permit manevrarea taskurilor conform strategiei de ordonare aleasa. Operatiile tipice sint:


-introducerea unui task in coada;

-scoaterea unui task (sau a celui curent) din coada;

-returnarea identificatorului urmatorului task din coada.


Functiile de gestiune a cozilor sint utilizate intr-un mod transparent de primitive si planificator.


static byte _file[ MAX_TASKS ]; /* coada de taskuri */

static byte _queue; /* indice de task in coada */


/* 1.Initializare coada. */

/*----- ----- --------- */


void file_init( void )



/* 2.Adauga un task in coada.*/



void add_t( register word n )


else/* daca lista nu e goala atunci */


/* 3.Scoate un task din lista. */

/*----- ----- --------------- */


void sub_t( register word t )


else /* scoate alt task decit cel curent */


}



/* 4. Scoate taskul curent din lista*/



void sub_task_c( void )


/* 5.Afla urmatorul task din lista */



word next_t( void )