Procesy
Algorytm Fork
Autor: Bartłomiej Kijanka
1.0 Interfejs programisty
1.1 Funkcja systemowa fork()
Fork jest jedną z najważniejszych funkcji systemowych. Jej
zadaniem jest stworzenie nowego procesu, potomka procesu
wywołującego. Interfejs to:
Zwracaną wartością jest PID potomka w procesie macierzystym
i 0 w procesie potomnym (proces potomny rozpoczyna się, jakby
sam wywołał forka i czekał na rezultat) bądź -1 w przypadku
niepowodzenia. Proces potomny dziedziczy cały kontekst (np.
tablicę deskryptorów, procedury obsługi sygnałów itd.) z procesu
macierzystego. Typowym przykładem użycia tej funkcji jest:
switch (fork()) {
case 0: to co ma się wykonać w procesie potomnym
}
Wywołanie fork jest jedynie interfejsem do
funkcji do_fork, zadeklarowanej w module kernel/fork.c; w niej
odbywa się cała praca.
1.2 Funkcja systemowa vfork()
Podobnie jak funkcja fork(), vfork tworzy proces potomny. Jednak w wyniku działania tej funkcji proces macierzysty zostaje zawieszony do chwili wywołania przez syna _exit() albo execve(). Syn stworzony przez vfork operuje na stronach pamięci rodzica. Podobnie jak fork(), vfork wywołuje funkcję wewnętrzną do_fork, ale w odróżnieniu od forka przekazuje w clone_flags parametry CLONE_VFORK i CLONE_VM.
1.3 Funkcja systemowa clone()
Funkcja ta jest rozszerzeniem forka o możliwość dzielenia
zasobów procesu, czyli udostępnienia synowi takich zasobów
ojca jak segmenty pamięci, tablice deskryptorów plików i
obsługi sygnałów. Funkcja clone przyjmuje jako argumenty min. flagi klonowania, określające rozdział zasobów pomiędzy rodzicem a procesem potomnym.
int __clone(int (*fn) (void *arg), void *child_stack,
int flags, void *arg)
Funkcje fork i vfork są jedynie szczególnymi przypadkami
funkcji clone - pierwsza nie uzywa żadnych flag klonowania,
druga korzysta z flagi dzielenia przestrzeni adresowej CLONE_VM.
Opis flag klonowania poniżej.
2.0 Implementacja: funkcja do_fork()
Funkcje systemowe istnieją wewnątrz jądra
jako zwykłe funkcje C, z tym, że ich nazwy poprzedzone są
prefiksem "sys_". Takie funkcje dla fork , clone i
vfork zadeklarowane są w module process.c . Ich zadaniem jest
wywołanie funkcji do_fork z odpowiednimi parametrami.
Najważniejszym z nich są flagi klonowania.
2.1 Flagi klonowania
Mogą się składać z następujących wartości:
- CLONE_VM - rodzic i potomek korzystają z tej samej
przestrzeni adresowej (czyt: wszystke zmienne są
dzielone ;).
- CLONE_FS - rodzic i potomek dzielą informację o
systemie plików (struktura fs_struct). Wywołanie takich
funkcji jak chroot(2), chdir(2), lub umask(2) przez jeden
proces spowoduje także zmiany w drugim.
- CLONE_FILES - rodzic i potomek dzielą tablice
deskryptorów plików.
- CLONE_SIGHAND - rodzic i potomek dzielą tablicę
obsługi sygnałów.
- CLONE_PID - potomek będzie miał taki sam PID jak
rodzic; możliwe tylko przy starcie systemu. Wywołanie
do_fork z tą flagą przez proces użytkownika spowoduje
błąd.
- CLONE_PTRACE - umożliwia śledzenie procesu potomnego.
- CLONE_VFORK - ojciec jest budzony przez potomka przy
wywołaniu mm_release (np. w wyniku exit lub exec).
- CLONE_PARENT - wskażnik na ojca w potomku będzie
pokazywał na ojca procesu 'klonującego'.
- CLONE_THREAD - ta sama grupa wątków.
- CLONE_SIGNAL = (CLONE_SIGHAND | CLONE_THREAD)
3.0 Algorytm do_fork
1) Sprawdzenie i ustawienie wartości struktury
task_struct na czas forkowania :
- Czy w clone_flags nie ma CLONE_PID. Jeśli jest, a proces
wywołujący ma PID różny od 0 to kończymy z błędem.
- Alokujemy pamięć dla nowej struktury task_struct; w
przypadku niepowodzenia wychodzimy z błędem.
- Kopiujemy informację o procesie macierzystym do nowej
struktury p. Pozostanie zmiana wartości niektórych pól.
- Sprawdza się , czy użytkownik nie przekracza limitu
procesów RLIMIT_NPROC; Jeśli tak, to błąd.
- Ustawianie początkowych wartości struktury (task_struct
*p) nowego procesu. Z ważniejszych:
- p->did_exec = 0; - jeszcze nie
wykonany
- p->swappable = 0; - nie można go
wyrzucić z pamięci
- p->state = TASK_UNINTERRUPTIBLE; -
nie mozna go przerwać; jest w trakcie tworzenia
swoich struktur i wykonanie np. funkcji obsługi sygnału
nie miałoby sensu.
- Pobieramy nowy, nieużywany PID (funkcja get_pid z
fork.c) dla nowego procesu.
- Jeśli podano flagę CLONE_VFORK to inicjalizujemy
strukturę completion (struktura i operacje na niej
zdefiniowano w completion.h).
2) Inicjalizacja 'własciwych' danych w task_struct:
- Zerujemy statystyki związane z nową strukturą, (np.
ilość odebranych sygnałów), obsługę sygnałów,
flagę przywództwa grupie procesów (przywództwo się
nie dziedziczy) itd.
- Uruchamiamy zegar czasu rzeczywistego nowego procesu.
3) Skopiowanie kontekstu procesu macierzystego;
wywoływane są funkcje:
- copy_files(clone_flags, p) - kopiuje tablice
deskryptorów rodzica i zwiększa liczbę odwołań do
i-nodów plików, do których odnoszą się otwarte w
tablicy deskryptory
- copy_fs(clone_flags, p) - tworzy strukturę
fs_struct (informacje o systemie plików; dowiazania do
i-nodów głównego i bieżącego katalogu)
- copy_sighand(clone_flags, p) - kopiuje funkcje
obsługujące sygnały
- copy_mm(clone_flags, p) - przydziela miejsce na
strukturę opisującą pamięć i zeruje statystyki
dostępu do pamięci. Nie kopiuje jednak zawartości
pamięci; zawartość bedzie skopiowana dopiero, gdy
powstanie żądanie modyfikacji przez rodzica lub potomka
(strategia Copy-on-Write).
Gdy którakolwiek z tych funkcji zwróci błąd, funkcja
do_fork wychodzi z błędem.
4) Kluczowe miejsce algorytmu fork: fizyczne rozdzielenie
procesów macierzystego i potomnego. Zajmuje się tym funkcja
copy_thread:
- copy_thread(0, clone_flags, stack_start, stack_size,
p, regs);
Od tego miejsca istnieją już dwa osobne procesy: ojciec,
który kontynuuje wykonanie funkcji i potomek, który będzie
miał złudzenie, że sam właśnie wykonał forka. Funkcja ta
zadba, żeby potomek otrzymał z tego 'wywołania' wartość 0.
Funkcja copy_thread zdefiniowana jest w pliku process.c i działa
bezpośrednio na rejestrach komputera.
5) Kończenie ustawień pól i wstawienie nowego procesu
do struktur systemowych:
- Włączamy wymianę pamięci (ang. swapping), wyłączane dla nowego procesu na początku do_fork.
- Określany jest rodzaj sygnału przesyłanego procesowi
macierzystemu procesu przez potomka przy jego
zakończeniu.
- Na zmienną retval (której wartość jest zwracana przez
do_fork) przypisany zostaje PID nowego procesu.
- Jeśli podano flagi CLONE_PARENT i CLONE_THREAD, to
ojcem nowego procesu zostanie ojciec procesu wywołującego fork.
- Jeśli otrzymaliśmy flagę CLONE_THREAD to dodajemy
proces/wątek do grupy wątków procesu macierzystego.
- SET_LINKS(p); - makro z pliku sched.h; dopisuje
nowy proces do dwukierunkowej listy cyklicznej procesów.
- PID nowego procesu zostaje wstawiony do haszowanej
tablicy PID-ów (funkcja hash_pid())
- Zwiększamy liczbę zadań w systemie (zmienna
nr_threads).
- Zmieniamy stan procesu potomnego (p->state) na
TASK_RUNNING oraz dodajemy go do kolejki procesów
aktywnych. Tym wszystkim zajmuje się funkcja
wake_up_process();
6) Jeśli jedną z flag była CLONE_VFORK - np w wyniku
wywołania vfork(), to proces macierzysty zostaje w tym
momencie wstrzymany do chwili wywołania przez potomka
mm_release. (zwolnienia przestrzeni adresowej)
7) Zwracamy wartość (PID nowego procesu)