Do strony głównej
Algorytm fork

Spis treści:


Fork dla początkujących

Fork jest jedną z najważniejszych funkcji systemowych. Jej zadaniem jest powołanie do życia nowego procesu na zasadzie relacji rodzic - dziecko. Proces, w którego kodzie znajdzie się wywołanie tej funkcji, staje sie ojcem nowego procesu.

Wykorzystanie funkcji fork w programie, a w konsekwencji powstanie nowego procesu następuje w wyniku wykonania kodu:

pid = fork ();

Po wykonaniu tej instrukcji w systemie będą już istnieć dwa osobne procesy - ich odróżnienie w programie jest możliwe dzięki wartoœści zwracanej przez funkcję fork - procesowi macierzystemu zwrócony zostanie identyfikator syna, procesowi potomnemu - zero.  Gdy system uzna, że nowego procesu nie da się utworzyć (na przykład z powodu braku pamięci lub przekroczenia limitów na liczbę istniejących procesów), funkcja fork zwróci wartośœć -1 i, oczywiśœcie, nie powstanie żaden nowy proces.

Początkującym badaczom Linuksa sprawia często problem zrozumienie, w jaki sposób z jednego powstają nagle dwa procesy, jak skorzystać z istnienia nowego procesu i jaki kod będzie on wykonywać -– wywołanie funkcji jest przecież czꜶcią kodu wcześœniej napisanego i skompilowanego programu. Możemy jednak przecież już w chwili kompilacji okreœślić kod obu procesów –- "starego" i "nowego", ojca i syna, i rozdzielić je w dowolnej chwili, wywołując właœśnie funkcję fork, na przykład tak:

Jeœśli kod programu, który ma wykonać nowo utworzony proces, jest dla nas zbyt długi lub z innej przyczyny chcemy go umieœścić w innym miejscu jako osobny program, to po wywołaniu fork możemy polecić synowi wykonanie tego wcześœniej napisanego i skompilowanego kodu, podając funkcji exec nazwę zawierającego go programu.

Wywołanie fork jest jedynym sposobem na stworzenie nowego procesu w systemie. Wyjatkiem od zasady rozwidlania procesów jest sposób powstawania procesu wymiany (o identyfikatorze zero), inicjowanego wewnętrznie przez jądro podczas ładowania systemu do pamięci (po włączeniu komputera). Jest on przodkiem wszystkich innych procesów działających w systemie.
 


Co warto wiedzieć przed rozpoczęciem analizy algorytmu i jego kodu źródłowego

Aby dowiedzieć się, jak działa funkcja fork, nieobce muszą być Czytelnikowi pojęcia: identyfikator procesu, kontekst procesu, stan procesu, stos jądra, tablica procesów –- nie ma tu miejsca, by je wszystkie wyjaœśniać; warto przeczytać wczeœniej garœść najważniejszych informacji o procesach w systemie Linux w jednym z szeroko dostępnych Ÿźródeł (na przykład w rozdziale 2.2.2 książki Bacha, wymienionej w bibliografii).

Do zrozumienia kodu funkcji fork niezbędna jest poza powyższym znajomoœść struktur, z jakich korzysta algorytm, co wiąże się z (choćby podstawową) znajomoœścią sposobu, w jaki system przechowuje informację o procesach oraz jak z tej informacji korzysta. Dla przykładu, powinieneśœ wiedzieć, że każdemu procesowi odpowiada wypełniona struktura task_struct, zawierająca jego podstawowe dane, a jądro utrzymuje tablicę takich struktur, przechowując w niej informację o procesach działających w systemie.  Opis pól struktury task_struct i innych, ważnych z punktu widzenia analizy kodu, można znaleŸźć w omówieniu struktur danych; bardziej szczegółowe informacje - bezpoœśrednio w komentarzu do kodu źŸródłowego funkcji.

Warto również zdawać sobie sprawę z ograniczeń nakładanych przez system na rozmiar pewnych struktur i ich liczbę - czꜶć z nich (na przykład definicję ograniczenia na liczbę procesów w systemie i maksymalną liczbę zadań, jakie może uruchomić pojedynczy użytkownik) zawiera plik tasks.h.
 


Algorytm fork
Wejśœcie:    brak
Wyjœście:    w procesie macierzystym: PID potomka,
                    w procesie potomnym: 0,
                    w przypadku błędu (nie powstaje nowy proces): -1

{

             }
}

W przypadku błędu w którejkolwiek z cz궜ci funkcji, spowodowanej przekroczeniem ograniczeń systemowych bądŸź brakiem pamięci, struktury zainicjowane do tej pory są kaskadowo niszczone.
Informacje o szczegółach algorytmu znajdziesz w komentarzu do kodu Ÿźródłowego funkcji fork.
 


Funkcja vfork

Stworzenie osobnej funkcji do rozwidlania procesów, vforka właœnie, miało na celu przyspieszenie zakończenia jego wywołania przez całkowita rezygnację z tworzenia fizycznych kopii stron pamięci procesu macierzystego. Zakładano przy tym, że proces-dziecko zaraz po powrocie z vforka wywoła jedną z funkcji exec - w tym wypadku kopiowanie stron jest po prostu bezcelowe (i tak zaraz załaduje się nowy program i przed chwilą skopiowane dane zostaną zamazane). Takie pobożne życzenie (nie sposób bowiem zmusić procesu do wywołania exec) może jednak doprowadzić do niebezpieczeństwa - potomek, wykonując się w przestrzeni adresowej ojca ma dostęp do jego segmentu danych i stosu, może je więc z łatwoœścią zmienić.

System, w którym zastosowano vforka, to Unix BSD - "zwykły" fork w tym systemie fizycznie kopiował strony procesu macierzystego, nowe rozwiązanie było więc znacznym usprawnieniem. W Linuksie vfork jest równoważny forkowi - zarządzanie pamięcią z zastosowaniem kopiowania dopiero w chwili zapisu do powielonej strony (copy-on-write) sprawiło, że implementacja osobnej funkcji stała się zbędna.


Funkcja clone

Warto przy opisie forka wspomnieć również o funkcji clone, posiadającej co prawda własny (zarezerwowany) numer w tablicy funkcji systemowych, ale domyœślnie niedostępnej (aby z niej skorzystać, trzeba zrekompilować jądro ze zdefiniowanym symbolem CLONE_ACTUALLY_WORKS_OK).

Funkcja ta jest rozszerzeniem forka o mozliwośœć klonowania 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, a nawet identyfikator procesu (pid).

Wywołanie funkcji wymaga podania owych atrybutów klonowania, a także (opcjonalnie) wskaŸźnika stosu dziecka:

pid_t clone (void* sp, unsigned long flags)

sp to właœnie ów wskaŸźnik (NULL wymusza obsługę standardową), zaśœ pole flags zawiera jedno lub wiele poleceń klonowania:

Najmłodszy bajt pola okreśœla sygnał wysyłany do rodzica w chwili śœmierci dziecka.

Jako że funkcja sys_clone realizująca wywołanie clone korzysta z funkcji do_fork (patrz: komentarz do kodu Ÿźródłowego), można używać clone jak standardowego forka - wywołanie miałoby wówczas postać:

clone (0, SIGCHLD | COPYVM).
 

Dla zainteresowanych

Funkcje systemowe istnieją wewnątrz jądra jako zwykłe funkcje C, z tym, że ich “"oficjalne”" nazwy poprzedzone są prefiksem ťsys_ (to duże uproszczenie, wywołanie wymaga jeszcze skorzystania z makra sys_call, wyszukującego funkcję w bibliotece oraz odpowiedniego obsłużenia parametrów).  Skorzystanie z forka polega zatem na wywołaniu funkcji jądra sys_fork (jednolinijkowej, plik process.c), która z kolei wywoła właśœciwą treść forka - do_fork – - tak właśœnie nazywa się funkcja zawierająca cały algorytm; nie warto szukać definicji funkcji nazwanej fork, gdyż takiej definicji po prostu nie ma.

Po analizie funkcji fork na pewno warto przeczytać również rozdział dotyczący funkcji exit, będącej niejako odwrotnoœścią forka. Analiza kodu exit pozwala docenić prostotę forka i wspomagających go rozwiązań (np. zastosowania makra-pętli for_each_task, dającej się łatwo zastąpić mądrzejszym algorytmem, ale eliminującej nie zawsze potrzebne dodatkowe struktury i symetrię w ich wypełnianiu na rzecz jednorazowego przejrzenia listy zadań w chwili tworzenia procesu).

Bardziej zainteresowanych działaniem vforka odsyłam na strony 314 i 332 książki Bacha. Można tam znaleźŸć kod i komentarz do programów, wykorzystujących niebezpieczne własnoœści vforka.

Więcej informacji o funkcji clone, dokładny wykaz plików i parametrów można uzyskać w dokumentacji (man clone).
 


Bibliografia

Maciej Ogrodniczuk
26 stycznia 1998 r.