Pojedynek schedulerów: Windows NT vs Linux kernel 2.6

Spis treści


Wstęp

Po Co jest scheduler? W systemach w których działa wiele procesów, potrzebny jest mechanizm zmiany konktekstu ('context switch'). Oznacza to wywłaszczenie procesora od aktualnie wykonywanego procesu i przekazanie go innemu oczekującemu procesowi. Zmiana kontekstu jest kosztowna i sama w sobie jest bezużyteczna dla użytkownika. Dlatego należy kierować sie jakimiś zasadami przy wybieraniu następnego procesu do wykonania na CPU. Za tę akcję (wybór i ogólne zarządzanie) w systemie operacyjnym odpowiedzialny jest scheduler.

Scheduler w Windows NT

Na początek dowiemy się jak działa scheduler w systemach opartych na Windows NT. Windows NT to rodzina systemów Windows typu New Technology firmy Microsoft. Wszystkie wersje systemów z rodziny NT (za wikipedia.org):


Wprowadzenie do schedulera w Windowsie NT

Podstawową jednostką szeregowania w Windows NT jest wątek. Wątki to częsci procesów. Kilka wątków może działać w jednej przestrzeni adresowej procesu. Programy mogą być jedno-procesowe, a procesy jedno-wątkowe. Wtedy sytuacja jest oczywista. Jednak równie naturalną sytuacją są programy wielo-procesowe, których procesy są wielo-wątkowe. Wtedy z punktu widzenia systemu operacyjnego i synchronizacji sytuacja jest najtrudniejsza. Natomiast scheduler traktuje wszystkie wątki niezależnie. Nie ma znaczenia, czy działają w ramach jednego procesu czy programu. Scheduler rozpatrzy je oddzielnie.

Scheduler Windowsa NT używa priorytetów od 0 do 31. Im większa wartość, tym wyższy priorytet.
Wątek o piorytecie 0 to wątek typu Idle lub Proces Bezczynności.
Wątki o priorytetach od 1 do 15 to wątki typu dynamic.
Wątki o priorytetach od 16 do 31 to wątki typu realtime.

Jeżeli programujemy jakiś wątek i chcemy nadać mu priorytet, nie możemy tego zrobić w sposób bezpośredni - liczbowy. Interfejs Win32 API udostępnia dwustopniowe, w pewnym sensie opisowe nadawanie priorytetów. Najpierw ustawiamy tzw. priority class wątku (w nawiasach wartości priorytetu).
Mamy tutaj do wyboru: idle (4), normal (8), high (13) i realtime (24). Te wartości są modyfikowane przez tzw. relative priority, które są dodawane do priority class (w nawiasach wartości).
Mamy tutaj do wyboru: lowest (-2), below normal (-1), normal (0), above normal (+1) i highest (+2).

Tak wygląda struktura priorytetów:

Priorytety

A tak wyglądają priorytety relatywne:

Relative priority

Interfejs Win32 API dostarcza również 2 modyfikatory specjalne. Są to idle i timecritical.
Timecritical powoduje, że wątek typu dynamic dostaje priorytet 15, a wątek typu realtime dostaje priorytet 31. Są to maksymalne wartości dla tych typów.
Idle powoduje, że wątek typu dynamic dostaje priotytet 1 a wątek typu realtime dostaje priorytet 16. Są to minimalne wartości dla tych typów (0 jest zarezerwowane dla specjalnego wątku który odpowiada za bezczynność).

Scheduler przydziela wątkom kwanty, tak by wątki mogły się wykonywać. Kwanty to czasy jakie przysługują wątkom na pobyt procesorze (o ile się do niego dostaną i nie zostanę wyrzocone przed czasem przez inny proces). Wiadomo, że dlugość kwantu musi być odpowiednio dobrana (przypomnijmy sobie problem udawania wspólbieżności przez system operacyjny oraz problem związany z kosztem zmiany kontekstu przy dużym ruchu wątków w procesorze).
Systemy typu NT Server i NT Workstation mają różne polityki dotyczące długości kwantu.
W NT Server kwanty wszystkich wątków wynoszą 120ms.
W NT Workstation kwanty wątków działających w tle wynoszą 20ms. Natomiast kwanty wątków "aktywnych" wynoszą od 20 do 60ms (patrz rysunek).

suwak kwantow


Scheduling na jeden procesor

Kilka słów na temat tego co będzie robił scheduler w Windowsie NT. Gdy kwant bieżącego wątka zostanie wyczerpany (a wątek się jeszcze nie zakończył), to scheduler wywoła algorytm FindReadyThread na stukturze Dispatcher Ready List (o niej za chwile) i sprawdzi czy nowy wątek ze stuktury ma wyższy priorytet od bieżącego. Jeżeli tak, to podmienia, jeżeli nie, to stary wątek zostaje w procesorze, a nowy wraca spowrotem do kolejki.

Wątki które zawieszają się w oczekiwaniu na zdarzenie, "marnują" kwant, odchodzą do kolejki wątków czekających na I/O i system wywołuje FindReadyThread. Po aktywacji wątka który czekal w kolejce I/O (tzn. po tym jak otrzymał zasoby/akcje) jest on traktowany tak, jakby był nowym wątkiem.

Jeżeli pojawia się nowy wątek, scheduler wywołuje algorytm ReadyThread, który mówi czy wątek ma przejąć do kolejki czy zacząć się wykonywać.

Żeby zobaczyć jak działają algorytmy FindReadyThread i ReadyThread zobaczmy czym jest i jak działa struktura Dispatcher Ready List (rysunek wraz z pokazaniem działania FindReadyThread):

DRL

Oto dokładniejszy opis Dispatcher Ready List, FindReadyThread i ReadyThread.
Dispatcher Ready List to struktura o 31 polach odpowiadających priorytetom wątków. Każde pole jest kolejką GOTOWYCH wątków o tym priorytecie.
FindReadyThread znajduje pole o najwyższym priorytecie w Dispatcher Ready List (idzie od góry DRL) w którym jest niepusta kolejka i wyjmuje z niej pierwszy wątek.
ReadyThread zajmuje się wstawianiem do Dispatcher Ready List. Algorytm dostaje wątek gotowy do wykonania i sprawdza czy ma on wyższy priorytet niż bieżący wątek wykonujący się w procesorze. Jeżeli tak, to podmienia bieżący wątek. Przerwany wątek jest kierowany na DRL. Wpp. bieżący wątek wykonuje się nadal, a nowy idzie na DRL. ReadyThread umieszcza wątek w kolejce w odpowiednim polu DRL. Jeżeli wątek wykorzystał już co najmniej jeden kwant, to idzie na początek kolejki, wpp. na koniec.

Priorytety wątków typu dynamic mogą być modyfikowane przez system (w przypadku wątków realtime system może używać tylko modyfikatorów typu time-critical i idle opisanych wcześniej).
Jeżeli następuje zdarzenie na które czekał zablokowany wątek typu dynamic, to dostaje on bonus do priorytetu.

Uwaga!
Górną granicą priorytetu dynamicznego jest 15. Tej granicy nie można przekroczyć (tzn. właściwy priorytet to min(prio + boost, 15)).

Jeżeli wątek otrzymał jakikolwiek bonus, to za każdym razem gdy wątek przerobi kwant, jego bonus maleje o 1. Pamiętajmy jednak, że maleje tylko jego bonus (czyli po jakimś czasie bonus będzie wynosił 0). Jego piorytet pierwotny (czyli priority class + relative priority) pozostaje nieruszony.

Bez zewnętrznej kontroli algorytmy FindReadyThread i ReadyThread mogą zagładzać wątki o niskim priorytecie (ich kolejki nigdy nie będą nawet dotykane przy FindReadyThread).
NT Balance Set Manager to mechanizm uruchamiany co sekundę. Uruchamia algorytmScanReadyQueues.
ScanReadyQueues skanuje DRL (od priorytetu 31 w dół). Szuka wątków które nie miały procesora przez ostatnie 3 sekundy i kiedy taki znajdzie, daje mu bonus. Jego kwant zostaje podwojony, a priorytet dostaje nawiększą możliwą wartość dla wątków typu dynamic (czyli 15, taka jak modyfikator time-critical). Gdy wątek skonsumuje przedłużony kwant, jego priorytet i długość kwantu wracają do poprzedniego stanu.


Scheduling na wiele procesorów (SMP) - soft affinity

Technologia NT wspiera SMP (symmetric multiprocessing), czyli architekturę opartą na wielu takich samych procesorach działających rownorzędnie.
Na następnym obrazku widać różnicę między SMP a ASMP (asymmetric multiprocessing).

SMP i ASMP

Struktury danych i algorytmy zawarte w systemach operacyjnych z rodziny Windowsów NT mogą obsługiwać nawet 32 procesory. Jednak NT Workstation ogranicza liczbę procesorów do 2, a NT Server do 4 procesorów. Jest to wynik polityki marketingowej - bogatsza wersja wymaga odrębnej licencji OEM (w niektórych opracowaniach znajdują się informację o 8 procesorach).
Kilka podstawowych faktów z działania schedulera:

Strategia realizowana przez scheduler NT dla wielu procesorów nazywa się soft affinity. W tej metodzie wątek pamięta:

FindReadyThread jest uruchamiany na konkretnym procesorze, kiedy jest on przygotowany do przyjęcia wątku. FindReadyThread zakłada blokadę na DRL i skanuje DRL od najwyższego priorytetu w celu znalezienia niepustej kolejki z wątkiem który ma wpis w hard affinity z danym procesorem. Znaleziony wątek jest markowany znacznikiem primary candidate. Jeżeli znaleziony kandydat spełnia jeden z warunków:

to procesor zaczyna go wykonywać. Wpp. szukamy następnego wątku który spełnia któryś z powyższych warunków (oczywiście bierzemy pod uwagę tylko te które mają wpisany szukający procesor w hard affinity). Jeżeli nie znajdziemy żadnego, to procesor dostaje wątek który oznaczyliśmy na samym początku (primary candidate). Po wszystkim FindReadyThread zwalnia blokadę na DRL.

Zobaczmy na rysunek:

FindReadyThread w soft affinity

ReadyThread jest uruchamiany z konkretnym wątkiem, kiedy przychodzi jako nowy, lub staje się gotowy. ReadyThread zakłada blokadę na DRL i sprawdza, czy któryś procesor z hard affinity wątka nie jest wolny (tzn czy procesor w tej chwili nie wykonuje jakiegoś innego wątka). Jeżeli taki wolny procesor się znajdzie, to koniec - sprawa jest załatwiona i procesor wykonuje dany wątek. Jeżeli nie ma wolnych procesorów z hard affinity wątka, to scheduler patrzy na ideal processor wątka (lub jeżeli nie ma ideal processor, to patrzy na ostatnio używany procesor). Jeżeli priorytet wątka na tym procesorze jest niższy od naszego wątka, to procesor przestaje wykonywać poprzedni wątek i zaczyna wykonywać nowy (tzn, ten z którym został uruchomiony ReadyThread). Wpp. nasz wątek wędruje do kolejki w odpowiednim miejscu na DRL Na sam koniec ReadyThread zwalnia blokadę na DRL.

Istnieją scenariusze, które pokazują, że strategia nie jest optymalna ze względu na operacje (np. pokazują, że lepiej jest wymieniać wątek o najniższym priorytecie, nie patrząc gdzie ostatnio wykonywał się proces), ale w przypadku schedulingu optymalność operacji w pesymistycznych scenariuszach to tylko jeden z wyznaczników.
Generalnie, strategia soft affinity jest używana z powodu dobrej współpracy z bazą SQL Server i na tej podstawie bada się skalowalność (tzn. czy zwiększanie liczby procesorów powoduje spodziewane zwiększenie wydajności). I tam sprawdza się ona dobrze.
Ciekawostką jest to, że scheduler powstał najpierw dla wersji wielo-procesorowej, a potem została wyprodukowana wersja obcięta na potrzeby jednego procesora. To może w pewnym stopniu tłumaczyć udziwnienie wersji schedulera również w wersji jedno-procesorowej. Niestety powoduje to wadę, że stacje klienckie, którym wieloprocesorowość i baza SQL Serwer jest daleka, często nie działają tak dobrze, jak byśmy się tego spodziewali.

Informacje n/t schedulera NT wraz z obrazkami pochodzą ze strony: http://www.windowsitpro.com .


Scheduler Linuxa (jądro) 2.6

Jądro 2.6 wprowadza kilka istotnych zmian do Linuxa. Jedną z nich jest przebudowanie systemu zarządzającego kolejkowaniem - schedulera. Zmiany są duże, a w schedulerze może nawet rewolucyjne, patrząc na politykę jaką scheduler kieruje się w nowej wersji jądra w porównaniu z tą z poprzedniego.


Po co nowy scheduler w jądrze 2.6 ?

Linux od czasu swego powstania zdominował rynek systemów operacyjnych dla serwerów. Odcisnęło się to na jego architekturze, która jest inna, niż systemy Microsoftu, lub Apple'a. Jednakże ostatnimi czasy, coraz więcej entuzjastów Linuxa twierdzi, że jest to także dobry system dla stacji roboczych (biurowych). Między innymi dlatego powstał scheduler 2.6.x (napisany przez Ingo Molnara) - aby zaspokoić wymogi rynku systemów operacyjnych typu desktop.

Oprócz tego mamy nastepujące założenia projektowe dla schedulera: - ma być wydajny (czas zużyty na jego aktywność jest czasem zmarnowanym z punktu widzenia użytkownika) - ma być interakcyjny - czyli szybko reagować na działania użytkownika - ma być w miare sprawiedliwy i nie dopuszczać do zagłodzeń - ma wspierać technologie SMP (Multiprocesorową - wyjaśnienie w dziale scheduler Windows NT) - ma wspierać procesy soft real-time

Różnie można mierzyć wydajność schedulera - jedni chcieliby, żeby jak najefektywniej zarządzal pracą procesów, inni chcieliby wysokiej interaktywności (która pociąga za sobą dużą liczbę zmian kontekstów) Scheduler 2.6.x pozwala na dostrajanie ręczne (tzw tuning) , czyli można nim zaspokoić rynek HPC (high-performance computing) oraz rynek desktop (aplikacje biurowe - szybki czas reakcji), a także pozostający po środku rynek serwerowy.


Jak wygląda nowy scheduler Linuxa 2.6

Ważny fakt !! Scheduler dziala w czasie O(1) dzieki zastosowaniu nowych struktur danych. Sa to Runqueue oraz Priority Arrays.

Zasada dzialania. Scheduler posiada dwie tablice priority arrays - active, expired. Priority arrays służą do trzymania w nich procesów (opriorytetowanych) i wyciągania z nich procesu o największym priorytecie w czasie O(1)
Idea dzialania schedulera jest taka:
Bierzemy proces o największym priorytecie z tablicy active, dajemy mu procesor, jak zużyje swój kwant czasu, to obliczamy mu nowy kwant i wrzucamy do tablicy expired.
Czynność wykonujemy, aż się skonczą procesy z tablicy active - wtedy zamieniami nazwami tablice expired<->active i zaczynamy od nowa. Do tego dochodzą triki o których poźniej.

Runqueue jest to struktura danych, ktora dla kazdego procesora trzyma informacje o procesach wykonujacych sie na nim. W szczegolnosci posiada pola:

prio_array_t arrays[2]
prio_array_t *active
prio_array_t *expired

Podmienianie wskaźników działa w czasie O(1). O ile wyciąganie procesu o największym priorytecie działa w czasie O(1) to nasz powyższy algorytm przedstawiający działanie schedulera też działa w czasie O(1).

Priority Arrays

Są to tablice do trzymania i odnajdowania procesu o największym priorytecie. Zasada działania jest taka:
trzymamy tablicę bitów, gdzie bitów jest tyle ile priorytetów

unsigned long bitmap[BITMAP_SIZE]
która to bitmapa ma mówic, czy jest proces o danym priorytecie (conajmniej jeden). Z takiej bitmapy możemy wyciągnąc w czasie O(1) pierwszy bit od lewej (a O(1) wynika z tego, że liczba różnych priorytetów jest stała i nie zależy od ilości procesów działających w systemie).

Teraz potrzebujemy jeszcze tablicy

struct list_head queue[MAX_PRIO]
w której to tablicy trzymamy listy procesów o danym priorytecie. Warto zauważyć, że jeśli mamy kilka procesów tego samego priorytetu, to bedą one dorzucane na koniec kolejki, co powoduje, że bedą dopuszczane do CPU strategią round-robin.

Priorytety

przy czym mniejsza wartosc, oznacza większy priorytet.

Prorytety procesów RT są obliczane statycznie - od razu dostają jakiś (stały) priorytet z zakresu 0..99 i z takim priorytetem się wykonują. Przy czym proces RT moze byc szeregowany jedna z dwoch strategii (uzytkownik decyduje) : FIFO - procesy RT o większym priorytecie najpierw (wywłaszczają procesy o mniejszym priorytecie) i dostają procesor aż się wykonają. RR - najpierw bedą się wykonywać procesy z wyższym priorytetem, a jeśli jest kilka o tym samym, to przyznawane im bedą rowne kwanty czasu i bedą cyklicznie dostawać procesor).

Priorytet zwykłych procesów składa się z:

Ad 1) mapuje priorytet procesu od 100-140.
Ad 2) pozwala na maksymalną modyfikację w wysokości 1/4 zasięgu (40) czyli proces może uzyskać albo -5 albo +5 za swoją interakcyjność/nieinterakcyjność.
Oczywiscie procesy te nie mogą wyjechać poza zakres 100..140.

Dynamiczny priorytet jest liczony w momentach kiedy dany proces się budzi, albo na żądanie scheduler_tick(). W każdym razie przelicza się tylko jeśli zmienił się średni czas spania (który to jest głównym elementem heurystyki z pkt. 2) ).

Kwanty czasu

Kwanty czasu liczone są prosto, mapuje się priorytet w wartości z MIN_TIMESLICE..MAX_TIMESLICE

Interaktywność

Jeśli dany proces jest uważany za interaktywny (często zasypia czekając na I/O), to o ile nie ma w tablicy expired procesu o większym priorytecie, wstawiamy go spowrotem do tablicy active zamiast do tablicy expired.

Wsparcie SMP

Scheduler 2.6.x wspiera technologię multiprocessingu, głównie dzięki mechanizmowi load_balancing, który jest niczym innym niż odciążaniem najbardziej zapracowanego procesora przez inne, mniej obciążone procesory. Oczywiście, load_balancing w Linuxie 2.6.x stara się przenosić jak najkorzystniej procesy, czyli żeby miały blisko pamieć z której chcą korzystać i inne zasoby. Odpowiadaja za to tzw. scheduler_domains, o których nie będę pisał, ponieważ są nieciekawe.

TIMESLICE_GRANULARITY

Aby wesprzeć jeszcze bardziej interakcyjność, wprowadzono stałą TIMESLICE_GRANULARITY. Odpowiada ona za to, że jeśli naraz mamy w tablicy active kilka procesów o tym samym priorytecie (dynamicznym) to zamiast dać cały kwant do wykorzystania pierwszemu z listy, potem drugiemu itp, to szeregujemy je algorytmem RR z kwantem równym TIMESLICE_GRANULARITY (i kładziemy spowrotem do tablicy active).


Dostrajanie schedulera Linuxa 2.6

Możliwości dostrajania schedulera

Na początku pliku kernel/sched.c znajduje się kilka makr (począwszy od MIN_TIMESLICE), które można edytować w celu zmiany działania schedulera Linuxa. Po ich zmianie konieczna jest ponowna kompilacja jądra (nie istnieje przy tym żaden sensowny sposób na ich zmianę w skompilowanym jądrze).

Należy pamiętać, że wprowadzone zmiany mogą mieć bardzo zróżnicowany efekt w zależności od środowiska, w jakim jądro będzie pracować. Najlepiej więc dostrajanie schedulera przeprowadzić metodą prób i błędów, za każdym razem obserwując zachowanie systemu w docelowym środowisku pracy.

MIN_TIMESLICE, MAX_TIMESLICE oraz DEF_TIMESLICE

Makro MIN_TIMESLICE określa najmniejszy kwant czasu, jaki wątek może otrzymać. MAX_TIMESLICE określa maksymalny kwant czasu jaki może przyznać scheduler. Średni kwant czasu jest określony przez uśrednienie wartości MIN_TIMESLICE oraz MAX_TIMESLICE. Zwiększenie wartości dowolnego z tych makr zwiększy średnią długość przedziału czasu przyznawanego procesom.

W najnowszych wersjach jądra nastąpiły zmiany w makrach określających przedziały czasu. Zamiast MAX_TIMESLICE mamy makro DEF_TIMESLICE określające domyślny kwant czasu który wątek może otrzymać. Domyślny maksymalny kwant czasu wzrósł z 200 milisekund do 800 milisekund. Ponieważ wartość HZ może być zmieniana podczas konfiguracji jądra wprowadzono też ograniczenie aby wartość MIN_TIMESLICE była niemniejsza od czasu jednego cyklu zegara.

Większe przedziały czasu oznaczają większą efektywność całego systemu ponieważ przełączanie kontekstu będzie wykonywane rzadzej. Warto jednak zwrócić uwagę na fakt, że procesy intearkcyjne mają wyższy dynamiczny priorytet od procesów wykonujących intensywne obliczenia. Jest więc bardzo prawdopodobne, że wywłaszczą one te drugie i to bez względu na wartość MIN_TIMESLICE i DEF_TIMESLICE. Duże przedziały czasu nie powinny więc znacząco wydłużyć czasu reakcji procesów interakcyjnych. Za to w przypadku dużej ilości procesów te z niskim priorytetem będą musiały czekać dłużej na swoją kolej. Gdyby jednak większość procesów pracowała z tym samym dynamicznym priorytetem czas reakcji byłby jeszcze dłuższy ponieważ żaden proces nie mógłby wywłaszczyć innego.

PRIO_BONUS_RATIO

Makro to określa jaką część (ile procent) możliwych wartości priorytetów proces może zyskać lub stracić przy wyliczaniu priorytetów.
Domyślnie makro to ma wartość 25. Ponieważ mamy 20 dodatnich i ujemnych wartości priorytetów domyślnie proces może dostać bonus (lub karę) maksymalnie o 5 poziomów.

Wartość tego makra określa na ile statyczne wartości nice są brane pod uwagę przy wyliczaniu priorytetów. Zwiększenie tej wartości powoduje, że nawet ustawienie dość dużej wartości nice mogą mieć mniejszy efekt niż podbicie priorytetu przez scheduler dla innego procesu.
Jeśli to makro ma małą wartość wartość nice ma większe znaczenie przy wyliczaniu priorytetów.

MAX_SLEEP_AVG

Im większa wartość tego makra, tym więcej czasu proces musi spełnić zawieszony aby był uznany za proces interakcyjny.
Zwiększenie tej wartości prawdopodobnie spowoduje wydłyżenie czasu reakcji procesów interakcyjnych. Taka sytuacja może być pożądana w systemie z nieinterakcyjnymi procesami ponieważ spowoduje to jednakowe traktowanie wszystkich procesów. Wydajność systemu może przy tym wzrosnąć ponieważ oznacza to rzadsze podbijanie priorytetu a przez to rzadsze wywłaszczanie.

STARVATION_LIMIT

Interakcyjne procesy są wstawiane na początek tablicy uporządkowanej według priorytetów. Może to doprowadzić do zagłodzenia innych procesów. Jeśli pewien proces nie wykonywał się przez czas dłuższy niż STARVATION_LIMIT interakcyjny proces jest wstrzymywany aby ten pierwszy mógł skorzystać z procesora. Zmniejszenie tej wartości powoduje wzrost czasu reakcji procesów interakcyjnych ponieważ częściej muszą one ustępować procesor innym procesom. Jednocześnie podział czasu procesora staje się bardziej sprawiedliwy. Zwiększenie tej wartości skraca czas reakcji procesów interaktywnych kosztem pozostałych procesów.


Porównanie z schedulerem w jądrze 2.4

W porównaniu z jądrem 2.6 algorytm szeregowania zadań jest dość prosty i nie zajmuje wiele miejsca (około trzy razy mniej kodu).

Wady schedulera 2.4

Niska skalowalność

Scheduler jądra 2.4 działa w czasie liniowym względem ilości procesów. Powoduje to, że obciążenie systemu związane z szeregowaniem zadań jest duże jeśli działa wiele procesów. Ponieważ scheduler musi przeiterować każdy proces przynajmniej raz, należy spodziewać się długich okresów czasu kiedy procesor nie wykonuje nic produktywnego. Ten problem rozwiązano w jądrze 2.6 przez wprowadzenie algorytmu działającego w czasie stałym. W jądrze 2.6 nowy kwant czasu dla procesu jest wyliczany kiedy ten zużyje swój dotychczasowy przedział. Jądro 2.4 wylicza te wartości dla wszystkich procesów jednocześnie po zakończeniu epoki. Struktury użyte w jądrze 2.6 pozwalają sprowadzić szukanie procesu o najwyższym priorytecie do znalezienia w bitmapie pierwszego bitu równego 1. Jądro 2.4 musi przeiterować po wszystkich procesach w celu wybrania tego o najwyższym priorytecie.

Długie kwanty czasu

Średni kwant czasu przyznawany przez jądro 2.4 trwa około 210ms. To całkiem długo w porównaniu ze średnim kwantem czasu przyznawanym przez jądro 2.6 (100ms). Ta wartość jest za duża dla systemów pracujących z dużą ilością procesów. Czas, przez który procesy o niskim priorytecie nie mają dostępu do procesora może być zbyt długi. Zakładając, że mamy w systemie 100 wątków i wszystkie zużywają cały kwant czasu (210ms) pechowy wątek będzie oczekiwał ponad 20 sekund na wolny procesor. Ten efekt został zmniejszony w jądrze 2.6 dzięki krótszym kwantom czasu. Do wywołania tego efektu potrzeba tam dwa razy większej ilości procesów. Jednocześnie warto zauważyć, że wywłaszczanie - o ile pomaga interkatywnym (wykonującym mało pracy) procesom utrzymać krótki czas reakcji - nie daje nic w przypadku procesów które nie pracują jako interaktywne a jednocześnie nie mogą długo oczekiwać na procesor. Dobrym przykładem jest tutaj serwer WWW, który musi wysłać odpowiedź w odpowiednio krótkim czasie (inaczej wystąpi timeout i klient się rozłączy).

Podbijanie priorytetu wątków wykonujących operacje wejścia-wyjścia

Preferowanie zadań wykonujących głównie operacje wejścia-wyjścia ma szereg wad.
Po pierwsze - nieinterkacyjne wątki wykonujące głównie operacje wejścia-wyjścia mają podwyższony priorytet nawet jeśli nie jest to potrzebne. Przykład: baza danych pobierająca lub zapisująca dużo danych.
Po drugie - wątki które mają charakter interakcyjny i jednocześnie wykonują dużo obliczeń mają długie czasy reakcji. Efekt podbicia priorytetu jest niwelowany przez "karę" nakładaną na procesy intensywanie korzystające z procesora. Taki charakter ma wiele programów "multimedialnych" i to zachowanie schedulera nie jest dopuszczalne z punktu widzenia współczesnego użytkownika komputera osobistego.

Rozwiązanie powyższych problemów wymaga innego sposobu na określenie interakcyjności wątku. Czas, jaki proces spędza zawieszony nie jest dobrym kryterium.

Aplikacje czasu rzeczywistego

Jądra 2.4 nie można wywłaszczyć i oznacza to brak wsparcia dla aplikacji systemu rzeczywistego. Powoduje to występowanie okresów, kiedy gotowe do wykonania procesy czasu rzeczywistego nie mogą być wykonane. Takich opóźnień nie może mieć system czasu rzeczywistego. Jądro 2.6 może być wywłaszczone więc aplikacje systemu rzeczywistego mogą pracować dużo lepiej. Nie można jednak wywłaszczyć jądra w dowolnym momencie i dlatego zalicza się Linuxa 2.6 do kategorii soft-realtime.

Jaki jest wynik tej konfrontacji?

Jedną z głównych zalet Windows NT (i pochodnych) które mogłem znaleźć był "krótki" czas reakcji na interakcje użytkownika. Niektórzy zaliczają wręcz Windows NT do kategorii hard-realtime. Jak to wygląda w praktyce?

W praktyce opóźnienia są małe jeśli obciążenie systemu jest małe. Wystarczy jednak uruchomić jakiś program wykonujący intensywnie obliczenia (np mathlab lub mencoder) i opóźnienia w reakcji można liczyć w sekundach lub nawet minutach. Z całą pewnością nie można w ten sposób spełnić wymagań systemu czasu rzeczywistego. W takich warunkach jądro Linuxa (nawet baardzo stare:->) pracuje dużo lepiej. Oczywiście takie zachowanie jest spowodowane nie tylko konstrukcją schedulera lecz także (a może przede wszystkim) wadliwą architekturą. Spośród wad schedulera WinNT warto wymienić:

Zbyt mało możliwych wartości priorytetów

Teoretycznie mamy możliwe 32 priorytety. Priorytet 0 jest zarezerwowany dla procesu bezczynności. Priorytety 31-16 są zarezerwowane dla procesów czasu rzeczywistego. Priorytety 1-15 są wykorzystywane przez "zwykłe" procesy. W niektórych zastosowaniach to zdecydowanie za mało.

Sposób określania które procesy są interakcyjne

Przyjęty algorytm jest dość podobny to stosowanego w jądrze 2.4. Miarą interaktywności procesu ma być czas przez który proces korzysta z procesora. Takie podejście było wystarczające dawno temu (np. dla edytorów tekstu) ale okazuje się niewystarczające chociażby dla aplikacji multimedialnych - interaktywnych i jednocześnie wykonujących intensywne obliczenia.

Warto też wymienić następujące wady, wynikające nie tylko z zachowania schedulera:

Inwersja priorytetów związana z obsługą GUI

Inwersja priorytetów to zjawisko występujące kiedy wykonuje się kilka procesów i procesy o różnych priorytetach komunikują się ze sobą. Może się okazać że proces o wysokim priorytecie nie może się wykonywać ponieważ czeka na odpowiedź procesu o niskim priorytecie, który z kolei jest ciągle wywłaszczany przez trzeci (mniej ważny niż pierwszy) proces. W rezultacie będzie się wykonywał głownie trzeci proces, mimo tego że pierwszy ma wyższy priorytet.

Komunikacja aplikacji z interfejsem użytkownika odbywa się za pomocą synchronicznych wywołań systemowych. Powoduje to zawieszenie programu wykonującego wywołanie systemowe do czasu zakończenia wywołania. Nawet jeśli program wykonywany był z wyższym niż domyślny priorytet (być może realtime) i tak będzie musiał czekać na obsłużenie przez interfejs który pracuje z domyślnym priorytetem.

Wadliwy mechanizm obsługi przerwań

Duże opóźnienia mogą powstawać na skutek mechanizmu obsługi przerwań. Czas działania procedury obsługi przerwania miał być skrócony do minimum. Miało to zapewnić krótki czas reakcji na przerwania. Handler wykonuje jedynie kolejkowanie DPC (Deferred Procedure Call; wykorzystuje kolejkę prostą!) i kończy działanie. Cała praca związana z obsługą przerwania jest wykonywana właśnie przez te procedury. Wykonanie odbywa się w kontekście jądra przed wszystkimi czekającymi procesami. To podejście ma następujące skutki:

  1. Czas działania handlera przerwania jest bardzo krótki.
  2. Gotowy wątek może długo oczekiwać na rozpoczęcie wykonywania ponieważ wywołania DPC są realizowane w pierwszej kolejkości i może ich ciągle przybywać.
  3. Umieszczając DPC w kolejce traci się informacje o priorytetach, nie działa maskowanie przerwań. Nietrudno więc wyobrazić sobie sytuację, gdy procedura obsługująca ważne przerwanie będzie ciągle przerywana handlerami przerwań o dużo niższym priorytecie. Ten problem rozwiązano w Windows CE - zastosowane tzw. dziedziczenie priorytetów.