Klasyczny algorytm Uniksa

Klasyczny algorytm Uniksa zaimplementowano m. in. w Wersji 3 Systemu V (System V Release 3) oraz w 4.3BSD Unix.

Algorytm ten wykorzystuje kolejki priorytetowe ze sprzężeniem zwrotnym. Dynamiczny priorytet procesu zmienia się w zależności od długości kwantów czasu, które zużywa proces. W przypadku, gdy budzony jest proces o wyższym priorytecie, proces o niższym priorytecie jest wywłaszczany.

Klasyczne jądro uniksowe jest niewywłaszczalne. Oznacza to, że procesowi działającemu w trybie jądra nie może być odebrany procesor. Proces może dobrowolnie oddać procesor, np. w sytuacji gdy zasypia w oczekiwaniu na dostęp do jakiegoś zasobu lub w oczekiwaniu na jakieś zdarzenie. Tylko proces działający w trybie użytkownika może zostać wywłaszczony.

Z każdym procesem są związane następujące wartości:

Niższa wartość oznacza niższy priorytet.5.1 Priorytety 0-69 przeznaczone są dla procesów działających w trybie użytkownika, a 70-119 dla procesów w trybie jądra.

W trybie jądra wartość priorytetu może zostać chwilowo podwyższona, aby przyspieszyć zwolnienie przez proces zajętych zasobów. Na przykład procesy oczekujące na odczyt z terminala otrzymują przy budzeniu priorytet 90, natomiast oczekujące na dyskowe operacje wejścia/wyjścia -- 99:

unix_wakeup(Process p, int reason) {
   p.pri = system_priority(reason); // 70-119
   if ( p.pri > current.pri )
      ...  // przeszereguj procesy
}

Powrót do trybu użytkownika powoduje przywrócenie priorytetowi dynamicznemu wartości z zakresu 0-69, jeśli proces dostał priorytet jądra z zakresu 70-119: 

unix_return_to_user_mode(Process p) {
   p.pri = p.umdpri;
}

Każdemu procesowi, który oddaje procesor, czy to w wyniku wykorzystania kwantu czasu, czy zaśnięcia, zwiększa się pole cpu o długość wykorzystanego kwantu czasu do wartości co najwyżej 127:

unix_lostcpu(Process p) {
   if ( quantum_expired(p) || sleeping(p) )
      p.cpu = min ( 127, p.cpu + last_quantum(p) );
}

Co sekundę wartość pola cpu ulega zmniejszeniu -- jest mnożona przez tzw. czynnik rozpadu (ang. decay factor), zależny od implementacji. W SVR3 przyjęto stały czynnik rozpadu równy 1/2:

unix_tick() {
   for_each_process(p) {
      p.cpu = p.cpu / 2;
      unix_recalc_umdpri(p); // zob. niżej
   }
}
W 4.3BSD czynnik rozpadu jest zależny od liczby act procesów aktywnych w systemie w ciągu ostatniej sekundy: 
bsd_tick() {
   for_each_process(p) {
      p.cpu = p.cpu * 2 * act / ( 2 * act + 1 );
      unix_recalc_umdpri(p);
   }
}

Przy stałym czynniku rozpadu, jak zaimplementowano to w SVR3, w systemie obciążonym procesy zorientowane na obliczenia znacznie podwyższają swój priorytet (tj. zmniejszają wartość średniej wykładniczej cpu). Przyczyną jest fakt, iż w systemie obciążonym procesy dostają procesor stosunkowo rzadko. W tej sytuacji priorytety procesów zorientowanych na wejście-wyjście i procesów zorientowanych na obliczenia są do siebie bardzo zbliżone. W 4.3BSD algorytm tym wolniej podwyższa priorytety, im bardziej obciążony jest system. Pozwala to zachować odpowiednie zróżnicowanie priorytetów dla tych dwóch rodzajów procesów.

Każda zmiana wartości pola cpu powoduje odświeżenie priorytetu trybu użytkownika: 

unix_recalc_umdpri(Process p) {
   p.umdpri = min ( 69, 29-(p.cpu / 4)+(2 * p.upri) );
}

Procesy silnie korzystające z procesora mają dużą wartość cpu i tym samym niższy priorytet. Procesy długo śpiące mają, dzięki postarzaniu, coraz mniejszą wartość cpu i wyższy priorytet. Wagi 1/4 dla cpu oraz 2 dla upri są przykładowe, lecz ilustrują proporcje przyjęte w większości implementacji.

Implementacja powyższego algorytmu korzysta z dwukierunkowych list procesów o tej samej wartości pri. Niektóre architektury, jak np. VAX-11 wspierają sprzętowo operacje na tak zorganizowanych kolejkach priorytetowych.

Do wykonania wybierany jest pierwszy proces z niepustej kolejki o najwyższym priorytecie:

Process unix_select() {
   Queue queue = find_highest_queue();
        // szukanie niepustej kolejki o największym numerze
   if ( queue == NULL ) 
      return NULL; // wszystkie kolejki puste
   else
      return first(queue); // pierwszy proces z kolejki
}

Proces, który zużył kwant czasu dostaje nowy kwant i wędruje na koniec kolejki priorytetowej, w której się znajduje:

unix_tqexpired(Process p) {
   p.counter = new_quantum();
   move_last(p);
   //
   // + kod doliczający kwant czasu, jak
   // w unix_lostcpu()
   //
}

Dokładny opis algorytmu Uniksa można znaleźć m. in. w [2] i [5].

Klasyczny algorytm Uniksa posiada następujące cechy:

-
Nie skaluje się dobrze. Jeśli w systemie jest dużo procesów, to przeliczanie co sekundę parametrów jest nieefektywne.

-
Nie ma możliwości zapewnienia procesowi albo grupie procesów określonej części czasu procesora, np. 50% dla procesów pracowników naukowych, 25% dla procesów studentów, 25% dla potrzeb procesów obliczeniowych.

-
Nie gwarantuje się pesymistycznych czasów reakcji.

-
W niewywłaszczalnym jądrze występuje także efekt tzw. odwrócenia priorytetów. Budzony proces, nawet jeśli posiada wysoki priorytet, musi poczekać aż bieżący proces zrzeknie się procesora lub wróci do trybu użytkownika.



Tomek Blaszczyk
1999-05-21