spis treści | => | Agnieszka Harasimczuk |
Aby zarządzać procesami jądro musi mieć pełne informacje na ich temat. Jest to zadanie deskryptora procesu, mającego strukturę typu task_struct
, której pola zawierają informacje dotyczące jednego procesu.
spis treści | => | Maciej Makowski |
|
spis treści | => | Agnieszka Harasimczuk |
struct task_struct *next_task, *prev_task
- pozwalają powiązać wszystkie procesy w dwukierunkową listę cykliczną;
unsigned long flags
- zmienna zawiera połączenie flag systemu:
PF_ALIGNWARN | /* jeszcze nie zaimplementowana */ | ||
PF_STARTING | /* proces tworzony */ | ||
PF_EXITING | /* proces kończony */ | ||
PF_FORKNOEXEC | /* proces potomny po operacji fork, jeszcze nie wykonywany */ | ||
F_SUPERPRIV | /* proces ma uprawnienia super użytkownika */ | ||
PF_DUMPCORE | /* proces "zrzuca" obszar pamięci */ | ||
PF_SIGNALED | /* uśmiercony sygnałem */ | ||
PF_MEMALLOC | /* proces w czasie alokacji pamięci */ |
struct mm_struct *mm
- wskaźniki do deskryptorów obszarów pamięciint has_cpu, processor
- procesor, na jakim proces się wykonujeunsigned long cpus_allowed
- informacja, na których procesorach proces może się wykonywaćstruct linux_binfmt *binfmt
- dzięki tej zmiennej możemy uruchamiać w Linuksie programy przez napisanie ich nazw/* identyfikator procesu */
pid_t pid
- numer identyfikacyjny procesupid_t pgrp
- numer identyfikacyjny grupy procesówpid_t session
- identyfikator sesjiint leader
- wartość mówiąca, czy proces jest liderem grupyint ngroups
- ilość grup, do których należy procesgid_t groups[NGROUPS]
- struktury opisujące grupy, do których proces należystruct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr
- zmienne te pokazują na:
|
unsigned long start_time
- moment, w którym proces został stworzonylong per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]
- tablice te przechowują informacje, ile czasu proces spędził odpowiednio w trybie użytkownika i w trybie systemowym, na poszczególnych procesorachstruct user_struct *user
- informacja o właścicielustruct tty_struct *tty
- terminal związany z procesem (jeżeli nie ma NULL)struct sem_undo *semundo
- informacja o zajmowanych semaforach/* szeregowanie */
volatile long state
- stan, w jakim znajduje się proces (-1 unrunnable, 0 runnable, >0 stopped)volatile long need_resched
- jeśli ta flaga jest ustawiona, oznacza to, że potrzebne jest wykonanie przeszeregowania procesów - wywołanie funkcji schedule()
;long counter
- liczba tyknięć zegara, które pozostały procesowi do zakończenia jego kwantu czasu; wywołanie funkcji update_process_times()
zmniejsza o jeden zawartość tego pola;long nice
- wpływa na długość kwantu czasu przyznanego procesowi (w starszych wersjach Linuksa jej rolę spełniało priority); zmienia się ona od -20 (najwyższy priorytet) do +19 (najniższy priorytet);unsigned long policy
- polityka szeregowania;unsigned long rt_priority
- statyczny priorytet procesów czasu rzeczywistego; przyjmuje wartości od 0 (proces zwykły) do 99 (najwyższy priorytet);/* tablica pidhash */
struct task_struct *pidhash_next
- wykorzystywane przez tablicę z hashowaniemstruct task_struct **pidhash_pprev
- j.w./* pliki */
struct fs_struct *fs
- tu znajdują się dane związane z systemem plikówstruct files_struct *files
- informacje o plikach otwartych/* sygnały */
int exit_code, exit_signal
- kod zakończenia procesu i sygnał wyjściaspinlock_t sigmask_lock
- ochrona i blokada sygnałówstruct signal_struct *sig
- wskaźnik do tablicy przechowującej informacje na temat sposobu obsługi otrzymanych sygnałówsigset_t blocked
- zawiera maski bitowe sygnałów otrzymywanych, ale tych które są obecnie zablokowane, więc trzeba będzie je obsłużyć późniejspis treści | => | Agnieszka Harasimczuk |
Proces w trybie jądra używa stosu zawartego w segmencie danych jądra, który różni się od stosu używanego w trybie użytkownika. Ścieżki wykonania jądra rzadko używają tego stosu, więc 8KB jest wystarczającym obszarem na stos i deskryptor procesu. Taką strukturę wyrażono za pomocą następującej unii:
union task_union { struct task_struct task; unsigned long stack[2048]; };
Rejestr esp jest wskaźnikiem stosu używanym do określenia położenia wierzchołka stosu. W systemach Intela stos zaczyna się na końcu i rozszerza w kierunku początku obszaru pamięci, gdzie znajduje się deskryptor procesu. Zaraz po przełączeniu się z trybu użytkownika do trybu jądra, stos jądra procesu jest pusty i rejestr esp wskazuje na bajt znajdujący się tuż za obszarem pamięci.
free_task_struct()
- Funkcja zwraca 8-kilobajtowe obszary pamięci task_union i umieszcza je w pamięci podręcznej do czasu zapełnienia.
alloc_task_struct()
- Funkcja ta alokuje obszary pamięci dla task_union
o wielkości 8 KB. (Uwaga: Z powodu wydajności jądro przechowuje te 8 KB jako dwa, znajdujące się obok siebie bloki stronicowe, gdzie pierwsza strona jest wyrównana do wielokrotności 213.)
spis treści | => | Agnieszka Harasimczuk |
Jądro może w prosty sposób określić wskaźnik deskryptora procesu aktualnie wykonującego się na podstawie wartości rejestru esp. (Obszar pamięci ma 213 bajtów, więc wystarczy, że jądro zamaskuje 13 najmniej znaczących bitów esp).
spis treści | => | Agnieszka Harasimczuk |
Istnieją sytuacje, w których jądro powinno mieć możliwość pobrania wskaźnika deskryptora na podstawie numeru pid (np. przy wywołaniu funkcji systemowych związanych ze schedule()
). W celu przyspieszenia przeszukiwania listy procesów i sprawdzania numerów pid w deskryptorach wprowadzono tablicę pidhash
, która obecnie ma 1024 elementy. Pozycje tabeli zawierają wskaźniki do deskryptorów procesu. Pid jest tłumaczony na indeks tablicy za pomocą następującego makra:
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (1023))
Zdarzają się sytuacje, że dwa numery dają ten sam indeks w tablicy. W tym celu Linux używa łańcuchów (każda pozycja tabeli to dwustronna lista kolidujących procesów, zrealizowana za pomocą pidhash_next
i pidhash_pprev
w deskryptorze procesu).
Do wstawiania i usuwania procesów z tablicy służą funkcje: hash_pid()
i unhash_pid()
. Funkcja find_task_by_pid()
zwraca wskaźnik deskryptora procesu o zadanym pid.
spis treści | => | Maciej Makowski |
TASK_RUNNING
- niekoniecznie wszystkie takie, bo operacje ustawienia stanu procesu na TASK_RUNNING
i wstawienia go do kolejki procesów gotowych nie są wykonywane atomowotask_struct
jako pole run_list
typu struct list_head
idle
nie jest umieszczany w kolejce procesów gotowychrunqueue_head
zadeklarowanej w pliku Linux/kernel/sched.c
spis treści | => | Maciej Makowski |
środowisko: | nr_running | - licznik aktywnych procesów |
static inline void add_to_runqueue(struct task_struct * p) { list_add(&p->run_list, &runqueue_head); nr_running++; }
spis treści | => | Maciej Makowski |
środowisko: | nr_running | - licznik aktywnych procesów | ||
jiffies | - licznik liczby taktów, która upłynęła od startu systemu |
static inline void del_from_runqueue(struct task_struct * p) { nr_running--; p->sleep_time = jiffies; list_del(&p->run_list); p->run_list.next = NULL; }
run_list.next
jest później sprawdzana w celu ustalenia, czy proces znajduje się w kolejce procesów gotowych
spis treści | => | Maciej Makowski |
środowisko: | runqueue_head | - wskaźnik do pierwszego elementu kolejki procesów gotowych |
static inline void move_last_runqueue(struct task_struct * p) { list_del(&p->run_list); list_add_tail(&p->run_list, &runqueue_head); }
spis treści | => | Maciej Makowski |
środowisko: | runqueue_head | - wskaźnik do pierwszego elementu kolejki procesów gotowych |
static inline void move_first_runqueue(struct task_struct * p) { list_del(&p->run_list); list_add(&p->run_list, &runqueue_head); }
spis treści | => | Maciej Makowski |
wait_queue_t
zawierająca
task_struct
list_head
wait_queue_head_t
, reprezentująca głowę kolejki, zawierająca
list_head
WQ_FLAG_EXCLUSIVE
spis treści | => | Maciej Makowski |
add_wait_queue()
:
add_wait_queue()
- wstawia element na początek kolejki bez ustawiania flagi WQ_FLAG_EXCLUSIVE
add_wait_queue_exclusive()
- wstawia element na koniec kolejki ustawiając flagę WQ_FLAG_EXCLUSIVE
wejście: | q | - kolejka procesów oczekujących | ||
wait | - element kolejki, który ma zostać wstawiony |
add_wait_queue(q, wait):
1: | wyzeruj flagę WQ_FLAG_EXCLUSIVE w wait | |
2: | wywołaj __add_wait_queue(q, wait) |
add_wait_queue_exclusive(q, wait)
1: | ustaw flagę WQ_FLAG_EXCLUSIVE w wait | |
2: | wywołaj __add_wait_queue_tail(q, wait) |
__add_wait_queue()
i __add_wait_queue_tail()
wywołują odpowiednio list_add()
i list_add_tail()
add_wait_queue()
i add_wait_queue_exclusive()
gwarantuje specyficzny układ procesów w kolejce:spis treści | => | Maciej Makowski |
remove_wait_queue()
sprowadza się do wywołania (z wyłączonymi przerwaniami) __remove_wait_queue()
, które z kolei ogranicza się do wywołania list_del()
spis treści | => | Maciej Makowski |
TASK_RUNNING
- proces wykonuje się, albo czeka na wykonanieTASK_INTERRUPTIBLE
- proces wstrzymany w oczekiwaniu na zajście warunku (np. na zwolnienie się zasobu). Może też zostać obudzony przez przerwania sprzętowe albo sygnałyTASK_UNINTERRUPTIBLE
- proces wstrzymany w oczekiwaniu na zajście warunku, sygnały nie zmieniają jego stanuTASK_STOPPED
- proces zatrzymany na skutek otrzymania sygnału SIGSTOP
, SIGTSTP
, SIGTTIN
lub SIGTTOU
, albo dowolnego sygnału, jeśli jest monitorowany przez inny proces (np. przez debugger)TASK_ZOMBIE
- proces zakończył się, ale jego rodzic nie wywołał jeszcze funkcji wait()
spis treści | => | Maciej Makowski |
sleep_on()
:
sleep_on()
- usypia proces w stanie TASK_UNINTERRUPTIBLE
interruptible_sleep_on()
- usypia proces w stanie TASK_INTERRUPTIBLE
sleep_on_timeout()
- usypia proces w stanie TASK_UNINTERRUPTIBLE
na określony w parametrze czas
interruptible_sleep_on_timeout()
- usypia proces w stanie TASK_INTERRUPTIBLE
na określony w parametrze czas
wejście: | q | - kolejka na której proces ma zasnąć | ||
środowisko: | current | - bieżący proces (to jego usypiamy) |
1: | zainicjalizuj wait - nowy element kolejki procesów oczekujących wskazujący na current | |
2: | ustaw stan aktualnego procesu | |
3: | wywołaj __add_wait_queue(q, wait) | |
4: | wywołaj odpowiednią wersję schedule() | |
5: | wywołaj __remove_wait_queue(q, wait) |
interruptible_
, czy nie_timeout
, czy nie. W pierwszym wypadku wywołana zostanie funkcja schedule_timeout()
, w drugim schedule()
__add_wait_queue()
i __remove_wait_queue()
wywołują odpowiednio list_add()
i list_del()
;add_wait_queue()
i remove_wait_queue()
sleep_on()
umieszczany jest na początku kolejki q
i nie ma ustawionej flagi WQ_FLAG_EXCLUSIVE
spis treści | => | Maciej Makowski |
try_to_wake_up(p, synchronous)
ustawia stan procesu p
na TASK_RUNNING
, wstawia p
do kolejki procesów gotowych (o ile się na niej jeszcze nie znajduje) i (w zależności od wartości argumentu synchronous
) wywołuje reschedule_idle(p)
0
jeśli p
był już w kolejce procesów gotowych, 1
w przeciwnym przypadkuwejście: | p | - proces do obudzenia | ||
synchronous | - tryb synchroniczny/asynchroniczny |
1: | ustaw stan p na TASK_RUNNING | |
2: | jeśli p znajduje się w kolejce procesów gotowych | |
3: | zwróć 0 | |
4: | koniec jeśli | |
5: | dodaj p do kolejki procesów gotowych | |
6: | jeśli (synchronous == 0 ) lub (p nie może się wykonywać na następnym z kolei procesorze) | |
7: | wywołaj reschedule_idle(p) | |
8: | koniec jeśli | |
9: | zwróć 1 |
reschedule_idle(p)
jest zawsze wykonywanespis treści | => | Maciej Makowski |
wake_up()
służą do budzenia procesów z zadanej kolejkiwake_up()
:
wake_up(q)
wake_up_nr(q, nr)
wake_up_all(q)
wake_up_sync(q)
wake_up_sync_nr(q, nr)
wake_up_interruptible(q)
wake_up_interruptible_nr(q, nr)
wake_up_interruptible_all(q)
wake_up_interruptible_sync(q)
wake_up_interruptible_sync_nr(q, nr)
_interruptible
- te wersje wake_up()
budzą tylko procesy znajdujące się w stanie TASK_INTERRUPTIBLE
; pozostałe budzą zarówno procesy w stanie TASK_INTERRUPTIBLE
jak i TASK_UNINTERRUPTIBLE
_all
- budzi wszystkie procesy (w odpowiednich stanach) bez względu na to, czy mają ustawioną flagę WQ_FLAG_EXCLUSIVE
_nr
- budzi wszystkie procesy bez ustawionej flagi WQ_FLAG_EXCLUSIVE
oraz podaną jako drugi argument liczbę procesów z ustawioną tą flagą_sync
- umożliwia obudzenie procesu bez dokonywania przeszeregowania (bez wywoływania reschedule_idle()
na rzecz tego procesu) w systemach wieloprocesorowych_all
, ani _nr
, to budzone są wszystkie procesy nie oznaczone flagą WQ_FLAG_EXCLUSIVE
i dokładnie jeden proces z ustawioną tą flagą (o ile istnieje)
wake_up()
sprowadzają się do wywołania funkcji __wake_up_common(q, mode, nr_exclusive, sync)
z odpowiednimi parametrami__wake_up_common()
:wejście: | q | - kolejka procesów oczekujących | ||
mode | - stany procesów, które mają być budzone | |||
nr_exclusive | - liczba procesów z ustawioną flagą WQ_FLAG_EXCLUSIVE do obudzenia | |||
sync | - tryb synchroniczny/asynchroniczny (argument dla try_to_wake_up() ) |
1: | powtarzaj dla każdego procesu p w kolejce q | |
2: | jeśli stan p zgadza się z mode | |
3: | jeśli wywołanie try_to_wake_up(p, sync) zwróciło 1 | |
4: | jeśli p ma ustawioną flagę WQ_FLAG_EXCLUSIVE | |
5: | zmniejsz nr_exclusive o 1 | |
6: | jeśli nr_exclusive == 0 | |
7: | zakończ | |
8: | koniec jeśli | |
9: | koniec jeśli | |
10: | koniec jeśli | |
11: | koniec jeśli | |
12: | koniec powtarzaj |
q
- usuwanie odbywa się w sleep_on()
spis treści | => | Agnieszka Harasimczuk |
Algorytm szeregujący dzieli czas procesora na epoki. Na początku każdej z nich każdemu procesowi jest przypisany pewien kwant czasu. Proces w trakcie jednej epoki może mieć dostęp do procesora wielokrotnie, dopóki nie skończy mu się jego kwant. Przy każdym tyknięciu zegara, bieżącemu procesowi odejmuje się jedynkę od pozostałego kwantu czasu. Gdy cały kwant jest skończony proces jest wywłaszczany. Epoka kończy się, gdy wszystkie procesy gotowe do działania mają już wykorzystany swój czas, wtedy następuje przeliczenie kwantów (dla każdego procesu w systemie) i rozpoczęcie nowej epoki.
spis treści | => | Agnieszka Harasimczuk |
Ze względu na czas reakcji na określone zdarzenie, procesy można podzielić na następujące grupy:
schedule()
. Na przykład: kompilatory, mechanizmy baz danych...spis treści | => | Agnieszka Harasimczuk |
W systemie Linux są zaimplementowane następujące klasy procesów:
fork()
, inicjalizowany makrodefinicjami INIT_TASK
. Proces ten cały czas znajduje się w stanie TASK_RUNNING
i w przypadku, gdy nie ma nikogo chętnego do pracy on otrzymuje procesor. Gdy tylko pojawia się jakikolwiek gotowy proces idle jest wywłaszczany.spis treści | => | Agnieszka Harasimczuk |
Jądro Linuksa w wersji 2.4.7 teoretycznie implementuje trzy polityki szeregowania. Z czego dwie pierwsze dotyczą tylko procesów czasu rzeczywistego, natomiast trzecia procesów zwykłych:
sched_yield()
- niestety jest to tylko teoria, gdyż obecnie funkcja ta nie działa poprawnie). Również proces o wyższym priorytecie (czyli musi to być proces czasu rzeczywistego), który przejdzie do stanu TASK_RUNNING odbierze mu procesor.goodness()
). Proces szeregowany zgodnie z tą polityką może stracić procesor, jeżeli:
sched_yield()
)
spis treści | => | Agnieszka Harasimczuk |
Funkcja ta jest zasadniczą częścią szeregowania procesów w systemie Linux. Jest ona wywoływana na dwa sposoby: bezpośrednio i pośrednio.
ret_from_sys_call
w pliku entry.S). Także bezpośrednio w sytuacji, gdy zaistnieje potrzeba zablokowania aktualnego procesu w trybie natychmiastowym, gdyż wymagane przez niego zasoby nie są dostępne (Jest to z reguły wykonywane przez przekroczenie czasu procesu - schedule_timeout()
). Również funkcje sleep_on()
oraz interruptible_sleep_on()
wywołują schedule()
. Jednakże w pierwszym przypadku po odzyskaniu procesora jest sprawdzana dostępność zasobów, a w drugim już nie.need_resched
aktywnego procesu. Jest to robione, gdy:
update_process_times()
)
reschedule_idle()
sched_setscheduler()
lub sched_yield()
spis treści | => | Agnieszka Harasimczuk |
current
- wskaźnik do procesu, który posiada procesor;
Pola struktury task_struct
:
state
- stan, w jakim znajduje się proces;need_resched
- jeśli ta flaga jest ustawiona, oznacza to, że potrzebne jest wykonanie przeszeregowania procesów - wywołanie funkcji schedule()
;policy
- polityka szeregowania;rt_priority
- statyczny priorytet procesów czasu rzeczywistego; przyjmuje wartości od 0 (proces zwykły) do 99 (najwyższy priorytet);nice
- wpływa na długość kwantu czasu przyznanego procesowi, zmienia się ona od -20 (najwyższy priorytet) do +19 (najniższy priorytet);counter
- liczba tyknięć zegara, które pozostały procesowi do zakończenia jego kwantu czasu; wywołanie funkcji update_process_times()
zmniejsza o jeden zawartość tego pola;
spis treści | => | Agnieszka Harasimczuk |
Powyższe makro jest zdefiniowane w następujący sposób:
#define NICE_TO_TICKS(nice) (TICK_SCALE(20-(nice))+1)
Makro TICK_SCALE
zależy od wartości zmiennej HZ, gdzie HZ jest stałą, zależną od architektury komputera, określającą częstotliwość przerwań zegara. Dla architektury Intela i386 HZ wynosi 100. Z tego też względu w naszym przypadku makro TICK_SCALE(x)
jest zdefiniowane jako x >> 2
. Z tego wynika, że zwraca ono wartość ((20-nice) >> 2) + 1
.
Przykład:
Stąd wniosek, że NICE_TO_TICKS
zwraca wartości od +11 do +1.
spis treści | => | Agnieszka Harasimczuk |
Funkcja decyduje jak pożądany jest proces. "Ważność" procesu zależy od jego typu: wsadowy, interakcyjny, czy czasu rzeczywistego oraz od aktualnego procesora.
counter
jest różne od 0, wagą staje się wartość tego pola. Jednakże w pewnych przypadkach goodness()
jest bardziej przychylna dla danego procesu (dodatkowo zwiększa jego wagę), tzn. wtedy, jeżeli proces pracował poprzednio na tym procesorze, waga jego jest dodatkowo zwiększana o stałą wartość równą PROC_CHANGE_PENALTY (ustawioną na stałe na 15); lub też, jeżeli proces ma tą samą przestrzeń adresową co bieżący, jego waga jest zwiększana o 1. Na zakończenie do wagi dodawana jest wartość (20 - nice
). rt_priority
.
Wnioski: Waga dodatnia oznacza, że proces jest dobrym kandydatem na otrzymanie procesora (im wyższa waga tym chętniej przyznajemy CPU), natomiast waga zerowa, bądź ujemna oznacza, że proces nie może otrzymać procesora, gdyż nie ma na to odpowiednich warunków.
spis treści | => | Agnieszka Harasimczuk |
Funkcja schedule()
działa według następującego algorytmu:
schedule()
gwarantujemy, że przerwania są włączone. (Jednakże są momenty, w których musimy je wyłączyć).NICE_TO_TICKS()
) oraz taki proces jest ustawiany na końcu kolejki procesów gotowych.schedule()
sprawdza, czy nadszedł nieblokowany sygnał, jeżeli tak zmienia mu stan na TASK_RUNNING.goodness()
opisanej powyżej. Wyboru dokonuje w kilku krokach.idle
. Jednakże otrzymuje on procesor tylko pod warunkiem, że w danej chwili nikt inny nie jest gotowy do działania. Z tego też względu jego "ważność" jest ustawiana na -1000 (Pozostałe procesy otrzymują wyższą wagę, co zapewnia poprawne działanie).schedule()
sprawdza, czy bieżący proces, (pod warunkiem, że jest w stanie TASK_RUNNING) mógłby otrzymać jeszcze raz procesor, (badane jest, czy dany proces może pracować na tym procesorze poprzez sprawdzenie stanu jego pola cpus_allowed
struktury task_struct
), jeżeli tak - jest uznawany za lepszego kandydata aniżeli idle
i jest brany jako pierwszy pod uwagę (schedule()
zapamiętuje jego wagę). Dzięki tej operacji w przypadku, gdy kilka procesów dostanie najwyższą wartość - w tym także bieżący, to jemu zostanie przyznany procesor.goodness()
. Waga kolejnych procesów jest porównywana z ostatnio zapamiętaną wartością, jeżeli jest wyższa to oznacza, ze ten proces jest lepszym kandydatem do procesora).
list_for_each(tmp, &runqueue_head) { p = list_entry(tmp, struct task_struct, run_list); if (can_schedule(p, this_cpu)) { int weight = goodness(p, this_cpu, prev->active_mm); if (weight > c) c = weight, next = p; } }Po przejściu powyższej pętli mogą wystąpić trzy różne sytuacje. Po pierwsze może się okazać, że znaleźliśmy proces, który powinien otrzymać CPU - czyli zapamiętana wartość jest dodatnia. Po drugie może ona mieć wartość -1000, co jest równoznaczne, że żaden proces nie chce lub nie może działać, więc proces jest przyznany
idle
. (goodness()
zwraca wartość 0 lub >0, dla każdego prawidłowego procesu, czyli gdyby w naszej kolejce runqueue
, był jakiś odpowiedni proces to pamiętana wartość byłaby nieujemna). I po trzecie może być sytuacja, w której najwyższą wartością okazało się zero - a to oznacza, że wszystkie procesy skończyły już swoje kwanty czasu. W tej sytuacji należy przyznać wszystkim procesom nowe kwanty czasu (nie tylko tym z listy procesów bieżących). W tym celu korzystamy z następującego algorytmu:
for_each_task(p) p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);Czyli do przeliczenia priorytetów dynamicznych, potrzebna jest znajomość czasu, który pozostał procesowi (wartość counter) z ostatniego przydziału oraz wartość wyliczona z priorytetu bazowego przy pomocy makra NICE_TO_TICKS.
schedule()
zdejmuje blokadę z runqueue
. Wydaje się to trochę dziwne, jednakże trzeba zauważyć, że przeliczeniu podlegają absolutnie wszystkie procesy, co może zająć stosunkowo dużo czasu, podczas gdy inny procesor mógłby chcieć wywołać funkcję schedule()
, w celu wybrania najlepszego procesu na ten procesor.schedule()
wraca z powrotem na początek punktu 4. task_struct: has_cpu
na 1 i processor
na this_cpu
. switch_to()
, przełączenie kontekstu.reschedule_idle()
, która to sprząta wszelkie pozostałe, niepoustawiane znaczniki po schedulerze (np. wyłącza flagę SCHED_YIELD, a w przypadku SMP ustawia pola dotyczące procesora). Funkcja ta w systemie wieloprocesorowym wywołuje też funkcję reschedule_idle()
(oczywiście pod warunkiem, że wywłaszczony proces nie był procesem idle
). reschedule_idle()
jest bardzo ważną funkcją - często jest wywoływana w celu ustawienia flagi need_resched
(w systemie jednoprocesorowym dotyczy to przypadku, gdy proces, dla którego ją wywołano ma większy priorytet od bieżącego). W Linuksie SMP sprawdza, czy proces, któremu właśnie odebrano procesor, nie mógłby wywłaszczyć kogoś z innego CPU.
spis treści | => | Agnieszka Harasimczuk |
Poniższe funkcje służą do zmiany priorytetów procesów i ich polityki szeregowania, oraz do odczytywania tych danych. Każdy użytkownik może dowolnie obniżać priorytety swoich procesów, ale tylko administrator może także je podwyższać.
nice(int increment)
nice
). Increment
może przyjmować dowolne wartości całkowite, ale jeżeli jest >+40, to zostaje obcięte do +40, natomiast, gdy jest <-40 zostaje obcięte do -40. Po wywołaniu tej funkcji pole nice ma wartość (nice + increment)
(ale oczywiście obcinamy to do zakresu -20...+19). Czyli im wartość increment jest mniejsza, tym bardziej podwyższamy priorytet.sched_yield(void)
runqueue
, o stanie TASK_RUNNING, który nie pracuje na żadnym innym procesorze), ustawia znacznik need_resched
, a w przypadku procesu zwykłego zaznacza również, że wywołał tą funkcję, tzn. ustawia w polu policy
dodatkowo flagę SCHED_YIELD.sched_getscheduler(pid_t pid)
sched_setscheduler(pid_t pid, int policy, struct sched_param *param)
rt_priority
). Proces taki jest przenoszony na początek kolejki procesów gotowych i jest ustawiana flaga need_resched
.sched_getparam(pid_t pid, struct sched_param *param)
rt_priority
).sched_setparam(pid_t pid, struct sched_param *param)
Funkcja analogiczna do sched_setscheduler()
, tylko nie zmienia priorytetu.sched_get_priority_min(int policy)
sched_get_priority_max(int policy)
sched_rr_get_interval(pid_t pid, struct timespec *interval)
NICE_TO_TICKS
) (innego niż SCHED_FIFO - ten teoretycznie pracuje bez ograniczeń).
spis treści | => | Huber Śledź |
Jak wiadomo, funkcje jądra są wykonywane w następstwie żądań, które mogą być zgłaszane na dwa sposoby:
Sekwencja instrukcji, wykonywana w trybie jądra w celu obsłużenia żądania, jest nazywana ścieżką wykonania jądra.
Ścieżki wykonania jądra pełnią rolę podobną do procesów, są jednak prostsze. Przede wszystkim nie jest z nimi związany żaden deskryptor, poza tym nie są szeregowane za pomocą jednej funkcji, ale przez wstawienie do kodu jądra sekwencji instrukcji zatrzymujących lub wznawiających ścieżki.
W najlepszym przypadku procesor wykonuje ścieżki wykonania jądra sekwencyjnie, od pierwszej do ostatniej instrukcji. Niestety, jeżeli się zdarzy:
to procesor musi przeplatać ścieżki wykonania jądra. I tutaj trzeba uważać na struktury danych zawierające kilka powiązanych zmiennych. Wszystkie instrukcje operujące na takiej strukturze trzeba umieścić w jednej sekcji krytycznej.
W Linuksie są wyróżnione cztery techniki synchronizacji:
spis treści | => | Huber Śledź |
W systemach jednoprocesorowych Linux oferuje jeden rodzaj blokad, tzw. semafory jądra.
W przypadku gdy ścieżka wykonania jądra chce pobrać zajęty, chroniony przez semafor jądra, zasób, to odpowiedni proces zostanie uśpiony. Zacznie swoje działanie wtedy, gdy zasób stanie się dostępny.
Semafory jądra są obiektami typu struct semaphore
[Linux/include/asm-i386/semaphore.h] i mają następujące pola:
atomic_t count
- przechowuje wartość całkowitą. Jeżeli jest większa niż 0, oznacza to, że zasób jest dostępny. Jeżeli wartość count jest mniejsza lub równa 0, to zasób jest zajęty. Zmienna typu atomic_t
jest strukturą zawierającą jeden atrybut typu int
, na której w sposób atomowy są zdefiniowane pewne operacje arytmetyczne.wait_queue_head_t wait
- przechowuje adres kolejki oczekiwania, która zawiera wszystkie uśpione procesy, czekające aktualnie na zasóbint sleepers
- zmienna pomocnicza, która mówi ile procesów w danej chwili przejdzie pod semaforem. Jest ona inicjalizowana na 0.Pole count jest zwiększane, gdy proces chce podnieść semafor, a zmniejszane gdy go opuszcza.
Jeżeli proces chce podnieść semafor, wywołuje funkcję up()
.
spis treści | => | Huber Śledź |
Najpierw proces zwiększa wartość count
i porównuje ją z zerem (porównanie i test są wykonywane atomowo). Jeżeli nowa wartość count
jest większa od zera, to w kolejce oczekiwania semafora nie ma żadnego procesu, tak więc proces nic więcej nie robi. W przeciwnym wypadku, (count <=0)
budzi pierwszy proces z kolejki procesów czekających na zasób (funkcja wake_up()
). Wykonanie funkcji wake_up()
jest chronione, tzn. przed wykorzystywaniem elementów kolejki zapamiętywane są flagi oraz wyłączane przerwania, a po zakończeniu działań na kolejce przerwania zostają włączane, a flagi odtwarzane.
void up(semaphore *sem) { /* Sekcja krytyczna */ sem->count ++; if (sem->count <= 0) /* wpp. kolejka oczekiwania jest pusta */ { /* Koniec sekcji */ Wstaw następny proces z kolejki oczekiwania semafora do kolejki procesów gotowych. /* Budzenie procesu - funkcja wake_up */ } }
Jeżeli proces chce opuścić semafor, wywołuje funkcję down()
.
spis treści | => | Huber Śledź |
Analogicznie jak w funkcji up()
, na początku proces zmniejsza wartość count
i porównuje ją z zerem (operacja atomowa). Jeżeli nowa wartość count
jest większa bądź równa zero, to procesowi udało się przejść pod semaforem i zająć zasób, w przeciwnym wypadku proces zmienia swój stan na TASK_UNINTERRUPTIBLE oraz wstawia się do kolejki wait
- procesów czekających na zasób (funkcja add_wait_exclusive()
- analogiczna ochrona jak w wake_up()
, przy wykonywaniu tej funkcji). Następnie proces wyłącza przerwania (makro spin_lock_irq()
) i zwiększa wartość sleepers
. Zaczyna wykonywać pętlę. Atomowo zwiększa count
o (sleepers - 1)
(makro atomic_add_negative()
) i sprawdza czy wartość count
jest większa bądź równa zero. Jeśli tak, to może pobrać zasób. Ustawia sleepers
na 0 (żeby zablokować inne procesy), wychodzi z pętli, odblokowuje przerwania (makro spin_unlock_irq()
), usuwa się z kolejki wait
(funkcja remove_wait_queue()
), zmienia stan na TASK_RUNNING i budzi pierwszy proces z kolejki wait
funkcją wake_up()
. Jeśli count < 0
, to musi zasnąć. Ustawia sleepers
na 1, odblokowuje przerwania i wywołuje funkcje schedule()
, która go usypia (wywłaszcza). Po obudzeniu powtarza pętlę aż do skutku.
void down(struct semaphore *sem) { /* Sekcja krytyczna */ sem->count--; if (sem->count < 0) /* wpp. udało się przejść pod semaforem */ { /* Koniec sekcji */ Zmień stan procesu na TASK_UNINTERRUPTIBLE; Wstaw proces do kolejki oczekiwania semafora. /* add_wait_queue_exclusive() */ Wyłączenie przerwań /* spin_lock_irq() */ sem->sleepers++; for (;;){ int sleepers = sem->sleepers; if ((sem->count += sleepers - 1) >= 0)/*!atomic_add_negative(sleepers-1, sem->count)*/ { /* ktoś podniósł semafor */ sem->sleepers = 0; break; /* możemy pobrać zasób*/ } sem->sleepers = 1; Włączenie przerwań /* spin_unlock_irq() */ schedule(); /* proces zostaje uśpiony */ Zmień stan procesu na TASK_UNINTERRUPTIBLE Wyłączenie przerwań } /* for */ Włączenie przerwań Usuń proces z kolejki oczekiwania semafora /* remove_wait_queue() */ Zmień stan procesu na TASK_RUNNING Wstaw pierwszy (jeśli jest) proces z kolejki oczekiwania semafora do kolejki procesów gotowych. /* wake_up() - budzenie procesu */ } }
spis treści | => | Huber Śledź |
W skład kontekstu procesu wchodzą: zawartość rejestrów procesora i przestrzeni adresowej procesu oraz struktury jądra związane z danym procesem. Przełączanie kontekstu polega na zamianie aktualnie wykonującego się procesu.
Sytuacje w jakich dochodzi do przełączania kontekstu to:
sched_yield()
)
Z każdym procesorem w systemie jest związana specjalna struktura TSS, określana mianem segmentu stanu zadania. W tej strukturze system przechowuje kontekst aktualnie działającego procesu.
W TSS przechowywane są następujące dane obecnie działającego procesu:
W deskryptorze procesu jest pole thread
typu thread_struct
, w którym znajdują się informacje o stanie tego procesu m.in. thread.esp0, esp, eip, fs, gs o znaczeniu analogicznym jak odpowiednie pola w TSS.
spis treści | => | Huber Śledź |
Makro switch_to()
[Linux/include/asm-i386/system.h] wykonuje przełączenie procesów. Używa trzech parametrów oznaczonych jako prev
, next
i last
. prev
jest wskaźnikiem do deskryptora procesu, który ma być przełączony do tła, a next
jest wskaźnikiem do deskryptora procesu, który ma być wykonywany przez procesor. Makro to jest wywoływane przez funkcję schedule()
.
Jak działa to makro?
prev->thread.esp
zapisywana jest bieżąca wartość rejestru ESP, natomiast z next->thread.esp
jest odtwarzana wartość tego rejestru, wskazująca na szczyt stosu procesu next
w segmencie danych jądra. W tym momencie jądro działa na stosie jądra next
, tak więc tutaj następuje przełączenie kontekstu
prev->thread.eip
jest zapisywany adres procedury, która powoduje odtworzenie ze stosu umieszczonych tam wartości rejestrów ESI, EDI oraz EBP.
next->thread.eip
. Jeżeli proces next
nie był jeszcze usypiany to w tym polu będzie adres etykiety ret_from_fork
, której adres jest zapisywany w thread.eip
procesu zaraz po jego utworzeniu przez funkcje systemową fork()
.
__switch_to(prev, next)
[Linux/i386/kernel/process.c], która kończy przełączanie kontekstu.
Jak działa __switch_to()
?
next->thread.esp0
prev.thread
next->thread
debug
i jeżeli tak, to ładowane są rejestry debugregs
Teraz jądro działa już w kontekście nowego procesu.
Po powrocie ze __switch_to()
makro switch_to odtwarza wartości rejestrów ESI, EDI, EBP.
spis treści | => | Huber Śledź |
Zadaniem tego algorytmu jest zastąpienie zawartości pamięci procesu przez nowy program. Algorytm ten załadowuje plik binarny do pamięci i rozpoczyna jego wywołanie, zastępując segment kodu, danych i stosu. W szczególności jeśli procesowi powiedzie się wywołanie funkcji exec, to nie tworzy się nowy proces, tylko ten sam proces działa dalej jednak ma już inny kod oraz podmieniony segment danych i stosu. Nie zmienia mu się PID, dziedziczy wszystkie deskryptory otwartych plików, które nie mają opcji zamykania podczas exec oraz zostaje przywrócona standardowa obsługa sygnałów, chociaż sygnały ignorowane przed wywołaniem funkcji nadal będą ignorowane.
Jednakże algorytm exec wykorzystuje tylko już istniejące funkcje w systemie, służące do ładownia plików binarnych.
Na początku zostaje wywołana funkcja execve()
. [Linux/include/asm-i386/unistd.h]
Deklaracja:
static inline int execve(const char *file, char **argv, char **envp);
gdzie:
file
- nazwa programu do wykonania
argv
- tablica zawierająca argumenty dla programu
envp
- tablica zawierająca ustawione zmienne środowiskowe
Funkcja ta wywołuje przerwanie (SYSCALL_VECTOR
) i przekazuje swoje parametry do procedury obsługi tego przerwania (sys_call
). Z kolei ta procedura uruchamia funkcję sys_execve()
[Linux/arch/i386/kernel/process.c], której parametrem jest struktura zawierająca stan rejestrów procesora (struct pt_regs
[Linux/include/asm-i386/ptrace.h]).
Funkcja sys_execve()
sprawdza, czy podany plik jest wykonywalny i czy nie jest katalogiem lub urządzeniem - jeśli tak zwraca błąd i funkcja exec()
na zmiennej errno
zapisuje kod błędu.
W przeciwnym wypadku zostaje wywołana funkcja do_execve()
z parametrami takimi jak execve()
, oraz dodatkowo z parametrem z sys_execve()
.
Struktura struct linux_binprm
[Linux/include/linux/binfmts.h] zawiera m.in. pola:
char buf[128]
- tu przechowywanych jest 128 pierwszych znaków ładowanego programu
int e_uid, e_gid
- zawierają odpowiednio, efektywny identyfikator użytkownika oraz identyfikator grupy procesu uruchamiającego.
i jest wykorzystywana przez do_execve()
.
Funkcja do_execve()
wykonuje następujące operacje:
linux_binprm
, która zostanie wypełniona danymi, dotyczącymi nowego pliku wykonywalnego
open_exec()
w celu pobrania obiektu pozycji katalogu. W przypadku niepowodzenia zwraca odpowiedni kod błędu.
prepare_binprm()
,po to by wypełnić strukturę linux_binprm
. Funkcja ta wykonuje operacje:
e_uid
i e_gid
struktury linux_binprm
, biorąc pod uwagę wartości flag setuid
i setgid
pliku wykonywalnego. Pola te reprezentują odpowiednie identyfikatory efektywnego użytkownika i grupy.
buf
struktury linux_binprm
pierwszymi 128 znakami pliku wykonywalnego. Bajty te zawierają informacje umożliwiające rozpoznanie formatu pliku wykonywalnego.
search_binary_handler()
, która zajmuje się właściwym uruchamianiem programu. Na początku pobiera wskaźnik do początku listy dostępnych formatów dzięki makru formats
. Następnie dla każdego formatu próbuje zastosować jego funkcję służącą do ładowania programu (load_binary()
), przekazując jej strukturę danych linux_binprm
. Przeszukiwanie listy formats
kończy się gdy tylko jakaś funkcja load_binary()
potwierdzi format wykonywalny pliku.
formats
, zwalnia wszystkie zaalokowane bloki stronicowe i zwraca kod błędu. Linux nie może rozpoznać formatu pliku wykonywalnego.
exec()
i zwraca kod zwrócony przez funkcję load_binary()
, związaną z formatem pliku wykonywalnego.
Niektóre błędy jakie może zgłosić funkcja exec()
:
EACCES | - zabroniony dostęp do pliku | ||
ENOMEM | - nowy proces wymaga zbyt dużo pamięci | ||
EAGAIN | - ilość pamięci podczas operacji we/wy jest niewystarczająca | ||
ENOENT | - błędna ścieżka dostępu do pliku |