Systemy Operacyjne – semestr drugi

Wykład trzeci

Szeregowanie procesów w Linuksie

Istnieją dwa typy systemów wielozadaniowych: systemy wielozadaniowe z kooperacją (bez wywłaszczania) i systemy wielozadaniowe z wyw a ł szczaniem. W systemach

pierwszego typu proces zawsze dobrowolnie zrzeka się procesora i oddaje sterowanie do systemu operacyjnego. Drugi typ systemów kontroluje wykorzystanie procesora przez realizowane zadania i mo e

ż przerywać ich działanie. Zapobiega to sytuacji, w której pojedynczy proces mógłby zmonopolizować dostęp do procesora. Linux jak większoś

ć wspó c

ł zesnych systemów operacyjnych realizuje wielozadaniowość z wywłaszczaniem.

Schemat szeregowania jaki zastosowano w tym systemie opiera się na schemacie wielopoziomowych kolejek ze sprz en ęż iem zwrotnym. Podobnie, jak w innych

systemach uniksowych faworyzowane są procesy ograniczone wej c

ś iem-wyjściem, gdyż do tej kategorii procesów należą procesy interaktywne. Planista nie mo e ż jednak

ignorować procesów ograniczonych procesorem i nie powinien prowadzić do ich zagłodzenia. Kryterium decydującym o kolejno c ś i wykonania procesów jest ich priorytet.

W Linuksie zadania o wy s

ż zym priorytecie trafiają do kolejek o dłu s

ż zym kwancie czasu, podczas gdy procesy o ni s

ż zym priorytecie, odpowiednio do kolejek o krótszym

kwancie. W obr b

ę ie pojedynczej kolejki procesy są szeregowane według algorytmu rotacyjnego1, który każdemu z nich przydziela jednakowy kwant czasu. Ponadto zadania o wy s

ż zym priorytecie są uruchamiane przed procesami o ni s

ż zym priorytecie. Wartość priorytetu zadania jest ustalana zarówno przez system operacyjny, jak i mo e

ż być modyfikowana przez u y

ż tkownika. Może ona ulegać zmianie podczas istnienia procesu w systemie, a wi c ę jest to priorytet dynamiczny. Każdy nowy proces

otrzymuje domy l

ś ny priorytet, który z czasem mo e

ż ulec zmianie, zale n

ż ie od tego, czy ów proces cz c

ęś iej wykonuje operacje wej c

ś ia – wyj c

ś ia, czy cz c

ęś iej korzysta

z procesora. Warto c

ś i priorytetów rozdzielone są na dwa zakresy: pierwszy to poziom uprzejmo c ś i (ang. nice), do którego należą warto c

ś i od -20 do 19 (im mniejsza

wartoś ,

ć tym wi k

ę szy priorytet), drugi zakres, to warto c

ś i od 0 do 99. Są to priorytety dla zadań czasu rzeczywistego. Linux nie jest rygorystycznym systemem czasu rzeczywistego, ale istnieją odmiany jego j d

ą ra, które spełniają wymogi nakładane na takie systemy. Procesy czasu rzeczywistego podzielone są na dwie grupy. Te, które należą do pierwszej z nich szeregowane są wed u

ł g algorytmu FCFS, natomiast te należące do drugiej z u y

ż ciem algorytmu rotacyjnego. Dla procesów czasu

rzeczywistego priorytet jest priorytetem statycznym. Zwykłe procesy w Uniksie otrzymują domy l ś ny kwant czasu większy niż 20 ms (w Linuksie jest to 100 ms), który

w zale n

ż o c

ś i od ich „zachowania” mo e

ż si

ę zwi k

ę sza

ć lub zmniejsza .

ć Proces nie musi od razu wykorzystywa

ć całego dostępnego mu czasu, ale je l

ś i wyczerpie cały kwant

to ulega przeterminowaniu i nie mo e

ż zostać ponownie uruchomiony do czasu, aż pozosta e

ł procesy nie wyczerpią swoich kwantów czasu i nie zostanie uruchomiona

procedura ich przeliczania. Proces, który wyczerpie swój kwant czasu podlega jednocześnie wyw a ł szczaniu. Mo e

ż on również zostać wywłaszczony, je l

ś i w systemie

pojawi si

ę nowy proces o wy s

ż zym priorytecie. Mechanizm szeregujący w j d

ą rze 2.6 Linuksa posiada kilka godnych uwagi cech:

1.

implementuje szeregowanie w czasie O(1),

2.

implementuje idealne skalowanie SMP,

3.

implementuje powi z

ą ania wątków z procesorami w trybie SMP,

4.

daje dobrą wydajność procesów interaktywnych,

5.

równomiernie rozkłada czas procesora,

6.

stosuje optymalizację dla często spotykanego przypadku, kiedy w systemie jest kilka procesów gotowych do wykonania.

Planista wią e

ż z każdym procesorem w systemie kolejkę procesów gotowych do wykonania. Jest to struktura danych o nazwie struct runqueue, która została zdefiniowana w pliku kernel/sched.c Dodatkowo zostały zdefiniowane odpowiednie makrodefinicje pozwalaj c ą e na a

ł twy dostęp do tej struktury. Dost p

ę do kolejek

procesów gotowych jest synchronizowany przy pomocy semaforów z aktywnych oczekiwaniem, zwanych krócej ryglami pętlowymi. Jeśli istnieje konieczność modyfikacji kilku kolejek równocze n

ś ie, to muszą one by

ć zajmowane wed u

ł g c

ś i l

ś e okre l

ś onej kolejno c

ś i. Zapobiega to powstawaniu zakleszczeń.

Ka d

ż a z kolejek procesów gotowych zawiera dwa wska n

ź iki na tablice priorytetów: aktywną i przeterminowaną (struct prio_array). W strukturze opisuj c ą ej taką tablicę

znajduje się pole, okre l

ś ające liczbę gotowych do wykonania procesów w tej tablicy oraz tablica wska n ź ików do list procesów. Indeksy w tej tablicy są warto c

ś iami

priorytetów zadań. W obr b

ę ie listy procesy są szeregowane według algorytmu rotacyjnego z wyjątkiem niektórych procesów czasu rzeczywistego. Dodatkowo ka d ż a

tablica priorytetów wyposa on

ż a jest w map

ę bitową, której poszczególne bity okre l

ś ają, czy są w tablicy zadania o danym priorytecie do wykonania. Aby znaleźć zadanie o najwy s

ż zym priorytecie gotowe do wykonania nale y

ż jedynie znaleźć pierwszy ustawiony bit w tej mapie.

Kwanty czasu zadań są przeliczane zaraz po ich wyczerpaniu przez zadanie i zanim zadanie trafi do tablicy przeterminowanej. Wymiana „starych” priorytetów na

„nowe” sprowadza się wyłącznie do wymiany wska n

ź ików na tablicę aktywną i przeterminowaną.

Wyboru następnego do wykonania zadania dokonuje funkcja schedule(), wywoływana zawsze kiedy trzeba zawiesić w t ą ek jądra lub wywłaszczyć zdanie. Oprócz wyboru

zadania do uruchomienia dokonuje ona również przełączenia kontekstu za pomocą funkcji context_switch(). Kod funkcji schedule() jest bardzo prosty i jego wykonanie nie zale y

ż od liczby procesów w systemie. Najbardziej krytyczną pod względem czasu wykonania jest więc funkcja context_switch, której implementacja jest zale n ż a od

architektury platformy na której działa system.

Priorytet ka d

ż ego „zwyk eg

ł o” zadania jest ustalany na podstawie priorytetu statycznego, jakim jest poziom uprzejmo c ś i oraz na podstawie stopnia interaktywności

procesu. Interaktywność jest wyznaczana heurystycznie jako stosunek długo c ś i okresów zawieszenia i aktywno c

ś i. W zależno c

ś i od tak wyliczonego stopnia

interaktywno c

ś i priorytet dynamiczny jest zwi k

ę szany lub zmniejszany o 5. Każdy nowo powstały proces w systemie otrzymuje połowę kwantu czasu jaki w chwili jego tworzenia miał proces macierzysty. Po wyczerpaniu kwant jest na nowo wyliczany dla tego procesu. Zadania o najwy s ż zym priorytecie otrzymują kwanty o wielko c

ś i

200 ms, zdania o najni s

ż zym priorytecie kwanty o warto c

ś i około 10 ms. r

Ś ednia długość kwantu czasu wynosi 100 ms. Jądro Linuksa promuje zadania o du y ż m

stopniu interaktywno c

ś i. Takie zadania po wykorzystaniu swojego kwantu czasu nie trafiają od razu do tablicy przeterminowanej, ale otrzymują drugą szansę i są umieszczane w tablicy aktywnej, na ko c

ń u tej samej listy, na której były poprzednio (dostają ten sam kwant czasu, co przed wykonaniem). Aby nie doszło do g od ł zenia

procesów jądro wykonuje makrodefinicję EXPIRED_STARVING(), która sprawdza, czy w tablicy przeterminowanej są procesy, które zbyt d u ł go czekają na realizacj .

ę

Proces mo e

ż zostać zawieszony w oczekiwaniu na jakieś zdarzenie, np.: realizację operacji wej c ś ia–wyj c

ś ia lub w oczekiwaniu na sygnał. Taki proces nie mo e

ż

znajdować się w kolejce procesów gotowych do wykonania. Najcz c

ęś iej dochodzi do tego po wywołaniu przez zadanie któregoś z wywołań systemowych, np.: read(), co powoduje jego dodanie do kolejki procesów oczekujących (zdefiniowanej strukturą wait_queue_head_t) i wywołanie funkcji schedule(), celem wyznaczenia innego procesu do wykonania. Budzenie zadań należących do okre l

ś onej kolejki oczekiwania odbywa się za pomocą wywołania wake_up(). Jeśli obudzone zadanie ma wy s ż zy

priorytet niż zadanie bieżąco wykonywane, ustawiane jest pole need_resched.

Wyw a

ł szczenie procesu nast p

ę uje wtedy, kiedy jest ustawiony znacznik need_resched, który ze względu na szybkość dostępu jest przechowywany w strukturze 1

Poza jednym wyj t

ą kiem, który zostanie opisany pó n

ź iej.

1

Systemy Operacyjne – semestr drugi

thread_info zadania. Celem ustalenia nowego procesu do wykonania jądro wywo u ł je funkcję schedule(), która z kolej wykonuje context_switch(). Ta ostatnia dokonuje zmiany kontekstu, wykonując zamianę odwzorowania pami c

ę i wirtualnej (za pomocą wywołania funkcji switch_mm()) oraz zmieniając stan procesora dla nowego zadania (za pomocą wywo a

ł nia funkcji switch_to()) zachowując przy tym stos i warto c

ś i rejestrów dla poprzedniego zadania. Wywłaszczenie procesu u y

ż tkownika może

zajść w ramach powrotu do przestrzeni użytkownika z wywołania systemowego, bądź z procedury obs u ł gi przerwania. Wywłaszczenie j d

ą ra natomiast mo e

ż nast p

ą ić

w ramach powrotu do przestrzeni jądra z procedury obsługi przerwania, kiedy istnieje mo l ż iwość jego wywłaszczenia, kiedy zadanie wykonywane w przestrzeni jądra

jawnie wywoła funkcję schedule() lub kiedy zadanie wykonujące kod jądra ulegnie zablokowaniu.

W systemach wieloprocesorowych zadania są kojarzone z poszczególnymi procesorami, ale czasem mo e ż zajść potrzeba zrównowa en

ż ia pracy systemu, wówczas część

zadań z kolejki procesów gotowych procesora mo e

ż zostać przeniesiona do kolejek innych procesorów lub odwrotnie. Mogą być dwie przyczyny takiego zdarzenia.

Pierwsza zachodzi wtedy, kiedy w kolejce któregoś z procesorów nie ma a

ż dnych zda ,

ń wówczas mogą one być przeniesione z kolejek innych procesorów. Druga to

wywo a

ł nie load_balance() za pomocą przerwania zegarowego. W tym przypadku zadanie równowa en ż ia obcią en

ż ia jest bardziej skomplikowane. W skrócie polega ono

na znalezieniu najbardziej obcią on

ż ej kolejki (ponad 25% obcią en

ż ia wszystkich kolejek w systemie) i rozło en

ż iu tego obciążenia na pozosta e

ł procesory.

Istnieje szereg wywołań dost p

ę nych dla aplikacji u y

ż tkownika, które mogą wp y

ł wać na sposób szeregowania zadań, oto niektóre z nich:

1.

nice() - okre l

ś a poziom uprzejmo c

ś i procesu,

2.

sched_setscheduler() - okre l

ś a strategię szeregowania procesów,

3.

sched_getscheduler() - pobiera strategię szeregowania procesów,

4.

sched_setparam() - ustala parametry szeregowania procesu zgodne ze okre l

ś oną dla niego strategią,

5.

sched_getparam() - pobiera parametry szeregowania procesu jakie okre l

ś a jego strategia szeregowania,

6.

sched_get_priority_max() - pobiera maksymalny priorytet dla okre l

ś onej strategii szeregowania,

7.

sched_get_priority_min() - pobiera minimalny priorytet dla okre l

ś onej strategii szeregowania,

8.

sched_rr_get_interval() - okre l

ś a wartoś

ć kwantu czasu procesu,

9.

sched_setaffinity() - kojarzy proces z procesorem,

10.

sched_getaffinity() - pobiera maskę procesorów, na których zadanie mo e

ż się wykonywa ,

ć

11.

sched_yield() - pozwala procesowi dobrowolnie zrzec się czasu procesora.

Ostatnie wywołanie jest w jądrach serii 2.6 zaimplementowane inaczej niż miało to miejsce w poprzednich wersjach. Dokonuje ono umieszczenia zrzekającego się zdania nie na końcu listy, ale w tablicy przeterminowanej. Z tego wywołania bezpośrednio powinny korzystać procesy użytkownika, natomiast j d ą ro powinno korzystać

z yeild(), które wywołuje sched_yield().

W wersji jądra 2.6.23 planista O(1) został zast p

ą iony planistą CFS (ang. Completely Fair Scheduler), który realizuje strategię sprawiedliwego szeregowania. W tej metodzie czas procesora dzielony jest między u y

ż tkowników i grupy u y

ż tkowników, a nie bezpośrednio między procesy. Je l

ś i w systemie pracuje czterech u y

ż tkowników

i ka d

ż y z nich ma uruchomiony jeden proces, to ka d

ż emu z tych procesów przysługuje 25% czasu procesora. Je l

ś i który

ś z u y

ż tkowników uruchomi dodatkowy proces, to

ka d

ż e z jego zadań otrzyma 12,5% czasu procesora. Sprawiedliwe szeregowanie uwzględnia jeszcze dodatkowo grupy u y ż tkowników. W plani c

ś ie CFS tablice

priorytetów zosta y

ł zastąpione drzewem czerwono-czarnym. Sposób umiejscowienia procesów w tym drzewie decyduje o kolejno c ś i ich uruchamiania. Ponieważ planista

CFS stosuje pomiar czasu z dokładno c

ś ią do nanosekund, to stosowanie heurystyk określających poziom interaktywno c ś i procesów staje się zbędne. Również kwanty

czasu nie są przydatne. Zasada dzia a

ł nia CFS opiera się na za o

ł en

ż iu, e

ż na idealnym procesorze ka d

ż y z procesów miałby przydzielany czas procesora według zasady

opisanej wy ej

ż . W fizycznym procesorze ka d

ż e zadanie oczywi c

ś ie musi czekać na przydział procesora, a czas jego oczekiwania w nanosekundach jest odnotowywany w polu wait_runtime umieszczonym w strukturze typu struct sched_entity wskazywanej przez deskryptor procesu. Wartość tego czasu zwi k ę szana jest w zale n

ż o c

ś i od

liczby procesów w systemie i priorytetu zadania. Kiedy proces otrzyma procesor to ta wartość okre l ś a czas przez jaki to zadanie mo e

ż korzystać z procesora. Nowo c

ś ią

w plani c

ś ie CFS jest to, e

ż z każdą strategią szeregowania skojarzona jest zmienna typu struct sched_class zawierająca głównie wska n ź iki na funkcje związane

z szeregowaniem wedle okre l

ś onej strategii. Pojawi y

ł się dwie nowe strategie, dla „zwyk y

ł ch” procesów: SCHED_BATCH i SCHED_IDLE. Struktura struct prio_array

została zastąpiona strukturą struct cfs_rq. Ze wzgl d

ę u na czas umieszczania zadania w drzewie czerwono-czarnym algorytm CFS ma zło on ż oś

ć obliczeniową wynoszącą

O(log2n), mimo to daje lepsze efekty, je l

ś i chodzi o wybór zadań do uruchomienia niż planista O(1).

W ramce na następnej stronie znajduje się kod modułu tworzącego dwa w t

ą ki. Pierwszy wątek jest realizowany za pomocą funkcji smiple_thread(). W tej funkcji, poprzez wywołanie makrodefinicji DECLARE_WAITQUEUE() tworzony jest wpis (element) kolejki oczekiwania. W przypadku opisywanego modułu tak kolejka nazywa się wq i jest tworzona dynamicznie przez wywołanie funkcji init_waitqueue_head() w funkcji inicjalizującej modu .ł Po wywo a ł niu wspomnianego makra w funkcji

simple_thread() realizowana jest „nieskończona p t

ę la”. W tej p t

ę li wykonywanych jest kilka czynno c

ś i. Najpierw wywo y

ł wana jest funkcja kthread_should_stop(). Je l

ś i

zwróci ona wartość wi k

ę szą od zera, to oznacza to, e

ż wątek powinien zakończyć swoją pracę. W przeciwnym przypadku w t

ą ek wypisuje komunikat przy pomocy

wywo a

ł nia funkcji printk(), dodaje się do kolejki oczekiwania za pomocą wywołania funkcji add_wait_queue() i zmienia swój stan na TASK_INTERRUPTIBLE, wywo u

ł jąc set_current_state(). Nast p

ę nie wywo u

ł je funkcję schedule(). Je l

ś i nastąpi powrót z tej funkcji, to oznacza to e

ż w t

ą ek otrzymał od planisty procesor, wi c

ę

w związku z tym zmienia swój stan na TASK_RUNNING i usuwa się z kolejki oczekiwania za pomocą wywołania funkcji remove_wait_queue(). Drugi w t ą ek

realizowany jest przez funkcję wakeit(). W tej funkcji znajduje się „nieko c ń ząca się pętla” o konstrukcji podobnej do tej z funkcji simple_thread(). W tej p t ę li wątek

sprawdza, czy powinien zakończyć działanie. Jeśli nie to usypia się na okre l ś oną liczbę mikrosekund, a następnie wywołuje funkcję wake_up() dla wątku simple_thread2. Po wykonaniu tych czynno c

ś i wywołuje funkcję schedule(). Oba w t

ą ki są tworzone przez wywołanie w funkcji inicjalizującej moduł funkcji kthread_run (), która zwraca wska n

ź ik na deskryptor utworzonego wątku, a przyjmuje co najmniej trzy parametry: wska n ź ik na funkcję realizuj c

ą ą wątek, wska n

ź ik (typu void *)

na dane dla w t

ą ku oraz ci g

ą znaków w standardzie języka C, który okre l

ś a nazwę wątku. W t

ą ki są zatrzymywane w funkcji „deinicjalizującej” modu ,ł przez wywołanie

funkcji kthread_stop(). Nale y

ż zauwa y

ż ,

ć e

ż w t

ą ki jądra niekoniecznie muszą podlegać wywłaszczaniu (je l

ś i jądro nie zostało skompilowane z odpowiednią opcją

wł c

ą zoną). Stąd bierze się konieczność badania warunku zakończenia ich działania oraz wywołania funkcji schedule(). Dodatkowo funkcje realizujące w t ą ki powinny

mieć zaimplementowaną obs u

ł gę sygna ó

ł w (w poni s

ż zym przyk a

ł dzie jej nie ma). W t

ą ki z przykładowego modułu mogą obcią a

ż ć dosyć znacznie system. Je l

ś i tak się

dzieje, to nale y

ż zmienić liczbę mikrosekund na które usypia wątek wakeit, ale nale y

ż uwa a

ż ,

ć aby nie doprowadzić do zawieszenia systemu.

2

Wła c

ś iwie dla wątku czekaj c

ą ego w kolejce wq, ale ponieważ jedynie wątek simple_thread z niej korzysta, wi c ę rezultat jest ten sam.

2

Systemy Operacyjne – semestr drugi

#include<linux/init.h>

#include<linux/module.h>

#include<linux/kthread.h>

#include<linux/wait.h>

#include<linux/delay.h>

static struct threadst

{

struct task_struct *thread, *thread2;

} ts;

static wait_queue_head_t wq;

static int simple_thread(void *data)

{

DECLARE_WAITQUEUE(wait,current);

for(;;) {

if(kthread_should_stop())

return 0;

printk(KERN_INFO "[simple_thread]: awake\n");

add_wait_queue(&wq,&wait);

set_current_state(TASK_INTERRUPTIBLE);

schedule();

set_current_state(TASK_RUNNING);

remove_wait_queue(&wq,&wait);

}

}

static int wakeit(void *data)

{

for(;;) {

if(kthread_should_stop())

return 0;

udelay(10000);

wake_up(&wq);

schedule();

}

}

static int __init thread_init(void)

{

init_waitqueue_head(&wq);

3

Systemy Operacyjne – semestr drugi

ts.thread = kthread_run(simple_thread,NULL,"simple_thread");

ts.thread2 = kthread_run(wakeit,NULL,"wakeit");

return 0;

}

static void __exit thread_exit(void)

{

kthread_stop(ts.thread2);

kthread_stop(ts.thread);

}

module_init(thread_init);

module_exit(thread_exit);

MODULE_LICENSE("GPL");

MODULE_DESCRIPTION("An example of using the linux kernel threads."); MODULE_AUTHOR("Arkadiusz Chrobot <a.chrobot@tu.kielce.pl>");

4