Spis treści:
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:
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:
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.
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ć wczeniej 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.
{
sprawdź dostępność zasobów jądra, czyli warunków na ograniczenia systemowe na liczbę procesów w systemie i dla danego użytkownika;
znajdź wolną pozycję w tablicy procesów;
skopiuj stos jądra procesu macierzystego do przygotowanego w pamięci miejsca na stos jądra potomka, by nowy proces mógł poprawnie powrócić z forka (to tylko jego złudzenie wywołane i kontrolowane przez jądro - przecież nowy proces dopiero powstaje);
zaznacz, że proces jest w stanie "tworzony";
skopiuj dane ze struktury opisującej proces macierzysty do struktury procesu potomnego - później zmienimy jedynie wartości tych pól, które w procesie potomnym są inne niż w procesie macierzystym;
ustal unikatowy identyfikator nowego procesu (pid);
skopiuj od ojca informację o deskryptorach otwartych plików;
skopiuj informację o katalogu procesu oraz masce praw dostępu nadawanych nowo tworzonym plikom;
skopiuj informację o obsłudze sygnałów;
skopiuj informację dotyczącą struktur pamięci procesu;
ustal rodzinne dowiązania między nowym procesem a innymi procesami z drzewa zadań;
wstaw strukturę opisującą nowy proces do tablicy procesów;
rozwidl fizycznie procesy ojca i syna;
if (wykonywany proces jest procesem macierzystym)
{
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.
Stworzenie osobnej funkcji do rozwidlania procesów, vforka włanie, 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.
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:
sp to włanie ów wskaźnik (NULL wymusza obsługę standardową), zaś pole flags zawiera jedno lub wiele poleceń klonowania:
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ć:
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).