Scheduler Activations in Windows 2003 Server
Despre User Level Threads si Kernel Level Threads si Apeluri Sistem
La ora actuala cea mai cunoscuta metoda de a crea aplicatii paralele este prin utilizarea Thread-urilor.Spre deosebire de aplicatiile clasice,unde avem un singur fir de control(un singur “flow”) ,o aplicatie care utilizeaza thread-uri poate avea mai multe fire de control,fiecare fir(thread) isi executa propriile functii,iar toate aceste fire se vor executa in paralel.Voi da un exemplu ca sa va fie mai clar:
#include <stdio.h>
//un program multi thread
main(){
functie1();
functie2();
functie3();
printf(“rezultatele procesarii sunt:…….etcn”) ;
//Aici am iesit din program
exit(0) ;
}
Acesta este un program monothread,adica cu un singur fir de executie.Acest fir de executie va excuta functia main().Aceasta functie,main() va apela mai intai functie1(),dupa care va apela functie2(),iar apoi functie functie3(), apoi va afisa rezultatele,dupa care va iesi din program prin apelul exit(0),insa chiarsi daca nu am fi avut acest apel aic,tot am fi iesit din program:-).Ideea e ca executia acestui program incepe de la un anumit punct si se termina la alt punct,adica este un fir(sau path daca vreti) de executie care poate fi urmarit chiar si cu “ochiul liber”(se incepe executia in main,se apeleaza cateva functii dupa care iesim).
Sa dam acum un exemplu de un program ,multithreaded in care avem mai multe path-uri(adica mai multe apeluri de functii se vor apela simultan).
#include <stdio.h>
//un program mono thread
functie1()
{
//Face ceva interesant
}
functie2()
{
//Face ceva interesant
}
functie3()
{
//Face ceva interesant
}
main(){
thread_tthread1,thread2,thread3;
create_thread(thread1,functie1,NULL);
create_thread(thread2,functie2,NULL);
create_thread(thread3,functie3,NULL);
printf(“rezultatele procesarii sunt:…….etc\n”) ;
//Aici am iesit din program
exit(0) ;
}
Dupa cum observati un program unde exista maii multe thread-uri sau fire de executie,nu este prea diferit la prima vedere de unul “normal”.Doar si intr-unul si in celalalt avem apeluri normal de functii…unde este diferenta?
Obeservati ca in functia main am apelat de 3 ori functie create_tread().Ok….Aceasta este o functie care va crea un thread…sau …un obiect de tip thread(by the way am zis obiect dar nu e nici un fel C++ mumbo jumbo aici…Prin obiect in contextul de fata intelegem o mai degraba o structura de date (un typedef struct thread{….}thread_t)Desi se poate implementa o librarie de thread-uri in C++, folosind OOP,insa nu am vazut o asemenea libraire.Majoritatea sunt implementate cu C “chior” fara nici un fel de OOP-eala in ele:P,si asta nu din cauza ca ar fi C++ mai prost decat C,…dar de ce sa nu ai un design simplu,si usor de inteles,in loc sa folosesti contructori /destructori si alte alea care ti-ar putea complica existenta:P).Aceste “obiecte” de tip thread vor avea o functie pe care o vor executa in contextul lor(vom discuta ceva mai incol odespre ce inseamna contextul unui thread).In exemplul de fata avem 3 structuri de tip thread_t(practic avem 3 thread-uri),pe care urmeaza sa le initializam apeland rutina create_thread(….).Aceasta rutina va initializa diverse campuri din cadrul unei structuri thread_t(vom discuta mai incolo despre ce campuri exista cam orice thread,indiferent de sistemul de operare),dupa care va “porni” executia thread-ului respectiv.Astfel cele 3 entitati isi vor executa functia lor(thread1 va executa functia functie1()…samd).Aceste functii se vor executa in paralel.In cazul in care avem un sistem multiprocesor,fiecare thread se va executa pe cate un procesor din cele existente,avand astfel parte de performanta superioara fata de primul program,deoarece cele 3 functii vor fi executate in acelasi timp,in paralel(iar programul se va termina mult mai repede),fata de primul program unude dupa ce se va executa codul din functie1(),vom reveni in main(),pentru a executa functie2(),iar apoi functie3().
Eh… cam asata e toata faza cu thread-urile.Mai pe romaneste un thread reprezinta o structura de date care are asociata “o bucata de cod” pe care o va executa.Aceasta “bucata de cod” este aici reprezentata printr-o functie,care la randul ei poate apela alte 100 de functii care sa faca lucruri utile.Asta este “tot ce se vede” intr-un program multithread.Ce nu se vede este ca fiecare thread are o stiva proprie.De exemplu in primul program unde avem un singur fir de control/executie,avem o singura stiva,folosita pentru apeluri/reveniri din functii…asa cum ati invatat la orele de informatica din liceu:P.Insa in cazul celui de-al doilea program unde avem 3 thread-uri,vom avea 3 stive,cate o stiva pentru fiecare thread din cadrul programului.Astfel fiecare thread isi va apela functia lui,care la randul ei ..asa cum am mai zis poate apela ori cate functii doreste,iar apelurile,respectiv revenirile din functii(asa numitele STACK FRAME-uri) se vor face pe stiva thread-ului respectiv.Astfel la un moment dat,thread-ul 1 dupa ce a apelat functie1(),care aceasta a apelat o functie,sa-i zicem,f1(),in tot acest timp,thread-ul 2 dupa ce a apelat functie2(),care la randul ei a apelat o functie,sa-i zicem,f2(),iar thread-ul 3 dupa ce a apelat functie3(),care la randul ei a apelat o functie,sa-i zicem,f3().Astfel,fiecare thread,are pe stiva sa o anumita “istorie”(a anumita inlantuire de apeluri de functii),independent de ce a facut alt thread in tot acest timp.CAm asta inseamna “Contextul” unui thread.De exemplu daca functia calculeaza_ecuatie_diferentiala()
) ,a fost apelata de catre functia f2(),care la randul,asa cum am mai zis, a fost apelata de catre rutina de start a thread-ului 2..adica de functia functie2(),spunem ca functia calculeaza_ecuatie_diferentiala(),este executata in contextul thread-ului 2.Sper ca cu acest exmplu s-a inteles exact ce inseamna contextul unui thread.Mai pe romaneste,daca “ceva” se executa,iar stiva curenta este a unui anumit thread X,spunem ca acel “ceva” se executa in contextul thread-ului care detine stiva curenta.
Acum dupa ce am terminat cu aceasta scurta introducere despre thread-uri,si despre ce reprezinta ele,cum lucreaza,etc,sa analizam diverse modalitati de implementare si mai ales sa vedem unde pot fi ele implementate.Pentru asta trebuie sa aveti cunostinta despre ce inseamna Ring0 aka Kernel Mode si Ring3 aka UserMode.Nu voi sta sa explic aici ce inseamna asta deoarece este peste scopul acestui articol,si in caz ca m-as pune sa fac asa,articolul ar deveni astfel prea mare si probabil ca m-as pierde in alte detalii decat cele related thread-uri
La ora actuala exista 4 modele existente pe baza carora putem crea aplicatii multithreaded.Aceastea sunt:User Level Threads/Kernel Level Threads/Hybrid Implementation/Scheduler Activations.
Voi incerca se le explic pe toate pentru a vedea exact ce avantaje si dezavantaje au fiecare in parte.
Sa incepem cu User Level Threads vs Kernel Threads.
In sistemele de operare monoprocesor/monoutilizator/monotasking precum MS-DOS,avem un singur program in executie la un moment dat,iar acest program era unul cu un singur fir de executie.Pentru a lansa un alt program in executie,trebuie sa-l terminam pe cel curent.Desi acest mod de implementare ofera foarte multa performanta,din cauza ca se evita toate complicatiile ce apar in sistemele multitasking,datorita faptului ca exista mai multe “bucati de cod”(thread-uri) care se pot executa simultan si pot accesa aceleasi resurse,deci trebe sa ne luam masuri de precautie.Intr-un sistem MS-DOS nu trebuie sa va faceti griji de asa ceva.
Fig1 – Exemplu de sistem MS-DOS
Intr-un sistem precum MS-DOS avem un singur spatiu de adrese(reprezentat prin patrat) in cadrul caruia avem un singur fir de executie(reprezentat prin acea curba).
Pe langa sistemele de tip MS-DOS mai exista si sistemele de tip UNIX,unde avem mai multe procese,iar in cadrul fiecarui proces avem cate un fir de control/executie.
Fig2 – Exemplu de sistem UNIX
Un sistem UNIX,suporta multitasking,si poate rula pe o masina cu mai multe procesoare.Astfel intr-un sistem UNIX,putem avea mai multe procese,fiecare proces fiind monothread.Evident putem avea mai multe procese decat procesoare,la fel de simplu putem avea mai multe procese si un singur procesor.Kernelul sistemeului de operare,se va ocupa de partajarea timpului,intre procese.Astfel fiecarui proces i se asigneaza o cuanta de timp,iar un algoritm implementat in kernel ,numit algoritm de scheduling,va “darui” un anumit proces,procesorului.Acum procesorul va executa instructiunile din acel proces(spatiu de adrese),pana cand va expira cuanta acestui proces.Cand cuanta(timpul) acestui proces a expirat,se va invoca din nou algorimul de scheduling(planificare) care va selecta un alt proces din sistem,si ii va “darui” acestuia procesorul…si tot asa.Astfel pe un sistem monoprocesor,unde avem mai multe procese,instructiunile din fiecare proces se vor executa intr-o maniera intretesata.Acest tip de paralelism se numeste paralelism logic,deoarece kernelul se foloseste de scheduler pentru a simula executia mai multor procese in paralel.Datorita vitezei cu care un porcesor executa instruciuni,chiar se realizeaza aceasta impresie.Intr-un sistem multiprocesor,evident pe fiecare procesor,se pot executa instruciuni din procese diferite,avand astfel parte de executie in paralel “pe bune”(aici nu se mai simuleaza nimik:P),sau paralelism fizic.
Acum probabil ca va intrebati ce este acela un proces,si care este diferenta dintre un proces si un program.Well now…un proces reprezinta un porgram in exectie.Un program este o entitate statica,adica un fisier in care cineva a scris niste instructiuni,un algoritm intr-un anumit limbaj de programare,dupa care a compilat acel fisier.Asta este un porgram.Atunci cand rulati un program,sistemul de operare se va ingriji sa creeze un nou proces(entitate dinamica),in care sa ruleze respectivul program(imagine).De exemplu..daca aveti un editor de texte,acela reprezinta un porgram,insa daca il deschideti de doua orimatunci sistemul de operare,va crea cate un proces,si in fiecare proces se va rula acelasi program.Intr-un sistem de operare ,un porces este reprezentat printr-un PCB(Process Control Block),adica o structura de date,in care sunt retinute diverse informatii referitoare la procesul respectiv,unele dintre acestea vor fi folosite de scheduler ,pentru planificare(ex:prioritate,afinitate,starea procesului,etc)…pentru mai multe informatii consultati Tanenbaum-Sisteme de Operare Moderne sau William Stallings – Operating Systems Design and Principles
Mai exista si sisteme de tip Windows NT/unde putem avea mai multe procese(spatii de adresa)in cadrul caroara putem avea mai multe thread-uri.
Fig3 – Exemplu de sistem Windows NT
De ce acest fel de implementare ales de cei de la Microsoft pentru Windows NT/XP/2000/2003?….raspunsul este…fiindca este mult mai eficient…In sistemel clasice UNIX,un proces se ocupa in primul rand de gestiunea resurselor asociate acelui proces ca de exemplu spatiul virtual de adrese(de fapt asta este cea mai importamta functie a unui proces in Windows,si anume sa furnizeze un spatiu de adrese in care thread-urile sale,sa se poate executa),precum si alte resurse…tabela de fisiere deschise.Acelasi functii leofera si un proces un window,mai putin pe cea de executie.Astfel in windows s-a realizat aceasta separare,intre gestiunea resurselor si executia unui program in sine.Chiar si un program simplu ca primul programel din acest articol in windows va fi reprezentat printr-un proces cu un singur thread in interiorul sau.
Practic asta reprezinta o abstactizare.In UNIX,un proces reprezinta o abstractizare a unui procesor fizic,sau mai degraba ca sa fiu si mai corect,un porces UNIX reprezinta o abstractizare a unei masini cu memorie si un procesor(unde memoria este reprezentata de spatiul virtual de adrese al procesului,iar procesorul este reprezentat de firul de control din cadrul procesului).Cei de la Microsoft au vrut sa fie mai smecheri si iata ce a iesit…Un proces in cadrul caruia avem mai multe thread-uri,reprezinta o abstractizare a unei masini cu memorie(spatiul virtual de adrese al procesului) cu mai multe procesoare(adica thread-urile din cadrul procesului).De aici se observa ca un thread este o abstractizare a unui procesor.In general se folosesc abstractizari de genul asta
.Si ca sa va convingeti ca este corect….Avem un calculator care are 10 procesoare si o memorie partajata intre aceste procesore.Fiecare procesor va executa ceva…acel “ceva” adica acele instructiuni pe care fiecare procesor le va executa vor fi extrase din memoria partajata.La fel ca si in cazul procesului care are mai multe thread-uri….fiecare thread va fi executat pe cate un procesor,sau vor partaja un anumit procesor…iar instructiunile fiecarui thread se gasesc in acelasi spatiu de adrese furnizat de proces.
So…sper ca ati inteles care e faze cu abstractizarile astea…si cu procese /thread-uri….bla bla…acum sa trecem mai departe.Stim ce este acela un proces…. stim ca exista mai multe modalitati de integrare a thread-urilor in procese(UNIX vs WINDOWS)…linsa mai exista ceva…..unde sunt implementate thread-urile?????????
Exista mai multe “locuri” de a implementa thread-urile.De exemplu acestea pot fi implementate in Kernel Space,asa cum se intampla in Windows.Atunci cand din programul dumneavoastra creati un thread,prin intermediul apelului sistem CreateThread() din Win32API,acel thread va fi recunoscut de catre kernel,si in consecinta va fi planificat de catre scheduler(In Windows nu se planifica procesele ca in UNIX,ci thread-urile,fara sa se tina cont din ce proces fac ele parte).Windows-ul mentine o structura de date numita Dispatcher Database,care este de fapt un array cu 32 de intrari,iar fiecare intrare reprezinta capul unei liste dublu inlatuite,unde sunt inlantuite thread-urile de o anumita prioritate.
Fig4 – Exemplu de Dispatcher Database.Un array cu 4 intrari,iar fiecare intrare este capul unei liste care “leaga” toate TCB-urile(Thread Control Block) de o anumita prioritate.Alogorimul de scheduling va cauta un thread in lista cu thread-uri care au cea mai mare prioritate(in exemplul de fata in lista cu thrread-uri de prioritate 4),iar daca va gasi acolo un thread atunci va extrage thread-ul din capul listei si va face un Context Switch la el.Din cand in cand thread-urilor care au o prioritate mai mica trebuie sa li se “creasca” prioritatea,pentru a nu intra in Starvation.
In consecinta Windows,asigeanza o cuanta de timp thread-urilor si nu proceselor.Cand cuanta thread-ului curent a expirat,se va invoca scheduler-ul care va cauta un alt thread de prioritate maxima pentru a face un context switch la el.Daca nu exista nici un thread de prioritate 32,atunci se va cauta un thread de prioritate 31;daca nici asa ceva nu exista se va cauta un thread de prioritate30.. s.a.m.d.In caz ca nu se va gasi nici un thread disponibil,atunci se va face un context switch la un thread special numit IdleThread,semn ca procesorul este acuma in starea Idle.Acuma poate va intrebati cum isi da seama kernelul ca cuanta de timp a thread-ului curent a exepirat,si de fapt ce este aceasta cuanta.In orice calculator exista un timer,care intrerupe procesorul la o anumita frecventa.De exemplu pe un sistem 386..din cate stiu eu….timer-ul sistemului intrerupe procesorul la o frecventa de 50HZ(Hertz)…in traducere romaneasca adica de 50 de ori pe secunda…destul de des =)))).De fiecare data cand acest timer intrerupe procesorul,se va apela handler-ul asociat acestei intreruperi…..(cititi Intel Architecture System Programming Guide….sau orice carte buna de sisteme de operare…nu de alta dar subiectul legat de intreruperile hardware este pretty complex)…acest handler va scadea o anumita valoare predefinita din cuanta thread-ul curent.Deci cuanta unui thread este de fapt un numar intreg…de exemplu in Windows XP cuanta unui thread este egala cu 6,iar la fiecare intrerupere generata de timer-ul sistemului,handler-ul asociat acestei intreruperi va scadea 3 din cuanta thread-ului curent.In consecinta fiecare thread va rula pentru doua ticuri de ceas…..si tinand cont de faptul ca procesorului ii ia aproximativ cateva nanosecunde ca sa execute o instructiune;deci intre cele doua ticaituri de ceas,procesorul va executa suficiente instructiuni ale thread-ului.Datorita faptului ca handler-ul intreruperii de ceas este executat de foarte multe ori intr-o secunda ….intr-o secunda se pot planifica mai multe thread-uri….in exemplul anterior cu un timer ce are o frecventa de 60HZ,adica va intrerupe procesorul de 60 de ori pe secunda,iar un thread se va executa pentru doua ticuri de ceas…deci intr-o secunda vom avea 30 de context switch-uri….impresionant nu?…:P…iar asta se intampla pe un 386…deci un procesor deja antic
…in concluzie se creaza iluzia paralelismului de care vorbeam mai devreme.In concluzie scheduler-ul se va apela la fiecare doua tick-uri ale ceasului pentru a cauta alt thread eligibil de executie.Toate aceste actiuni de care am vorbit,au loc in kernel,iar asa cum am spus,un proces poate avea destul thread-uri.Daca de exemplu un thread se blocheaza in kernel datorita unui apels sitem blocant,sau intra intr-o stare de waiting,atunci scheduler-ul poate planifica alt thread,din acelasi proces,sau din alt proces.In concluzie thread-urile care sunt recunoscute de kernel sunt bine integrate in sistem.
Problemea mare a thread-urilor care sunt recunoscute de kernel este ca fiecare actiune related threads,adica crearea unui thread,distrugerea unui thread,suspendarea,yielding-ul,resuming-ul si scheduling-ul,respectiv sincronizarea thread-urilor prin mutexururi,semafoare,pipe-uri etc sunt actiuni care vor trebui facute in kernel mode.Win32API,pune la dispozitie o gramada de apeluri sistem related threads si sincronizarea intre acestea,insa fiecare apel sistem va necesita trecerea in kernel mode(din nou….cititi Intel Architecture System Programming Guide ca sa vedeti ce implica asta)..deci in aplicatiile unde exista multe thread-uri care interactioneaza foarte mult intre ele(prin primitive de sincronizare sau transmiteri/receptionari de mesaje),tranzitia din user mode in kernel mode respectiv intoarcerea din kernel mode inapoi in user mode reprezinta un overhead destul de mare.
O alta problema legata de thread-urile gestionate de kernel ete faptul ca ele trebuie sa fie cat mai generale…..adica trebuie sa fie “de toate pentru toata lumea”.De exemplu la orice context switch,se va salva si starea FPU(Floating Point Unit),alt overhead destul de mare,cu toate ca anumite thread-uri nu au nevoie de asa ceva.In consecinta thread-urile gestionate de kernele trebuie sa fie general purpose,si sa ofere destule facilitati mergand pe premiza ca cineva va folosi acele facilitati(dar nu toata lumea).
Datorita faptului ca kernel threads,manifesta performanta relativ slaba ,din cauza ca orice este legat de ele trebuie facut in kernel(necesitand cate o translatie in kernel mode la fiecare astfel de actiune)…a fost adoptat un alt model de implementare a thread-urior..si anume in user mode.Cand thread-urile sunt implementate in UserMode,acestea numai sunt cunoscute de catre kernel,deci orice operatie asupra lor(sincronizare/creare/suspendare/planificare/distrugere) vor fi facute in spatiul utilizator fara interventia kernelului.Thread-urile sunt implemnentate in user mode sub forma unei librarii(un DLL de exemplu) partajate intre mai multe procese din sistem.Atunci cand doriti sa va creati propriile thread-uri in programul dumneavoastra,si pe care sa le folositi sa faca diverse chestii,nu va trebui decat sa apelati rutinele din acel DLL sharat.De exemplu crearea unui user level thread(sau lightweight thread) ce va face prin apelul unei functii (cum era create_thread()..in cel de-al doilea programel din articolul asta) care va initializa o structura de tip thread,dupa care va introduce acest TCB intr-o lista(sau un dispatcher database asa cum este in kernelul de Windows) cu thread-uri care sunt “gata pentru executie”(sau in starea READY cum se mai zice).Apoi scheduler-ul (cel din libraria noastra,nu cel din kernel de care am tot vorbit pana acuma) va planifica acest thread(printr-un context switch).Unul dintre cele mai importante campuri din structura de tip thread sunt:un pointer la functia de start a thread-ului(pointer pe care il dam ca parametru functiei de creare a thread-ului),lista de argumente pe care o va primi functia de start a thread-ului(daca avem mai multe argumente pentru funtia de start,putem folosi un pointer la o structura care contina toate aceste argumente…asta din cauza ca functia care creaza si initializeaza un thread nu are de unde sa “stie” cate argumente vrem noi sa transmitem functiei de start,si tot ce cere ea,este un pointer,considerand ca acel pointer va fi singurul argument pe care o sa-l transmitem functiei….iar noi putem sa folosim acest pointer,ca un pointer o structura,ca sa trasmitem prin intermediul sau mai multe argument cool nu?:P).Este posibil ca functia care creaza un thread sa ceara ca parametru si un integer care sa reprezinte dimensiunea stivei thread-ului ,dacu nu…atunci se va considera o dimensiune by default…de obicei doua pagini de 4KB(depinde de libraria respectiva).O librarie ce implementeaza thread-uri in spatiul utilizator mai furnizeaza si primitive de sincronizare(semafoare/mutexuri/spinlock-uri/variabile conditie/bariere/monitoare..etc..si aici depinde de fiecare librarie in parte)..si anume funtii necesare de acquireing si releasing a acestor primitive de sincronizare.Pe langa asta,mai trebuie implementat si codul care gestioneaza wait queues(atunci cand un thread intra intr-o stare de asteptare ..datorata unui semafor spre exemplu…),precum si un algoritm de scheduling.Dupa cum puteti vedea,o asemenea librarie reprezinta de fapt un mic kernel implementat in user mode.Toate acestea vor fi folosite pentru a putea sa creati aplicatii multi thread,fara sa invocati deloc kernelul.Evident ca o aplicatie multithread care foloseste facilitatile puse la dispozitie de o asemenea librarie,va avea parte de viteze de executie mult mai bune decat aceiasi aplicatie care foloseste insa serviciile kernelului.De ce?…..este logic…deoarece aplicatia care foloseste user level threads,pentru a se bucura de multithreading,va face simple apeluri de functii din acea librarie partajata.Pe de alta parte aplicatia care va folosi serviciile kernelului, va trebui ca la fiecare aciune sa invoce kernelul printr-un apel sistem,iar un apel sistem este mult mai costisitor decat un in ciclii de procesor decat un apel de functie.Asta din cauza ca la un apel sistem sistemul de operare va invoca o instructiune speciala a procesorului(int XXX) printr-un trap gate…care va necesita un stack switch(de la stiva din user mode la cea din kernel mode),copierea argumentelor de pe UM stack in KM stack si invocarea dispatcher-ului de apeluri sistem din kernel care va trebui sa invoce o functie interna a kernelului a carui pointer este stocat intr-o tabela (KeSystemServiceTable…asa se cheama in Windows) interna..iar apoi acea rutina sa faca treaba dorita:P..deci este overhead ceva mai mare.Intr-o aplicatie care nu foloseste prea multe thread-uri putem sa folosim kernel threads…deoarece acest overhead nu este deranjant…insa intr-o aplicatie care are multe thread-uri …si unde se folosesc multe si complicate mecanisme de sincronizare este preferabil sa folosim user level threads…pentru a economisi ciclii de procesor,ce urmeaza sa fie folositi in calcule efective in loc sa fie irositi pe intrari si iesiri in/din kernel mode.
Asadar user level threads ofera performanta superioara fata de kernel level threads si mai mult de atat puteti sa folositi orice librarie disponibila pe net sau daca va suparati va puteti face singuri o librarie nou nouta,unde sa folositi ce primitive de sincronizare doriti si exact ce algoritmi de planificare doriti…deci un alt avantaj este faptul ca librariile user level threads sunt customizabile dupa preferintele fiecaruia,spre deosebire de kernel care este acelasi pentru toti
.Un alt avantaj il reprezinta faptul ca puteti avea aplicatii multi thread chiar si in sisteme precum MS-DOS.
Insa implementarile thread-urilor in user mode au anumite dezavantaje aproape dezastuoase.Sa vedem exact despre ce este vorba.
Cel mai mare dezavantaj al thread-urlor implementate in user space este faptul ce ele nu sunt cunosctute de catre kernel(nu sunt integrate in sistem).In cazul proceselor UNIX,tot ce cunoaste kernelul sunt procesele(mai precis PCB-urile acestora) ,iar el(kernelul) planifica procese.Se consideram un exemplu:Avem in proces intr-un sistem de tip UNIX,in care am creat 100 de user level threads.Planificarea acestor din urma va fi facuta de noi prin apeluri catre scheduler(thread_yield()…atunci cand thread-ul curent doreste sa lase procesorul si si planifice alt thread)…si un obiect timer(o alarma)pe baza caruia apelam scheduler-ul din librarie.Aceste thread-uri se vor executa atat timp cat procesul in care ele sunt nu i-a expirat cuanta de timp.Cand cuanta a expirat..atunci kernelul va planifica alt proces.pana aici toate bune si frumoase.Insa daca de exemplu o instructiune din cadrul unui user level thread creaza un page fault/sau un apel sistem (open() srpe exemplu) blocant…atunci procesul curent va trebui sa astepte pana cand vin informatii de la dispozitivul I/O…adica pana cand pagina solicitata a fost adusa in RAM,sau fisierul a fost deschis…deci pana atunci kernelul va pune procesul respectiv intr-o coadsa cu procese ce asteapta ceva(in starea de wait)…dece va puna procesul si nu thread-ul respectiv este evident…deoarece cum am mai zis kernelul nu stie ce sunt alea user level threads…el stie doar de procese…deci va interpreta un page fault,ca a fost generat de catre proces…indiferent ce se afla in acel proces(user level threads si ale chestii de genul asta).Asa cum ziceam in acel proces aveam 100 de thread-uri,si iata cum din cauza unui thread care a creat un apel sistem blocant sau un page fault ,si restul de 99 de thread-uri care sunt gata de executie,vor astepta,practic degeaba,deoarece ele nu au nici o vina:P…si este incorect ca si ele sa astepte.

Fig5 – Exemplu de proces in care avem mai multe user level threads gestionate de libraria partajata intre procese(Runtime System).In josul pozei(in kenrel mode) observam tabela cu porcese aflate in starea ready,ce vor fi planificate de catre kernel
Concluzia este ca thread-urile implementate in user space ofera performanta superioara fata de kernel threads si mai multa flexibilitate,insa faptul ca nu sunt cunoscute de catre kernel la face sa fie mai “proaste” decat thread-urile recunoscute de kernel.
Acum ca am vazut ce sunt user level threads,ce suntkernel level threads,si ce avantaje si ce dezavantaje are fiecare din aceste doua modalitati de implementare,sa vedem daca se poate face ceva.Ce ne-am putea dori in acest moment…evident sa avem thread-uri care sa aiba performanta celor implementate in user mode,dar cu un comportament “normal” ca thread-urile din kernel,adica sa nu existe thread-uri blocate aiurea din cauza altui thread.La prima vedere asa ceva pare imposibil…da de fapt este posibil si mai mult de atat fara vreun efort supraomenesc
…let’s see.Insa inainte de a trece mai departe ..adica la a explica ce sunt scheduler activations…scopul principal al acestui articol….sa vedem diverse mapari intre user level threads si kernel level threads.
Fig6 – Mapare Many:One(n:1) intre ULT si KLT.Avem mai multe User level threads care se vor executa in contextul unui singur thread recunoscut de kernel .In Windows atunci cand ce creaza un proces automat se va crea si un thread care ruleze codul acelui program.Acel program poate crea mai multe thread-uri(prin apelul CreateThread) sau poate crea mai multe FIBERS(asa se numesc user level threads in Windows 2000/2003/XP).Astfel aceste fibere se vor executa in contextul respectivului thread.Apar aceleasi probleme …daca un fiber se blocheaza in kernel din cauza unui apel sistem sincron(ReadFile de exemplu)…atunci kernelul va interpreta acest lucru in felul urmator…va pune respectivul thread sa astepte pana cand I/O Manager-ul si sistemul de fisiere si-au terminat treaba(atunci cand HDD va intrerupe CPU)…blocandu-se astfel toate fiberele din acel thread.

Fig 7 – Mapare One:One(1:1) intre ULT si KLT…exista cate un kernel thread pentru fiecare user level thread…

Fig8 – Mapare Many:Many(m:n) intre ULT si KLT….Libraria care implementeaza thread-urile in user mode ofera facilitatea de a migra un user level thread de la un kernel level thread la altul.De exemplu in cadrul unui proces avem mai multe user level threads sa zicem in numar de m. si n kernel threads,unde n<m.Astfel fiecare kernel level thread are un pool propriu de user level threads care se vor executa in co9ntextul acelui kernel level thread.Daca un user thread se va bloca in kernel ,sau va produce un page fault,atunci kernelul va pune in waiting respectivul kernel thread si va planifica un alt kernel thread,care are si el propriul pool de user threads.Pe langa aceasta libraria de user level threads mai ofera facilitatea de a face load balancing intre aceste pool-uri de user threads,astfel un user level thread poate fi migrat de la un kernel thread la alt kernel thread,si dupa aceasta el se va executa in contextul acelui kernel thread.Aceasta implementare incearca sa combine sa performanta user level threads si integrarea kernel level threads,insa nici aceasta nu ofera performanta buna datorita faptului ca anutite thread-uri care sunt gata de executie vor fi blocate de un anumit thread care a creat un page fault ,deci in consecinta kernelul va bloca kernel thread-ul in care se executau acele user threads.Aceasta modalitate de implementare se numeste implementrare hibrida si a fost implementata in sistemul de operare SOLARIS produs de firma SUN.
Vom explica acum o metoda foarte buna pentru a beneficia de performanta thread-urilor implementate in spatiul utilizator,dar in acelasi timp sa nu mai avem parte de problemele acestora legate de faptul ca nu sunt cunoscute de catre kernel.Principala abstractizare folosita este scheduler activations,care este un soi de kernel thread,dar care are cateva proprietati interesante.In aceasta noua metoda kernelul este folosit in principal pentru a raporta aplicatiilior care folosesc user level threads,diverse evenimente care ar putea afecta starea,sau ar putea bloca mai multe user level threads.Acest mecanism de notificare folosit de kerenel se numeste UPCALL,asemanator semnalelor din UNIX/LINUX sau a APC-urilor din Windows




