Informatica
Obiecte nucleu - ce sunt obiectele nucleu, creare obiective nucleu, proceseNote de cursObiecte nucleuCe sunt obiectele nucleu ?Fiecare obiect nucleu este pur si simplu un bloc de memorie alocat de nucleul sistemului de operare si accesibil doar acestuia. Acest bloc de memorie este o structura de date ai carei membri mentin informatii despre obiect. Unii membri ( descriptorii de securitate, contorul de utilizare, si asa mai departe) sunt la fel pentru toate obiectele nucleu, dar majoritatea sunt specifice unor obiecte nucleu particulare. De exemplu un obiect nucleu proces are un ID de proces, o prioritate de baza, un cod de iesire. Deoarece structurile de date ale unui obiect nucleu sunt accesibile doar nucleului, este imposibil ca o aplicatie sa localizeze aceste structuri de date direct in memorie si sa le modifice direct continutul. Windows ofera un set de functii care manipuleaza aceste structuri in modalitati bine definite. Cand apelam o functie care creeaza un obiect nucleu, functia returneaza un identificator al acelui obiect. Putem privi acest indicator ca o valoare opaca care poate fi folosita de orice fir din procesul nostru. Putem transmite acest identificator mai multor functii Windows astfel incat sistemul sa stie care obiect nucleu vrem sa il manipulam. Pentru a face sistemul de operare mai robust, acesti identificatori sunt relativi la proces. Deci nu putem transmite direct identificatorul unui obiect nucleu unui alt fir din cadrul altui proces. Pentru acest lucru exista alte tehnici specializate pe care le vom discuta mai tarziu. Contorul de resurseObiectele nucleu sunt proprietatea nucleului sistemului de operare, si nu a unui proces. Nucleul stie cate procese folosesc un anumit obiect nucleu deoarece fiecare obiect nucleu are un contor de utilizare. Acesta este una din datele membre comune tuturor obiectelor nucleu. Atunci cand un obiect este creat, contorul sau de utilizare este setat la 1. Apoi cand un alt proces primeste acces la un obiect nucleu existent, contorul de utilizare este incrementat. Atunci cand un proces isi incheie executia, nucleul sistemului decrementeaza automat contorul de utilizare pentru toate obiectele nucleu pe care le-a deschis procesul. Daca contorul de utilizare devine 0, nucleul distruge in mod automat obiectul. Acest lucru ne asigura ca nici un obiect nucleu nu va ramane in sistem daca nu mai exista procese care referentiaza acel obiect. SecuritateObiectele nucleu pot fi protejate cu un descriptor de securitate. Acesta descrie cine creeaza obiectul, cine poate primi acces sau poate folosi obiectul si cine nu are acces la obiect. Descriptorii de securitate sunt de obicei folositi atunci cand scriem aplicatii server; putem ignora aceasta facilitate daca scriem aplicatii pentru partea de client. Windows nu este conceput ca un sistem de operare pe partea de server. Din aceasta cauza, Microsoft nu a implementat partea de securitate in Windows. Aproape toate functiile care creeaza obiecte nucleu au un pointer la o structura SECURITY _ ATTIBUTES ca un argument, ca in exemplul de mai jos : HANDLE CreateFileMapping ( HANDLE hFile, PSECURITY _ ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); Majoritatea aplicatiilor vor transmite FALSE pentru acest argument astfel incat obiectul este creat cu setarile de securitate implicite. Acestea inseamna ca oricare membru al grupului administrator si creatorul obiectului vor avea acces nelimitat la obiect; toti ceilalti nu au acces. O structura SECURITY _ ATTRIBUTES arata in modul urmator : typedef struct _ SECURITY _ ATTRIBUTES SECURITY _ ATTRIBUTES; Chiar daca aceasta structura este numita SECURITY _ ATTRIBUTES, ea de fapt include doar un membru care are o legatura cu securitatea: lpSecurityDescriptor. Daca dorim sa restrangem accesul la obiectele nucleu pe care le cream, trebuie sa cream un descriptor de securitate si apoi sa initializam structura SECURITY _ ATTIBUTES in modul urmator : SECURITY _ ATTRIBUTES sa; sa.nLength = sizeof ( sa); // Folosit pentru redarea versiunii. sa.lpSecurityDescriptor = pSD; // Adresa unei SD neinitializate. sa.bInheritHandle = FALSE; // Discutat mai tarziu. HANDLE hFileMapping = CreateFileMapping ( INVALID _ HANDLE _ VALUE, &sa, PAGE _ READWRITE, 0, 1024, 'MyFileMapping'); Atunci cand dorim sa primim acces la un obiect nucleu existent ( mai degraba decat sa cream unul nou), trebuie sa precizam operatiile pe care dorim sa le efectuam asupra obiectului. De exemplu, daca dorim sa accesam un obiect fisier mapat in memorie existent pentru a citi date din el, putem apela OpenFileMapping : HANDLE hFileMapping = OpenFileMapping ( FILE _ MAP _ READ, FALSE, 'MyFileMapping'); Prin trimiterea valorii FILE _ MAP _ READ ca primul parametru al functiei OpenFileMapping, am indicat ca intentionam sa citim din acest fisier mapat in memorie dupa ce primim acces la el. Functia OpenFileMapping efectueaza o verificare a securitatii mai intai, inainte de a intoarce o valoare de identificator valida. Daca utilizatorul are voie sa acceseze obiectul nucleu fisier mapat in memorie existent, OpenFileMapping returneaza un identificator valid. Totusi, daca utilizatorul nu are acces, OpenFileMapping returneaza NULL, iar un apel al functiei GetLastError va returna valoarea 5 ( ERROR _ ACCES _ DENIED). Totusi, majoritatea aplicatiilor nu folosesc securitatea. Deoarece Windows nu are acesti descriptori de securitate, pot aparea probleme la portarea programelor pe sistemul de operare Windows 2000. De exemplu, daca o aplicatie care citea in Windows o valoare din registri, prin intermediul functiei RegOpenKeyEx, cu parametrul KEY _ ALL _ ACCES nu va functiona in Windows 2000 daca utilizatorul nu are drepturi depline. In acest caz apelul functiei trebuie efectuat cu parametrul KEY _ QUERY _ VALUE. Tabelul de identificatori de obiecte nucleu al unui procesAtunci cand procesul este initializat, sistemul aloca un tabel de identificatori pentru el. Acest tabel este folosit doar pentru obiectele nucleu, nu pentru obiectele utilizator sau obiectele GUI. Detaliile despre cum este structurat si administrat acest tabel nu sunt documentate. Tabelul de identificatori al unui proces arata ca in modul urmator. El este pur si simplu un sir de structuri de date. Fiecare structura contine un pointer la un obiect nucleu, o masca de acces si cativa indicatori.
Crearea unui obiect nucleuLa initializarea unui proces, tabelul de identificatori este gol. Atunci cand un fir din proces apeleaza o functie care creeaza un obiect nucleu, cum ar fi CreateFileMapping, nucleul sistemului de operare aloca un bloc de memorie pentru obiect si il initializeaza; apoi nucleul scaneaza tabelul de identificatori pentru o intrare goala. Deoarece tabelul anterior este gol, nucleul gaseste structura la indexul 1 si il initializeaza. Membrul pointer va fi setat la adresa interna de memorie a structurii de date a obiectului nucleu, masca de acces va fi setata la acces deplin iar indicatorii vor fi setati. In continuare vom discuta niste functii care creeaza obiecte nucleu : HANDLE CreateThread ( PSECURITY _ ATTRIBUTES psa, DWORD dwStackSize, LPTHREAD _ START _ ROUTINE pfnStartAddr, PVOID pvParam, DWORD dwCreationFlags, PDWORD pdwThreadId); HANDLE CreateFile ( PCTSTR pszFileName, DWORD dwDesiredAccess, DWORD dwShareMode, PSECURITY _ ATTRIBUTES psa, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); HANDLE CreateFileMapping ( HANDLE hFile, PSECURITY _ ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); HANDLE CreateSemaphore ( PSECURITY _ ATTRIBUTES psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName); Toate functiile care creeaza obiecte nucleu returneaza identificatori relativi la proces care pot fi folositi de toate firele care ruleaza in acel proces. Aceasta valoare este de fapt indexul din tabelul de identificatori ai procesului care identifica unde a stocat sistemul de operare informatia obiectului. Astfel, atunci cand depanam o aplicatie si examinam valoarea identificatorului unui obiect nucleu, vom vedea valori mici ( 1, 2 si asa mai departe). Totusi trebuie sa retinem ca tabelul de identificatori nu este documentat si se poate schimba. De fapt, in Windows 2000 valoarea returnata identifica numarul de octeti din tabelul de identificatori ai procesului mai degraba decat indexul propriu-zis. Trebuie sa fim foarte atenti atunci cand verificam valoarea returnata de o functie care creeaza un obiect nucleu. Si anume, putem compara valoarea cu INVALID _ HANDLE _ VALUE doar atunci cand cream CreateFile. Urmatorul cod este incorect : HANDLE hMutex = CreateMutex ( …); if ( hMutex == INVALID _ HANDLE _ VALUE) De asemenea, si codul urmator este incorect : HANDLE hFile = CreateFile ( …); if ( hFile == NULL) Inchiderea unui obiect nucleuIndiferent de modalitatea de creare a obiectului nucleu, vom indica sistemului ca am terminat de utilizat obiectul apeland CloseHandle : BOOL CloseHandle ( HANDLE hobj);Aceasta functie verifica mai intai tabelul de identificatori ai procesului pentru a se asigura ca indexul transmis identifica un obiect la care procesul are de fapt drept de acces. Daca indexul este valid, sistemul obtine adresa structurii de date a obiectului nucleu si decrementeaza contorul de utilizare din structura; daca contorul este 0, nucleul sistemului de operare distruge obiectul nucleu din memorie. Daca trimitem un identificator invalid functiei CloseHandle, se poate intampla un lucru din doua. Daca procesul nostru ruleaza normal, functia CloseHandle returneaza FALSE si GetLastError returneaza ERROR _ INVALID _ HANDLE. Dar, daca procesul este depanat, sistemul anunta depanatorul astfel incat putem depana eroarea. Chiar inainte ca functia CloseHandle sa returneze, ea curata intrarile din tabelul de identificatori ai procesului – acest identificator este de acum invalid pentru procesul nostru si nu trebuie sa mai incercam sa il folosim. Aceasta curatare are loc indiferent daca obiectul nucleu a fost distrus sau nu ! Dupa ce apelam CloseHandle, nu vom mai avea acces la acest obiect nucleu; totusi, daca contorul obiectului nucleu nu a fost decrementat la 0, obiectul nu a fost distrus. Acest lucru inseamna ca alte procese folosesc acel obiect nucleu. Atunci cand si celelalte procese vor inceta sa foloseasca acel obiect ( apeland CloseHandle), obiectul va fi distrus. Sa spunem ca uitam sa apelam CloseHandle – va fi atunci o scurgere de memorie? Da si nu. Este posibil ca un proces sa consume inutil resurse ( cum ar obiectele nucleu) in timp ce procesul de executa. Totusi, la terminarea executiei procesului, sistemul de operare se asigura ca orice resursa folosita de proces este eliberata. Pentru obiectele nucleu, sistemul efectueaza urmatoarele actiuni : atunci cand procesul isi termina executia, sistemul scaneaza automat tabelul de identificatori ai procesului. Daca acest proces are intrari valide, sistemul inchide aceste obiecte nucleu pentru noi. Daca contorul de utilizare a oricarui dintre aceste obiecte devine 0, nucleu sistemului de operare distruge obiectul. Astfel, aplicatia noastra poate avea scurgeri de memorie in timpul executiei sale, dar in momentul terminarii executiei sistemul garanteaza ca totul este curatat in mod corespunzator. Acest lucru este efectuat pentru toate obiectele, nu doar pentru obiectele nucleu. Partajarea obiectelor nucleu dincolo de granitele procesuluiIn mod frecvent, fire care ruleaza in procese diferite au nevoie sa partajeze obiecte nucleu. Exista mai multe modalitati : obiectele fisiere mapate in memorie ne permit sa partajam blocuri de date intre doua procese care ruleaza pe aceeasi masina. mailslot-urile si pipe-urile cu nume permit aplicatiilor sa trimita blocuri de date intre procese care ruleaza pe masini diferite conectate la retea. mutexurile, semafoarele si evenimentele permit firelor de executie din procese diferite sa isi sincronizeze executia, ca in cazul in care o aplicatie are nevoie sa anunte o alta aplicatie atunci cand si-a terminat de executat sarcina. Deoarece obiectele nucleu sunt relative la proces, efectuarea acestor operatii este dificila. Totusi, Microsoft a avut cateva motive intemeiate atunci cand a proiectat ca identificatorii sa fie relativi la proces. Cel mai important motiv a fost robustetea. Daca identificatorii obiectelor nucleu ar fi fost valori sistem, un proces ar fi putut cu usurinta obtine identificatorul la un obiect pe care il folosea un alt proces si ar fi putut afecta acel proces. Un alt motiv pentru aceasta alegere a fost legata de securitate. Obiectele nucleu sunt protejate iar un proces trebuie sa ceara permisiunea de a utiliza acel obiect inainte de a incepe folosirea lui. Creatorul obiectului poate preveni accesul unui utilizator neautorizat la acel obiect prin simpla interzicere a accesului la acel obiect. Mostenirea identificatorilor obiectelorMostenirea poate fi folosita doar atunci cand procesele sunt in relatia parinte-copil. In acest caz, unul sau mai multi identificatori sunt disponibili procesului parinte, iar acesta decide sa creeze un proces copil, dandu-i acestuia acces la identificatorii obiectelor procesului parinte. Pentru ca acest fel de mostenire sa functioneze, procesul parinte trebuie sa efectueze cativa pasi. In primul rand, atunci cand procesul parinte creeaza un proces copil, el trebuie sa indice sistemului ca vrea ca identificatorul obiectului sa fie mostenibil. Trebuie sa avem in vedere ca, chiar daca identificatorii obiectelor nucleu sunt mostenibile, obiectele in sine nu sunt. Pentru a crea un identificator mostenibil, procesul parinte trebuie sa aloce si sa initializeze o structura SECURITY _ ATTRIBUTES si sa transmita adresa acestei structuri functiei Create. Urmatorul cod creeaza un mutex si returneaza un identificator mostenibil la acesta : SECURITY _ ATTRIBUTES sa; sa.nLength = sizeof ( sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; // Make the returned handle inheritable. HANDLE hMutex = CreateMutex ( &sa, FALSE, NULL); Aceasta portiune de cod initializeaza o structura SECURITY _ ATTRIBUTES indicand faptul ca obiectul trebuie creat folosind descriptorii impliciti de securitate ( ignorati in Windows) si ca identificatorul returnat trebuie sa fie mostenibil. Chiar daca Windows nu are suport integral pentru securitate, el totusi suporta mostenirea; de aceea, Windows foloseste in mod corect valoarea membrului bInheritHandle. Ajungem acum la indicatorii care sunt stocati in intrarile tabelului de identificatori. Fiecare intrare in acest tabel are un indicator care ne arata daca identificatorul este mostenibil. Daca trimitem NULL pentru parametrul PSECURITY _ ATTRIBUTES atunci cand cream un obiect nucleu, identificatorul returnat nu este mostenibil si acest bit este 0. Setarea membrului bInheritHandle la TRUE dace ca acest indicator sa devina 1. Sa presupunem ca tabelul de identificatori ai unui proces arata ca mai jos :
Acest tabel arata ca procesul are acces la doua obiecte nucleu ( identificatorii 1 si 3). Identificatorul 1 nu este mostenibil si identificatorul 3 este mostenibil. Urmatorul pas este ca procesul parinte sa creeze procesul copil. Acest lucru este realizat cu ajutorul functiei CreateProcess : BOOL CreateProcess ( PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY _ ATTRIBUTES psaProcess, PSECURITY _ ATTRIBUTES pszThread, BOOL bInheritHandles, DWORD dwCreationFlags, PVOID pvEnvironment, PCTSTR pszCurrentDirectory, LPSTARTUPINFO pStartupInfo, PPROCESS _ INFORMATION pProcessInformation); De obicei, atunci cand cream un nou proces, trimitem valoarea FALSE pentru parametrul bInheritHandle. Aceasta valoare spune sistemului ca nu dorim ca procesul copil sa mosteneasca identificatorii mostenibili care sunt in tabelul de identificatori ai procesului parinte. Daca totusi trimitem TRUE pentru acest parametru, copilul va mosteni identificatorii mostenibili ai procesului parinte. Atunci cand trimitem valoarea TRUE, sistemul creeaza noul proces copil dar nu ii permite executia imediat. Bineinteles, sistemul creeaza si un tabel gol de identificatori pentru procesul copil – la fel ca si la orice proces nou. Dar deoarece am trimis TRUE pentru parametrul bInheritHandle al functiei CreateProcess, sistemul mai face inca un lucru : parcurge tabelul de identificatori ai procesului parinte si pentru fiecare intrare care contine un identificator mostenibil, sistemul copie acea intrare direct in tabelul de identificatori ai procesului copil. Aceasta intrare este copiata la exact aceeasi pozitie in tabelul procesului copil ca cea din procesul parinte. Acest lucru este important deoarece inseamna ca valoarea identificatorului care identifica un obiect nucleu este identica in ambele procese, parinte si copil. In plus fata de copierea intrarii in tabel, sistemul incrementeaza contorul de utilizare a obiectului nucleu deoarece acum doua procese folosesc obiectul. Pentru ca obiectul nucleu sa fie distrus, ambele procese trebuie sa apeleze CloseHandle sau sa isi termine executia. Ordinea in care sunt efectuate acest operatii nu este importanta. Urmatorul tabel arata tabelul de identificatori ai procesului copil imediat ce acestuia ii este permis sa isi inceapa executia. Se observa ca intrarile 1 si 2 nu sunt initializate si de aceea sunt identificatori invalizi. Totusi, indexul 3 identifica un obiect nucleu. De fapt, el identifica obiectul nucleu de la adresa 0xF0000010, acelasi obiect ca in tabelul de identificatori ai procesului parinte. Masca de acces este identica cu masca din parinte, iar indicatorii sunt identici. Acest lucru inseamna ca daca procesul copil va crea la randul sau un alt proces copil, acesta va mosteni si el acest identificator al obiectului nucleu cu aceeasi valoare a identificatorului, acelasi acces, aceiasi indicatori, iar contorul obiectului nucleu va fi din nou incrementat.
Trebuie sa retinem ca mostenirea identificatorilor obiectelor nucleu are loc doar la momentul crearii procesului fiu. Daca procesul parinte creeaza noi obiecte nucleu, un proces copil care deja ruleaza nu va mosteni aceste noi obiecte. Mostenirea identificatorilor obiectelor are o caracteristica ciudata : atunci cand o folosim, procesul copil nu are nici o idee ca a mostenit vreun identificator. Mostenirea este utila doar atunci cand procesul copil spune ca asteapta acces la un obiect nucleu atunci cand este creat de alt proces. De obicei, aplicatiile parinte si copil sunt scrise de aceeasi companie; totusi, o companie diferita poate scrie aplicatia copil daca acea companie documenteaza ce asteapta procesul copil. Pe departe cel mai obisnuit mod ca un proces copil sa determine valoarea identificatorului pe care il asteapta este de a trimite aceasta valoare ca un argument de la linia de comanda catre procesul copil. Codul de initializare al procesului copil parseaza linia de comanda si extrage valoarea identificatorului. Dupa ce procesul copil are valoarea identificatorului, el are acces nelimitat la obiect. De notat este ca functionarea mostenirii identificatorilor are loc deoarece valoarea obiectului nucleu partajat in ambele procese este identica; din aceasta cauza procesul parinte este capabil sa trimita valoarea identificatorului ca un argument de la linia de comanda. Bineinteles, putem folosi alte metode de comunicare intre procese pentru a transfera valoarea identificatorului obiectului nucleu de la procesul parinte la procesul copil. O posibilitate ar fi sa asteptam pana cand procesul copil isi termina initializarea ( folosind WaitForInputIdle); apoi parintele poate trimite sau posta un mesaj unei ferestre create de un fir in procesul copil. O alta modalitate este ca procesul parinte sa adauge o variabila de mediu la blocul sau de mediu. Numele variabilelei va fi ceva pe care procesul copil stie sa caute, iar valoarea variabilelei va fi valoarea identificatorului obiectului nucleu care va fi mostenit. Apoi, cand procesul parinte creeaza procesul, procesul copil mosteneste variabilele de mediu ale parintelui si poate apela usor GetEnvironmentVariable pentru a obtine valoarea identificatorului obiectului nucleu. Aceasta abordare este excelenta atunci cand procesul copil urmeaza sa creeze un alt proces copil, deoarece variabilele de mediu pot fi din nou mostenite. Schimbarea indicatorilor unui identificatorIn mod ocazional, putem intalni situatia in care un proces parinte creeaza un obiect nucleu extragand identificatorul mostenibil si apoi creeaza doua procese copil. Procesul parinte doreste ca doar unul din copii sa mosteneasca identificatorul obiectului nucleu. Cu alte cuvinte, este posibil sa dorim uneori sa controlam care proces copil sa mosteneasca identificatorii obiectelor nucleu. Pentru a modifica indicatorul de mostenirea a identificatorului unui obiect nucleu, putem apela functia SetHandleInformation : BOOL SetHandleInformation ( HANDLE hObject, DWORD dwMask, DWORD dwFlags); Functia are 3 parametri : hObject este un identificator valid, al doilea, dwMask, spune functiei care indicator sau indicatori vrem sa il modifica. In mod curent, la fiecare identificator sunt asociati 2 indicatori : #define HANDLE _ FLAG _ INHERIT 0x00000001 #define HANDLE _ FLAG _ PROTECT _ FROM _ CLOSE 0x00000002 Putem face OR pe biti intre acesti indicatori daca dorim sa schimbam ambii indicatori simultan. Al treilea parametru al functiei SetHandleInformation, dwFlags, indica valoarea la care dorim sa modificam indicatorii. Daca dorim sa setam indicatorul de mostenire pentru identificatorul unui obiect nucleu, putem face in felul urmator : SetHandleInformation ( hobj, HANDLE _ FLAG _ INHERIT, HANDLE _ FLAG _ INHERIT); Pentru a reseta acest indicator putem face ca in continuare : SetHandleInformation ( hobj, HANDLE _ FLAG _ INHERIT, 0); Indicatorul HANDLE _ FLAG _ PROTECT _ FROM _ CLOSE indica sistemului ca acest identificator nu trebuie sa i se permita sa se inchida : SetHandleInformation ( hobj, HANDLE _ FLAG _ PROTECT _ FROM _ CLOSE, HANDLE _ FLAG _ PROTECT _ FROM _ CLOSE); CloseHandle ( hobj); // Apare o exceptie. Daca un fir incearca sa inchida un identificator protejat, functia CloseHandle cauzeaza aparitia unei exceptii. Rareori dorim ca un identificator sa fie protejat la inchidere. Totusi, acest indicator poate fi util daca avem un proces care a creat un proces copil care la randul lui a creat un proces nepot. Procesul parinte se poate astepta ca procesul nepot sa mosteneasca identificatorul obiectului dat copilului imediat. Este posibil, totusi, ca primul copil sa inchida identificatorul inainte de a crea copilul nepot. In acest caz, procesul parinte nu va fi in stare sa comunice cu procesul nepot deoarece acesta nu a mostenit obiectul nucleu. Prin marcarea identificatorului ca protejat la inchidere, procesul nepot va mosteni obiectul. Aceasta abordare are totusi un defect : primul copil ar putea apela codul urmator pentru a reseta indicatorul HANDLE _ FLAG _ PROTECT _ FROM _ CLOSE si apoi sa inchida identificatorul : SetHandleInformation ( hobj, HANDLE _ FLAG _ PROTECT _ FROM _ CLOSE, 0); CloseHandle ( hobj); Procesul parinte se bizuie pe faptul ca procesul copil nu va executa acest cod. Bineinteles, procesul parinte se bazeaza de asemenea ca procesul copil va crea procesul nepot, deci acest pariu nu este chiar atat de riscant. Exista si functia GetHandleInformation : BOOL GetHandleInformation ( HANDLE hObj, PDWORD pdwFlags); Aceasta functie returneaza setarile curente ale indicatorilor pentru identificatorul precizat in DWORD-ul pointat de catre pdwFlags. Pentru a vedea daca un identificator este mostenibil, putem proceda astfel : DWORD dwFlags; GetHandleInformation ( hObj, &dwFlags); BOOL fHandleIsInheritable = ( 0 != ( dwFlags & HANDLE _ FLAG _ INHERIT)); Obiecte cu numeA doua metoda de a partaja obiecte nucleu dincolo de granitele proceselor este de a denumi obiectele. Multe – desi nu toate – obiecte nucleu pot fi denumite. De exemplu, toate functiile urmatoare creeaza obiecte nucleu cu nume : HANDLE CreateMutex ( PSECURITY _ ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName); HANDLE CreateEvent ( PSECURITY _ ATTRIBUTES psa, BOOL bManualReset, BOOL bInitialState, PCTSTR pszName); HANDLE CreateSemaphore ( PSECURITY _ ATTRIBUTES psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName); HANDLE CreateWaitableTimer ( PSECURITY _ ATTRIBUTES psa, BOOL bManualReset, PCTSTR pszName); HANDLE CreateFileMapping ( HANDLE hFile, PSECURITY _ ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); HANDLE CreateJobObject ( PSECURITY _ ATTRIBUTES psa, PCTSTR pszName); Toate aceste functii au un ultim parametru comun, pszName. Atunci cand transmitem NULL pentru acest parametru, indicam sistemului ca dorim sa cream un obiect nucleu fara nume. Atunci cand cream un astfel de obiect, il putem partaja dincolo de granitele procesului folosind mostenirea sau DuplicateHandle. Pentru a partaja un obiect nucleu prin nume, trebuie sa ii atribuim acestuia un nume. Daca nu trimitem NULL pentru parametrul pszName, ar trebui sa trimitem adresa unui sir terminat cu zero. Acest nume poate fi de lungime maxima egala cu MAX _ PATH ( definit ca 260). Din pacate, Microsoft nu ofera nici o indrumare pentru asignarea numelor pentru obiectele nucleu. De exemplu, daca dorim sa cream un obiect numit „JeffObj”, nu avem nici o garantie ca un obiect cu acest nume nu exista deja in sistem. Pentru a inrautati lucrurile, toate aceste obiecte partajeaza acelasi singur spatiu de nume. Din aceasta cauza, urmatorul apel al functiei CreateSemaphore va returna intotdeauna NULL : HANDLE hMutex = CreateMutex ( NULL, FALSE, 'myObj'); HANDLE hSem = CreateSemaphore ( NULL, 1, 1, 'myObj'); DWORD dwErrorCode = GetLastError ( ); Daca examinam valoarea lui dwErrorCode dupa executarea codului anterior, vom vedea ca valoarea returnata este 6 ( ERROR _ INVALID _ HANDLE). Aceasta eroare nu este foarte elocventa, dar asa a fost proiectata de Microsoft. Acum ca stim cum sa numim un obiect nucleu, sa vedem cum putem partaja obiectele in acest mod. Sa spunem ca procesul A isi incepe executia si apeleaza urmatoarea functie : HANDLE hMutexProcessA = CreateMutex ( NULL, FALSE, 'myMutex'); Aceasta functie creeaza un nou mutex si ii atribuie numele „myMutex”. Sa observam ca in identificatorul procesului A, hMutexProcessA nu este un identificator mostenibil – si nu are nevoie sa fie atunci cand doar denumim obiectele. Dupa o perioada de timp, un proces creeaza procesul B. Acesta nu trebuie sa fie copilul procesului A; el poate fi creat din Explorer sau din alta aplicatie. Faptul ca procesul B nu trebuie sa fie copilul procesului A este un avantaj al utilizarii obiectelor cu nume in locul mostenirii. Atunci cand procesul B isi incepe executia, el executa urmatorul cod : HANDLE hMutexProcessB = CreateMutex ( NULL, FALSE, 'myMutex'); Atunci cand apelul CreateMutex al procesului B este efectuat, sistemul verifica mai intai daca nu exista deja un obiect nucleu cu acelasi nume. Deoarece exista un obiect nucleu cu acelasi nume, nucleu sistemului de operare verifica tipul obiectului. Deoarece incercam sa cream un mutex si obiectul cu numele „myMutex” este de asemenea un mutex, sistemul face o verificare a securitatii pentru a vedea daca apelantul are acces deplin la obiect, iar in caz afirmativ sistemul gaseste o intrare libera in tabelul de identificatori ai procesului B si initializeaza aceasta intrare pentru a pointa la obiectul nucleu existent. Daca tipurile obiectelor difera sau apelantului ii este respins accesul, CreateMutex esueaza ( returneaza NULL). Atunci cand apelul CreateMutex al procesului B este reusit, nu este creat de fapt un mutex. Procesului B ii este atribuit un identificator relativ la proces care identifica obiectul mutex existent. Bineinteles, deoarece o noua intrare din tabelul de identificatori ai procesului B referentiaza acest obiect, contorul de utilizare a mutexului este incrementat; obiectul nu va distrus pana cand ambele procese A si B si-au inchis identificatorii la acest obiect. Este foarte posibil ca valorile identificatorilor din cele doua procese sa difere. Atunci cand obiectul B apeleaza CreateMutex, el transmite informatia de atribute de securitate si un al doilea parametru functiei. Acesti parametri sunt ignorati daca un obiect cu numele specificat exista ! O aplicatie poate sa isi dea seama daca a creat de fapt un nou obiect nucleu sau daca a deschis unul deja existent apeland functia GetLastError imediat dupa apelul functiei Create* : HANDLE hMutex = CreateMutex ( &sa, FALSE, 'JeffObj'); if ( GetLastError ( ) == ERROR _ ALREADY _ EXISTS) else Exista o metoda alternativa pentru partajarea obiectelor nucleu prin nume. In loc sa apelam functia Create , un proces poate apela una din functiile Open urmatoare : HANDLE OpenMutex ( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenEvent ( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenSemaphore ( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenWaitableTimer ( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenFileMapping ( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); HANDLE OpenJobObject ( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName); Aceste functii au acelasi prototip. Ultimul parametru, pszName, indica numele obiectului nucleu. Nu putem trimite NULL pentru acest parametru; trebuie sa trimitem adresa unui sir incheiat cu zero. Aceste functii pur si simplu cauta in singurul spatiu de nume al obiectelor nucleu pentru a gasi o potrivire. Daca nu exista nici un obiect nucleu cu numele precizat, functia returneaza NULL si GetLastError returneaza 2 ( ERROR _ FILE _ NOT _ FOUND). Totusi, daca exista un obiect nucleu cu acel nume si daca este de acelasi tip, sistemul verifica sa vada daca accesul cerut ( prin intermediul parametrului dwDesiredAcces) este permis; daca da, tabelul de identificatori ai procesului este actualizat si contorul de utilizare al obiectului este incrementat. Identificatorul returnat va fi mostenibil daca trimitem valoarea TRUE pentru parametrul bInheritHandle. Principala deosebire dintre functia Create* si functia Open* este ca daca obiectul nu exista deja, functia Create* il va crea, in timp ce apelul functiei Open* va esua. Dupa cum am precizat si mai devreme, Microsoft nu ofera principii calauzitoare pentru a crea nume unice de obiecte. Altfel spus, ar fi o problema daca un utilizator ar incerca sa ruleze doua programe de la companii diferite si fiecare program ar incerca sa creeze un obiect numit „MyObject”. Pentru unicitate, se recomanda creare unui GUID si sa folosim reprezentarea sub forma de sir de caractere a GUID-ului pentru numele obiectele noastre. Obiectele cu nume sunt de obicei folosite pentru a preveni rularea instantelor multiple a unei aplicatii care ruleaza. Pentru a realiza acest lucru, putem sa apelam functia Create* in functia noastra main sau WinMain pentru a crea obiecte cu nume ( nu conteaza ce tip de obiecte cream). Atunci cand functia Create* returneaza, apelam GetLastError. Daca aceasta returneaza ERROR _ ALREADY _ EXISTS, atunci o alta instanta a aplicatiei noastre ruleaza si noua instanta isi poate inceta executia. int WINAPI WinMain ( HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow) '); if ( GetLastError ( ) == ERROR _ ALREADY _ EXISTS) // Aceasta este prima instanta a aplicatiei. // Inainte de iesi, se inchide obiectul. CloseHandle ( h); return ( 0); Spatiile de nume Terminal ServerTrebuie notat ca Terminal Server schimba putin scenariul anterior. O masina Terminal Server va avea spatii de nume multiple pentru obiectele nucleu. Exista doar un spatiu de nume global, care este folosit de obiectele nucleu care sunt proiectate sa fie accesibile din toate sesiunile client. Acest spatiu de nume este in general folosit de catre servicii. In plus, fiecare sesiune client are propriul spatiu de nume. Acest lucru face ca doua sau mai multe sesiuni care ruleaza aceeasi aplicatie sa nu interfereze una cu alta – o sesiune nu poate accesa obiectele celeilalte sesiuni chiar daca obiectele au acelasi nume. Pe o masina fara Terminal Server, serviciile si aplicatiile partajeaza acelasi spatiu de nume. Obiectele nucleu cu nume ale unui serviciu intotdeauna se gasesc in spatiul global de nume. Implicit, in Terminal Server, un obiect nucleu cu nume al unei aplicatii este in spatiul de nume al sesiunii. Totusi, este posibil sa fortam ca un obiect nucleu cu nume sa mearga in spatiul de nume global prefixand numele cu „Global”, ca in exemplul urmator : HANDLE h = CreateEvent ( NULL, FALSE, FALSE, 'GlobalMyName'); Putem de asemenea sa precizam ca vrem ca obiectul nucleu sa mearga in spatiul de nume al sesiunii prefixand numele cu „Local ”, ca mai jos : HANDLE h = CreateEvent ( NULL, FALSE, FALSE, 'LocalMyName'); Microsoft considera cuvintele Global si Local drept cuvinte rezervate pe care nu trebuie sa le folosim in numele obiectelor exceptand cazul in care dorim sa fortam un anumit spatiu de nume. Microsoft considera de asemenea cuvantul Session drept un cuvant rezervat, desi deocamdata nu are nici inteles deosebit. Toate aceste trei cuvinte rezervate sunt case-sensitive. Ele sunt ignorate daca masina gazda nu ruleaza Terminal Server. Duplicarea identificatorilor obiectelorUltima tehnica pentru a partaja obiectele nucleu dincolo de granitele proceselor este folosirea functiei DuplicateHandle : BOOL DuplicateHandle ( HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, PHANDLE phTargetHandle, DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions); Aceasta functie ia o intrare din tabelul de identificatori ai procesului si face o copie a acesteia in tabelul de identificatori ai altui proces. DuplicateHandle primeste cativa parametri dar este de fapt foarte directa. Modul de utilizare cel mai direct al functiei DuplicateHandle implica trei procese diferite care ruleaza in sistem. Atunci cand apelam DuplicateHandle, primul si al treilea parametru – hSourceProcessHandle si hTargetProcessHandle – sunt identificatori de obiecte nucleu. Identificatorii la randul lor trebuie sa fie relativi la procesul care apeleaza DuplicateHandle. In plus, acesti doi parametri trebuie sa identifice obiecte nucleu proces; functia esueaza daca trimitem identificatori ai unor obiecte ale caror tip nu este obiect nucleu proces. Al doilea parametru, hSourceHandle, este un identificator la orice tip de obiect nucleu. Totusi, valoarea identificatorului nu este relativa la procesul care apeleaza functia DuplicateHandle. In schimb, identificatorul trebuie sa fie relativ la procesul identificat de identificatorul hSourceProcessHandle. Al patrulea parametru, phTargetHandle, reprezinta adresa unei variabile HANDLE care va primi indexul intrarii care primeste copia informatiei identificatorului din sursa. Aceasta valoare de identificator care reiese este relativa la procesul dat de hTargetProcessHandle. Ultimii trei parametri ai functiei DuplicateHandle ne permit sa indicam valoarea mastii de acces si indicatorul de mostenire care trebuie folosit pentru acest identificator al obiectului nucleu. Parametrul dwOptions poate fi 0 sau orice combinatie ale indicatorilor DUPLICATE _ SAME _ ACCESS si DUPLICATE _ CLOSE _ SOURCE. Precizarea valorii DUPLICATE _ SAME _ ACCESS spune functiei DuplicateHandle ca dorim sa avem aceeasi masca de acces ca si identificatorul obiectului din sursa. Folosirea acestui parametru face ca functia DuplicateHandle sa ignore parametrul dwDesiredAccess. Folosirea valorii DUPLICATE _ CLOSE _ SOURCE are ca efect inchiderea identificatorului in procesul sursa. Acest indicator usureaza transmiterea unui obiect nucleu de la un proces la altul. Atunci cand acest indicator este folosit, contorul de utilizare a obiectului nucleu nu este afectat. Vom folosi un exemplu pentru a arata modul de functionare al functiei DuplicateHandle. Pentru aceasta demonstratie, procesul S este procesul sursa care are acces la un obiect nucleu iar procesul T este procesul destinatie care va primi acces la acest obiect nucleu. Procesul C este procesul care va executa apelul DuplicateHandle. Tabelul procesului C contine doua valori de identificatori, 1 si 2. Valoarea 1 identifica obiectul nucleu al procesului S iar valoarea 2 identifica obiectul nucleu al procesului T.
Tabelul de mai jos reprezinta tabelul de identificatori ai procesului S, care contine o singura intrare cu o valoare de identificator egala cu 2. Acest identificator poate identifica orice tip de obiect nucleu – nu trebuie sa fie un obiect nucleu proces.
Urmatorul tabel arata ce contine tabelul procesului T inainte ca procesul C sa apeleze DuplicateHandle. Dupa cum se observa, tabelul contine doar o singura intrare cu o valoare de identificator egala cu 2; intrarea identificator 1 este goala.
Daca procesul C apeleaza DuplicateHandle folosind urmatorul cod, doar tabelul procesului T se schimba : DuplicateHandle ( 1, 2, 2, &hObj, 0, TRUE, DUPLICATE _ SAME _ ACCESS);
A doua intrare in tabelul de identificatori ai procesului S a fost copiata in prima intrare a tabelului de identificatori ai procesului T. DuplicateHandle a completat variabila hObj a procesului C cu valoarea 1, care este indexul din tabelul procesului T unde a fost plasata noua intrare. Deoarece indicatorul DUPLICATE _ SAME _ ACCESS a fost trimis functiei DuplicateHandle, masca de acces pentru acest identificator in tabelul procesului T este identica cu masca de acces din tabelul procesului S. De asemenea, trimiterea indicatorului DUPLICATE _ SAME _ ACCESS face ca DuplicateHandle sa ignore parametrul dwDesiredAccess. Indicatorul de mostenire a fost activat deoarece am trimis valoarea TRUE pentru parametrul bInherithandle al functiei DuplicateHandle. Ca si la mostenire, un lucru ciudat la functia DuplicateHandle este ca procesul destinatie nu primeste nici o notificare despre faptul ca un nou obiect nucleu ii este acum accesibil. Astfel, procesul C trebuie sa anunte cumva procesul T ca are acum acces la un obiect nucleu si trebuie sa foloseasca o modalitate de comunicare pentru a transmite valoare identificatorului din hObj procesului T. Evident, folosirea unui argument de la linia de comanda sau schimbarea variabilelor de mediu ale procesului T nu intra in discutie deoarece procesul deja ruleaza. Trebui folosit un mesaj de fereastra sau un alt mecanism IPC. Functia DuplicateHandle este totusi destul de rar folosita cu ajutorul a trei procese. De obicei, functia este apelata atunci cand sunt implicate doua procese. Sa ne imaginam o situatie in care un proces are acces la un obiect la care doreste accesul un alt proces, sau un caz in care un proces doreste sa dea accesul la un obiect nucleu altui proces. De exemplu, sa spunem ca procesul S are acces la un obiect nucleu si doreste sa dea acces procesului T la acest obiect. // Tot codul urmator este executat de procesul S. // Creeaza un obiect mutex accesibil procesului S. HANDLE hObjProcessS = CreateMutex ( NULL, FALSE, NULL); // Deschide un identificator la obiectul nucleu al procesului T. HANDLE hProcessT = OpenProcess ( PROCESS _ ALL _ ACCESS, FALSE, dwProcessIdT); HANDLE hObjProcessT; // Un identificator neinitializat relativ la procesul T. // Da acces procesului T la obiectul mutex. DuplicateHandle ( GetCurrentProcess ( ), hObjProcessS, hProcessT, &hObjProcessT, 0, FALSE, DUPLICATE _ SAME _ ACCESS); // Folosirea unui mecanism IPC pentru trimiterea valorii din hObjProcessS in procesul T. // Nu mai avem nevoie sa comunicam cu procesul T. CloseHandle ( hProcessT); //Atunci cand procesul S nu mai are nevoie sa foloseasca mutexul, trebuie sa il inchida. CloseHandle ( hObjProcessS); Apelul functiei GetCurrentProcess returneaza un identificator relativ care identifica intotdeauna procesul apelant – procesul S in acest exemplu. De indata ce functia DuplicateHandle returneaza, hObjProcessT este un identificator relativ la procesul T care identifica acelasi obiect pe care hObjProcessS il identifica in procesul S. Procesul S nu ar trebui sa execute niciodata un astfel de cod : CloseHandle ( hObjProcessT); Daca procesul S executa acest cod, apelul ar putea sa esueze sau nu. Apelul reuseste daca procesul S are acces la un obiect nucleu cu aceeasi valoare de identificator ca hObjProcessT. Acest apel are ca efect inchiderea unui obiect astfel incat procesul S nu mai trebuie sa il acceseze, fapt care va face ca aplicatia sa aiba un comportament nedorit. In continuare avem un alt exemplu de folosire a functiei DuplicateHandle : sa presupunem ca procesul are drept de citire si de scriere intr-un obiect fisier mapat in memorie. La un moment dat, o functie este apelata care se presupune ca acceseaza fisierul cu drept de citire. Pentru a face mai robusta aplicatia noastra, putem folosi DuplicateHandle pentru a crea un nou identificator pentru obiectul existent si sa ne asiguram ca acest nou identificator are doar acces de citire. Apoi vom trimite acest identificator ca parametru functiei; in acest mod, codul din functie nu va scrie niciodata din greseala in fisierul mapat in memorie. int WINAPI WinMain ( HINSTANCE hinstExe, HINSTANCE, LPSTR szCmdLine, int nCmdShow) ProceseIn acest capitol vom prezenta modalitatea prin care sistemul coordoneaza toate aplicatiile care ruleaza. Vom incepe prin definirea notiunii de proces si vom explica modul in care sistemul creeaza un obiect nucleu pentru a coordona fiecare proces. Apoi vom afla cum manipulam un proces folosind obiectul nucleu asociat lui. Dupa aceea, vom discuta despre proprietatile si atributele proceselor, la fel ca si alte cateva functii care sunt disponibile pentru interogarea si schimbarea acestor proprietati. Vom examina si functiile care ne dau posibilitatea sa cream sau sa sporim procesele in sistem. Si bineinteles nici o discutie despre procese nu ar fi completa fara o privire amanuntita asupra modului in care ele se termina. Un proces este in mod normal definit ca o instanta a unui program in rulare. In Win32, un proces este posesorul unui spatiu de adrese de 4 GB. Spre deosebire de corespondentele lor din MS-DOS si Windows pe 16 biti, procesele Win32 sunt inerte : adica, un proces Win32 nu face nimic, el doar are un spatiu de 4 GB de spatiu de adrese care contine codul si data pentru fisierul unei aplicatii .EXE. Orice DLL necesar fisierului .EXE va avea codul si datele sale incarcate in spatiul de adrese al procesului. In completarea spatiului de adrese, un proces este posesorul unor diferite resurse, cum ar fisiere, alocari dinamice ale memoriei si fire de executie. Diferitele resurse create in timpul vietii unui proces sunt distruse atunci cand procesul se termina – garantat. Dupa cum am spus, procesele sunt inerte. Pentru ca un proces sa realizeze ceva, el trebuie sa fie posesorul unui fir de executie; acest fir de executie este responsabil cu executarea codului continut in spatiul de adrese al procesului. De fapt, un proces poate contine mai multe fire de executie, toate ruland un anumit cod in mod simultan. Pentru a face acest lucru, fiecare fir are propriul sau set de registri ai procesorului si stiva sa proprie. Fiecare proces are cel putin un fir care executa cod continut in spatiul de adrese al procesului. Daca nu ar fi fost nici un fir de executie, atunci nu ar fi fost nici un motiv ca procesul sa mai existe, iar sistemul ar distruge in mod automat procesul si spatiul sau de adrese.
Pentru ca toate procesele sa ruleze, sistemul de operare programeaza o parte din timpul procesorului pentru fiecare fir de executie. Sistemul de operare da iluzia ca toate firele ruleaza concurent, oferind bucati de timp ( numite cuante) firelor. Atunci cand este creat un proces Win32, primul sau fir de executie, numit firul principal de executie, este creat in mod automat de catre sistem. Firul principal poate apoi crea alte fire iar acestea pot crea la randul lor alte fire. Windows NT este capabil sa utilizeze masini care contin mai multe procesoare. Astfel, pe un calculator cu doua procesoare pot rula in acelasi timp doua fire de executie diferite. Nucleul Windows NT face tot managementul si programarea firelor in acest tip de sistem. Noi ca programatori nu trebuie sa facem nimic special pentru a ne putea bucura de avantajele oferite de un calculator multiprocesor. Windows 95 poate folosi doar un singur procesor. Chiar daca masina pe care ruleaza Windows 95 are mai mult de un procesor, Windows 95 poate programa doar un singur fir la un moment dat; celelalte procese stau si asteapta. Prima aplicatie Win32Win32 suporta doua tipuri de aplicatii : bazate pe interfata cu utilizatorul ( GUI) sau doar pe consola ( CUI). O aplicatie GUI este bazat pe grafica. Aplicatiile GUI creeaza ferestre, au meniuri, interactioneaza cu utilizatorul prin intermediul casutelor de dialog si folosesc toate facilitatile Windows. Aproape toate aplicatiile de accesorii care insotesc Windows-ul ( Notepad, Calculator, Wordpad de exemplu) sunt exemple tipice de aplicatii GUI. Aplicatiile de consola seamana mai mult cu aplicatiile text MS-DOS : iesirea lor este bazata pe text, ele nu creeaza ferestre sau proceseaza mesaje si nu au nevoie de o interfata cu utilizatorul. Desi aplicatiile CUI sunt continute intr-o fereastra pe ecran, fereastra contine doar text. Linia dintre aceste doua tipuri de aplicatii este foarte neclara. Este posibil sa cream aplicatii CUI care afiseaza casute de dialog. De exemplu, shell-ul de comanda are o comanda speciala care face ca el sa afiseze un casuta grafica de dialog, dandu-ne posibilitatea de a selecta comanda pe care dorim sa o executam in loc de a tine minte toate comenzile suportate de catre shell. Putem de asemenea crea o aplicatie GUI care trimite texte catre o fereastra de consola ( to a console window) catre care putem trimitem informatii pe masura ce aplicatia se executa. Dintre cele doua tipuri de aplicatii, suntem incurajati sa folosim aplicatiile GUI in loc de aplicatiile demodate CUI. Este un lucru dovedit ca aplicatiile GUI au o interfata mult mai prietenoasa. Singura parte a sistemului de operare care este influentata de faptul ca o aplicatie este CUI sau GUI este system loader-ul. Atunci cand acesta incarca o aplicatie, o valoare de subsistem incarcata in interiorul fisierului EXE indica daca o aplicatie este CUI sau GUI. Daca valoarea indica o aplicatie CUI, atunci loader-ul asigura in mod automat ca o fereastra text in mod consola este creata pentru aplicatie. Daca valoarea indica o aplicatie GUI, loader-ul nu creeaza fereastra consola si doar incarca aplicatia. Atunci cand cream un proiect C/C++, Visual C++ seteaza un indicator de linker ( linker switch) astfel incat valoarea de subsistem din fisierul EXE va fi setata la CUI, iar codul de pornire vrea sa apeleze o functie principala pe care trebuie sa o scriem. Atunci cand cream un proiect GUI in C/C++, Visual C++ seteaza un intrerupator de linker care indica un subsistem GUI iar codul de inceput vrea sa apeleze o functie WinMain pe care trebuie sa o scriem. Toate aplicatiile GUI Win32 trebuie sa aiba o functie WinMain pe care o implementam in codul nostru. Functia trebuie sa aiba urmatorul prototip : int WINAPI WinMain ( HINSTANCE hinstExe, HINSTANCE hinstExePrev, LPSTR lpszCmdLine, int nCmdShow); Aceasta functie nu este apelata de fapt de sistemul de operare. In schimb, sistemul de operare apeleaza functia de pornire C/C++ ( C/C++ run-time’s startup function). Linker-ul Visual-ului stie ca numele acestei functii este _ WinMainCRTStartup, dar putem schimba acest nume folosind indicatorul /ENTRY al linker-ului. Functia _ WinMainCRTStartup este responsabila pentru efectuarea urmatoarelor actiuni : 1. Extrage un pointer catre linia de comanda completa a noului proces. 2. Extrage un pointer catre variabilele de mediu ale procesului. 3. Initializeaza variabilele globale C run-time accesibile din codul nostru incluzand STDLIB.H. 4. Initializeaza zona heap folosita de functiile C run-time de alocare a memoriei ( adica malloc si calloc) si alte rutine de nivel redus de intrare si iesire. Variabilele globale disponibile aplicatiei noastre
5. Apeleaza functia WinMain ca mai jos : GetStartupInfoA ( &StartupInfo); int nMainRetVal = WinMain ( GetModuleHandle ( NULL), NULL, lpszCommandLine, ( StartupInfo.dwFlags & STARTF _ USESHOWWINDOW) ? StartupInfo.wShowWindow : SW _ SHOWDEFAULT); 6. Atunci cand functia WinMain returneaza, codul de start apeleaza functia C run-time exit, trimitandu-i valoarea de retur a functiei WinMain ( nMainRetVal). Functia exit face putina curatenie iar apoi apeleaza functia Win32 ExitProcess, transmitandu-i valoarea de retur a functiei WinMain. In continuare vom discuta despre diferitele atribute care sunt acordate unui nou proces. Identificatorul de instanta al procesuluiFiecarui fisier EXE sau DLL incarcat in spatiul de adrese al unui proces ii este atribuit un identificator de instanta unic. Instanta fisierului EXE este transmisa ca fiind ca primul parametru al functiei WinMain, hinstExe. Valoarea identificatorului este in mod normal necesara pentru apeluri care incarca resurse. De exemplu, pentru a incarca o resursa de tip icoana dintr-un fisier EXE, vom apela : HICON LoadIcon ( HINSTANCE hinst, LPCTSTR lpszIcon); Primul parametru al functiei LoadIcon indica ce fisier ( EXE sau DLL) contine resursa pe care dorim sa o incarcam. Multe aplicatii salveaza parametrul hinstExe al functiei WinMain intr- variabila globala pentru a fi usor accesibila intregului cod din fisierul EXE. Documentatia Win32 declara ca unele functii Win32 au nevoie de un parametru de tipul HMODULE. Un exemplu este functia GetModuleFileName : DWORD GetModuleFileName ( HMODULE hinstModule, LPTSTR lpszPath, DWORD cchPath); Totusi, API-ul Win32 nu face nici o diferenta dintre valorile HMODULE si HINSTANCE, ele sunt una si aceeasi valoare. Ori de cate ori documentatia Win32 a unei functii indica faptul ca HMODULE este necesar, ii putem transmite HINSTANCE sau viceversa. Valoarea reala a parametrului hinstExe a functiei WinMain este adresa de baza a memoriei care indica unde a incarcat sistemul imaginea fisierului EXE in spatiul de adrese al procesului. De exemplu, daca sistemul deschide fisierul EXE si ii incarca continutul la adresa 0x00400000, parametrul hinstExe a functiei WinMain va avea valoarea 0x00400000. Adresa de baza la care o aplicatie este incarcata este determinata de catre linker. Linker-e diferite pot folosi adrese de baza diferite. Linker-ul Visual C++ foloseste o adresa de baza implicita egala cu 0x00400000 deoarece aceasta este cea mai mica adresa la care se poate incarca imaginea unui fisier executabil in Windows 95. Unele linker-e mai vechi folosesc o adresa de baza implicita egala cu 0x00010000 deoarece aceasta este cea mai mica adresa la care imaginea unui fisier executabil poate fi incarcata in Windows NT. Putem schimba adresa de baza pe care o incarca aplicatia noastra folosind switch-ul de adresa al linker-ului pentru linker-ul Microsoft. Daca incercam sa incarcam un fisier executabil care are o adresa de baza mai mica decat 0x00400000 in Windows 95, loader-ul Windows-ului trebuie sa realoce executabilul la o alta adresa. Aceasta realocare creste timpul de incarcare al aplicatiei, dar macar aceasta va putea sa ruleze. Daca dezvoltam o aplicatie care vrem sa ruleze atat sub Windows 95, cat si sub Windows NT, trebuie sa ne asiguram ca adresa de baza a aplicatiei este 0x00400000 sau mai mare. Functia GetModuleHandle HMODULE GetModuleHandle ( LPCTSTR lpszModule); returneaza adresa de baza care indica unde este incarcat un fisier EXE sau DLL in spatiul de adrese al procesului. Atunci cand apelam aceasta functie, ii transmitem un sir terminat cu zero care precizeaza numele fisierului EXE sau DLL incarcat in spatiul de adrese al procesului apelant. Daca sistemul gaseste numele acelui fisier, GetModuleHandle returneaza adresa de baza la care este incarcat fisierul. Sistemul returneaza NULL daca nu poate gasi fisierul specificat. Putem de asemenea apela GetModuleHandle, transmitandu-i valoarea NULL parametrului lpszModule. Atunci cand procedam astfel, GetModuleHandle returneaza adresa de baza a fisierului EXE. Acest lucru este facut de codul C run-time de pornire atunci cand apeleaza functia noastra WinMain. Trebuie sa tinem minte doua caracteristici importante ale functiei GetModuleHandle. In primul rand, GetModuleHandle examineaza doar spatiul de adrese al procesului apelant. Daca acest proces nu foloseste nici o functie GDI, apelarea functiei GetModuleHandle si transmiterea catre acesta valoarea „GDI32” va cauza returnarea valorii NULL, desi este posibil ca fisierul GDI32.DLL este incarcat in spatiul de adrese al procesului. In al doilea rand, apelarea GetModuleHandle transmitandu-i valoarea NULL returneaza adresa de baza a fisierului EXE in spatiul de adrese al procesului. Asa ca chiar daca apelam GetModuleHandle ( NULL) din codul continut in interiorul unui fisier DLL, valoarea returnata este adresa de baza a fisierului EXE si nu cea a fisierului DLL. Functia GetModuleHandle functioneaza diferit in Windows pe 16 biti. In Windows pe 16 biti, atributul hModule al unei operatii indica modulul de baza de date ( un bloc de informatie folosit intern de catre sistem pentru a administra modulul) pentru un fisier EXE sau DLL. Chiar daca ruleaza 200 de instante ale aplicatiei Notepad, exista doar un modul de baza de date pentru Notepad si din aceasta cauza doar o singura valoare hmodExe care comuna pentru toate instantele. O singura instanta a unui DLL poate fi incarcata in Windows pe 16 biti, astfel incat exista o singura valoare hModExe pentru fiecare DLL incarcat. In Windows pe 16 biti, fiecare instanta care ruleaza a unei sarcini primeste propria sa valoare hinstExe. Aceasta valoare identifica segmentul implicit de date ale unei sarcini. Daca ruleaza 200 de instante de Notepad, exista 200 de valori hinstExe, una pentru fiecare instanta. Deoarece DLL-urile au la randul lor un segment implicit de date, fiecare DLL incarcat primeste de asemenea propria sa valoare hinstExe. Am putea sa credem ca deoarece un DLL poate fi incarcat o singura data, Windows-ul pe 16 biti ar putea sa foloseasca aceeasi valoare pentru hmodExe si hinstExe ale unui DLL. Acest lucru nu se intampla deoarece hmodExe identifica modulul de baza de date al DLL-ului, in timp ce hinstExe identifica segmentul implicit de date al DLL-ului. In Win32, fiecare proces are propriul spatiu de adrese, asta insemnand ca fiecare proces crede ca este singurul proces care ruleaza in sistem. Un proces nu poate vedea usor un alt proces. Din acest motiv, nu este facuta nici o deosebire intre valorile hinstExe si hmodExe ale unui proces, ele sunt si aceeasi valoare. Pentru motive de compatibilitate, cei doi termeni continua sa existe in documentatia Win32. Asa dupa cum am mai spus in sectiunea precedenta, valoarea hinstExe a unei aplicatii identifica adresa de baza de memorie la care sistemul a incarcat codul din fisierul EXE in spatiul de adrese al procesului. Din aceasta cauza, este foarte posibil ca mai multe procese sa aiba aceeasi valoare hinstExe. De exemplu, lansarea aplicatiei Notepad face ca sistemul sa aloce procesului un spatiu de adrese de 4 GB si sa incarce codul si datele Notepad-ului in aceasta zona. Codul si datele s-ar putea sa fie incarcate la adresa de memorie 0x00400000. Daca pornim o alta instanta a Notepad-ului, sistemul va crea un nou spatiu de adrese si va incarca din nou codul si datele Notepad-ului la adresa 0x00400000. Deoarece valoare hinstExe a unei aplicatii este egala cu adresa de memorie de baza unde sistemul incarca codul din fisierul EXE, valoarea hinstExe pentru ambele procese este 0x00400000. In Windows pe 16 biti, este posibil sa apelam functia DialogBox si sa ii transmitem o valoare hinstExe care apartine unei operatii diferita de a noastra : int DialogBox ( HINSTANCE hInstance, LPCTSTR lpszTemplate, HWND hwndOwner, DLGPROC dlgproc); Acesta face ca Windows-ul pe 16 biti sa incarce template-ul casutei de dialog din resursele altei aplicatii. Bineinteles, a asemenea actiune este indoielnica oricum – dar in Win32 nu mai este posibila. Atunci cand apelam o functie care asteapta o valoare hinstExe, Win32 interpreteaza apelul in sensul ca noi cerem informatii de la un fisier EXE sau DLL incarcat in spatiul de adrese al procesului nostru la adresa indicata de hinstExe. Identificatorul precedent de instanta al unui procesAsa dupa cum am spus anterior, codul C run-time de pornire intotdeauna trimite NULL parametrului hinstExePrev a functiei WinMain. Acest parametru exista pentru compatibilitate si nu are nici o insemnatate pentru aplicatiile Win32. In aplicatiile Windows pe 16 biti, parametrul hinstEXEPrev specifica identificatorul unei alte instante ale aceleiasi aplicatii. Daca nici o alta instanta a aplicatiei nu ruleaza, hinstEXEPrev este transmis ca NULL. O aplicatie Windows pe 16 biti examineaza frecvent aceasta valoare din doua motive : 1. Pentru a determina daca ruleaza o alta instanta a aceleiasi aplicatii si, daca da, sa termine noua instanta invocata. Aceasta terminare apare daca un program cum ar fi Print Manager vrea sa permita doar unei instante a lui sa ruleze la un moment dat. 2. Pentru a determina daca clasele de fereastra trebuie sa fie inregistrate. In Windows pe 16 biti, clasele de fereastra trebuie sa fie inregistrate numai o singura data pe modul. Aceste clase sunt apoi impartite intre toate instantele aceleiasi aplicatii. Daca o a doua instanta incearca sa inregistreze aceeasi clasa de fereastra a doua oara, apelul RegisterClass esueaza. In Win32, fiecare instanta a unei aplicatii trebuie sa isi inregistreze propria clasa de fereastra deoarece clasele de fereastra nu mai sunt impartite intre toate instantele unei aplicatii. Pentru a usura trecerea aplicatiilor Windows pe 16 biti catre API-ul Win32, Microsoft a decis ca intotdeauna sa transmita valoarea NULL parametrului hinstEXEPrev al functiei WinMain. Deoarece multe aplicatii Windows pe 16 biti examineaza acest parametru cand isi inregistreaza clasele de fereastra, toate instantele vad ca parametrul hinstEXEPrev este NULL si isi reinregistreaza in mod automat clasele de fereastra proprii. Desi aceasta decizie inlesneste portarea aplicatiilor, acest lucru inseamna ca aplicatiile nu pot folosi valoarea lui hinstEXEPrev pentru a preveni rularea unei a doua instante ale aplicatiei. O aplicatie trebuie sa foloseasca metode alternative pentru a determina daca alte instante ale ei ruleaza deja. Intr-una din metode, aplicatia apeleaza FindWindow si cauta o anumita clasa de fereastra sau titlu care identifica in mod unic acea aplicatie. Daca FindWindow returneaza NULL, aplicatia stie ca este singura instanta care ruleaza. Linia de comanda a unui procesAtunci cand este creat un nou proces, ii este transmisa o linie de comanda. Aceasta nu este aproape niciodata goala; cel mai putin, numele fisierului executabil folosit pentru a crea noul proces este primul sir din linia de comanda. Totusi, este posibil ca un proces sa primeasca o linie de comanda care consta dintr-un singur caracter : zero-ul de sfarsit de sir. Atunci cand codul C run-time de pornire incepe sa se execute, el extrage linia de comanda a procesului, sare peste numele fisierului executabil si trimite un pointer al restului din linia de comanda catre parametrul lpszCmdLine al functiei WinMain. Este important de retinut ca parametrul lpszCmdLine pointeaza intotdeauna catre un sir de caractere ANSI. Microsoft a ales ANSI pentru a ajuta la portarea aplicatiilor din Windows pe 16 biti pe Win32, deoarece aplicatiile Windows pe 16 biti asteapta un sir ANSI. O aplicatie poate parsa si interpreta un sir ANSI in orice fel doreste. Deoarece lpszCmdLine este un LPSTR in loc de un LPCSTR, suntem liberi sa scriem in bufferul la care pointeaza, dar nu trebuie sub nici o circumstanta sa scriem dincolo de capatul bufferului. Daca dorim sa facem modificari ale liniei de comanda, putem copia bufferul de linie de comanda intr-un buffer local in aplicatia noastra; apoi putem modifica bufferul local. Putem de asemenea obtine un pointer la linia completa de comanda a procesului nostru apeland functia GetCommandLine : LPTSTR GetCommandLine ( VOID); Aceasta functie returneaza un pointer la un buffer care contine linia de comanda completa, incluzand calea completa la fisierul executabil. Probabil cel mai important motiv de a folosi GetCommandLine in loc de parametrul lpszCmdLine este ca atat versiunea ANSI, cat si cea UNICODE a functiei GetCommandLine exista in Win32 pe cand parametrul lpszCmdLine intotdeauna indica catre un buffer care contine un sir de caractere ANSI. Multe aplicatii prefera sa aiba linia de comanda parsata in tokenuri diferite. O aplicatie poate accesa componentele individuale ale liniei de comanda folosind variabilele globale _ _ argv si _ _ argc. Dar, din nou, variabila _ _ argv este un tablou de pointeri de caractere la siruri ANSI si nu UNICODE. Win32 ofera o functie care separa un sir in tokenuri separate, CommandLineToArgvW : LPWSTR * CommandLineToArgvW ( LPWSTR lpCmdLine, LPINT pArgc); Asa dupa cum arata si W de la sfarsitul numelui functiei, aceasta functie exista doar in versiunea UNICODE. ( W vine de la wide larg) Primul parametru, lpCmdLine pointeaza catre un sir de linie de comanda. Acesta este de obicei valoare de retur de la un apel timpuriu al functiei GetCommandLine. Parametrul pArgc este adresa unei variabile de tip integer; aceasta va fi setata la numarul de argumente din linia de comanda. CommandLineToArgvW returneaza adresa unui tablou de pointeri la siruri UNICODE. CommandLineToArgvW aloca intern memorie. Majoritatea aplicatiilor nu vor elibera aceasta memorie si va ramane in sarcina sistemului de operare eliberarea acestei zone la terminarea procesului. Acest lucru este acceptabil. Totusi, daca dorim sa eliberam noi insine aceasta memorie, modalitatea corecta de a face acest lucru este urmatoarea : int nArgc; LPWSTR ppArgv CommandLineToArgvW ( GetCommandLineW ( ), &nArgc); //Folosim argumentele if ( *ppArgv[1] == ‚x’) // Eliberam blocul de memorie HeapFree ( GetProcessHeap ( ), 0, ppArgv); Variabilele de mediu ale unui procesFiecare proces are un bloc de mediu asociat lui. Un astfel de bloc este un bloc de memorie alocat in interiorul spatiului de adrese al unui proces. Fiecare bloc contine un set de siruri cu urmatorul aspect : VarName1 = VarValue10 VarName2 = VarValue20 VarName3 = VarValue30 VarNameX = VarValueX0 0 Prima parte a fiecarui sir este numele unei variabile de mediu. Acest nume este urmat de semnul egal, care este urmat de valoarea pe care vrem sa i-o atribuim variabilei. Toate sirurile din blocul de mediu trebuie sa fie sortate alfabetic dupa numele variabilelor de mediu. Deoarece semnul egal este folosit pentru a separa numele de valoare, simbolul egal nu poate sa apara in nume. De asemenea, spatiile sunt semnificative. De exemplu, daca declaram aceste doua variabile : XZY= Win32 ( Dupa egal este un spatiu gol.) ABC=Win32 si apoi le comparam valorile, sistemul va declara ca cele doua variabile sunt diferite. Acest lucru deoarece orice spatiu gol care apare imediat inainte sau dupa egal este luat in considerare. De exemplu, daca ar trebui sa adaugam doua siruri la blocul de mediu, XYZ =Home XYZ=Work atunci varibila de mediu “XYZ ” va contine “Home” iar cealalta variabila va contine “Work”. In final, un caracter 0 trebuie plasat la sfarsitul tuturor variabilelor de mediu pentru a marca sfarsitul blocului de mediu. Pentru a crea un set initial de varibile de mediu pentru Windows 95, trebuie sa modificam fisierul sistem AUTOEXEC.BAT prin plasarea unor linii SET. Fiecare linie treebuie sa fie de forma : SET VarName=VarValue Atunci cand repornim sistemul, continutul fisierului este parsat si variabilele de mediu pe care le-am setat vor fi disponibile fiecarui proces pe care il pornim in sesiunea Windows. Atunci cand un utilizator se logheaza in Windows NT, sistemul creeaza nucleul procesorului si ii asociaza un set de siruri de mediu. Sistemul obtine setul initial de siruri de mediu examinand doua intrari din registrul Windows. Prima cheie, HKEY _ LOCAL _ MACHINESYSTEMCurrentControlSetControlSessionManagerEnvironment contine lista cu toate variabilele de mediu din sistem. Cea de-a doua, HKEY _ CURRENT _ USEREnvironment, contine lista tuturor variabilelor de mediu care se aplica doar pentru utilizatorul logat in acel moment. Un utilizator poate adauga, sterge sau schimba oricare din caeste intrari facand dublu clic pe optiunea System din Control Panel si apoi selectand tabul Environment. Doar un utilizator cu drepturi de administrator pe acel sistem poate modifica variabilele din lista System Variables. Aplicatia noastra poate folosi diferite functii din registrul Windows pentru a modifica aceste intrari. Totusi, pentru ca schimbarile sa aiba loc pentru toate aplicatiile, utilizatorul trebuie sa se delogheze si apoi sa se logheze din nou. Unele aplicatii, cum ar fi Explorer, Task Manager si Control Panel pot sa isi actualizeze blocul de mediu cu intrari noi de registru atunci cand fereastra lor principala primeste un mesaj WM _ WININICHANGE. De exemplu, daca actualizam intrarile din registrul Windows si vrem ca anumite procese sa-si actualizeze blocurile lor de mediu, putem apela : SendMessage ( HWND _ BROADCAST, WM _ WININICHANGE, 0L, ( LPARAM) “Environment”); In mod normal, un proces copil mosteneste un set de variabile de mediu care sunt identice cu cele ale parintelui. Totusi, procesul parinte poate controla ce variabile de mediu mosteneste copilul. Prin mostenire intelegem ca procesul copil primeste copia proprie a blocului de mediu al parintelui, nu ca procesul copil si procesul parinte impart acelasi bloc de mediu. Acest lucru inseamna ca un proces copil poate adauga, sterge sau modifica o variabila in blocul propriu de mediu iar schimbarea sa nu fie reflectata in blocul de mediu al parintelui. Variabilele de mediu sunt in mod normal folosite de aplicatii pentru a permite utilizatorului sa modifice cu finete comportarea aplicatiei. Utilizatorul creeaza o variabila de mediu si o initializeaza. Apoi, atunci cand utilizatorul executa aplicatia, aceasta examineaza blocul de mediu pentru a gasi acea variabila. Daca aplicatia gaseste variabila, aplicatia parseaza valoarea variabilelei si ii modifica comportamentul. Problema cu variabilele de mediu este ca ele nu sunt intelese usor de catre utilizatori. Acestia trebuie sa foloseasca corect numele variabilelor si de asemenea trebuie sa cunoasca sintaxa exacta ceruta a valorii variabilei. Majoritatea ( daca nu toate) aplicatiile grafice, pe de alta parte, permit utilizatorilor sa modifice cu finete comportarea aplicatiei folosind casute de dialog. Aceasta modalitate este mult mai prietenoasa si este putenic incurajata de catre Microsoft. Daca totusi dorim sa folosim variabilele de mediu, Win32 ofera cateva functii pe care le poate folosi aplicatia noastra. Functia GetEnvironmentVariable ne permite sa detectam prezenta si valoarea unei variabile de mediu : DWORD GetEnvironmentVariable ( LPCTSTR lpszName, LPTSTR lpszValue, DWORD cchValue); Atunci cand apelam GetEnvironmentVariable, lpszName pointeaza la numele de variabila dorit, lpszValue pointeaza la bufferul care retine valoarea variabilei si cchValue indica marimea acestui buffer in caractere. Functia returneaza fie numarul de caractere copiate in buffer, fie 0 daca numele variabilei nu a fost gasit in mediu. Functia SetEnvironmentVariable ne da posibilitatea sa adaugam o variabila, sa stergem o variabila sau sa modificam valoarea unei variabile : BOOL SetEnvironmentVariable ( LPCTSTR lpszName, LPCTSTR lpszValue); Aceasta functie seteaza variabila identificata de parametrul lpszName la valoarea identificata de parametrul lpszValue. Daca variabila cu numele specificat nu exista, variabila este adaugata si daca valoarea variabilei lpszValue este NULL, atunci valoarea este stearsa din blocul de mediu. Trebuie ca intotdeauna sa folosim aceste functii pentru manipularea blocului de mediu al procesului nostru. Dupa cum am mai spus, sirurile din blocul de mediu trebuie sortate alfabetic dupa numele variabilelor. Atunci cand acestea sunt alfabetizate, GetEnvironmentVariable le poate localiza mai repede si SetEnvironmentVariable este indeajuns de „inteligenta” pentr a mentine variabilele de mediu in ordinea sortata. Modulul de eroare al unui procesFiecarui proces ii este asociat un set de indicatori care precizeaza sistemului cum trebuie sa raspunda procesul la erorile serioase. Erorile serioase reprezinta erori de disc, exceptii netratate, esecuri de gasire de fisiere si aranjare defectuoasa a datelor. Un proces poate spune sistemului cum sa trateze fiecare din aceste erori apeland functia SetErrorMode : UINT SetErrorMode ( UINT fuErrorMode); Parametrul fuErrorMode este o combinatie intre oricare dintre indicatorii din tabelul urmator ( OR pe biti):
In mod implicit, un proces copil mosteneste indicatorii de moduri de eroare ai parintelui sau. In alte cuvinte, daca un proces are in mod curent indicatorul SEM _ NOGPFAULTERRORBOX setat si apoi creeaza un proces copil, acesta din urma va avea la randul sau acest indicator setat. Totusi, procesul copil nu este anuntat de acest lucru si este posibil sa se nu se fi scris in indicatorul de erori GP. Daca apare o eroare GP in unul din firele procesului copil, aplicatia corespunzatoare acestui proces s-ar putea sa se termine fara a anunta utilizatorul. Un proces parinte poate preveni ca un proces copil sa mosteneasca modul de erori al parintelui precizand indicatorul CREATE _ DEFAULT _ EROR _ MODE atunci cand apeleaza CreateProcess. Unitatea de disc curenta si directorul curent ale unui procesDirectorul curent al unitatii de disc curente reprezinta locatia unde diferite functii Win32 cauta fisiere si directoare atunci cand nu sunt caile complete nu sunt furnizate. de exemplu, daca un fir in interiorul unui proces apeleaza CreateFile pentru a deschide un fisier ( fara a specifica calea completa), sistemul va cauta acel fisier in unitatea de disc si directorul curente. Sistemul pastreaza intern evidenta unitatii de disc si a directorului curent al unui proces. Deoarece aceasta informatie este mentinuta pe un intreg proces, un fir de executie care schimba aceste valori le schimba pentru toate firele din proces. Un fir poate obtine si poate seta unitatea de disc curenta proprie si directorul curent propriu apeland urmatoarele doua functii : DWORD GetCurrentDirectory ( DWORD cchCurDir, LPTSTR lpszCurDir); BOOL SetCurrentDirectory ( LPCTSTR lpszCurDir); Directoarele curente ale unui procesSistemul pastreaza numele unitatii de disc si a directorului curent, dar nu pastreaza numele directorului curent pentru fiecare unitate de disc. Totusi, exista asistenta din partea sistemului de operare pentru tratarea directoarelor curente pentru mai multe unitati de disc. Acest sprijin este oferit prin folosirea sirurilor de mediu ale procesului. De exemplu, un proces poate avea 2 variabile de mediu, ca mai jos : =C:=C:UtilityBin =D:=D:Program Files Aceste variabile indica faptul ca directorul curent pentru unitatea de disc C este UtilityBin si ca directorul curent pentru unitatea de disc D este Program Files. Daca apelam o functie Win32, trasmitand un nume care identifica o unitate de disc care nu este cea curenta, sistemul cauta in blocul de mediu al procesului pentru variabila asociata cu litera unitatii de disc precizata. Daca aceasta varabila exista, sistemul foloseste valoarea variabilei ca director curent. Daca variabila nu exista, sistemul presupune ca directorul curent pentru unitatea de disc specificata este directorul radacina. De exemplu, daca directorul curent al procesului este C:UtilityBin si apelam CreateFile pentru a deschide D:ReadMe.txt, sistemul va cauta variabila de mediu =D:. Deoarece variabila =D: exista, sistemul incearca sa deschida fisierul ReadMe.txt din directorul D:Program Files. Daca variabila =D: nu exista, sistemul poate sa incerce sa deschida fisierul din directorul radacina a unitatii de disc D. Functiile de lucru cu fisiere Win32 nu adauga sau schimba niciodata o variabila de mediu de unitate de disc ( drive-letter environment variable) – ele doar citesc aceste variabile. Putem folosi functia C run-time _ chdir in locul functiei Win32 SetCurrentDirectory pentru a schimba directorul curent. Functia _ chdir apeleaza SetCurrentDirectory intern, dar in plus _ chdir adauga si modifica variabilele de mediu astfel incat directorul curent al fiecarei unitati de disc sa fie retinut. Daca un proces parinte creeaza un bloc de mediu pe care vrea sa il transmita procesului fiu, blocul de mediu al procesului fiu nu va mosteni in mod automat directoarele curente ale procesului parinte. In schimb, directoarele curente ale procesului copil vor fi setate in mod implicit la directoarele radacina de pe fiecare unitate de disc. Daca vrem ca procesul copil sa pastreze directoarele curente ale procesului parinte, procesul parinte trebuie sa creeze aceste variabile de mediu de unitati de disc si sa le adauge la blocul de mediu inainte de a crea un proces copil. Procesul parinte poate obtine directoarele curente proprii prin apelul functiei GetFullPathName : DWORD GetFullPathName ( LPCTSTR lpszFile, DWORD cchPath, LPTSTR lpszPath, LPTSTR *ppszFilePart); De exemplu, pentru a afla directorul curent pentru unitatea de disc C, am putea face urmatorul apel : TCHAR szCurDir [MAX _ PATH]; DWORD GetFullPathName ( _ _ TEXT ( “C:”), MAX _ PATH, szCurDir, NULL); Trebuie sa tinem minte ca variabilele de mediu ale unui proces trebuie intotdeauna sa fie pastrate in ordine alfabetica. Din cauza acestei necesitati, variabilele de mediu de litera de unitate de disc vor trebui sa fie plasate la inceputul blocului de mediu Versiunea sistemuluiIn mod frecvent, o aplicatie are nevoie sa stie pe ce versiune de Windows ruleaza utilizatorul aplicatia. De exemplu, o aplicatie ar putea sa beneficieze de avantajele de securitate apeland functiile de securitate Win32. Totusi, aceste functii sunt implementate integral doar in Windows NT. API-ul Windows are functia GetVersion : DWORD GetVersion ( void); Aceasta functie simpla are o intreaga istorie in urma ei. Ea a fost gandita pentru Windows pe 16 biti. Ideea era una simpla : returneaza numarul versiunii MS-DOS in octetul superior si numarul versiunii Windows in octetul inferior. Pentru fiecare cuvant, octetul superior reprezinta numarul major de versiune si octetul inferior reprezinta numarul minor de versiune. Din pacate, programatorul care a scris acest cod a facut o greseala, inversand octetii. Deoarece multi programatori incepusera deja sa foloseasca aceasta functie, Microsoft a fost nevoit sa lase aceasta functie asa cum este si a schimbat documentatia pentru a reflecta greseala. Din cauza confuziei din jurul acestei functii, Microsoft a adaugat o noua functie in API-ul Win32, anume GetVersionEx : BOOL GetVersionEx ( LPOSVERSIONINFO lpVersionInformation); Aceasta functie cere ca noi sa alocam o structura OSVERSIONINFO in aplicatia noastra si sa transmitem adresa acestei structuri functiei GetVersionEx. Structura OSVERSIONINFO este : typedef struct OSVERSIONINFO, *LPOSVERSIONINFO; Trebuie sa remarcam ca structura are membri diferiti pentru fiecare componenta individuala a numarului de versiune a sistemului. Acest lucru a fost facut intentionat pentru ca programatorii sa nu trebuiasca sa isi mai bata capul cu extragerea octetului superior, inferior etc.; astfel este mult mai usor pentru o aplicatie sa compare numarul de versiune cerut cu cel existent pe sistem. Urmatorul tabel descrie membri structurii OSVERSIONINFO :
Functia CreateProcessUn proces este creat atunci cand aplicatia noastra apeleaza functia CreateProcess : BOOL CreateProcess ( LPCTSTR lpszApplicationName, LPCTSTR lpszCommandLine, LPSECURITY _ ATTRIBUTES lpsaProcess, LPSECURITY _ ATTRIBUTES lpsaThread, BOOL fInherithandles, DWORD fdwCreate, LPVOID lpvEnvironment, LPTSTR lpszCurDir, LPSTARTUPINFO lpsiStartupInfo, LPPROCESS _ INFORMATION lppiProcInfo); Atunci cand un fir din interiorul unei aplicatii apeleaza CreateProcess , sistemul creeaza un proces obiect nucleu ( process kernel object) cu un contor de utilizare initial egal cu 1. Acesta nu este procesul in sine, ci mai degraba o mica structura de date pe care sistemul de operare o foloseste pentru a administra procesul – putem sa ne gandim la acest proces ca o structura de date care consta din informatii statistice despre proces. Apoi sistemul creeaza un spatiu virtual de adrese de 4 GB pentru noul proces si apoi incarca codul si datele pentru fisierul executabil si orice alte fisiere DLL necesare in spatiul de adrese de 4 GB. Sistemul creeaza apoi un fir de executie obiect nucleu ( cu un contor initial de utilizare egal cu 1) pentru firul principal al noului proces. La fel ca si procesul obiect nucleu, firul obiect nucleu este o structura mica de date pe care o foloseste sistemul de operare pentru a administra firul. Acest fir principal va incepe prin executarea codului C run-time de pornire, care eventual apeleaza functia noastra WinMain ( sau functia main daca aplicatia noastra este de tipul CUI). Daca sistemul creeaza cu succes noul proces si firul principal de executie, CreateProcess returneaza TRUE. Daca suntem obisnuiti cu functiile Windows pe 16 biti pentru crearea unui proces, WinExec si LoadModule, putem observa din compararea numarului de parametri pentru aceste doua functii cu noua functie CreateProcess ca aceasta din urma ofera mult mai mult control asupra crearii procesului. Atat WinExec cat si LoadModule sunt implementate intern ca apeluri la functia CreateProcess. Si deoarece aceste functii sunt furnizate doar pentru compatibilitatea cu Windows pe 16 biti, nu exista nici o versiune UNICODE pentru aceste functii – le putem apela doar transmitandu-le siruri ANSI. lpszApplicationName si lpszCommandLineParametrii lpszApplicationName si lpszCommandLine specifica amandoi numele fisierului executabil pe care noul proces il va folosi si sirul de linie de comanda care va fi transmis catre noul proces. Parametrul lpszCommandLine ne permite sa precizam o linie de comanda completa pe care CreateProcess o foloseste pentru a crea noul proces. Atunci cand functia CreateProcess parseaza sirul lpszCommandLine, ea examineaza primul token din sir si presupune ca acesta este numele fisierului executabil pe care dorim sa il executam. Daca numele fisierului executabil nu are extensie, este presupusa implicit extensia EXE. CreateProcess va cauta de asemenea fisierul executabil in ordinea urmatoare : 1. Directorul care contine fisierul executabil al procesului apelant. 2. Directorul curent al procesului apelant. 3. Directorul sistem al Windows-ului. 4. Directorul Windows-ului. 5. Directoarele din variabila de mediu PATH. Bineinteles, daca fisierul include o cale completa, sistemul cauta executabilul folosind calea completa si nu mai cauta in directoare. Daca sistemul gaseste fisierul executabil, el creeaza un nou proces si mapeaza codul si datele fisierului executabil in spatiul de adrese al noului proces. Apoi sistemul apeleaza rutina C run-time de pornire. Aceasta examineaza linia de comanda a procesului si trimite adresa primului argument de dupa numele fisierului executabil ca fiind argumentul lpszCmdLine al functiei WinMain Tot ce am descris mai sus se intampla atata timp cat parametrul lpszApplicationName este NULL ( ceea ce ar acoperi circa 99 din cazuri). In loc sa trimitem NULL, putem trimite adresa unui sir care contine numele fisierului executabil pe care dorim sa il rulam in parametrul lpszApplicationName. De notat este faptul ca trebuie sa specificam extensia fisierului executabil; sistemul nu va presupune in mod automat ca fisierul se termina cu extensia EXE. CreateProcess presupune ca fisierul este in directorul curent doar daca nu este specificata o cale care precede parametrul. Daca fisierul nu poate fi gasit in directorul curent, CreateProcess nu cauta fisierul in orice alt director – CreateProcess pur si simplu esueaza. Chiar daca precizam un fisier in parametrul lpszApplicationName totusi, CreateProcess transmite continutul parametrului lpszCommandLine catre noul proces ca linia sa de comanda. De exemplu, sa spunem ca apelam CreateProcess ca mai jos : CreateProcess ( “C:WINNTSYSTEM32NOTEPAD.EXE”, “WORDPAD README.TXT”, _ ); Sistemul apeleaza aplicatia Notepad; dar linia de comanda a aplicatiei Notepad este „WORDPAD README.TXT”. Aceasta intorsatura este in mod sigur putin ciudata, dar acesta este modul cum functioneaza CreateProcess. Aceasta capacitate furnizata de catre lpszApplicationName a fost adaugata functiei CreateProcess pentru a suporta subsistemul POSIX al Windows-ului NT. lpsaProcess, lpsaThread si fInheritHandlesPentru a crea un nou proces, sistemul trebuie sa creeze un obiect nucleu proces si un obiect nucleu fir ( pentru firul principal de executie al procesului). Deoarece acestea sunt obiecte nucleu, procesul parinte are posibilitatea de a asocia atributele de securitate cu aceste doua obiecte. parametrii lpsaProcess si lpsaThread ne permit sa precizam securitatea dorita pentru obiectul proces si respectiv pentru obiectul fir de executie. Putem transmite NULL pentru acesti doi parametri, caz in care sistemul le acorda acestor obiecte descriptorii de securitate impliciti. Dar noi putem aloca si initializa doua structuri SECURITY _ ATTRIBUTES pentru a crea si a atribui propriile privilegii de securitate pentru obiectele proces si nucleu. Un alt motiv pentru a folosi structurile SECURITY _ ATTRIBUTES pentru parametrii lpsaProcess si lpsaThread este atunci cand dorim ca macar unul din aceste doua identificatoare de obiect sa poata sa fie mostenite de catre un proces copil care va fi creat in viitor de catre acest proces parinte. In continuare vom vedea un mic program care demonstreaza mostenirea identificatorului de obiect nucleu. Sa spunem ca procesul A creeaza procesul B prin apelul CreateProcess cu parametru lpsaProcess egal cu adresa unei structuri de tipul SECURITY _ ATTRIBUTES in care membrul bInherithandle este setat la TRUE. In acelasi apel, parametrul lpsaThread pointeaza la alta structura SECURITY _ ATTRIBUTES in care membrul bInheritHandle este setat la FALSE. Atunci cand sistemul creeaza procesul B, el aloca atat un proces obiect nucleu cat si un fir de executie obiect nucleu si returneaza identificatorii procesului A intr-o structura la care pointeaza parametrul lppiProcInfo. Procesul A poate acum manipula obiectul proces si obiectul fir de executie nou create folosind acesti identificatori. Acum sa presupunem ca procesul A va apela CreateProcess a doua oara pentru a crea procesul C. Procesul A poate decide daca ii acorda dreptul procesului C de a manipula unele din obiectele nucleu la care are acces procesul A. parametrul fInheritHandles este folosit in acest scop. Daca fInherithandles este setat la TRUE, sistemul face ca procesul C sa mosteneasca orice identificatori care pot fi mosteniti din procesul A. In acest caz, identificatorul la obiectul proces al procesului B poate fi mostenit. Identificatorul firului principal de executie al procesului B nu este mostenit indiferent de valoarea parametrului fInheritHandles. De asemenea, daca procesul A apeleaza CreateProcess, trimitand FALSE pentru parametrul fInheritHandles, procesul C nu va mosteni nici unul dintre identificatorii folositi in mod curent de catre procesul A. INHERIT.C #include <windows.h> int WINAPI WinMain ( HINSTANCE hInstExe, HINSTANCE hInstPrev, LPSTR lpszCmdLine, int nCmdShow) fdwCreateParametrul fdwCreate identifica indicatorii care afecteaza modul de creare a noului proces. Indicatorii multipli pot fi precizati atunci cand folosim operatorul logic OR. Indicatorul DEBUG _ PROCESS spune sistemului ca procesul parinte doreste sa depaneze procesul copil si orice alte procese create de procesul copil in viitor. Acest indicator indica sistemului sa anunte procesul parinte ( acum depanatorul) atunci apare un eveniment in procesele copil ( depanate). Indicatorul DEBUG _ ONLY _ THIS _ PROCESS este asemanator cu cel precedent cu exceptia faptului ca depanatorul este anuntat de aparitia unor evenimente deosebite doar in procesele copil imediate. Daca procesul copil creeaza noi procese, depanatorul nu stie de aparitia unor evenimente in aceste procese. Indicatorul CREATE _ SUSPENDED face ca noul proces sa fie creat, dar avand firul principal de executie suspendat. Un depanator asigura un exemplu bun pentru folosirea acestui indicator. Atunci cand unui depanator i se cere sa incarce un depanat, trebuie ca sistemul sa initializeze procesul si firul principal de executie; dar depanatorul nu vrea sa permita firului principal de executie sa isi inceapa deja executia. Folosind acest indicator, utilizatorul care depaneaza aplicatia poate seta diferite puncte de oprire in program in caz ca exista evenimente speciale care trebuie tratate. Odata ce toate au fost setate, utilizatorul poate spune depanatorului ca firul principal de executie poate sa isi inceapa executia. Indicatorul DETACHED _ PROCESS blocheaza accesul unui proces CUI la fereastra de consola a procesului parinte si spune sistemului de operare sa trimita iesirea la o noua fereastra de consola. Daca un proces CUI este creat de catre alt proces CUI, procesul nou creat in mod implicit va folosi fereastra de consola a parintelui. Prin precizarea acestui indicator, noul proces isi va trimite iesirile catre o noua fereastra consola. Indicatorul CREATE _ NEW _ CONSOLE spune sistemului sa creeze o noua fereastra de consola pentru noul proces. Este eronat sa folosim ambii indicatori CREATE _ NEW _ CONSOLE si DETACHED _ PROCESS. Indicatorul CREATE _ NO _ WINDOW spune sistemului sa nu creeze o noua fereastra consola pentru aplicatie. Acest indicator ne permite sa executam o aplicatie consola fara interfata cu utilizatorul. Indicatorul CREATE _ NEW _ PROCESS _ GROUP este folosit pentru a modifica lista proceselor care sunt anuntate atunci utilizatorul apasa Ctrl+C sau Ctrl+Break. Daca avem mai multe procese CUI care ruleaza atunci cand utilizatorul apasa aceste combinatii de taste, sistemul anunta toate procesele din grupul procesului ca utilizatorul vrea sa intrerupa operatia curenta. Prin precizarea acestui indicator atunci cand cream un proces CUI, cream un nou grup de procese. Daca utilizatorul apasa Ctrl+C sau Ctrl+Break in timp ce un proces din grup este activ, sistemul anunta doar procesele din grup de cererea utilizatorului. Indicatorul CREATE _ SEPARATE _ WOW _ VDM este folositor doar atunci cand apelam o aplicatie Windows pe 16 biti in Windows NT. Daca indicatorul este precizat, sistemul va crea o Virtual DOS Machine separata si ruleaza aplicatia in aceasta VDM. Implicit, toate aplicatiile Windows pe 16 biti sunt executate intr-o singura VDM, pe care o impart toate. Avantajul rularii unei aplicatii intr-o VDM separata este ca daca aplicatia isi termina anormal executia ea opreste doar acea VDM in care rula; orice alte programe care rulau in VDM distincte isi continua executia normal. De asemenea, aplicatiile Windows pe 16 biti care ruleaza in VDM-uri separate au cozi de intrare separate. Acest lucru inseamna ca daca o aplicatie se blocheaza pentru moment, aplicatiile din VDM-uri separate continua sa primeasca intrari. Dezavantajul rularii mai multor VDM-uri este ca fiecare VDM consuma o cantitate apreciabila de memorie fizica. Windows 95 ruleaza toate aplicatiile Windows pe 16 biti intr-o singura masina virtuala – nu putem modifica acest comportament. Indicatorul CREATE _ SHARED _ WOW _ VDM este folositor doar atunci cand apelam o aplicatie Windows pe 16 biti in Windows NT. Implicit, fiecare aplicatie Windows pe 16 biti ruleaza intr-o singura VDM daca nu precizam indicatorul CREATE _ SHARED _ WOW _ VDM. Totusi, acest comportament implicit poate fi ocolit daca valoarea DefaultSeparateVDM din registrul Windows din HKEY _ LOCAL _ MACHINESystemCurrentControlSetControlWOW este “yes”. Daca aceasta valoare este „yes”, indicatorul CREATE _ SHARED _ WOW _ VDM suprascrie aceasta valoare si ruleaza aplicatia Windows pe 16 biti in VDM comun al sistemului. Trebuie sa restartam dupa schimbarea acestei valori din registru. Indicatorul CREATE _ UNICODE _ ENVIRONMENT spune sistemului de operare ca blocul de mediu al procesului copil trebuie sa contina caractere UNICODE. Implicit, blocul de mediu al unui proces contine siruri ANSI. Putem de asemenea sa specificam o clasa de prioritate atunci cand cream un nou proces. Totusi, nu trebuie sa specificam o clasa de prioritate si pentru majoritatea aplicatiilor este recomandat sa nu definim; sistemul va atribui o clasa implicita de prioritate noului proces. Urmatorul tabel arata clasele posibile de prioritate in Windows NT si Windows. Vom vedea ca in Windows 2000 au fost introduse cateva clase de prioritate suplimentare.
Aceste clase de prioritate afecteaza modul in care firul continut in proces sunt programate, pentru obtinere timp CPU, respectand firele de executie ale celorlalte procese. lpvEnvironmentParametrul lpvEnvironment pointeaza cate un bloc de memorie care contine siruri de mediu pe care le va folosi noul proces. Majoritatea timpului este transmis NULL pentru acest parametru, care face ca procesul copil sa mosteneasca setul de siruri de mediu pe care le foloseste procesul parinte. Sau putem folosi functia GetEnvironmentStrings : LPVOID GetEnvironmentStrings ( VOID); Aceasta functie primeste adresa blocului de siruri de mediu pe care procesul apelant le foloseste. Putem folosi adresa returnata de aceasta functie ca parametrul lpvEnvironment al functiei CreateProcess. Exact acest lucru face CreateProcess daca ii transmitem NULL pentru parametrul lpvEnvironment. lpszCurDirParametrul lpszCurDir permite procesului parinte sa seteze unitatea de disc si directorul curente ale procesului copil. Daca acest parametru este NULL; directorul de lucru al noului proces va fi acelasi cu cel al aplicatiei care a creat noul proces. Daca acest parametru nu este NULL, lpszCurDir trebuie sa pointeze la un sir terminat cu zero care contine unitatea de disc si directorul curente dorite. Trebuie remarcat ca trebuie sa precizam o litera de unitate de disc in cale. lpsiStartInfoParametrul lpsiStartInfo pointeaza la o structura STARTUPINFO : typedef struct _ STARTUPINFO STARTUPINFO, *LPSTARTUPINFO; Win32 foloseste membrii acestei structuri atunci cand creeaza noul proces. Majoritatea aplicatiilor vor dori ca aplicatiile create pur si simplu sa foloseasca valorile implicite. Minimal, ar trebui sa initializam toti membrii din aceasta structura la zero si apoi sa setam membrul cb la marimea structurii : STARTUPINFO si; ZeroMemory ( &si, sizeof ( si)); si.cb = sizeof ( si); CreateProcess ( _ , &si, _ ); Una din cele mai frecvente greseli pe care le fac programatorii este esuarea stergerii continutului structurii inainte de a apela CreateProcess. Daca dorim sa initializam unii dintre membrii structurii, doar trebuie sa facem acest lucru inainte de apelul CreateProcess.
Mai trebuie sa discutam membrul dwFlags. Acest membru contine un set de indicatori care modifica modul in care un proces copil este creat. Majoritatea lor pur si simplu precizeaza functiei CreateProcess daca alti membri a structurii STARTUPINFO contin informatii utile, sau daca unii ar trebui ignorati. Urmatorul tabel arata lista indicatorilor posibili si intelesul lor.
Alti doi indicatori, STARTF _ FORCEONFEEDBACK si STARTF _ FORCEOFFFEEDBACK ne dau controlul asupra cursorului mouse-ului atunci cand cream un nou proces. Deoarece Windows 95 si Windows NT suporta operatiile cu multiple fire de executie, este posibil sa invocam o aplicatie si, in timp ce procesul se initializeaza, sa folosim un alt program. Pentru a da un feed-back vizual utilizatorului, CreateProcess schimba temporar cursorul mouse-ului intr-un nou cursor numit „start glass”. Acest cursor indica faptul ca putem astepta sa se intample ceva sau putem sa continuam sa folosim sistemul. Functia CreateProcess ne da un control mai strict asupra mouse-ului atunci cand apelam un alt proces. Atunci cand folosim indicatorul STARTF _ FORCEOFFFEEDBACK, procesul CreateProcess nu schimba forma cursorului, lasandu-l ca o sageata simpla. Folosirea indicatorului STARTF _ FORCEONFEEDBACK face ca CreateProcess sa monitorizeze initializarea noului proces si sa modifice forma cursorului in functie de rezultat. Atunci cand CreateProcess este apelat cu acest indicator, cursorul se schimba in cursor de tip clepsidra. Daca in 2 secunde noul proces nu face un apel GUI, CreateProcess reseteaza forma cursorului. Daca procesul face un apel GUI in urmatoarele 2 secunde, functia CreateProcess asteapta ca aplicatia sa afiseze o fereastra. Acest lucru se intampla la fiecare 5 secunde dupa ce procesul efectueaza apelul GUI. Daca fereastra nu este afisata, CreateProcess reseteaza cursorul. Daca este afisata o fereastra, CreateProcess pastreaza cursorul pentru inca 5 secunde. Daca in orice moment aplicatia apeleaza functia GetMessage, care indica faptul ca s-a terminat initializarea, CreateProcess reseteaza imediat cursorul si opreste monitorizarea noului proces. Ultimul indicator este STARTF _ SCREENSAVER. Acesta indica sistemului ca aplicatia este o aplicatie de tip screen-saver, care face ca sistemul sa initializeze aplicatia intr-un mod special. Atunci cand procesul isi incepe executia, sistemul permite procesului sa fie initializat cu prioritatea clasei din prim-plan care a fost specificata in apelul CreateProcess. De indata ce procesul face un apel la una din functiile GetMessage sau PeekMessage, sistemul schimba automat prioritatea procesului cu prioritatea clasei nefolosit ( idle priority class). Daca aplicatia de tip screen-saver este activa si utilizatorul apasa o tasta sau misca mouse-ul, sistemul creste automat prioritatea clasei aplicatiei de tip screen-saver inapoi la prioritatea clasei precizate in apelul CreateProcess Pentru a porni o aplicatie de tip screen-saver, ar trebui sa apelam CreateProcess folosind indicatorul NORMAL _ PRIORITY _ CLASS. Procedand astfel obtinem doua efecte : 1. Sistemul permite aplicatiei de tip screen-saver sa se initializeze inainte de a rula in masura resurselor disponibile. Daca aplicatia ar rula 100 din timpul ei la prioritatea cea mai mica, procesele cu prioritate normala si ridicata ar rula inaintea ei si aplicatia nu ar mai reusi sa se initializeze. 2. Sistemul permite aplicatiei de tip screen-saver sa se termine. De obicei un screen-saver isi termina executia deoarece utilizatorul incepe folosirea altei aplicatii. Aceasta aplicatie ruleaza probabil la prioritate normala, lucru care face ca executia firelor de executie din aplicatia screen-saver sa fie intarziata nedefinit si atunci screensaver-ul nu ar fi in stare sa isi incheie executia. Un membru al structurii STARTUPINFO este wShowWindow. Initializam acest membru cu valoarea care este transmisa ultimului parametru al functiei WinMain, nCmdShow. Aceasta valoare indica cum dorim sa fie afisata fereastra principala a aplicatiei. Valoarea care poate fi transmisa este unul din indicatorii care pot fi transmisi functiei ShowWindow. De obicei, valoarea nCmdShow este fie SW _ SHOWNORMAL, fie SW _ SHOWMINNOACTIVE. Totusi, valorile pot fi uneori SW _ SHOWDEFAULT. Atunci cand apelam o aplicatie din Explorer prin dublu clic, functia WinMain a aplicatiei este apelata cu valoarea SW _ SHOWNORMAL pentru parametrul nCmdShow. Daca tinem apasat tasta Shift atunci cand facem dublu clic, aplicatia este pornita cu SW _ SHOWMINNOACTIVE ca valoare pentru nCmdShow. In acest fel, utilizatorul poate porni foarte usor o aplicatie care are fereastra sa principala fie in starea normala, fie minimizata. In final, o aplicatie poate apela VOID GetStartupInfo ( LPSTARTUPINFO lpStartupInfo); pentru a obtine o copie a structurii STARTUPINFO care a fost initializata de procesul parinte. Procesul copil poate examina aceasta structura si ii poate schimba comportamentul in functie de valorile membrilor structurii. Desi documentatia Win32 nu precizeaza acest lucru explicit, ar trebui sa initializam membrul cb al structurii inainte de a apela GetStartupInfo ca mai jos : STARTUPINFO si; si.cb = sizeof ( si); GetStartupInfo ( &si); lppiProcInfoParametrul lppiProcInfo pointeaza la o structura PROCESS _ INFORMATION pe care trebuie sa o alocam; CreateProcess va initializa membrii acestei structuri inainte de returna. Aceasta structura arata in modul urmator : typedef struct PROCESSINFORMATION PROCESS _ INFORMATION; Asa dupa cum am precizat, crearea unui nou proces face ca sistemul sa creeze un proces obiect nucleu si un fir de executie obiect nucleu. La momentul crearii, sistemul da fiecarui obiect un contor initial de utilizare egal cu 1. Apoi, chiar inainte ca functia CreateProcess sa returneze, functia deschide obiectul proces si obiectul fir de executie si plaseaza identificatorii relativi la proces in membrii hProcess si hThread ai structurii PROCESS _ INFORMATION. Atunci cand CreateProcess deschide intern aceste obiecte, contorul de utilizare este incrementat la 2. Acest lucru inseamna ca inainte ca sistemul sa elibereze obiectul proces, procesul trebuie sa isi incheie executia ( decrementand contorul de utilizare la 1) si procesul parinte trebuie sa apeleze CloseHandle ( decrementand contorul de utilizare la 0). In mod asemanator, pentru a elibera obiectul fir de executie, firul trebuie sa isi incheie executia si procesul parinte trebuie sa inchida identificatorul obiectului fir de executie. Este foarte important sa inchidem acesti identificatori. Esuarea inchiderii acestora este una din cele mai frecvente greseli pe care le fac programatorii si are ca efect o scurgere de memorie pana cand procesul care a apelat CreateProcess isi termina executia. Atunci cand este creat un proces, sistemul atribuie procesului un identificator unic; nici un alt proces care ruleaza nu va avea acelasi identificator. Acelasi lucru este valabil pentru fire de executie. Atunci cand cream un fir de executie, acestuia ii este atribuit un numar unic in sistem. Inainte ca CreateProcess sa returneze, el completeaza membrii dwProcessId si dwThreadId a structurii PROCESS _ INFORMATION cu acesti identificatori. Procesul parinte poate folosi acesti doi identificatori pentru a comunica cu procesul copil. Este extrem de important sa stim ca sistemul refoloseste identificatorii proceselor si firelor de executie. De exemplu, sa spunem ca atunci cand este creat un proces, sistemul aloca un obiect proces si ii atribuie identificatorul 0x22222222. Daca noul obiect proces este creat, sistemul nu atribuie acelasi identificator. Totusi, daca primul obiect proces este eliberat, sistemul ar putea sa atribuie valoarea 0x22222222 unui alt nou proces. Trebuie sa tinem minte sa evitam scrierea de cod care referentiaza un obiect proces incorect ( sau fir de executie). Este usor sa retinem identificatorul unui proces si sa il pastram; dar s-ar putea ca procesul identificat de acest numar sa fie eliberat si un nou proces este creat si ii este atribuita aceasta valoare. Atunci cand folosim identificatorul salvat al unui proces, ajungem sa manevram noul proces, nu procesul caruia i-am retinut identificatorul. Noi putem garanta foarte usor ca acest lucru nu se intampla asigurandu-ne ca avem o protectie pe obiectul proces. In alte cuvinte, trebuie sa fim siguri ca am incrementat contorul de utilizare pentru obiectul proces. Sistemul nu va elibera niciodata procesul atat timp cat are un contor de utilizare mai mare de 0. In majoritatea situatiilor, vom incrementa deja contorul de utilizare. De exemplu, apelul CreateProcess returneaza dupa incrementare contorului de utilizare pentru obiectul proces. Cu contorul incrementat, putem folosi identificatorul procesului asa cum dorim. Atunci cand nu mai avem nevoie de acest identificator, apelam CloseHandle pentru a decrementa contorul. Trebuie doar sa ne asiguram sa nu folosim identificatorul procesului dupa apelul CloseHandle. Oprirea executiei unui procesUn proces se poate termina in trei feluri : 1. Un fir de executie din proces apeleaza functia ExitProcess. ( cel mai frecvent) 2. Un fir din alt proces apeleaza functia TerminateProcess ( ar trebui evitat) 3. Toate firele din proces se opresc singure. ( foarte rar) Functia ExitProcessUn proces se termina cand unul din firele din proces apeleaza ExitProcess : VOID ExitProcess ( UINT fuExitCode); Aceasta functie opreste executia procesului si seteaza codul de iesire al procesului la fuExitCode. ExitProcess nu returneaza o valoare deoarece procesul s-a terminat. Daca includem orice cod dupa apelul functiei ExitProcess, acest cod nu se va executa niciodata. Aceasta este metoda cea mai folosita pentru terminarea unui proces deoarece ExitProcess este apelata atunci cand functia WinMain returneaza in codul C run-time de pornire. Acest cod apeleaza ExitProcess, transmitand valoarea returnata din WinMain. Orice alt fir de executie care ruleaza in cadrul procesului isi va termina la randul lui executia. Documentatia Win32 stipuleaza faptul ca un proces nu se termina pana cand toate firele isi termina executia. Din punctul de vedere al sistemului de operare, acest lucru este adevarat. Totusi, codul C run-time impune o abordare diferita a aplicatiei; codul C run-time se asigura ca procesul se termina atunci cand firul principal de executie returneaza din functia WinMain, chiar si in cazul in care celelalte fire sunt in executie, apeland ExitProcess. Totusi, daca apelezi ExitThread in functia WinMain in loc sa apeleze ExitProcess sau pur si simplu sa returneze, firul principal de executie al aplicatiei isi va inceta executia, dar procesul nu se va termina daca macar unul din celelalte fire din proces inca mai ruleaza. Functia TerminateProcessUn apel al functiei TerminateProcess poate randul lui opri executia unui proces : BOOL TerminateProcess ( HANDLE hProcess, UINT fuExitCode); Aceasta functie este diferita de ExitProcess in mod foarte important : orice fir poate apela TerminateProcess pentru a termina executia unui alt proces sau a propriului proces. Parametrul hProcess reprezinta identificatorul procesului care trebuie sa fie terminat. Atunci cand procesul se termina, propriul sau cod de iesire devine valoarea pe care am transmis-o ca fiind parametrul fuExitCode. Folosirea functiei TerminateProcess este descurajata; este de preferat sa o folosim doar daca nu putem sa oprim executia unui proces folosind alte metode. In mod normal, la terminarea executiei unui proces, sistemul anunta orice DLL atasat procesului faptul ca procesul isi incheie executia. Daca apelam TerminateProcess, totusi, sistemul nu anunta nici un DLL atasat procesului, lucru care face ca procesul sa nu se termine corect. De exemplu, putem scrie un DLL pentru a goli datele intr-un fisier pe disc in momentul in care procesul se detaseaza de DLL. Aceasta detasare are loc atunci cand o aplicatie descarca DLL-ul apeland FreeLibrary. Deoarece DLL-ul nu este notificat de aceasta detasare, el nu poate sa isi desfasoare activitatea. Sistemul anunta DLL-ul atunci cand un proces se termina normal sau cand apelam ExitProcess. Desi este posibil ca DLL-ul sa nu poata sa isi termine actiunea, sistemul garanteaza faptul ca toata memoria alocata este eliberata, toate fisierele deschise vor fi inchise, toate obiectele nucleu vor avea contorul de utilizare decrementat si toate celelalte obiecte utilizator sau GDI vor fi eliberate indiferent de modul de terminare al procesului. Ce se intampla atunci cand firele din proces isi incheie executiaDaca toate firele din proces isi incheie executia ( in urma fie a unor apeluri ExitThread, fie TerminateThread), sistemul de operare presupune ca nu mai este nici un motiv pentru a mai pastra spatiul de adrese al procesului. Aceasta este o presupunere corecta deoarece nu mai exista nici un fir in executie in spatiul de adrese al procesului. Atunci cand sistemul detecteaza ca firele nu mai ruleaza, el opreste procesul. Atunci cand se intampla acest lucru, codul de iesire al procesului este setat la codul de iesire al ultimului fir de executie care s-a terminat. Ce se intampla la terminarea unui procesLa terminarea unui proces au loc urmatoarele actiuni : 1. Orice alte fire din proces sunt oprite. 2. Toate obiectele utilizator sau GDI alocate de catre proces sunt eliberate iar obiectele nucleu sunt inchise. 3. Starea procesului obiect nucleu devine semnalata. Alte fire de executie din sistem pot sa se suspende pana la terminarea procesului. 4. Codul de iesire al procesului se schimba de la STIL _ ACTIVE la codul transmis de catre ExitProcess sau TerminateProcess. 5. Contorul de utilizare al obiectului nucleu proces este decrementat cu 1. La terminarea unui proces, procesul obiect nucleu asociat lui nu este eliberat pana cand toate referintele existente la acel obiect nu sunt inchise. De asemenea, oprirea unui proces nu face ca procesele sale copil sa se opreasca si ele. Atunci cand procesul se termina, codul procesului si orice alte resurse alocate de catre proces sunt scoase din memorie. Totusi, memoria privata alocata de sistem procesului obiect nucleu nu este eliberata pana cand contorul de utilizare nu atinge valoarea 0.Acest lucru se poate intampla numai daca toate celelalte procese care au creat sau au deschis identificatori catre procesul a carei executie este acum oprita anunta sistemul ca nu mai au nevoie sa referentieze procesul. Aceste procese anunta sistemul apeland CloseHandle. Dupa ce un proces nu mai ruleaza, procesul parinte nu poate face mare lucru cu identificatorul procesului. Totusi, el poate apela GetExitCodeProcess pentru a verifica daca procesul identificat prin hProcess s-a terminat si daca da, sa ii verifice codul de iesire. BOOL GetExitCodeProcess ( HANDLE hProcess, LPWORD lpwdExitCode); Codul de iesire este returnat in DWORD-ul lpwdExitCode. Daca procesul nu s-a terminat atunci cand apelam GetExitCode, functia seteaza acest parametru la valoarea STILL _ ACTIVE ( definit ca 0x103). Daca functia a fost apelata cu succes, este returnat TRUE. Procesele copilAtunci cand proiectam o aplicatie, pot aparea situatii cand dorim sa se execute alte blocuri de cod. Noi facem acest lucru de fiecare data cand apelam functii sau subrutine. Atunci cand apelam o functie, codul de unde am facut apelul nu poate continua pana cand functia nu returneaza. Si in majoritatea situatiilor este necesara sincronizarea operatiunilor simple. O metoda alternativa de a lasa un bloc de cod sa efectueze ceva este de a crea un nou fir de executie in cadrul procesului nostru si a-i lasa lui prelucrarea. Aceasta abordare permite codului nostru sa isi continue executia in acelasi timp cu prelucrarea pe care am dorit-o. Aceasta tehnica este folositoare, dar creeaza probleme de sincronizare atunci cand firele existente au nevoie sa vada rezultatele noului fir de executie. O alta modalitate este de a crea un nou proces, un proces copil pentru a ajuta la indeplinirea sarcinii cerute. Sa spunem ca ceea ce dorim sa facem este destul de complex. Pentru a procesa aceasta munca, noi pur si simplu hotaram sa cream un nou fir de executie in cadrul procesului. Scriem niste cod, il testam si obtinem rezultate eronate. Putem avea o eroare in algoritm sau poate am dereferentiat ceva incorect si am suprascris in mod accidental ceva foarte important in spatiul de adrese al procesului. O modalitate de a proteja spatiul de adrese atunci cand procesam aceasta operatie este de a crea un nou proces pentru aceasta operatie. Vom putea apoi astepta terminarea noului proces inainte de a continua cu munca noastra proprie sau putem continua sa lucram in paralel cu noul proces. Din pacate, noul proces va avea probabil nevoie sa efectueze operatii pe date continute in spatiul nostru de adrese. In acest caz, este o idee buna sa rulam procesul in propriul sau spatiu de adrese si pur si simplu sa ii dam acces la datele relevante continute in spatiul de adrese al procesului parinte, in acelasi timp ascunzandu-i datele care nu sunt relevante pentru operatie. Win32 ne ofera cateva metode pentru transferarea datelor intre procese diferite : Dynamic Data Exchange ( DDE), OLE, Pipes, MailSlots si altele. Unul din cele mai convenabile metode este de a folosi fisiere mapate in memorie. Daca dorim sa cream un nou proces care sa efectueze o operatie si noi sa asteptam rezultatul, putem folosi o bucata de cod ca mai jos : PROCESS _ INFORMATION pi DWORD dwExitCode; BOOL fSuccess = CreateProcess ( _ , &pi); if ( fSuccess) In codul anterior am creat un nou proces si, in caz de reusita, am apelat functia WaitForSingleObject : DWORD WaitForSingleObject ( HANDLE hObject, DWORD dwTimeout); Functia asteapta pana cand obiectul identificat de parametrul hObject devine semnalat. Obiectele proces devin semnalate atunci cand se termina. Apelul WaitForSingleObject suspenda firul parintelui pana cand procesul copil se termina. Dupa ce functia WaitForSingleObject returneaza, putem obtine codul de iesire al procesului copil apeland GetExitCodeProcess. Apelurile functiei CloseHandle din codul anterior face ca sistemul sa decrementeze contoarele de utilizare pentru obiectele fir si proces la 0, permitand eliberarea memoriei alocata obiectelor. Observam ca in fragmentul de cod am inchis identificatorul firului principal de executie al procesului copil imediat dupa ce functia CreateProcess a returnat. Acest lucru nu face ca firul principal de executie al procesului copil sa fie oprit – el este doar decrementat cu 1. Explicatia pentru care aceasta metoda este corecta este : sa presupunem ca firul principal de executie al procesului copil creeaza un alt fir si apoi firul principal isi termina executia. La acest punct, sistemul poate elibera firul principal obiect nucleu al copilului din memorie daca procesul parinte nu are un identificator deschis catre acest obiect. Dar daca procesul parinte are un identificator catre acest obiect, sistemul nu poate elibera acest obiect pana cand procesul parinte nu inchide identificatorul. Rularea proceselor copil detasateMajoritatea timpului, o aplicatie porneste un alt proces ca un proces detasat. Acest lucru inseamna ca dupa ce procesul este creat si este in executie, procesul parinte nu are nevoie sa comunice cu noul proces sau nu are nevoie ca noul proces sa isi termine sarcina inainte ca procesul parinte sa continue. Acesta este modul de functionare al Explorer-ului. Dupa ce acesta creeaza un nou proces pentru utilizator, pe el nu il intereseaza daca procesul isi continua executia sau dac utilizatorul il opreste. Pentru a renunta la toate legaturile la procesele copil, Explorer-ul trebuie sa isi inchida identificatorii noului proces si al firului principal de executie al acestuia apeland CloseHandle. Codul urmator arata cum sa cream un nou proces si sa il lasam sa ruleze detasat. PROCESS _ INFORMATION pi; BOOL fSuccess = CreateProcess ( _ , &pi if ( fSuccess) Fire de executieIn acest capitol vom discuta conceptul de fir de executie si vom descrie modul in care sistemul foloseste fire de executie pentru a executa codul aplicatiei noastre. La fel ca si procese, firele au proprietati asociate lor si vom discuta despre unele dintre functiile disponibile pentru interogarea si schimbarea acestor proprietati. De asemenea vom examina functiile care ne permit sa cream noi fire in sistem. In final vom vorbi despre terminarea firelor de executie. Cand cream un fir de executieUn fir de executie descrie o cale de executie in interiorul unui proces. De fiecare data cand un proces este initializat, sistemul creeaza un fir de executie principal. Acest fir porneste codul C run-time de pornire, care la randul lui apeleaza functia noastra WinMain, apoi continua sa se execute pana cand functia WinMain returneaza si codul C run-time de pornire apeleaza ExitProcess. Pentru multe aplicatii, firul principal de executie este singurul fir de executie de care are nevoie aplicatia. Totusi, procesele pot crea fire suplimentare pentru a le ajuta sa isi indeplineasca sarcinile. Ideea din spatele crearii firelor de executie este de a utiliza cat mai mult timp de procesor. De exemplu, un program care lucreaza cu foi de calcul are nevoie sa efectueze recalculari pe masura ce utilizatorul introduce date in celule. Deoarece recalcularile unei foi de calcul complexe pot avea de nevoie de cateva secunde pentru a fi efectuate, o aplicatie bine gandita nu ar trebui sa recalculeze foaia de calcul dupa fiecare schimbare pe care o efectueaza utilizatorul. In schimb, recalcularea ar trebui facuta intr-un fir de executie cu prioritate mai redusa decat firul principal de executie. In acest mod, daca utilizatorul introduce date, firul principal ruleaza, lucru care inseamna ca sistemul nu va programa nici un timp de calcul firului care efectueaza recalcularea. Atunci cand utilizatorul se opreste din scris, firul principal este suspendat, asteptand o intrare iar firului care face recalcularea ii este dat timp de lucru. De indata ce utilizatorul incepe sa scrie din nou, firul principal de executie, avand prioritate mai mare, trece in fata firului cu recalcularea. Crearea firelor de executie aditionale face ca aplicatia sa aiba un timp de raspuns mult mai bun. De asemenea este destul de usor de implementat. Intr-un exemplu similar, putem crea un fir de executie suplimentar pentru o functie de repaginare intr-un procesor de texte care trebuie sa repagineze textul pe masura ce utilizatorul introduce text in document. Microsoft Word pentru Windows, de exemplu, trebuie sa simuleze un comportament multifiliar in Windows pe 16 biti dar poate foarte usor sa creeze un nou fir dedicat repaginarii documentului in Win32. Firul principal de executie va fi responsabil cu procesarea intrarilor utilizatorului, iar firul din fundal va fi responsabil cu localizarea salturilor la pagina noua. Este de asemenea folositor pentru a crea un fir de executie separat pentru a trata tiparirea in cadrul unei aplicatii. In acest mod, utilizatorul poate continua sa foloseasca aplicatia in timpul tiparirii. In plus, atunci cand efectuam o operatie care intr-o perioada mare de timp, multe aplicatii afiseaza o casuta de dialog care permite utilizatorului sa opreasca operatia. De exemplu, atunci cand Explorer-ul copie niste fisiere, afiseaza o casuta de dialog care, in afara faptului ca arata progresul operatiei, de asemenea contine un buton Cancel. Daca apasam acest buton in timp ce fisierele sunt copiate, oprim operatia. In Windows pe 16 biti, implementarea acestui tip de functionalitate are nevoie de apeluri periodice ale functiei PeekMessage in interiorul cozii de mesaje a copierii fisierelor. Apelurile PeekMessage pot fi facute doar intre citirea si scrierea unui fisier. Daca citim un block mare de date, raspunsul la apasarea butonului nu apare pana cand blocul nu este citit. Daca fisierul este citi de pe o unitate de discheta, acest lucru poate dura cateva secunde. Deoarece timpul de raspuns este atat de lent, putem apasa de mai multe ori pe un astfel de buton crezand ca sistemul nu stie ca am oprit operatia. Punand copierea fisierului intr-un fir de executie separat, nu trebuie sa imprastiem apeluri ale functiei PeekMessage prin codul nostru – firul nostru opereaza independent. Acest lucru inseamna ca o apasare a butonului Cancel are un efect imediat. Putem de asemenea sa folosim fire de executie pentru a crea aplicatii care simuleaza evenimente din lumea reala. Un exemplu este o simulare a unui supermarket. Deoarece fiecare cumparator este reprezentat de propriul fir de executie, teoretic fiecare este independent unul de altul si poate intra in magazin si poate iesi oricand crede de cuviinta. Simularea poate monitoriza aceste activitati pentru a determina cat de bine functioneaza supermarket-ul. Desi putem rula simulari, exista probleme potentiale. In primul rand, in mod ideal am vrea ca fiecare fir de executie sa fie executat de catre un procesor propriu. Deoarece nu este o solutie practica sa ne asteptam sa avem un procesor pentru fiecare fir de executie, solutia este de a atribui un timp maxim dupa care sistemul sa pauzeze executia unui fir, permitand executia altuia. De exemplu, daca simularea noastra are doua fire de executie si masina pe care ruleaza are 8 procesoare, sistemul poate atribui fiecare fir unui procesor diferit. Totusi, daca simularea are 1000 de fire de executie, sistemul va trebui sa le atribuie continuu cate unul din cele 8 procesoare. Totusi poate aparea o suprasolicitare atunci cand sistemul programeaza un numar mare de fire pentru un numar redus de procesoare. Daca aplicatia noastra are un timp de executie mai mare, suprasolicitarea are un impact relativ scazut asupra simularii. Totusi, daca simularea se face intr-o perioada scurta de timp, suprasarcina poate lua un procent mai mare din timpul total de executie al aplicatiei. In al doilea rand, sistemul in sine are nevoie de unele fire de executie care sa ruleze in timp ce alte procese sunt in executie. Toate aceste fire de executie trebuie sa fie programate pentru timp de procesor la randul lor, lucru care in mod sigur va afecta rezultatele aplicatiei. Si in al treilea rand, simularea este folositoare doar daca pastram evidenta progresului ei. De exemplu, simularea supermarket-ului adauga intrari intr-un listbox pe masura ce cumparatorii efectueaza diferite actiuni; adaugarea acestor intrari ia din timpul aplicatiei. Principiul de Nesiguranta al lui Heisenberg afirma ca determinarea mai precisa a unei cantitati are ca efect calcularea mai precisa a altora. Acest principiu este in mod sigur adevarat in cazul de fata. Cand nu trebuie sa creeam un fir de executiePrima data cand le este dat accesul la un mediu care suporta fire de executie multiple, majoritatea programatorilor sunt extaziati. Daca ei ar fi avut firele de executie mai devreme, aplicatiile lor ar fi fost mai usor de scris. Si, pentru un motiv necunoscut, acesti programatori incep divizarea unei aplicatii in piese individuale, fiecare putand fi executata ca propriul sau fir de executie. Acesta nu este modul corect de dezvoltare a unei aplicatii. Firele sunt incredibil de folositoare si isi au locul lor, dar atunci cand folosim fire de executie putem crea noi potentiale probleme in timp ce incercam sa rezolvam pe cele vechi. De exemplu, sa spunem ca dezvoltam a aplicatie care proceseaza cuvinte si vrem sa permitem ca functia de tiparire sa ruleze ca propriul fir de executie. Acest lucru pare o idee buna deoarece utilizatorul poate imediat sa se intoarca la editarea documentului in timp ce acesta este tiparit. Dar totusi acest lucru inseamna ca datele din document pot fi schimbate in timp ce documentul este tiparit. Acest lucru creeaza un nou tip de problema pe care trebuie sa o rezolvam. Totusi poate ar fi mai bine sa nu lasam tiparirea in propriul fir de executie; dar aceasta „solutie” este putin mai drastica. Ce ar fi daca am permite utilizatorului sa editeze un alt document dar sa blocam documentul care trebuie tiparit astfel incat sa nu poata fi modificat pana cand procesul de tiparire nu s-a terminat ? Cea de-a treia solutie este : copiem fisierul de tiparit intr-un fisier temporar si lasam utilizatorul sa il modifice pe cel original. Atunci cand fisierul temporar care contine documentul s-a terminat de tiparit, putem sterge fisierul temporar. Dupa cum se observa, firele de executie rezolva unele probleme cu riscul crearii altora noi. O alta utilizare gresita a firelor de executie poate aparea in dezvoltarea interfetei utilizator a unei aplicatii. In majoritatea aplicatiilor, toate componentele interfetei utilizator ar trebui sa imparta acelasi fir de executie. Daca cream o casuta de dialog, de exemplu, nu ar avea nici un sens ca un listbox sa fie creat de un fir de executie si un buton de alt fir. Sa mergem putin mai departe si sa spunem ca avem propriul control listbox care sorteaza datele de fiecare data este adaugat sau sters un element. Operatia de sortare poate dura cateva secunde, asa ca decidem sa ii atribuim acestui control un fir de executie propriu. In acest mod, utilizatorul poate continua sa lucreze cu alte controale in timp ce firul controlului listbox continua sa lucreze. Procedand in acest mod nu este o ideea foarte buna, totusi. In primul rand, fiecare fir de executie care creeaza o fereastra trebuie sa contina de asemenea o bucla GetMessage. In al doilea rand, firul listbox-ului contine propria coada GetMessage, am putea sa avem niste probleme de sincronizare intre firele de executie. Putem rezolva aceste probleme atribuind controlului listbox un fir dedicat al carui singur scop este de a sorta elementele in fundal. In putine situatii, atribuirea de fire individuale obiectelor de interfata utilizator este folositoare. In sistem, fiecare proces are propriul fir de executie care controleaza propria interfata utilizator. De exemplu, aplicatia Calculator are un fir de executie care trateaza toate ferestrele aplicatiei, iar aplicatia Paint are propriul fir de executie care creeaza si gestioneaza ferestrele proprii ale Paint-ului. Aceste fire separate au fost create pentru protectie si pentru robustete. Daca firul de executie al Calculator-ului intra intr-o bucla infinita, problema rezultata nu are nici un efect asupra firului de executie al Paint-ului. Acest comportament este destul de diferit de ceea ce vedem in Windows pe 16 biti. In Windows pe 16 biti, daca o aplicatie se blocheaza, intregul sistem se blocheaza. Sistemele Win32 ne permit sa iesim din aplicatia Calculator ( chiar daca aceasta s-a blocat) si sa incepem folosirea aplicatiei Paint. Probabil ca cel mai bun exemplu de aplicatie care creeaza ferestre cu fire multiple de executie este Explorer-ul. Daca utilizatorul lucreaza cu o fereastra Explorer, sa spunem unitatea de disc C:, iar unul dintre firele de executie al acestei ferestre intra intr-o bucla infinita, utilizatorul nu poate sa mai foloseasca firul pentru aceasta fereastra dar poate totusi sa foloseasca alte ferestre Explorer. Dupa cum observam, aceasta facilitate este folositoare deoarece utilizatorilor nu le place cand shell-ul sistemului de operare nu mai raspunde la comenzi. O alta intrebuintare pentru firele multiple din componentele GUI este in aplicatiile cu interfata documente multiple MDI in care fiecare fereastra copil MDI ruleaza propriul fir de executie. Daca unul din firele de executie fiu intra intr-o bucla infinita sau incepe o procedura mare consumatoare de timp, utilizatorul poate schimba la alta fereastra MDI fiu si poate incepe sa lucreze, iar celalalt fir MDI fiu sa ramana blocat. Acest lucru poate fi de fapt foarte folositor, ca Win32 ofera o functie speciala a carei rezultat este similar cu crearea unei ferestre MDI fiu trimitand mesajul WM _ MDICREATE catre o fereastra MDI client. HWND CreateMDIWindow ( LPTSTR lpszClassName, LPTSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hwndparent, HINSTANCE hInst, LONG lParam); Singura diferenta este ca functia CreateMDIWindow permite ca MDI-ul fiu sa fie creat cu propriul fir de executie. Morala este ca trebuie sa folosim in mod logic multiplicarea firelor. Nu trebuie sa le folosim doar pentru ca putem face acest lucru. Putem scrie multe aplicatii folositoare si puternice folosind doar firul principal de executie atribuit procesului. Firele de executie in MFCMFC incapsuleaza fire de executie in clasa CWinThread. De asemenea incapsuleaza evenimente, mutex-uri si alte obiecte de sincronizare Win32 in clase C++ usor de folosit. Face MFC-ul multiplicarea firelor mai usoara ? Nu chiar. Dezvoltatorii de software care au scris aplicatii Windows cu multiple fire de executie sunt adeseori surprinsi sa afle ca MFC-ul adauga complexitati proprii. Secretul scrierii unui program cu multiple fire de executie in MFC este de a intelege foarte bine ceea ce faci si de a sti unde pot aparea probleme. Din punctul de vedere al Windows-ului toate firele de executie sunt la fel. Totusi, MFC-ul distinge doua tipuri de fire de executie : fire de executie cu interfata cu utilizatorul si fire de executie de lucru. Diferenta dintre cele doua este ca primele pot crea ferestre si pot procesa mesaje trimise catre aceste ferestre. Cel de-al doilea tip de fire de executie efectueaza operatii in fundal care nu primesc o intrare directa de la utilizator si de aceea nu au nevoie de ferestre si de cozi de mesaje. Sistemul in sine furnizeaza doua exemple foarte bune despre modul in care firele de executie cu interfata cu utilizatorul si firele de executie de lucru pot fi folosite. Atunci cand deschidem un director in shell-ul sistemului de operare, shell-ul lanseaza un fir de executie cu interfata cu utilizatorul care creeaza o fereastra care arata continutul directorului. Daca copiem prin „drag and drop” un grup de fisiere in directorul pe care l-am deschis, firul de executie al acelui director efectueaza transferurile de fisiere. ( Uneori acesta creeaza un alt fir de executie – de data aceasta un fir de executie de lucru pentru copierea fisierelor. ) Avantajele acestei arhitecturi cu multiple fire de executie este ca, de indata ce copierea a inceput, utilizatorul poate comuta in ferestrele deschise in alte directoare si poate continua sa lucreze in timp ce fisierele sunt copiate in fundal. Lansarea unui fir de executie cu interfata cu utilizatorul care creeaza o fereastra este conceptual similar lansarii unei aplicatii in interiorul unei aplicatii. Cea mai des folosita intrebuintare pentru acest tip de fire de executie este pentru crearea mai multor ferestre deservite de fire de executie separate. Firele de executie de lucru sunt ideale pentru efectuarea unor operatii izolate care pot fi despartite de restul aplicatiei si pot fi efectuate in fundal. Un exemplu clasic de fir de executie de lucru este cel folosit de un control de animatie pentru a rula clipuri cu extensia .AVI. Acel fir de executie face mai mult decat sa deseneze un cadru, se pune in starea de sleep pentru o fractiune de secunda iar apoi se „trezeste” si repeta procesul. In acest mod el adauga doar putin la incarcatura de lucru a procesorului deoarece isi petrece majoritatea vietii suspendat intre cadre si totusi asigura un serviciu de valoare. Acesta este un exemplu foarte bun de proiectare cu multiple fire de executie deoarece firul de executie din fundal primeste de facut o anumita operatie si apoi ii este permis sa repete acea operatie pana cand firul principal de executie anunta ca operatia se poate termina. Functia de fir de executie in SDKToate firele de executie isi incep executia cu o functie care trebuie sa o precizam in prealabil. Functia trebuie sa aiba urmatorul prototip : DWORD WINAPI ThreadFunction ( LPVOID lpvThreadParm); La fel ca si WinMain, functia nu este apelata chiar de sistemul de operare. In schimb, acesta apeleaza o functie interna, care nu face parte din functiile C run-time, continuta in KERNEl32. Putem sa o numim StartOfThread; numele real nu este important. Aceasta functie arata astfel : void StartOfThread ( LPTHREAD _ START _ ROUTINE lpStartAddr, LPVOID lpvThreadParm) _ _ except ( UnhandledExceptionFilter ( GetExceptionInformation ( ))) Functia StartOfThread pune in miscare urmatoarele actiuni : 1. Este setat un frame structurat de tratare a erorilor in jurul functiei noastre a firului de executie astfel incat orice exceptie care apare in timpul executiei firului va avea o tratare implicita din partea sistemului. 2. sistemul apeleaza functia noastra a firului de executie, transmitandu-i parametrul pe 32 biti lpvThreadParm pe care l-am transmis functiei CreateThread. 3. Atunci cand functia firului returneaza, functia StartOfThread apeleaza ExitThread, transmitandu-i valoarea de retur a functiei firului. Contorul de utilizare al obiectului nucleu este decrementat iar firul isi intrerupe executia. 4. Daca firul determina aparitia unei exceptii care nu este tratata, atunci frame-ul SEH setat de functia StartOfThread va trata exceptia. In mod normal, acest lucru inseamna ca o casuta de dialog este prezentata utilizatorului iar atunci cand acesta inchide aceasta casuta, functia StartOfThread apeleaza ExitProcess pentru a termina executia intregului proces, nu doar firul in cauza. Firul principal de executie al unui proces incepe executand functia StartOfThread a sistemului. Aceasta apeleaza la randul ei codul C run-time de startup, care apeleaza functia noastra WinMain. Codul C run-time de startup, totusi, nu se intoarce in functia StartOfThread deoarece codul de startup apeleaza in mod explicit ExitProcess. Functia firului de executie in MFCO functie de fir de executie trebuie sa fie o functie statica membra a unei clase sau o functie globala declarata in afara unei clase. Prototipul ei este : UINT ThreadFunction ( LPVOID pParam) pParam este o valoare pe 32 de biti a carei valoare este egala cu parametrul pParam transmis functiei AfxBeginThread. Foarte des, pParam este adresa unei structuri de date definita de aplicatie care contine informatia transmisa firului de executie de lucru de catre firul care l-a creat. De asemenea poate fi o valoare scalara, un handle sau chiar un pointer catre un obiect. Este posibil sa folosim aceeasi functie de fir de executie pentru doua sau mai multe fire de executie dar trebuie sa fim atenti la problemele de reentranta cauzate de variabilele globale si statice. Atata timp cat variabilele ( si obiectele) pe care le foloseste un fir de executie sunt create in stiva, problemele de reentranta nu mai apar deoarece fiecare fir de executie are stiva sa proprie. Crearea unul fir de executie de lucru in MFCCea mai buna modalitate de a lansa un fir de executie intr-o aplicatie MFC este apelul AfxBeginThread. MFC defineste 2 versiuni diferite de AfxBeginThread : una care porneste un fir de executie cu interfata cu utilizatorul si alta care porneste un fir de executie de lucru. Codul sursa pentru ambele este gasit in Thrdcore.cpp. Nu trebuie sa folosim functia Win32::CreateThread pentru a crea un fir de executie intr-un program MFC decat doar daca firul nu foloseste MFC. AfxBeginThread nu este numai un simplu wrapper pentru functia CreateThread ; in plus fata de lansarea unui nou fir de executie, ea initializeaza informatia interna de stare folosita de cadrul de lucru, efectueaza verificari la diferite momente din timpul procesului de creare si se asigura ca functiile din biblioteca run-time C sunt accesate intr-o modalitate sigura din punctul de vedere al firelor de executie. AfxBeginThread faciliteaza crearea unui fir de executie de lucru. Atunci cand este apelata, ea creeaza un nou obiect de tip CWinThread, lanseaza un fir de executie, il ataseaza obiectului CWinThread si returneaza un pointer CWinThread. Declaratia CWinThread* pThread = AfxBeginThread ( ThreadFunction, &threadInfo); porneste un fir de executie de lucru si ii transmite structura de date predefinita de aplicatie ( &ThreadInfo) care contine intrarea catre firul de executie. ThreadFunction este functia de fir de executie – functia care este executata atunci cand firul de executie incepe sa se execute. O functie de fir de executie foarte simpla care se invarte intr-o bucla ce „mananca” din timpul procesorului si apoi se termina arata in felul urmator : UINT ThreadFunction ( LPVOID pParam) In acest exemplu, valoarea transmisa in pParam nu este un pointer, ci un UINT normal. Functia AfxBeginThread accepta patru parametri optionali care specifica prioritatea firului, marimea stivei, indicatorii de creere si atributele de securitate. Prototipul complet al functiei este : CWinThread* AfxBeginThread ( AFX _ THREADPROC pfnThreadProc, LPVOID pParam, int nPMority = THREAD _ PRIORITY _ NORMAL. UINT nStackSize = 0, DWORD dwCreateFlags = 0. LPSECURITY _ ATTRIBUTES 1pSecurityAttrs = NULL) nPriority defineste prioritatea firului de executie. Fire de executie cu prioritate mare sunt programate pentru procesor in fata celor cu prioritate mai scazuta, dar in practica, chiar si fire de executie cu prioritate foarte scazuta de obicei au tot timpul de procesor de care au nevoie. nPriority nu reprezinta un nivel absolut de prioritate. El precizeaza un nivel de prioritate relativ fata de nivelul de prioritate al procesului caruia ii apartine firul. Implicit prioritatea este THREAD _ PRIORITY _ NORMAL, care atribuie firului de executie aceeasi prioritate ca si a procesului care il detine. Nivelul de prioritate se poate schimba in orice moment cu comanda CWinThread::SetThreadPriority. Parametrul nStackSize transmis catre AfxBeginThread precizeaza marimea maxima a stivei a firului. In mediul Win32, fiecare fir de executie primeste stiva sa proprie. Valoarea 0 implicita a variabilei nStackSize permite stivei sa atinga marimea maxima de 1MB. Aceasta nu inseamna ca fiecare fir de executie are nevoie de minim 1 MB de memorie; inseamna ca fiecarui fir de executie i se aloca 1 MB din spatiul de adrese din spatiul de 4GB in care se executa aplicatiile Windows pe 32 de biti. Memoria nu este alocata spatiului de adrese al stivei pana cand nu este necesar, asa ca majoritatea stivelor nu folosesc niciodata mai mult de cativa KB din memoria fizica. Precizand o limita pentru marimea stivei permite sistemului de operare sa „prinda” functiile care se apeleaza recursiv la infinit si care eventual consuma stiva. Limita standard de 1 MB este suficienta pentru aproape toate aplicatiile. dwCreateFlags poate fi doar una din doua valori. Valoarea implicita 0 precizeaza sistemului sa execute firul imediat. Daca este specificat in schimb CREATE _ SUSPENDED, firul porneste in starea suspendat si nu isi incepe executia pana cand alt fir de executie ( de obicei firul care l-a creat ) apeleaza CWinThread::ResumeThread asupra firului suspendat, ca mai jos : CWinThread pThread = AfxBeginThread ( ThreadFunc, SthreadInfo, THREAD _ PRIORITY _ NORMAL, 0, CREATE _ SUSPENDED); pThread->ResumeThread ( ); //Porneste firul de executie. Uneori este util sa cream un fir si sa ii amanam executia pana mai tarziu. Indicatorul CREATE _ SUSPENDED este mecanismul prin care putem declara o executie cu intarziere. Ultimul parametru din lista de argumente a functiei AfxBeginThread, IpSecurityAttrs, este un pointer catre o structura SECURITY _ ATTRIBUTES care contine atributele de securitate ale firului de executie nou creat si care de asemenea precizeaza sistemului daca procesele copil ar trebui sa mosteneasca handler-ul firului de executie. Valoarea implicita NULL atribuie noului fir de executie aceleasi proprietati ca si a firului care l-a creat. Crearea unui fir de executie cu interfata cu utilizatorul in MFCCrearea unui astfel de fir de executie este diferita de procesul de creare a unui fir de executie de lucru. Un fir de lucru este definit de functia sa iar comportarea unui fir de executie cu interfata este data de o clasa care se poate crea dinamic si care este derivata din CWinThread, asemanator unei clase de aplicatie derivata din CWinApp. Clasa de mai jos creeaza o fereastra de cadru de tip top-level care se inchide singura cand este apasat butonul stanga al mouse-ului. Inchiderea ferestrei opreste si firul de executie deoarece functia CWnd::OnNcDestroy trimite un mesaj WM _ QUIT catre coada de mesaje a firului de executie. Trimiterea acestui mesaj WM _ QUIT catre un fir de executie principal opreste firul de executie si termina aplicatia. // Clasa CUIThread class CUIThread : public CWinThread DECLARE _ DYNCREATE ( CUIThread) public: virtual BOOL Initlnstance ( ); IMPLEMENT _ DYNCREATE ( CUIThread, CWinThread) BOOL CUIThread::Initlnstance ( ) // Clasa CMainWindow class CMainWindow : public CFrameWnd BEGIN _ MESSAGE _ MAP ( CMainWindow, CFrameWnd) ON _ WM _ LBUTTONDOWN ( ) END _ MESSAGE _ MAP ( ) CMainWindow::CMainWindow ( ) void CMainWindow::OnLButtonDown ( UINT nFlags, CPoint point) Trebuie remarcat parametrul SW _ SHOW transmis catre ShowWindow in locul parametrului normal m _ nCmdShow. Acesta este un membru al CWinApp, asa ca atunci cand cream o fereastra top-level de la un fir de executie cu interfata este de datoria noastra sa precizam starea initiala a ferestrei. Putem lansa un CUIThread apeland o forma a functiei AfxBeginThread care accepta un pointer CRuntimeClass catre clasa firului de executie : CWinThread* pThread = AfxBeginThread ( RUNTIME _ CLASS ( CUIThread)); Versiunea functiei AfxBeginThread pentru fire de executie cu interfata accepta aceiasi patru parametri optionali ca si versiunea pentru firele de lucru, dar nu accepta o valoare pParam. Odata pornit, un fir de executie cu interfata ruleaza asincron respectand firul care l-a creat. Suspendarea si repornirea firelor de executieUn fir de executie in MFC care ruleaza poate fi suspendat prin apelul CWinThread::SuspendThread si poate si repornit cu CWinThread::ResumeThread. Un fir de executie poate apela functia SuspendThread pe el insusi sau un alt fir poate face acest apel pentru el. Totusi, un fir suspendat nu poate apela ResumeThread pentru a se reporni; altcineva trebuie sa faca acest apel. Un fir de executie suspendat nu consuma aproape deloc din timpul procesorului si impune in sistem o suprasarcina practic nula. Pentru fiecare fir de executie Windows-ul pastreaza un contor de suspendare care este incrementat de SuspendThread si decrementat de ResumeThread. Un fir de executie este programat pentru timpul procesorului doar atunci cand contorul sau de suspendare este 0. Daca functia SuspendThread este apelata de doua ori succesiv, ResumeThread trebuie apelata de asemenea de doua ori. Un fir de executie creat fara atributul CREATE _ SUSPENDED are initial contorul de suspendare egal cu 0. Un fir creat cu atributul CREATE _ SUSPENDED se porneste avand initial contorul de suspendare initializat cu valoarea 1. Atat SuspendThread cat si ResumeThread returneaza contorul de suspendare anterior, asa ca pentru a fi siguri ca un fir de executie va fi repornit indiferent de cat este de mare valoarea contorului de suspendare trebuie sa apelam ResumeThread in mod repetat pana cand returneaza valoarea 1. ResumeThread returneaza 0 daca firul nu este apelat sau daca nu este suspendat. In viata reala, o aplicatie trebuie sa fie atenta atunci cand apeleaza SuspendThread deoarece nu avem nici o idee despre ce poate acesta sa faca atunci cand incercam sa ii reluam executia. Daca firul incearca sa aloce memorie dintr-un heap, de exemplu, firul va trebui sa se blocheze pe heap. Pe masura ce alte fire incearca sa acceseze heap-ul, executia lor va fi oprita pana cand primul fir isi reia executia. SuspendThread este neprimejdios doar atunci cand stim ce este exact firul tinta ( sau ce face) si luam masuri extreme pentru a evita problemele sau blocajele cauzate de suspendarea firului. In SDK, un fir de executie poate fi suspendat prin apelul functiei SuspendThread si apoi ii putem relua executia cu ajutorul functiei ResumeThread. Ambele functii sunt asemanatoare cu functiile corespunzatoare din MFC. Punerea unui fir de executie in starea de „sleep”Un fir de executie poate sa fie suspendat pentru o anumita perioada de timp prin apelul functiei API ::Sleep. Un fir suspendat nu consuma din timpul procesorului. Declaratia ::Sleep ( 10000) suspenda firul de executie curent pentru 10 secunde. O utilizare pentru ::Sleep este pentru a implementa fire de executie ale caror actiuni sunt nativ bazate pe timp, ca de exemplu firul de executie din fundalul unui control de animatie sau un fir care muta acele unui ceas. ::Sleep poate fi folosita si pentru a elibera resturile unor alte perioade de timp ale firului de executie. Declaratia ::Sleep ( 0) suspenda firul curent si permite planificatorului sa ruleze alte fire de aceeasi prioritate sau de prioritate mai ridicata. Daca nici unul din astfel de fire nu asteapta executia, apelul functiei returneaza imediat si firul curent isi continua executia. In Windows NT 4.0 sau mai sus se poate comuta pe alt fir de executie prin apelul functiei SwitchToThread. Vom folosi ::Sleep ( 0) daca codul pe care il scriem trebuie sa mearga pe toate platformele Win32. Daca scriem o aplicatie care foloseste mai multe fire de executie pentru a desena pe ecran, cateva declaratii ::Sleep ( 0) plasate strategic pot face adevarate minuni pentru calitatea iesirii. Sa presupunem ca realizam o animatie cu miscarea a patru obiecte si atribuim fiecarui obiect firul sau propriu de executie. Fiecare fir este responsabil cu miscarea unui obiect pe ecran. Daca doar rulam fiecare fir intr-o bucla si ii dam posibilitatea sa acapareze tot timpul de procesor pe care il poate lua, este foarte posibil ca miscarea obiectelor sa fie neregulata. Dar daca lasam fiecare fir sa isi mute obiectul cativa pixeli si apoi apelam ::Sleep ( 0), animatia va fi mult mai lina. Valoarea pe care o transmitem functiei ::Sleep nu garanteaza ca firul va fi repornit exact la momentul terminarii intervalului de timp. Transmiterea catre ::Sleep a valorii 10000 ne garanteaza ca firul va fi repornit dupa cel putin 10 secunde. Firul va putea sa ramana suspendat 10 secunde, sau chiar 20, acest lucru fiind in functie de sistemul de operare. In practica, firul de obicei va fi repornit cu o fractiune de secunda dupa terminarea intervalului de timp, dar nu exista nici o garantie. La momentul actual nu exista in nici o versiune de Windows o metoda de a suspenda un fir de executie pentru o perioada precisa de timp. Schimbarea catre un alt fir de executieSistemul ofera o functie numita SwitchToThread care permite rularea unui alt fir de executie daca exista : BOOL SwitchToThread ( ); Atunci cand apelam aceasta functie, sistemul verifica daca exista un fir care este privat de timp de procesor. Daca nu este gasit nici un astfel de fir, functia SwitchToThread returneaza imediat. Daca exista un astfel de fir, functia planifica firul ( care este posibil sa aiba o prioritate mai scazuta decat firul care a apelat functia SwitchToThread). Acestui fir ii este permis sa ruleze pentru o cuanta de timp iar dupa aceea planificatorul functioneaza normal. Aceasta functie permite unui fir de executie care doreste accesul la o resursa sa forteze un fir cu o prioritate mai mica care detine acea resursa sa elibereze resursa. Daca nici un fir nu poate fi executat dupa apelul SwitchToThread, functia returneaza FALSE; in caz contrar, ea returneaza o valoare nenula. Apelul SwitchToThread este similar cu apelul functiei Sleep cu parametrul avand valoarea 0. Diferenta este ca SwitchToThread permite executia firelor de executie cu prioritate mai mica. Sleep replanifica firul apelant chiar daca exista fire cu prioritate mai redusa care sunt private de timp de procesor. Windows nu are o implementare folositoare pentru aceasta functie. Stiva unui procesFiecarui proces ii este alocat propria sa stiva din spatiul de 4 GB de memorie al procesului parinte. Atunci cand folosim variabile statice sau globale, firele multiple de executie pot accesa variabilele in acelasi timp, putand sa corupa continutul variabilelor. Totusi, variabilele locale si automate sunt create in stiva firului de executie si din aceasta cauza sunt mai putin probabil sa fie corupte de un alt fir de executie. Din acest motiv, trebuie intotdeauna sa incercam sa folosim variabile locale sau automate atunci cand scriem functiile noastre si sa evitam folosirea variabilelor statice si globale. Terminarea unui fir de executie in SDKAsemenea unui proces, un fir poate fi oprit in trei feluri : 1. Firul se opreste singur apeland functia ExitThread. ( cea mai intalnita metoda) 2. Un fir din acelasi proces sau din alt proces apeleaza TerminateThread. ( de evitat) 3. Procesul care contine firul se opreste. Functia ExitThreadUn fir se opreste atunci cand apeleaza ExitThread : VOID ExitThread ( UINT fuExitCode); Aceasta functie opreste firul si seteaza codul de iesire al firului la fuExitCode. Functia ExitThread nu returneaza o valoare deoarece firul nu s-a oprit. Aceasta metoda este cea mai intalnita deoarece ExitThread este apelat atunci cand functia de fir returneaza catre functia interna a sistemului StartOfThread. Aceasta apeleaza la randul ei ExitThread, transmitandu-i valoarea returnata din functia noastra de fir. Functia TerminateThreadUn apel TerminateThread termina de asemenea firul : BOOL TerminateThread ( HANDLE hThread, DWORD dwExitCode); Functia seteaza firul identificat de parametrul hThread si seteaza codul de iesire la dwExitCode. Functia TerminateThread exista astfel incat sa putem sa oprim un fir atunci cand nu mai raspunde. Este recomandat sa o folosim doar ca ultim mijloc. Atunci cand un fir isi termina executia prin apelul ExitThread, stiva firului este distrusa. Totusi, daca firul este oprit prin TerminateThread, sistemul nu distruge stiva pana cand procesul care este proprietarul firului se termina, deoarece alte fire de executie s-ar putea sa foloseasca pointeri care referentiaza date continute pe stiva firului care s-a oprit. Daca aceste alte fire incearca sa acceseze stiva, apare o violare de acces. Atunci cand firul se termina, sistemul anunta orice DLL-uri atasate procesului proprietar al firului ca firul se opreste. Daca apelam insa TerminateThread, sistemul nu anunta nici un DLL atasat procesului, adica procesul nu va fi inchis corect. De exemplu, un DLL poate fi scris pentru a goli un buffer intr-un fisier pe disc atunci cand firul se detaseaza de DLL. Deoarece DLL-ul nu este anuntat de sistem de detasare atunci cand folosim TerminateThread, DLL-ul nu poate efectua operatia sa normala de curatire. Oprirea unui fir de executie in MFCOdata ce un fir de executie este pornit, el poate fi oprit in doua moduri. Un fir de executie de lucru se opreste atunci cand functia de fir executa o declaratie return sau cand orice alta functie oriunde in fir apeleaza AfxEndThread. Un fir de executie cu interfata este oprit atunci cand este trimis un mesaj WM _ QUIT catre coada de mesaje sau cand firul insusi apeleaza AfxEndThread. Un fir de executie poate adauga un mesaj WM _ QUIT la el insusi cu functia API ::PostQuitMessage. AfxEndThread, ::PostQuitMessage si return accepta toate un cod de iesire pe 32 de biti care poate fi extras cu ::GetExitCodeThread dupa oprirea firului de executie. Urmatoarea declaratie copie codul de iesire al firului indicat de pThread in dwExitCode DWORD dwExitCode; ::GetExitCodeThread ( pThread->m _ hThread, &dwExitCode); Daca functia ::GetExitCodeThread este apelata pentru un fir care este in executie , atunci atribuie variabilei dwExitCodeThread valoarea STILL _ ACTIVE ( 0x103). In acest exemplu, identificatorul firului transmis catre functia ::GetExitCodeThread este extras din data membru m _ hThread a obiectului CWinThread care incapsuleaza firul de executie. Ori de cate ori avem un CWinThread si vrem sa apelam o functie API care are nevoie de un handle de fir, putem obtine acest handle din m _ hThread. Stergerea automata a CWinThread-urilorMostra de cod din sectiunea precedenta pare suficient de nevinovata, dar de fapt poate conduce la aparitia de erori daca nu suntem constienti de caracteristica speciala a obiectelor CWinThread si nu facem nimic sa luam in seama acest lucru. Stim ca functia AfxBeginThread creeaza un obiect CWinThread si returneaza adresa catre apelant. Dar cum este sters acel obiect CWinThread ? Pentru a nu fi nevoie sa apelam noi delete pe pointerul CWinThread returnat de AfxBeginThread, MFC-ul apeleaza el delete asupra pointerului dupa ce firul de executie a fost oprit. Mai mult, destructorul CWinThread utilizeaza functia API ::CloseHandle pentru a inchide handler-ul firului. Handler-ele de fire de executie trebuie inchise in mod expres deoarece ele raman deschise chiar si dupa ce firele de executie asociate cu ele au fost oprite. Handler-ele trebuie sa ramana deschise; altfel, functii ca ::GetExitCodeThread nu ar putea sa functioneze. In aparenta, faptul ca MFC-ul sterge in mod automat obiectele CWinThread si inchide handler-ele de fire care corespund firelor pare convenabil. Daca MFC-ul nu ar fi avut grija de aceste lucruri pentru noi, am fi trebuit sa le facem noi insine. Dar exista o problema, sau cel putin una potentiala. Sa analizam din nou declaratia : ::GetExitCodeThread ( pThread->m _ nThread, &dwExitCode Totul este in ordine cu acest cod daca firul de executie nu este terminat, deoarece pThread este inca un pointer valid. Dar daca firul este terminat, este foarte posibil ca MFC-ul sa fi sters obiectul CWinThread si acel pThread este acum un pointer invalid. ( Am precizat „foarte posibil” deoarece o scurta perioada de timp desparte terminarea firului si stergerea obiectului CWinThread asociat lui.) O solutie evidenta este de a copia handler-ul firului din obiectul CWinThread intr-o variabila locala inainte de oprirea firului de executie si de a folosi acel handle in apelul ::GetExitCodeThread : // In timp ce firul ruleaza. HANDLE hThread = pThread->m _ hThread; // Candva mai tarziu. : :GetExitCodeThread ( hThread, &dwExitCode); Dar si acest cod poate prezenta probleme. De ce ? Deoarece daca obiectul CWinThread nu mai exista, handler-ul firului nu mai exista nici el; a fost inchis de mult timp. Greseala de a lua in calcul stergerea automata a obiectelor CWinThread si apelul ::CloseHandle executat de destructorul CWinThread poate conduce la mari greseli de programare daca folosim functii ca ::GetExitCodeThread care presupun ca handler-ul firului este inca valid chiar daca firul nu mai ruleaza. Din fericire, aceasta problema are o solutie, de fapt doua. Prima solutie este de a impiedica MFC-ul sa stearga obiectul CWinThread setand data membru m _ bAutoDelete cu valoarea FALSE. Valoarea implicita este TRUE, care permite stergerea automata. Daca alegem aceasta varianta, trebuie sa apelam delete asupra pointerului CWinThread returnat de AfxBeginThread, altfel aplicatia va suferi scurgeri de memorie. Urmatorul fragment de cod ilustreaza acest lucru : CWinThread* pThread = AfxBeginThread ( ThreadFunc, NULL, THREAD _ PRIORITY _ NORMAL, 0, CREATE _ SUSPENDED); pThread->m _ bAutoDelete = FALSE; pThread->ResumeThread ( ); // Candva mai tarziu. DWORD dwExitCode; ::GetExitCodeThread ( pThread->m _ hThread, &dwExitCode); if ( dwExitCode == STILL _ ACTIVE) else La fel de important ca stergerea obiectului CWinThread este crearea unui fir in starea suspendat. Daca nu procedam astfel, exista o sansa mica dar foarte reala ca noul fir de executie sa isi termine existenta inainte ca firul care l-a creat sa execute instructiunea care seteaza m _ bAutoDelete la valoarea FALSE. Trebuie sa ne aducem aminte ca o data ce un fir de executie este pornit, Windows-ul nu ofera nici o garantie despre cat timp de procesor ii va fi acordat acelui fir. Cea de-a doua solutie este sa permitem obiectului CWinThread sa fie sters automat, dar sa folosim functia Win32 ::DuplicateHandle pentru a crea o copie a handler-ului firului. Handler-urile firelor au un contor de referinta si folosirea functiei ::DuplicateHandle pentru a duplica un fir proaspat deschis incrementeaza contorul de referinta de la 1 la 2. In consecinta, atunci cand destructorul CWinThread apeleaza ::CloseHandle, handler-ul nu este de fapt inchis; el are pur si simplu contorul de referinta decrementat. Partea negativa este ca nu trebuie sa uitam sa apelam ::CloseHandle pentru a inchide handler-ul. Un exemplu : CWinThread* pThread = AfxBeginThread ( ThreadFunc, NULL, THREAD _ PRIORITY _ NORMAL, 0, CREATE _ SUSPENDED); HANDLE hThread; ::DuplicateHandle ( GetCurrentProcess ( ), pThread->m _ nThread, GetCurrentProcess ( ), &hThread, 0, FALSE, DUPLICATE _ SAME _ ACCESS); pThread->ResumeThread ( ); // Candva mai tarziu. DWORD dwExitCode; ::GetExitCodeThread ( hThread, &dwExitCode); if ( dwExitCode == STILL _ ACTIVE) else Din nou, firul de executie este creat intr-o stare de suspendare astfel incat firul care l-a creat sa poata sa fie absolut sigur de executia codului inainte de terminarea noului fir de executie. Vorbind la modul general, firele de executie se pot opri doar pe ele insele. Daca dorim ca firul A sa opreasca firul B, trebuie sa punem la punct un mecanism de semnalizare care permite firului A sa spuna firului B sa se opreasca singur. O variabila simpla poate servi ca un atribut de cerere a terminarii, asa cum este demonstrat mai jos : //Thread A nContinue = 1; CWinThread* pThread = AfxbeginThread ( ThreadFunction, &nContinue); nContinue = 0; // Spunem firului B sa isi termine executia. //Thread B UINT ThreadFunction ( LPVOID pParam) return 0; In acest exemplu, firul B verifica din timp in timp si se opreste daca nContinue se schimba de la o valoare diferita de 0 la 0. In mod normal nu este o idee prea buna ca doua fire de executie sa acceseze aceeasi variabila fara sa isi sincronizeze actiunile, dar in acest caz este acceptabil deoarece firul B verifica doar daca nContinue este 0. Bineinteles, pentru a previni violarile de acces trebuie sa ne asiguram ca nContinue nu iese din raza de definire atat timp cat firul B ruleaza. Putem face acest lucru fie declarand variabila nContinue ca statica sau globala. Sa presupunem ca vrem modificam acest exemplu in asa fel incat cand firul A seteaza nContinue la 0 sa faca o pauza pana cand firul B inceteaza sa mai ruleze. Modul corect de a face acest lucru este urmatorul : //Firul A nContinue = 1; CWinThread* pThread = AfxBeginThread ( ThreadFunc, &nContinue); HANDLE hThread = pThread->m _ nThread; // Salveaza identificatorul firului nContinue = 0; // Spune firului B sa se opreasca. ::WaitForSingleObject ( hThread, INFINITE); //Firul B UINT ThreadFunc ( LPVOID pParam) return 0; ::WaitForSingleObject blocheaza firul de executie apelant pana cand obiectul specificat – in acest caz un alt fir – intra intr-o stare „semnalata”. Un fir devine semnalat cand se termina. Cand un fir de executie se blocheaza in ::WaitForSingleObject, el asteapta foarte eficient deoarece este efectiv suspendat pana cand apelul functiei returneaza. Acest exemplu presupune ca firul B nu se va opri pana cand firul A nu ii va spune acest lucru. In caz contrar ,daca firul B se poate opri inaintea ca firul A sa ii ceara acest lucru, atunci firul A ar trebui sa creeze firul B intr-o stare se suspendare si ar trebui sa faca o copie a identificatorului firului cu ::DuplicateHandle. In caz contrar, firul A ar putea fi prins in greseala transmiterii unui identificator invalid de fir catre WaitForSingleObject. ::WaitForSingleObject este o functie indispensabila pe care o vom folosi foarte des atunci cand vom scrie cod cu multiplicare de fire. Primul parametru transmis catre aceasta este identificatorul obiectului pe care vrem sa il asteptam. ( Poate fi de asemenea un identificator de proces, identificatorul unui obiect de sincronizare sau identificatorul unei declaratii de schimbare a unui fisier, printre alte lucruri.) In exemplul precedent, firul A extrage identificatorul firului B inainte de a seta nContinue cu 0 deoarece obiectul CWinThread reprezentand firul B s-ar putea sa nu mai existe cand se executa apelul ::WaitForSingleObject. Al doilea parametru al functiei ::WaitForSingleObject este lungimea timpului pe care suntem dispusi sa asteptam. INFINITE inseamna atat de mult cat trebuie. Atunci cand precizam INFINITE, ne asumam riscul ca firul apelant sa poata sa se blocheze daca obiectul pe care il asteapta nu devine niciodata semnalat. Daca precizam in schimb un numar de milisecunde, ca in ::WaitForSingleObject ( hThread, 5000); atunci ::WaitForSingleObject va returna dupa ce timpul specificat, in cazul de fata 5 secunde, se va scurge chiar daca obiectul nu a devenit semnalat. Putem verifica valoarea returnata pentru a verifica de ce a returnat functia. WAIT _ OBJECT _ 0 inseamna ca obiectul a fost semnalat si WAIT _ TIMEOUT inseamna ca perioada de timp a expirat. Fiind dat un identificator de fir sau un obiect CWinThread valid care incapsuleaza un identificator de fir de executie, putem determina rapid daca firul inca ruleaza apeland ::WaitForSingleObject si specificand 0 pentru perioada de expirare, ca mai jos : if ( ::WaitForSingleObject ( hThread, 0) WAIT _ OBJECT _ 0) else Apelata in acest mod, ::WaitForSingleObject nu mai asteapta; ea returneaza imediat. O valoare de retur egala cu WAIT _ OBJECT _ 0 inseamna ca firul este semnalat ( nu mai exista), iar o valoare egala cu WAIT _ TIMEOUT inseamna ca firul nu este semnalat ( inca mai exista). Ca de obicei, ramane la latitudinea noastra sa ne asiguram ca identificatorul pe care il transmitem catre ::WaitForSingleObject este unul valid, fie duplicand identificatorul firului original, fie prevenind stergerea automata a obiectului CWinThread. Exista o modalitate prin care un fir poate termina direct executia unui alt fir, dar aceasta metoda trebuie folosita doar ca o ultima optiune. Declaratia : ::TerminateThread ( hThread, 0); opreste firul de executie a carui identificator este hThread si ii atribuie codul de iesire 0. Referirea API Win32 documenteaza o parte din multele probleme pe care :.TerminateThread le poate cauza, care variaza de la obiecte de sincronizare care raman fara firele de executie aferente pana la DLL-uri care nu mai apuca sa ruleze un cod normal de inchidere a firelor de executie. Ce se intampla atunci cand procesul se opresteFunctiile ExitProcess sau TerminateProcess discutate anterior termina de asemenea executia firelor. Diferenta este ca aceste functii opresc toate firele continute in procesul care se termina. Ce se intampla la oprirea unui fir de executieUrmatoarele actiuni au loc la oprirea unui fir de executie : 1. Toti identificatorii obiectelor utilizator apartinand firului sunt eliberati. In Win32, majoritatea obiectelor sunt proprietatea procesului care contine firul care a creat obiectele. Totusi, doua obiecte utilizator pot fi proprietatea unui fir de executie : ferestre si legaturi. Atunci cand firul care a creat aceste obiecte isi termina executia, sistemul distruge in mod automat obiectele. Celelalte obiecte sunt distruse doar atunci cand procesul proprietar se termina. 2. Starea obiectului nucleu fir de executie devine semnalata. 3. Codul de iesire al firului se schimba de la STILL _ ACTIVE la codul de terminare al firului. 4. Daca firul este ultimul fir activ din proces, procesul se opreste. 5. Contorul de utilizare al obiectului nucleu fir este decrementat cu 1. Atunci cand firul se termina, obiectul nucleu fir asociat lui nu devine automat liber pana cand toate referintele ramase la obiect nu sunt inchise. De indata de un fir nu mai ruleaza, orice alt fir din sistem nu mai are ce face cu identificatorul primului gir. Totusi, aceste alte fire pot apela GetExitCodeThread pentru a verifica daca firul identificat prin hThread s-a terminat si, in caz afirmativ, sa ii determine codul sau de iesire. BOOL GetExitCodeThread ( HANDLE hThread, LPDWORD lpdwExitCode); Valoarea de iesire este returnata in DWORD-ul la care pointeaza lpdwExitCode. Daca firul nu este terminat atunci cand este apelat GetExitCodeThread, functia scrie in DWORD identificatorul STILL _ ACTIVE ( definit ca 0x103). Daca functia reuseste este returnat TRUE. Prioritatea proceselor si a firelor de executieFire de executie, procese si prioritatiPlanificatorul este acea componenta a sistemului de operare care decide care fire de executie ruleaza, cand si pentru cat timp. Planificarea firelor de executie este o sarcina complexa al carei obiectiv este de a imparti timpul procesorului intre firele de executie cat mai eficient cu putinta pentru a crea iluzia ca toate ruleaza in acelasi timp. Pe masinile cu mai multe procesoare, Windows NT si Windows 2000 ruleaza in realitate doua sau mai multe fire de executie in acelasi timp distribuind firele intre procesoare folosind o schema numita procesare multipla simetrica, sau SMP. Windows 95 si Windows nu sunt sisteme de operare SMP, astfel incat ele distribuie toate firele aceluiasi procesor chiar si pe calculatoarele cu mai multe procesoare. Planificatorul foloseste o varietate de tehnici pentru a imbunatati performanta procesarii multiple si pentru a incerca sa asigure faptul ca fiecare fir de executie primeste suficient timp de procesor. In cele din urma, totusi, decizia care fir sa se execute mai intai este data de prioritatea firelor. La orice moment dat, fiecare fir are atribuita un nivel de prioritate de la 0 la 31, cu un numar mai mare indicand o prioritate mai mare. Daca un fir cu prioritatea 11 asteapta executia si toate celelalte fire care asteapta executia au prioritati mai mici decat 11, firul cu prioritatea 11 ruleaza primul. Daca doua fire au aceeasi prioritate, planificatorul il executa pe cel care a asteptat mai mult. Atunci cand perioada de timp, sau cuanta, acordata firului expira, celalalt fir cu prioritate egala cu primul este executat daca toate celelalte fire active au prioritati mai mici. Ca o regula generala, planificatorul intotdeauna acorda urmatoarea perioada de timp firului in asteptare cu cea mai mare perioada. Acest lucru inseamna ca firele cu prioritate scazuta nu sunt executate niciodata ? Nici gand. In primul rand, sa ne aducem aminte ca Windows-ul este un sistem de operare bazat pe masaje. Daca un fir de executie apeleaza ::GetMessage si coada sa de mesaje este goala, firul se blocheaza pana cand devine disponibil un mesaj. Acest lucru da sansa firelor cu prioritate redusa de a fi executate. Majoritatea firelor cu interfata isi petrec mare parte din timpul lor blocate pe coada de mesaje, asa ca atat timp cat un fir de lucru cu prioritate ridicata nu monopolizeaza procesorul ( un fir de executie de lucru nu se blocheaza pe coada de mesaje deoarece nu proceseaza mesaje), chiar si firele cu prioritate redusa practic beneficiaza de tot timpul de procesor de care au nevoie. Planificatorul face mai multe trucuri cu nivelele de prioritate pentru a imbunatati disponibilitatea sistemului de raspuns si pentru a reduce pericolul ca un fir oarecare sa nu primeasca timp de procesor. Daca un fir cu prioritatea 7 sta mai mult timp fara sa primeasca timp de procesor, planificatorul poate mari temporar prioritatea firului la 8, 9 sau chiar mai mare pentru a-i da sansa de a se executa. Windows NT 3.x creste prioritatile firelor care se executa in prim-plan pentru a imbunatati timpul de raspuns al aplicatiei in care lucreaza utilizatorul, iar Windows NT 4.0 Workstation creste perioada de timp acordata acestor fire. Windows-ul foloseste o tehnica numita mostenirea prioritatii pentru a preveni blocarea pentru prea mult timp a firelor cu prioritate ridicata pe obiecte de sincronizare detinute de fire cu prioritate scazuta. De exemplu, daca un fir de executie cu prioritatea 11 incearca sa revendice un mutex detinut de un fir cu prioritatea 4, planificatorul poate sa mareasca prioritatea celui de-al doilea fir pentru ca mutex-ul sa se elibereze mai devreme. De fapt cum sunt atribuite prioritatile prima data Atunci cand apelam AfxBeginThread sau CWinThread::SetThreadPriority noi specificam prioritatea relativa a firului. Sistemul de operare combina nivelul de prioritate relativ cu clasa de prioritate a procesului tata al firului pentru a calcula un nivel de prioritate de baza pentru fir. Nivelul real de prioritate al firului, un numar intre 0 si 31, poate varia continuu deoarece prioritatea poate creste si scadea. Nu putem controla cresterea ( si nici nu am vrea sa facem acest lucru chiar daca am putea sa il facem), dar putem sa controlam nivelul de prioritate de baza setand clasa de prioritate a procesului si nivelul de prioritate relativ al firului. Clasele de prioritate ale proceselorMajoritatea proceselor isi incep existenta cu clasa de prioritate NORMAL _ PRIORITY _ CLASS. O data pornite insa, un proces sa isi schimbe prioritatea apeland ::SetPriorityClass, care accepta ca argumente un identificator de proces ( care poate fi obtinut cu apelul ::GetCurrentProcess) si unul din specificatorii din tabelul urmator : Clasele de prioritate ale proceselor
Majoritatea aplicatiilor nu au nevoie sa isi schimbe clasa de prioritate. Procesele cu prioritatile HIGH _ PRIORITY _ CLASS sau REALTIME _ PRIORITY _ CLASS pot afecta timpul de raspuns al sistemului si pot chiar intarzia activitati sistem vitale, cum ar golirea zonei de cache a harddiskului. O folosire corecta a clasei HIGH _ PRIORITY _ CLASS este pentru aplicatiile sistem care raman ascunse majoritatea timpului dar produc o fereastra atunci cand apare un eveniment de intrare. Aceste aplicatii impun o sarcina foarte mica asupra sistemului atat timp cat sunt blocate asteptand o intrare, dar o data ce apare o intrare ele primesc o prioritate mai mare decat restul aplicatiilor. Clasa REALTIME _ PRIORITY _ CLASS este furnizata in primul rand pentru programele care primesc date in mod real si care trebuie sa aiba parte de partea leului din timpul procesorului pentru a functiona corect. Clasa IDLE _ PRIORITY _ CLASS este ideala pentru aplicatii pentru protectia ecranului, monitoare de sistem sau alte aplicatii cu prioritate redusa care sunt proiectate sa opereze neobstructiv in fundal. O observare abstracta a prioritatilorAtunci cand dezvoltatorii de la Microsoft au facut planificatorul de fire de executie, ei si-au dat seama ca acesta nu va corespunde preferintelor tuturor. Ei si-au dat seama de asemenea ca “scopul” calculatorului se va schimba in timp. Atunci cand a aparut prima data Windows NT, aplicatiile OLE de abia incepeau sa fie scrise. Acum, ele sunt ceva obisnuit. Programele pentru jocuri ocupa un spatiu foarte important si in mod sigur Internetul nu a fost discutat prea mult in primii ani ai Windows-ului NT. Algoritmul de planificare are un efect important asupra tipurilor de aplicatii pe care le pot rula utilizatorii. Inca de la inceput, dezvoltatorii de la Microsoft si-au dat seama ca vor trebui sa modifice algoritmul de planificare in timp, pe masura ce scopul calculatoarelor se va schimba. Dar dezvoltatorii de software au nevoie sa scrie programe acum si Microsoft garanteaza ca programele vor rula si in versiuni ulterioare ale sistemului. Cum poate Microsoft sa schimbe modulde functonare al sistemului si totusi sa pastreze softul in stare de functionare ? Iata cateva rapunsuri : Microsoft nu ofera documentatia completa a planificatorului; Microsoft nu permite aplicatiilor sa beneficieze de toate avantajele planificatorului; Microsoft ne spune ca algoritmul de planificare poate fi schimbat astfel incat noi programam defensiv. API-ul Windows ofera un strat abstract pentru planificatorul sistemului, astfel incat nu avem acces direct la planificator. In schimb, noi apelam functii Windows care interpreteaza parametrii nostri in functie de versiunea sistemului de operare pe care rulam. Atunci cand proiectam o aplicatie, trebuie sa ne gandim la alte aplicatii pe care utilizatorul le va rula impreuna cu aplicatia noastra. Apoi va trebui sa alegem o clasa de prioritate bazata pe viteza de raspuns pe care dorim sa o aiba firele de executie din aceasta aplicatie. O data ce alegem o clasa de prioritate, nu trebuie sa ne mai gandim cum interactioneaza aplicatia noastra cu alte aplicatii si trebuie sa ne concentram asupra firelor din aplicatie. Windows suporta sapte prioritati relative pentru fire de executie : idle, lowest, below normal, normal, above normal, highest si time-critical. Aceste prioritati sunt relative la clasa de prioritate a procesului. Din nou, majoritatea firelor au prioritatea normal.
In concluzie, procesul face parte dintr-o clasa de prioritate si in cadrul acestuia atribuim prioritati relative firelor de executie. Prioritatea absoluta difera de la un sistem de operare la altul. La Windows 2000 ea se calculeaza in modul urmator :
Dupa cum se observa nu exista nivelul de prioritate 0, care este rezervat. De asemenea, nivelurile 17, 18, 19, 20, 21, 27, 28, 29 sau 30 pot fi obtinute doar daca scriem un driver care ruleaza in mod nucleu. O aplicatie utilizator nu poate obtine aceste prioritati. De asemenea trebuie observat ca un fir dintr-o clasa de prioritate real-time nu poate avea o prioritate mai mica de 16. De asemenea, un fir dintr-o clasa non-real-time nu poate avea o prioritate mai mare de 15. Procesele nu sunt niciodata planificate, doar firele pot fi planificate. Clasa de prioritate a proceselor este o abstractiune introdusa de Microsoft pentru a ne indeparta de functionarea interna a planificatorului. Un fir de executie cu prioritate ridicata ar trebui sa execute mai putine instructiuni, avand acces la procesor aproape imediat, iar cele cu prioritate raman planificabile pentru o perioada mai mare de timpi executa mai multe instructiuni. Daca se respecta aceste reguli, intregul sistem de operare va raspunde mult mai repede la actiunile utilizatorilor. Programarea prioritatilorCum atribuim unui proces o clasa de prioritate in SDK? Atunci cand apelam CreateProcess, putem transmite clasa de prioritate dorita in parametrul fdwCreate. O data ce procesul copil ruleaza, el poate sa isi schimbe propria prioritate apeland SetPriorityClass : Bool SetPriorityClass ( HANDLE hProcess; DWORD fdwPriority; Aceasta functie schimba clasa de prioritate identificata de hProcess la valoarea specificata de parametrul fdwPriority. Acest parametru poate fi unul din identificatorii din tabelul de mai sus. Deoarece aceasta functie ia identificatorul unui proces, putem schimba clasa de prioritate a oricarui proces care ruleaza in sistem atata timp cat avem un identificator al lui si avem accesul corespunzator. In mod normal, un proces va incerca sa isi modifice propria sa clasa de prioritate. Bool SetPriorityClass ( GetCurrentProcess ( ), IDLE _ PRIORITY _ CLASS); O functie complementara folosita pentru a extrage clasa de prioritate a unui proces este : DWORD GetPriorityClass ( HANDLE hProcess); Functia returneaza unul din identificatorii din tabelul de mai sus. Task Managerul din Windows 2000 permite utilizatorilor sa schimbe prioritatea unui proces. La crearea unui fir de executie, prioritatea sa relativa este intotdeauna setata implicit la normal. Pentru a seta prioritatea unui fir, trebuie sa apelam functia SetThreadPriority : BOOL SetThreadPriority ( HANDLE Thread, int nPriority); Bineinteles, parametrul hThread identifica firul singular a carui prioritate vrem sa o schimbam, iar parametrul nPriority este unul din cei 7 identificatori din tabelul de mai jos.
Functia complementara care extrage prioritatea relativa a firului este: int GetThreadPriority ( HANDLE hThread); Aceasta functie returneaza unul din identificatorii din tabelul de mai sus. In MFC putem trimite una din valorile de mai sus functiilor AfxBeginThread si CWinThread::SetThreadPriority. In SDK, CreateThread creeaza intotdeauna un nou fir cu prioritatea relativa normal. Pentru a face ca firul sa aiba prioritatea „idle”, putem transmite indicatorul CREATE _ SUSPENDED functiei CreateThread. Acest lucru face ca firul sa nu execute cod deloc. Putem apela apoi SetThreadPriority pentru a schimba prioritatea firului la prioritatea „idle”. Apoi apelam ResumeThread astfel incat firul poate fi planificabil. Nu stim cand va primi mai mult timp de procesor, dar planificatorul ia in considerare faptul ca acest fir are prioritatea „idle”. In cele din urma, inchidem identificatorul catre noul fir astfel incat obiectul nucleu poate fi distrus de indata ce firul isi termina executia. Windows nu ofera o functie care returneaza nivelul de prioritate a unui fir de executie. Aceasta omisiune este deliberata. Microsoft isi rezerva dreptul de a schimba algoritmul de planificare in orice moment. Este recomandat sa nu dezvoltam o aplicatie care are nevoie de cunostinte specifice ale alogoritmului planificatorului. Daca ramanem cu clasele de prioritate ale proceselor si cu nivelele relative de prioritate ale firelor de executie, aplicatia noastra va rula bine atat in sistemul actual, cat si in versiunile urmatoare. Cresterea dinamica a nivelelor de prioritateSistemul determina nivelul de prioritate a firului de executie combinand nivelul relativ de prioritate al firului de executie cu clasa de prioritate a procesului firului. Acesta este uneori cunoscut sub numele de nivelul de prioritate de baza. Uneori, sistemul creste nivelul de prioritate a unui fir – in mod normal ca raspuns la niste evenimente I/O cum ar fi mesajele de fereastra sau citirea de pe disc. De exemplu, un fir cu o prioritate normala intr-un proces cu o clasa de prioritate ridicata are un nivel de prioritate de baza egal cu 13. Daca utilizatorul apasa o tasta, sistemul plaseaza un mesaj WM _ KEYDOWN in coada de mesaje a firului. Datorita aparitiei unui mesaj in coada, firul devine planificabil. In plus, driverul tastaturii poate cere sistemului sa creasca temporar prioritatea firului. Astfel incat firul poate ajunge sa aiba prioritatea 15. Firul este planificat pentru o portie de timp de procesor. De indata ce aceasta perioada expira, sistemul scade cu 1 prioritatea firului pana la urmatoarea portie de timp de procesor. Cea de-a treia executie a firului se va face efectua cu firul avand prioritatea 13. Urmatoarele executii vor avea loc cu nivelul de prioritate 13, nivelul de prioritate de baza a firului. Trebuie sa notam ca nivelul de prioritate a unui fir nu scade niciodata mai jos de nivelul de prioritate de baza. De asemenea, driver-ul este cel care hotaraste dimensiunea cu care trebuie marit nivelul de prioritate. Din nou, Microsoft nu documenteaza marimea cresterii nivelului de prioritate datorata unui driver. Acest lucru permite Microsoft-ului permanent sa regleze cu finete cresterea dinamica pentru a determina cel mai bun timp de reactie. Sistemul creste nivelul de prioritate doar pentru firele care un nivel de prioritate de baza intre 1 si 15. De fapt, de aceea acest interval este denumit intervalul de prioritate dinamica. In plus, sistemul nu creste niciodata prioritatea unui fir in real-time. Deoarece firele din real-time efectueaza majoritatea functiilor sistemului, setarea unei limite superioare pentru cresterea prioritatii previne situatia in care o aplicatie poate interfera cu sistemul de operare. De asemenea, sistemul nu creste prioritatea in zona real-time ( de la 16 la 31). Unii dezvoltatori s-au plans ca cresterea dinamica a prioritatii a avut efecte negative asupra performantei firelor, astfel incat Microsoft a adaugat doua functii care ne permit sa oprim cresterea dinamica a prioritatii : BOOL SetProcessPriorityBoost ( HANDLE hProcess, BOOL DisablePriorityBoost); BOOL SetThreadPriorityBoost ( HANDLE hThread, BOOL DisablePriorityBoost); SetProcessPriorityBoost spune sistemului sa activeze sau sa dezactiveze cresterea dinamica a prioritatilor in interiorul unui proces; SetThreadPriorityBoost ne permite sa facem acelasi lucru pentru fire individuale de executie. Aceste functii au functii complementare care ne permit sa determinam daca cresterea dinamica este activata sau nu : BOOL GetProcessPriorityBoost ( HANDLE hProcess, PBOOL pDisablePriorityBoost); BOOL GetThreadPriorityBoost ( HANDLE hThread, PBOOL pDisablePriorityBoost); Pentru fiecare din aceste functii, trebuie sa transmitem identificatorul procesului sau al firului pentru care dorim sa efectuam aceste operatii si adresa unei variabile de tip BOOL care va fi setata de catre functie. Windows nu ofera implementari utile pentru aceste patru functii. Ele returneaza FALSE iar un apel ulterior al functiei GetLastError returneaza ERROR _ CALL _ NOT _ IMPLEMENTED. Cresterea dinamica este aplicata si in alta situatie. Sa ne imaginam un fir de prioritate 4 care este privat de timp de procesor. Atunci cand sistemul detecteaza ca acest fir a fost privat de timp de procesor intre 3 si 4 secunde, el va creste dinamic prioritatea firului la 15 si ii permite rularea pentru o perioada dubla de timp. Atunci cand aceasta expira, nivelul de prioritate revine imediat la nivelul de prioritate de baza. Folosirea unor functii C run-time in aplicatii cu multiple fire de executieUnele functii din biblioteca run-time C pun probleme pentru aplicatiile cu fire de executie multiple ( strtok, asctime), iar alte cateva functii C run-time folosesc variabile globale pentru a stoca date intermediare. Daca firul A apeleaza una din aceste functii si firul B isi incepe executia, intrerupand executia firului A si apeleaza aceeasi functie, atunci data globala stocata de firul B pate suprascrie data globala stocata de firul A, sau invers. O solutie pentru aceasta problema este de a folosi obiecte de sincronizare pentru a serializa accesul la functiile C run-time. Dar chiar si cele mai simple obiecte de sincronizare pot fi foarte costisitoare cand ne referim la timpul procesorului. Din aceasta cauza, cele mai moderne compilatoare C sau C vin cu doua versiuni ale bibliotecii C run-time : una care este sigura din punctul de vedere a firelor de executie si una care nu este. Functiile din prima versiune in mod normal nu se bazeaza pe obiectele de sincronizare ale firelor. In schimb, ele stocheaza valorile intermediare structuri de date pe fiecare fir de executie. Visual C vine cu 6 versiuni ale bibliotecii C run-time. Pe care o alegem depinde de construirea unei versiuni cu depanare sau o versiune release, daca vrem sa ne legam in mod dinamic sau static de biblioteca C run-time si, in mod evident, daca aplicatia este cu un singur sau cu mai multe fire de executie. Urmatorul tabel prezinta numele bibliotecilor comutatoarele de compilator aferente : Versiunile Visual C ale bibliotecii C run-time
Libc.lib, Libcd.lib, Libcmt.lib si Libcmtd.lib sunt biblioteci de legatura statice care contin cod C run-time. Msvcrt.lib si Msvcrtd.lib sunt biblioteci de importare care dau posibilitatea unei aplicatii sa se lege in mod dinamic din DLL-ul run-time al lui Visual C . Bineinteles, nu trebuie sa ne agitam cu comutatoarele de compilator in afara cazului in care ne construim propriile noastre fisiere de compilare. Daca folosim Visual C++, trebuie doar sa selectam intrarea corespunzatoare din campul Use Run-time Library din casuta de dialog a setarilor proiectului si IDE-ul le va adauga pentru noi. Chiar daca scriem o aplicatie cu mai multe fire de executie care nu foloseste functii C run-time, ar trebui sa facem legatura cu una din bibliotecile pentru fire de executie multiple deoarece MFC-ul apeleaza chiar el cateva functii C run-time. Intr-o aplicatie MFC, acest lucru este tot ce trebuie facut pentru a apela o functie C run-time intr-un mod sigur din punctul de vedere al firelor de executie. Trebuie doar sa setam comutatoarele pentru compilator si sa lasam biblioteca de clase sa faca restul. Intr-o aplicatie SDK trebuie de asemenea sa inlocuim apelurile ::CreateThread cu apeluri la _ beginthreadex. Programatorii MFC nu trebuie sa isi faca probleme din aceasta privinta deoarece AfxBeginThread apeleaza automat _ beginthreadex. Apelarea functiilor membre MFC in afara granitelor firelor de executieIn continuare vom prezenta partea proasta a scrierii de aplicatii MFC cu fire de executie multiple. Atat timp cat firele nu apeleaza functii membru apartinand obiectelor create de alte fire, sunt putine restrictii cu privire la ceea ce pot face. Totusi, daca firul A transmite un pointer CWnd catre pointerul B si firul B apeleaza o functie membru a acelui obiect CWnd, este posibil ca MFC-ul sa faca o asertie intr-o versiune cu depanare. O versiune release s-ar putea sa functioneze corect, dar din nou, s-ar putea sa nu functioneze corect. Exista de asemenea posibilitatea sa nu apara o asertie intr-o versiune cu depanare, dar nici aceasta nu va functiona corect. Totul depinde de ceea ce se intampla in interiorul cadrului de lucru cand acel membru CWnd este apelat. Putem evita o multime de probleme compartimentand firele si dand permisiunea fiecarui fir de a folosi doar acele obiecte pe care le creeaza mai degraba decat sa se bazeze pe obiecte create de alte fire. Dar in cazurile in care acest lucru nu este posibil, exista cateva reguli pe care trebuie sa le urmam. In primul rand, multe functii membru MFC pot fi apelate in siguranta asupra unor obiecte din alte fire de executie. Majoritatea functiilor inline definite in fisierele INL din directorul Include al MFC-ului pot fi apelate si in afara granitelor firului deoarece ele sunt mai mult decat niste containere pentru functiile API. Dar apelarea unei functii membru non-inline poate genera probleme. De exemplu, codul urmator, care transmite un pointer namedpWnd de tip CWnd de la firul A catre firul B si in care B apeleaza CWnd::GetParent prin intermediul acestui pointer, functioneaza fara probleme : CWinThread* pThread = AfxBeginThread ( ThreadFunc, pWnd); UINT ThreadFunc ( LPVOID pParam) Prin simpla schimbare a lui GetParent cu GetParentFrame obtinem o asertie: CWinThread* pThread = AfxBeginThread ( ThreadFunc, pWnd); UINT ThreadFunc ( LPVOID pParam) De ce functia GetParent functioneaza atunci cand GetParentFrame nu functioneaza ? Deoarece apelul functiei GetParent conduce aproape direct catre apelul functiei ::GetParent din API. Functia CWnd::GetParent este definita in modul urmator in Afxwin2.ini, cu o reformatare pentru a imbunatati citirea : _ AFXWIN _ INLINE CWnd* CWnd::GetParent ( ) const ASSERT ( ::IsWindow ( m _ hWnd)) return CWnd::FromHandle ( ::GetParent ( m _ hWnd)); Acest cod nu prezinta nici o problema; m _ hWnd este valid deoarece este o parte a obiectului CWnd dat de pointerul pWnd si functia FromHandle converteste HWND-ul returnat de ::GetParent intr-un pointer CWnd. Sa consideram acum ce se intampla atunci cand apelam GetParentFrame, a carei cod sursa se gaseste in fisierul Wincore.cpp. Linia care cauzeaza aceasta eroare de inserare ( assert error) este : ASSERT _ VALID ( this); ASSERT _ VALID apeleaza CWnd::AssertValid, care efectueaza o verificare logica asigurandu-se ca HWND-ul asociat cu this apare intr-o harta temporara sau permanenta de identificatori pe care cadrul de lucru o foloseste pentru a converti HWND-urile in CWnd-uri. Calea de la un CWnd la HWND este simpla deoarece HWND este un membru de date a lui CWnd, dar calea de la un HWND la un CWnd este posibila doar prin intermediu hartilor de identificatori. Astfel apare o noua problema : hartile de identificatori sunt locale fiecarui fir de executie si nu sunt vizibile pentru alte fire de executie. Daca firul A creeaza un CWnd a carui adresa este trecuta catre functia ASSERT _ VALID, HWND-ul corespunzator nu va aparea in harta permanenta sau temporara de identificatori si MFC-ul va cauza aparitia unei asertii. Majoritatea functiilor membru ale MFC-ului apeleaza ASSERT _ VALID, dar functiile inline nu fac acest lucru, cel putin nu in distributiile curente. In mod frecvent, asertiile MFC-ului ne ajuta sa nu apelam functii care oricum nu ar functiona. Intr-o versiune release, functia GetParentFrame returneaza valoarea NULL atunci cand este apelata dintr-un fir diferit de cel din care cadrul parinte a fost creat. Dar in cazurile in care erorile de asertie sunt false – acest lucru se intampla in cazurile in care functia functioneaza corect in ciuda tabelelor de identificatori pe fiecare fir – putem evita asertiile transmitand identificatori reali in locul pointerilor de obiecte. De exemplu, este mai sigur sa apelam CWnd::GetTopLevelParent intr-un fir secundar daca am apelat FromHandle in primul rand pentru a crea o intrare in harta permanenta sau temporara de identificatori, dupa cum este aratat in continuare : CWinThread* pThread = AfxBeginThread ( ThreadFunc, pWnd->m _ hWnd); UINT ThreadFunc ( LPVOID pParam) Din aceasta cauza documentatia MFC ne avertizeaza ca ferestrele, obiectele GUI si alte obiecte ar trebui transmise intre fire folosind identificatori in locul pointerilor. In general, vom avea mai putine probleme daca transmitem identificatori si folosim FromHandle pentru a recrea obiecte in firele destinatie. Dar asta nu inseamna ca orice functie va merge. Cum ramane cu functiile membru apartinand obiectelor create din clase „pure” MFC cum ar fi CDocument si CRect, clase care nu impacheteaza HWND-uri, HDC-uri sau alte tipuri de identificatori si din aceasta cauza nu se bazeaza pe harti de identificatori ? Raspuns : unele merg si altele nu merg. Nu este nici o problema cu acest cod : CWinThead* pThread = AfxBeginThread ( ThreadFunc, pRect); UINT ThreadFunc ( LPVOID pParam) Urmatorul cod insa va cauza aparitia unei asertii : CWinThread* pThread = AfxBeginThread ( ThreadFunc, pDoc); UINT ThreadFunc ( LPVOID pParam) Chiar si unele functii aparent inofensive cum ar fi AfxGetMainWnd nu functioneaza atunci cand sunt apelate din alta parte decat din firul principal al aplicatiei. Important este ca inainte de a apela functii membru asupra obiectelor MFC create in alte fire de executie, trebuie sa intelegem implicatiile. Si unica modalitate de a face acest lucru este de a studia codul sursa MFC pentru a vedea cum se comporta o anumita functie membru. Trebuie de asemenea sa stim ca MFC-ul nu este sigur din punct de vedere al firelor de executie. Chiar daca o functie membru pare sa fie sigura, trebuie sa ne intrebam ce s-ar putea intampla daca firul B ar accesa un obiect creat de firul A si firul A si-ar incepe executia , firul B fiind oprit brusc. Acest lucru este foarte greu de rezolvat si doar creste complexitatea scrierii de aplicatii cu fire de executie multiple. Din aceasta cauza in realitate aplicatiile MFC cu fire de executie multiple tind sa isi execute majoritatea operatiilor tinand de interfata cu utilizatorul in firul principal de executie. Daca un fir din fundal vrea sa actualizeze interfata, el trimite sau adauga un mesaj firului principal pentru ca firul principal sa poata face actualizarea.
|