Do spisu treści tematu 4

4.7 Algorytm fork




Spis treści


Wprowadzenie

Dokładny opis funkcji fork jest w rozdziale poświęconemu tworzeniu nowego procesu w temacie Algorytm Fork. Tutaj zajmiemy się tą funkcją koncentrując się na pamięci. Zaleca się by przed przeczytaniem tego rozdziału zapoznać się z podstawowymi infomacjami dotyczącymi pamięci w Linuxie oraz ze strukturami: mm_struct, vm_area_struct, pte_t.


Opis ogólny

Linux udostępnia trzy funkcje systemowe do tworzenia nowego procesu. Są to fork i clone oraz execve. W chwili obecnej funkcji clone nie ma w standartowej bibliotece C - libc. Dlatego skoncentruję się na mniej uniwersalnej ale o wiele częściej używanej funkcji fork. Jak wiadomo funkcja fork tworzy nowy proces na podstawie wywołującego ją procesu. Oto, jak to się odbywa z punktu widzenia pamięci wirtualnej procesów.

Wydawać by się mogło, że naturalnym sposobem powielania procesu jest następujący schemat: kopiujemy obszar danych procesu rodzica, a obszar kodu jest przez oba procesy, rodzica i potomka, współdzielony. Taki algorytm jednak jest bardzo kosztowny w przypadku dłuższych programów, które w danej chwili mogą mieć cześć pamięci wirtualnej na dysku. W poprzednich unixowych systemach istniały dwie funkcje: fork i vfork. Fork działał tak jak opisałem powyżej, a vfork był wywoływany tylko wtedy, gdy następną instrukcją potomka była instrukcja exec. W drugim przypadku pamięć wirtualna nie była kopiowana.

W Linuxie ten problem doczekał się lepszego rozwiązania. Stosuje się tu technikę zwaną "copy on write", czyli kopiowanie w razie pisania, co oznacza, że wirtualna pamięć będzie skopiowana jedynie wtedy, gdy jeden z dwóch procesów będzie próbował coś zapisać. Jedynie pamięć służąca tylko do odczytu, np. kod programu, zawsze będzie dzielona. Jak dokładnie się to odbywa, opisuję poniżej.


Opis szczegółowy

Główną funkcją, która jest wywoływana przy używaniu forka jest do_fork, znajdująca się w pliku fork.c.

Na początku tworzona jest nowa struktura task i dodana do tablicy procesów, następnie tworzony jest nowy stos kontekstu jądra dla procesu, nowy identyfikator procesu po czym kopiowane są kolejne zasoby procesu, czyli deskryptory plików, procedury obsługi sygnałów, struktury opisujące pamięć wirtualną rodzica.

Do_fork jest wywoływany z trzema parametrami, z których właściwie interesuje nas tylko parametr clone_flags. Flagi które mogą być ustawione to m.in. CLONE_FILES, CLONE_SIGHAND, CLONE_VM, które określają czy dany zasób ma być kopiowany, czy klonowany (czyli właściwie współdzielony przez oba procesy). I tak np. jeśli wywołamy do_fork z ustawioną flagą CLONE_FILES, zwiększy się jedynie licznik odwołań do struktury files, która będzie współdzielona.

Podobnie jest z interesującą nas najbardziej flagą CLONE_VM. Do_fork wywołuje funkcję copy_mm z parametrem clone_flags (są to te same flagi, z którymi wywołano do_fork). Funkcja sprawdza, czy jest ustawiona flaga CLONE_VM. Jeśli tak, funkcja ogranicza się do wywołania makra SET_PAGE_DIR, które ustawia katalog stron procesu na katalog rodzica a następnie zwiększa licznik odwołań do struktury mm_struct. W tej sytuacji oba procesy mają prawo zapisu do tej samej pamięci. Jest to wykorzystywane przy implementacji wątków.

Standardowo funkcja systemowa sys_fork (z pliku process.c) wywołuje do_fork z ustawioną tylko flagą SIGCLD, która określa, jaki sygnał zostanie wysłany do rodzica po zakończeniu działalności przez potomka. W tym więc przypadku, funkcja copy_mm wywoływana jest bez ustawionej flagi CLONE_VM i wykonuje następujące czynności: tworzona jest nowa struktura opisująca pamięć wirtualną procesu (struktura mm_struct); a następnie, przez funkcję new_page_tables, tworzony jest nowy katalog stron, wypełnia go zerami, a przestrzeń 3-4 GB odwzorowuje na pamięć jądra , po prostu przepisując wpisy z katalogu swapper_pg_dir. Zera w katalogu stron oraz w tablicy stron, są traktowane jako strona alokowana przy pierwszym dostepie.

Następnie copy_mm wywołuje funkcję dup_mmap. W tej funkcji następuje skopiowanie całej listy vm_area_struct z procesu-rodzica do procesu-potomka. Nie jest to jednak same skopiowanie, muszą być wykonane oprócz tego także inne czynności.

W przypadku, gdy obszar pochodzi z odwzorowania i-węzła zwiększamy jedynie licznik odwołań do tego obszaru, a sam obszar dodajemy do struktury obszarów dzielonych. W przypadku, gdy mamy do czynienia z normalnym obszarem wywoływana jest funkcja copy_page_range. Po pierwsze sprawdza ona, czy możemy pisać na obszarze i czy nie jest on dzielony. Następnie w pętli (wywołanie copy_pmd_rang, copy_pte_range, copy_one_pte) dla każdej strony należącej do obszaru zerowana jest flaga RW, czyli zakaz zapisu na tej stronie. Jest to robione jednak jedynie wtedy gdy obszar do którego należy strona, nie jest dzielony i można na nim pisać. Po czym zwiększamy licznik odwołań do strony.

W takiej sytuacji, jeśli jeden z procesów będzie chciał zapisać coś do takiej strony, zostanie wywołany błąd ochrony strony. System sprawdzi czy w strukturze vm_area_struct do której jest przypisana strona, jest ustawiona flaga do zapisu i jeśli tak, to strona zostanie skopiowana. W przypadku obszaru kodu zwiększany jest tylko licznik odwołań do pamięci, przez co jest ona zwalniana dopiero po zakończeniu pracy przez oba procesy.

Na końcu funkcji dup_mmap, w przypadku braku jakichkolwiek błędów budowane jest drzewo AVL z listy obszarów pamięci wirtualnej procesu - potrzebne do poprawnego i sprawnego działania funkcji na pamięci wirtualnej.


Bibliografia

  1. Pliki źródłowe Linuxa:
  2. David A. Rusling: The Linux Kernel


Autor: Michał Smoktunowicz