1 - Kielce

Transkrypt

1 - Kielce
Systemy Operacyjne – semestr drugi
Wykład trzeci
Szeregowanie procesów w Linuksie (do wersji 2.6.22)
Istnieją dwa typy systemów wielozadaniowych: systemy wielozadaniowe z kooperacją (bez wywłaszczania) i systemy wielozadaniowe z wywłaszczaniem. 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ółczesnych systemów operacyjnych realizuje wielozadaniowość z wywłaszczaniem.
Schemat szeregowania jaki zastosowano w tym systemie opiera się na schemacie wielopoziomowych kolejek ze sprzężeniem zwrotnym. Linux, zgodnie ze standardem POSIX, wyróżnia trzy grupy procesów: SCHED_OTHER – zwykłe procesy, które mogą być wykonywane w imieniu wszystkich użytkowników oraz SCHED_FIFO i SCHED_RR, które są procesami czasu rzeczywistego wykonywanymi z uprawnieniami użytkowników uprzywilejowanych. Najpierw zostanie opisane szeregowanie dla pierwszej grupy. Podobnie, jak w innych systemach uniksowych faworyzowane są procesy ograniczone wejściem­wyjściem, gdyż do tej kategorii 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ści wykonania procesów jest ich priorytet. W Linuksie zadania o wyższym priorytecie trafiają do kolejek o dłuższym kwancie czasu, podczas gdy procesy o niższym priorytecie umieszczane są w kolejkach o krótszym kwancie. W obrębie pojedynczej kolejki procesy są szeregowane według algorytmu rotacyjnego, który każdemu z nich przydziela jednakowy kwant czasu. Ponadto zadania o wyższym priorytecie są uruchamiane przed procesami o niższym priorytecie. Wartość priorytetu zadania jest ustalana zarówno przez użytkownika, jak i może być modyfikowana przez system operacyjny. Może ona ulegać zmianie podczas istnienia procesu w systemie, a więc jest to priorytet dynamiczny. Każdy nowy proces otrzymuje domyślny priorytet, który z czasem może ulec zmianie, zależnie od tego, czy ów proces częściej wykonuje operacje wejścia – wyjścia, czy częściej korzysta z procesora. Wartości priorytetów rozdzielone są na dwa zakresy: pierwszy to poziom uprzejmości (ang. nice), do którego należą wartości od ­20 do 19 (im mniejsza wartość, tym większy priorytet), drugi zakres, to wartości od 0 do 99. Ten ostatni zakres to priorytety dla zadań czasu rzeczywistego. Linux nie jest rygorystycznym systemem czasu rzeczywistego, ale istnieją odmiany jego jądra, 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ług algorytmu FCFS, natomiast te należące do drugiej z użyciem algorytmu rotacyjnego. Dla procesów czasu rzeczywistego priorytet jest priorytetem statycznym. Zwykłe procesy w Uniksie otrzymują domyślny kwant czasu większy niż 20 ms (w Linuksie jest to 100 ms), który w zależności od ich „zachowania” może się zwiększać lub zmniejszać. Proces nie musi od razu wykorzystywać całego dostępnego mu czasu, ale jeśli 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łaszczaniu. Może on również zostać wywłaszczony, jeśli w systemie pojawi się nowy proces o wyższym priorytecie. Mechanizm szeregujący w jądrze 2.6 Linuksa posiada kilka godnych uwagi cech:
1.
implementuje szeregowanie w czasie O(1),
2.
implementuje idealne skalowanie SMP,
3.
implementuje powiązania 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ące na łatwy 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śnie, to muszą one być zajmowane według ściśle określonej kolejności. Zapobiega to powstawaniu zakleszczeń. Każda z kolejek procesów gotowych zawiera dwa wskaźniki na tablice priorytetów: aktywną i przeterminowaną (struct prio_array). W strukturze opisującej taką tablicę znajduje się pole, określające liczbę gotowych do wykonania procesów w tej tablicy oraz tablica wskaźników do list procesów. Indeksy w tej tablicy są wartościami priorytetów zadań. W obrębie listy procesy są szeregowane według algorytmu rotacyjnego z wyjątkiem niektórych procesów czasu rzeczywistego. Dodatkowo każda tablica priorytetów wyposażona jest w mapę bitową, której poszczególne bity określają, czy są w tablicy zadania o danym priorytecie do wykonania. Aby znaleźć zadanie o najwyższym 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źników na tablicę aktywną i przeterminowaną. Wyboru następnego do wykonania zadania dokonuje funkcja schedule(), wywoływana zawsze kiedy trzeba zawiesić wątek 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 czas jego wykonania nie zależy od liczby procesów w systemie. Najbardziej krytyczną pod tym względem jest wywoływana przez schedule() funkcja context_switch(), której implementacja jest zależna od architektury platformy sprzętowej na której działa system. Priorytet każdego zwykłego zadania jest ustalany na podstawie priorytetu statycznego, jakim jest poziom uprzejmości oraz na podstawie stopnia interaktywności procesu. Interaktywność jest wyznaczana heurystycznie jako stosunek długości okresów oczekiwania i aktywności. W zależności od tak wyliczonego stopnia interaktywności priorytet dynamiczny jest zwiększany 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ższym priorytecie otrzymują kwanty o wielkości 200 ms, zdania o najniższym priorytecie kwanty o wartości około 10 ms. Podstawowa długość kwantu czasu wynosi 100 ms. Jądro Linuksa promuje zadania o dużym stopniu interaktywności. 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ńcu tej samej listy, na której były poprzednio (dostają ten sam kwant czasu, co przed wykonaniem). Aby nie doszło do głodzenia procesów jądro wykonuje makrodefinicję EXPIRED_STARVING(), która sprawdza, czy w tablicy przeterminowanej są procesy, które zbyt długo czekają na realizację. Proces może zostać zawieszony w oczekiwaniu na jakieś zdarzenie, np.: realizację operacji wejścia–wyjścia lub w oczekiwaniu na sygnał. Taki proces nie może znajdować się w kolejce procesów gotowych do wykonania. Najczęściej 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ślonej kolejki oczekiwania odbywa się za pomocą wywołania np. funkcji wake_up(). Jeśli obudzone zadanie ma wyższy priorytet niż zadanie bieżąco wykonywane, ustawiany jest znacznik need_resched. Wywłaszczenie procesu następuje wtedy, kiedy jest ustawiony znacznik need_resched, który ze względu na szybkość dostępu jest przechowywany w polu flags struktury thread_info zadania. Celem ustalenia nowego procesu do wykonania jądro wywołuje funkcję schedule(), która z kolej wykonuje context_switch(). Ta ostatnia dokonuje zmiany kontekstu, wykonując zamianę odwzorowania pamięci wirtualnej (za pomocą wywołania funkcji switch_mm()) oraz zmieniając stan procesora dla nowego zadania (za pomocą wywołania funkcji switch_to()) zachowując przy tym stos i wartości rejestrów dla poprzedniego zadania. Wywłaszczenie procesu użytkownika może zajść w ramach powrotu do przestrzeni użytkownika z wywołania systemowego, bądź z procedury obsługi przerwania. Wywłaszczenie jądra natomiast może nastąpić w ramach powrotu do przestrzeni jądra z procedury obsługi przerwania, kiedy istnieje możliwość 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 (wejdzie w stan oczekiwania). W systemach wieloprocesorowych zadania są kojarzone z poszczególnymi procesorami, ale czasem może zajść potrzeba zrównoważenia 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 żadnych zdań, wówczas mogą one być przeniesione z kolejek innych procesorów. Druga to 1
Systemy Operacyjne – semestr drugi
wywołanie load_balance() za pomocą przerwania zegarowego. W tym przypadku zadanie równoważenia obciążenia jest bardziej skomplikowane. W skrócie polega ono na znalezieniu najbardziej obciążonej kolejki (ponad 25% obciążenia pozostałych kolejek w systemie) i rozłożeniu tego obciążenia na pozostałe procesory. Istnieje szereg funkcji dostępnych dla aplikacji użytkownika, które mogą wpływać na sposób szeregowania zadań, oto niektóre z nich:
1.
nice() ­ określa poziom uprzejmości procesu,
2.
sched_setscheduler() ­ określa strategię szeregowania procesów,
3.
sched_getscheduler() ­ pobiera strategię szeregowania procesów,
4.
sched_setparam() ­ ustala parametry szeregowania procesu zgodne ze określoną dla niego strategią,
5.
sched_getparam() ­ pobiera parametry szeregowania procesu jakie określa jego strategia szeregowania,
6.
sched_get_priority_max() ­ pobiera maksymalny priorytet dla określonej strategii szeregowania, 7.
sched_get_priority_min() ­ pobiera minimalny priorytet dla określonej strategii szeregowania,
8.
sched_rr_get_interval() ­ określa wartość kwantu czasu procesu,
9.
sched_setaffinity() ­ kojarzy proces z procesorem (procesorami),
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 (do 2.6.22) 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ądro powinno korzystać z yield().
W wersji jądra 2.6.23 planista O(1) został zastąpiony planistą CFS (ang. Completely Fair Scheduler), który realizuje strategię sprawiedliwego szeregowania. Działanie tego planisty zostanie opisane na następnym wykładzie.
W ramkach znajduje się kod modułu tworzącego dwa wątki. Pierwszy wątek jest realizowany za pomocą funkcji smiple_thread(). W tej funkcji, poprzez wywołanie makrodefinicji DEFINE_WAIT() tworzony jest wpis (element) kolejki oczekiwania, który reprezentuje ten wątek. W przypadku opisywanego modułu taka kolejka nazywa się wq i jest tworzona dynamicznie przez wywołanie funkcji init_waitqueue_head() w funkcji inicjacyjnej moduł. Po wywołaniu wspomnianego makra w funkcji simple_thread() realizowana jest „nieskończona pętla”. W tej pętli wykonywanych jest kilka czynności. Najpierw wątek dodaje wpis go reprezentujący do kolejki oczekiwania za pomocą add_wait_queue(), a następnie wykonuje pętlę while(...), w której oczekuje na zdarzenie sygnalizowane zmianą wartości zmiennej condition z false na true. Przy każdej iteracji tej pętli wywoływana jest funkcja prepare_to_wait(), która zmienia stan wątku na TASK_INTERRUPTIBLE i w razie konieczności dodaje go z powrotem do kolejki oczekiwania. Wątek może być obudzony z innego powodu, niż wystąpienia zdarzenia na które oczekiwał. W ogólnym przypadku jest to sygnał, który należy obsłużyć. Wątek simple_thread nie obsługuje żadnych sygnałów poza tym, który nakazuje mu skończyć pracę i którego wystąpienie jest zgłaszane przez funkcję kthread_should_stop(). Jeśli zwróci ona wartość większą od zera, to oznacza to, że wątek powinien się zakończyć. W przeciwnym przypadku umieszcza on w buforze jądra komunikat przy pomocy wywołania funkcji printk(), a potem wywołuje funkcję schedule(). Jeśli nastąpi powrót z tej funkcji, to oznacza to, że wątek otrzymał od planisty procesor i przystępuje do wykonania kolejnej iteracji pętli while(...), w której sprawdza, czy wystąpiło oczekiwane zdarzenie. Jeśli tak, to wątek kończy pętlę while(...), ustawia wartość zmiennej condition na false, sygnalizując tym samym obsługę zdarzenia, zmienia swój stan na TASK_RUNNING i usuwa się z kolejki oczekiwania. Dwie ostatnie operacje są wykonywane za pomocą wywołania funkcji finish_wait(). Drugi wątek realizowany jest przez funkcję wakeit(). W tej funkcji znajduje się „niekończąca się pętla” o konstrukcji podobnej do tej z funkcji simple_thread(). W tej pętli wątek sprawdza, czy powinien zakończyć działanie. Jeśli nie, to zmienia swój stan na TASK_INTERRUPTIBLE i wywołuje funkcję schedule_timeout(), która zostanie przedstawiona na przyszłych wykładach, a następnie zmienia wartość zmiennej condition na true i wywołuje funkcję wake_up() dla wątku simple_thread 1. Oba wątki są tworzone przez wywołanie w funkcji inicjującej moduł funkcji kthread_run(), która zwraca wskaźnik na deskryptor utworzonego wątku, a przyjmuje co najmniej trzy parametry: wskaźnik na funkcję realizującą wątek, wskaźnik (typu void *) na dane dla wątku oraz ciąg znaków w standardzie języka C, który określa nazwę wątku. Wątki są zatrzymywane w funkcji „deinicjalizującej” moduł, przez wywołanie funkcji kthread_stop(). Należy zauważyć, że wątki jądra niekoniecznie muszą podlegać wywłaszczaniu (jeśli jądro nie zostało skompilowane z odpowiednią opcją włączoną). Stąd bierze się konieczność badania warunku zakończenia ich działania oraz wywołania funkcji schedule(). Dodatkowo funkcje realizujące wątki powinny mieć zaimplementowaną obsługę sygnałów (w poniższym przykładzie prawie jej nie ma).
#include<linux/module.h>
#include<linux/kthread.h>
#include<linux/wait.h>
static struct threadst
{ struct task_struct *thread, *thread2;
} ts;
static wait_queue_head_t wq;
static bool condition;
static int simple_thread(void *data)
{ DEFINE_WAIT(wait);
1
Właściwie dla wątku czekającego w kolejce wq, ale ponieważ jedynie wątek simple_thread z niej korzysta, więc rezultat jest ten sam.
2
Systemy Operacyjne – semestr drugi
for(;;) {
add_wait_queue(&wq,&wait);
while(!condition) {
prepare_to_wait(&wq, &wait, TASK_INTERRUPTIBLE);
if(khread_should_stop())
return 0; printk(KERN_INFO "[simple_thread]: awake\n");
schedule();
}
condition = false;
finish_wait(&wq, &wait);
}
}
static int wakeit(void *data)
{
for(;;) {
if(kthread_should_stop())
return 0;
set_current_state(TASK_INTERRUPTIBLE);
if(schedule_timeout(10*HZ))
printk(KERN_INFO "Signal received!\n");
condition = true;
wake_up(&wq);
}
}
static int __init threads_init(void)
{
init_waitqueue_head(&wq);
ts.thread = kthread_run(simple_thread,NULL,"simple_thread");
ts.thread2 = kthread_run(wakeit,NULL,"wakeit");
return 0;
}
static void __exit threads_exit(void)
{
kthread_stop(ts.thread2);
kthread_stop(ts.thread);
}
module_init(threads_init);
module_exit(threads_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("An example of using the linux kernel threads.");
MODULE_AUTHOR("Arkadiusz Chrobot <[email protected]>");
3

Podobne dokumenty