Do spisu treści tematu 3


3.3.1 Szeregowanie procesów

     
Spis treści



Wprowadzenie

Autor ninejszego opracowania staje przed trudnym zadaniem. W Projekcie Linux, zeszłoroczej pracy starszych kolegów, rozdział Sposoby szeregowania procesów dosyć obszernie wyjaśnia mechanizm szeregowania procesów w Linuxie. Na szczęście nie wyczerpuje całkowicie tematu, a także zawiera klika błędów i nieścisłości. Dlatego autor starał się nie powtarzać już opisanych rzeczy, a raczej wyjaśnić kwestie mogące budzić wątpliwości oraz opisać szczegóły implementacji odsyłając do komentarzy w kodzie jądra Linuxa.

Opis powstał przede wszystkim na bazie analizy źródeł jądra Linuxa w wersji 2.0.32. W części dotyczącej szeregowania procesów, w stosunku do wersji 2.0.0 (opisanej we wspomnianym rozdziale Projektu Linux), dokonano niewielu zmian - poprawiono klika drobnych błędów.

Zacznijmy od wyjaśnienia kilku kluczowych pojęć, struktur i zmiennych związanych z szeregowaniem procesów.


Struktury i zmienne

Tablica task[]  (tablica procesów)

Jądro Linuxa, podobnie jak i jądra innych systemów unixopodobnych, informacje o procesach utrzymuje w statycznej tablicy: Jest zdefiniowana w kernel/sched.c (plik ten zawiera większość funkcji i zmiennych związanych z szeregowaniem procesów). Rozmiar tablicy określony jest przez stałą NR_TASKS, której wartość standardowo równa się 512. System nakłada pewne ograniczenia na maksymalną liczbę procesów dla użytkowników oraz dba o to, aby pozostały wolne miejsca w tablicy dla procesów nadzorcy systemu. Szczegóły zapewne można znaleźć w części poświęconej funkcji fork(). Z punktu widzenia modułu szeregującego procesy, ważniejsza jest kolejka procesów gotowych.
   

Zmienna init_task  (proces idle i kolejka procesów gotowych)

Zmienna ta ma dwojakie znaczenie. Po pierwsze, zawiera informacje o procesie o identyfikatorze równym 0. Jest to specjalny proces, pierwszy jaki powstaje w systemie. Tworzony jest w "sztuczny" sposób, bez udziału fork(). Opis procesu 0 w Projekcie Linux mija się w kilku kwestiach z prawdą. Fakty dotyczące tego procesu są następujące:

Po drugie, zmienna init_task jest początkiem (i jednocześnie końcem) dwukierunkowej listy cyklicznej, w której znajdują się procesy gotowe do wykonania. Dalej lista ta będzie nazywana kolejką procesów gotowych.
   

Struktura task_struct

Jest to struktura związana z każdym z istniejących w systemie procesów. Pola wykorzystywane przez kod modułu szeregowania to:

Struktura zawiera jeszcze kilka pól związanych z czasem i tzw. zegarami interwałowymi, które są opisane tutaj.
    

Zmienna current

Wskazuje na task_struct bieżącego procesu. W rzeczywistości jest to makrodefinicja zwracająca wskaźnik na task_struct bieżącego procesu na "bieżącym" procesorze (wskazuje na odpowiedni element tablicy current_set[], która zawiera wskaźniki na bieżące procesy dla każdego z procesorów systemu. O tym jak wygląda szeregowanie na architekturach wieloprocesorowych można przeczytać poniżej).
    

Zmienna need_resched

Flaga wskazująca czy ma być wywołane schedule(). Istnienie takiej zmiennej podyktowane jest niemożnością wywołania funkcji schedule() bezpośrednio z procedury obsługi przerwania sprzętowego (wynika to z choćby z implementacji, a także z tego, że wybór procesu może trwać za długo, aby działo się to w procedurze obsługi przerwania).
    

Zmienna jiffies

Jest to licznik czasu systemowego. Standardowo zwiększany o 1 jest co 10ms (wartość standardowa dla Linuxa/i386, dla Linuxa/AXP jest to 1ms).


Algorytm szeregowania

Dostępne tryby szeregowania oraz działanie algorytmu zostały bardzo dobrze (i zgodnie z prawdą) opisane w Projekcie Linux. Trudno właściwie tu coś dodać - algorytm szeregowania zaimplementowany w Linuxie jest rzeczywiście bardzo prosty.

Najważniejsze jego cechy to:


Schemat algorytmu

schedule()
{
   if (czekają wolne procedury obsługi przerwań)
      wykonaj je;

   wywołaj listę funkcji w kolejce tq_scheduler;

   if (bieżący proces szeregowany jest w SCHED_RR i wykorzystał swój kwant czasu)    
      przydziel mu nowy kwant i przesuń na koniec kolejki procesów gotowych;

   if (bieżący ma zostać uśpiony, ale może odbierać sygnały)
      if (otrzymał nieblokowany sygnał || minął jego czas budzenia)
         zmień jego stan na gotowy;
      else
         usuń go z kolejki procesów gotowych;

   for (każdy proces z kolejki procesów gotowych)
      wybierz proces o najwyższym priorytecie (wartości goodness);

   if (najwyższy priorytet = 0)
     for (każdy proces)
        zmodyfikuj jego wartość counter (dynamiczny priorytet);
   else
      if (brak procesu do wykonania)
         wybierz proces idle;

   if (wybrany został inny proces niż bieżący)
   {
      ustaw ewentualny czas budzenia dla bieżącego procesu;
      wznów działanie wybranego procesu;
   }
 
   if (wznowiony proces miał ustawiony budzik i obudził się przed czasem)
       wyłącz budzik;
}

Szczegółowe wyjaśnienie znajduje się w komentarzach funkcji schedule(), która realizuje wyżej opisany algorytm.

Wartość goodness

Określa na ile "dobry" jest dany proces, aby przydzielić mu procesor. Im większa wartość tym lepsza. Równa się odpowiednio:

Algorytm idle

Proces 0 wykonuje nieskończoną pętlę, tzw. idle-loop. Implementacja pętli jest zależna od architektury. W przypadku Intela, początkowo wykonuje aktywne czekanie. Jeśli przez określony czas (HARD_IDLE_TIMEOUT tyknięć zegara) nie pojawi się żądanie przeszeregowania procesów (flaga need_resched)zatrzymuje działanie procesora. Opóźnienie ma na celu uniknięcie sytuacji, kiedy natychmiast po zatrzymaniu procesora trzeba go ponownie uruchomić.

Patrz opis funkcji sys_idle(), hard_idle().


Implementacja

Większość funkcji związanych z szeregowaniem znajduje się w pliku kernel/sched.c. Część w kernel/sys.c, include/linux/sched.h, include/asm-i386/system.h, arch/i386/kernel/process.c, arch/i386/kernel/entry.S. Poniżej mały przewodnik po funkcjach.

Najważniejsze z nich to: schedule(), goodness(), update_process_times().

Funkcje pomocnicze: add_to_runqueue(), del_from_runqueue(), move_last_runqueue(), wake_up_process(), process_timeout().

Makrodefinicja dokonująca przełączenia kontekstu (w asemblerze, zależna od architektury): switch_to().

Funkcja inicjalizująca scheduler: sched_init()

Funkcje umożliwiające modyfikację i sprawdzenie parametrów szeregowania:

sys_sched_setscheduler(), sys_sched_getscheduler(), sys_sched_setparam(), sys_nice(), sys_setpriority(), sys_getpriority()

Inne funkcje związane z szeregowaniem:

sys_sched_get_priority_max(), sys_sched_get_priority_min(), sys_sched_rr_get_interval(), sys_sched_yield().

W pliku kernel/sched.c, oprócz funkcji związanych ze schedulerem i czasem, znajduje się też kilka innych: show_state() oraz rodzina funkcji sys_getXid().

Procedura powrotu z trybu jądra, gdzie m.in. może dojść do wywołania schedule() to: ret_from_sys_call.



Szeregowanie a wieloprocesorowość

W jądrze SMP Linuxa (SMP - Symmetric MultiProcessor), które wykorzystuje wieloprocesorowość niektórych platform, różnice dotyczące algorytmu szeregowania procesów są minimalne. Fakty są następujące:

W strukturze task_struct dodano następujące pola:

Pozostałe różnice to:


Realizację wieloprocesorowości w jądrze w wersji 2.1.x, znajdującej się jeszcze w stadium alpha, zaimplementowano od nowa, wprowadzając niezależne blokady dla poszczególnych części jądra (tzw. fine grained locking). Zwiększa to oczywiście wydajność systemu, komplikuje jednak znacznie jego implementację.


FSQ
czyli "Frequently scheduled questions"

W trakcie zajęć z przedmiotu "Systemy operacyjne", jak i podczas naszych "burz mózgów" pojawiało się sporo pytań, które pozostały bez odpowiedzi, bądź też skończyło się na spekulacjach i przypuszczeniach. Pytania i odpowiedzi na nie, znalezione u samych źródeł, zamieszczone są poniżej.

  1. Do jakiego przedziału należą wartości pola counter?

    Najwyższy możliwy priorytet to 40. Wobec tego, proces taki otrzyma na działanie kwant czasu równy 40 "tyknięciom" zegara (400ms). Załóżmy, że natychmiast, nim licznik counter zostanie mu zmniejszony, odda procesor i na długo zaśnie (np. wywoła funkcję pause()). Następnie, zgodnie z algorytm szeregowania, proces zostanie "postarzany", czyli kolejno będzie przyjmował wartości: 40+20=60, 40+30=70, 40+35=75. Z prostych obliczeń (suma nieskończonego ciągu geometrycznego) wynika, że wartość ta dąży do 80, nigdy jej nie osiągając.
         
  2. Co to są "bottom halves" ?

    Są tzw. wolne procedury obsługi przerwań. Wykonanie funkcji związanych z niektórymi przerwaniami mogłoby trwać zbyt długo, aby wykonywały się w kontekście przerwania. Dlatego wprowadzono mechanizm "dolnych połówek" obsługi przerwań w celu zminimalizowania czasu spędzanego w przerwaniu - procedura jego obsługi dzielona jest na dwie części, pierwsza robi to co jest niezbędne oraz zaznacza konieczność wykonania drugiej, poza przerwaniem. Służy do tego makro mark_bh(). Funkcje bottom half wywoływane są w do_bottom_half(), które z kolei jest wywoływane przy wyjściu z jądra w ret_from_sys_call oraz w schedule().

    Przewidziano 32 funkcje bottom half - umieszczane są w tablicy bh_base[]. Definicje dla poszczególnych przerwań znajdują się w pliku linux/interrupt.h. Tam też znajdują się makra init_bh(), mark_bh(), enable_bh(), disable_bh().

    Przykładem wykorzystania opisywanego mechanizmu są funkcje do_timer() oraz timer_bh(), odpowiedzialne za czas systemowy.
           
  3. Czy w schedule() proces może być wywłaszczony przez inny proces?

    Nie. Proces znajdujący się w trybie jądra nie może być wywłaszczony przez inny proces. Chwilowo wywłaszczyć proces w trybie jądra może, oczywiście, przerwanie sprzętowe (które, z kolei może być wywłaszczone przez przerwanie o wyższym priorytecie). Po skończeniu jego obsługi sterowanie wróci do przerwanego procesu. Innymi słowy, jądro jest sekcją krytyczną dla procesów użytkownika.
        
  4. Kiedy i gdzie proces jest wywłaszczany?

    Wtedy gdy wykorzysta przydzielony mu kwant czasu. Odpowiedzialna za stwierdzenie tego faktu jest funkcja update_process_times(), wykonująca się jako "bottom half" przerwania zegarowego. Jeśli proces wykorzysta swój kwant czasu, ustawiana jest flaga need_resched. Będzie sprawdzona w procedurze powrotu z wywołania systemowego (ta sama procedura wykonywana jest także przy powrocie z procedury obsługi przerwania). Jeśli wywłaszczenie nastąpi poza jądrem - proces zostanie wstrzymany dokładnie tam, gdzie go przerwanie zastało. Jeśli przerwanie wystąpiło w momencie gdy proces przebywa w trybie jądra, to w ret_from_sys_call proces zostanie zmuszony do wejścia do schedule() i zostanie wstrzymany w miejscu wywołania switch_to().
          
  5. Czemu niektóre funkcje jądra mają w nazwie prefiks sys_?

    Są to funkcje dostępne są poza jądrem dla programów użytkownika - widziane są bez prefiksu. Oczywiście, samo dodanie sys_ nie wystarcza aby funkcja była widziana poza jądrem.
           
  6. A co zmienna XXX oznacza i gdzie jest zdefiniowana?

    W znalezieniu odpowiedzi na tego typu pytania bardzo może pomóc skrypt grepr, bez którego opis ten nie miałby szans powstać.



Bibliografia

  1. Pliki źródłowe jądra Linuxa 2.0.32
  2. Man pages: nice (2), setpriority (2), getpriority (2), sched_setscheduler (2), sched_getscheduler (2), sched_getparam (2), sched_setparam (2), sched_get_priority_max (2), sched_get_priority_min (2), sched_rr_get_interval (2), sched_yield (2), idle (2)
  3. Krzysztof Arciszewski, Krzysztof Sobusiak Projekt-Linux, rozdział Sposoby szeregowania procesów
  4. David A. Rusling Linux Documentation Project - The Linux Kernel, rozdział Scheduling
  5. Michael K. Johnson The Linux Kernel Hacker's Guide, rozdział The Linux scheduler
  6. Alan Cox An implementation Of Multiprocessor Linux
  7. Markus Kuhn POSIX.1b Compatibility and Real-Time Support
  8. Maurice J. Bach Budowa systemu operacyjnego UNIX, wyd. II, WNT 1995
  9. Berny Goodheart, James Cox The Magic Garden Explained, PRENTICE HALL 1994


Autor: Grzegorz Całkowski