next_inactive up previous


Procesy w Linuksie - funkcje fork(),vfork() i clone(); przełączanie kontekstu

Grzegorz Kaczor ( 181264 )


Spis rzeczy

Wprowadzenie

Wstęp

W tym referacie zostanie omówione zastosowanie i implementacja funkcji fork(), vfork() i clone() w Linuksie, a także implementacja samego mechanizmu przełączania kontekstu realizowanego przez jądro Linuksa. Temat dotyczy jądra w wersji 2.4.7

Rozdział pierwszy opisuje zastosowanie wyżej wymienionych funkcji i ogólną problematykę z nimi związaną. W rozdziale drugim postaram się dokładniej przyjrzeć funkcjom fork(), vfork() i clone() ''od środka'', czyli omówię funkcję do_fork(). W rozdziale trzecim mowa będzie o implementacji przełączania kontekstu - o makrze switch_to(). Czwarty rozdział stanowić będą zadania do obu tematów, a piąty rozwiązania i szkice rozwiązań.

Zamieszczanie kodu i opisy

W referacie postaram się zamieszczać jak najmniej kodu źródłowego. Postaram się to robić tylko tam, gdzie jest to konieczne lub tam, gdzie rzeczywiście to coś wyjaśnia. Poza tym w przypadku kodu składającego się z dużej ilości asemblera postaram się napisać to w sposób uproszczony, nie niszcząc funkcjonalności, za to poprawiając czytelność.

W miarę możliwości opisy algorytmów i struktur danych będą dokładne, w większości nie będę jednek opisywać struktur danych, z których będę korzystać. Zakładam, że są one Czytelnikowi znane.

Tworzenie procesów

W tym podrozdziale przedstawię sposób tworzenia procesów w Linuksie od strony bardziej ogólnie - do szczegółów implementacji przejdę w rozdziale drugim.

Od strony programisty

Od strony programisty tworzenie nowego procesu przebiega najczęściej w sposób następujący: programista wywołuje funkcję fork() na przykład w taki sposób:

pid = fork();
  if ( pid < 0 ) syserr("nie udalo sie fork");
  if ( pid ) {
    // jestesmy w procesie macierzystym
    ... kod dla procesu macierzystego ...

  }
  else {
    // jestesmy w procesie potomnym
    ... kod dla procesu potomnego ...
    [ execve(...); ]
  }

Z funkcjami vfork() i clone() sprawa wygląda podobnie. Jak widzimy, żeby podany powyżej kod mógł się wykonywać, funkcja fork() musi tworzyć nowy proces i w procesie macierzystym zwracać coś niezerowego, a w procesie potomnym zero. Tak jest w rzeczywistości, funkcja fork() w procesie macierzystym zwraca PID właśnie utworzonego procesu potomnego, a w procesie potomnym 0. Ponadto, żeby programista mógł używać zadeklarowanych wcześniej zmiennych w kodzie potomka, muszą one być potomkowi znane. Wcale nie znaczy to, że potomek od początku wykonuje kod rodzica. Nie - odpowiednio aktualizuje się tylko przestrzeń adresowa potomka i dane dotyczące rejestrów. Tym właśnie zajmuje się funkcja fork() - oprócz tworzenia nowego procesu dba o spójność informacji związanych z nowym procesem, a także wprowadza proces do struktur jądra i przygotowuje do wykonywania się w środowisku wieloprogramowym.
Różnice Chociaż działają podobnie - tworzą nowe procesy - to jednak funkcje fork(), vfork() i clone() różnią się od siebie - fork() tworzy nowy proces i kopiuje jego przestrzeń adresową i inne informacje o procesie ( z dokładnością do zastosowania copy-on-write, o tym będzie później ); vfork() wstrzymuje wykonanie procesu macierzystego do momentu wykonania przez potomka _exit() lub execve() - przydaje się to, jeśli tworzymy nowy proces i zaraz wykona on execve(), żeby wykonywać inny kod, wtedy nie ma sensu kopiowanie przestrzeni adresowej rodzica, skoro nie będziemy z niej korzystać; clone() umożliwia zdecydowanie, które fragmentu struktur danych związanych z jądrem procesy będą współdzielić - przestrzeń adresową, procedury obsługi sygnałów, informację o systemie plików, tablicę deskryptorów plików. Clone() służy do tworzenia LWP - tzw. lekkich procesów - współdzielących ze sobą pewne elementy struktur danych związanych z jądrem. Na LWP oparta jest w Linuksie np. implementacja wątków.

Wywołanie

pid_t fork(void);
pid_t vfork(void);
int __clone(int (*fn) (void *arg),
            void *child_stack,
            int flags,
            void *arg);

Argumenty funkcji:

Przełączanie kontekstu

W każdym systemie wieloprogramowym pojawia się problem przełączania kontekstu. Zwłaszcza w systemach jednoprocesorowych, kiedy chcemy stworzyć płynnie działający system, umożliwiający ''jednoczesne'' wykonywanie różnych programów. W tym rozdziale opiszę ogólnie podstawowe problemy związane ze zmianą kontekstu.

Kontekst sprzętowy

Żeby jądro Linuksa mogło wywłaszczyć proces, musi zachować stan odpowiednich rejestrów procesora z momentu wywłaszczania tak, żeby proces, kiedy znowu przyjdzie czas na jego wykonanie, mógł wykonywać się bez problemów, i żeby programista nie musiał troszczyć się o obsługę sytuacji, kiedy proces jest wywłaszczany.
Kontekstem sprzętowym nazywamy zestaw danych potrzebny do załadowania do rejestrów procesora przed wznowieniem wykonania procesu. Zmiana kontekstu jest to więc zmiana wartości rejestrów procesora powiązana z zapamiętaniem wartości poprzednich w celu przywrócenia ich w chwili przyznania procesowi procesora.
Dane potrzebne procesowi odpowiadające zawartości rejestrów, która ma być zachowana, przechowywane są w strukturze thread_struct (p->thread) procesu. Strukturę tą przedstawiam poniżej:

struct thread_struct {
        unsigned long   esp0;
        unsigned long   eip;
        unsigned long   esp;
        unsigned long   fs;
        unsigned long   gs;
/* Hardware debugging registers */
        unsigned long   debugreg[8];  /* %%db0-7 debug registers */
/* fault info */
        unsigned long   cr2, trap_no, error_code;
/* floating point info */
        union i387_union        i387;
/* virtual 86 mode info */
        struct vm86_struct      * vm86_info;
        unsigned long           screen_bitmap;
        unsigned long           v86flags, v86mask, v86mode, saved_esp0;
/* IO permissions */
        int             ioperm;
        unsigned long   io_bitmap[IO_BITMAP_SIZE+1];
};

Do przechowywanych w tej strukturze rejestrów należą między innymi rejestry segmentowe fs i gs, wskaźnik instrukcji eip, wskaźnik stosu esp i esp0, a także rejestry zmiennoprzecinkowe, którym odpowiada pole i387. Zapisywana tutaj jest także mapa uprawnień IO, która jest ładowana do segmentu TSS związanego z procesorem, na którym wykonuje się proces. W bliższe szczegóły tej struktury nie będę się na razie zagłębiać.

Kiedy następuje przełączenie

Przełączenie kontekstu następuje na skutek wywołania makra switch_to() przez funkcję schedule(). Zdarzenie takie zachodzi wtedy, kiedy proces aktualnie się wykonujący na danym procesorze skończy się, pojawi się proces o wyższym priorytecie niż działający aktualnie, proces zawiesi się w oczekiwaniu na jakieś zdarzenie, np. operację wejścia/wyjścia, albo skończy się procesowi kwant czasu.

Funkcje fork(),vfork() i clone()

Wywołania funkcji do_fork()

W przypadku wszystkich trzech funkcji fork(), vfork() i clone(), wywoływana jest funkcja do_fork(). Omówieniem tej funkcji zajmiemy się w następnym podrozdziale.

Sygnatura funkcji do_fork() wygląda tak:

int do_fork(unsigned long clone_flags, unsigned long stack_start,
            struct pt_regs *regs, unsigned long stack_size)

Argumenty:

Nie wdając się w szczegóły, warto powiedzieć, że wywołanie fork() jest to wywołanie z clone_flags = SIGCHLD, co oznacza niewspółdzielenie zasobów, a vfork() to wywołanie z clone_flags = SIGCHLD | CLONE_VM | CLONE_VFORK. Clone() może być wywoływane z różnymi ustawieniami clone_flags ( zob. opis vfork() w rozdziale 1 ).

Funkcja do_fork()

Przyjrzyjmy się po kolei etapom tworzenia nowego procesu przez funkcję do_fork().

  1. Sprawdzane jest, czy w clone_flags została ustawiona flaga CLONE_PID, jeśli tak, to sprawdza się, czy proces wywołujący ma PID == 0. Jeśli nie, zwracamy błąd EPERM (więcej w rozdziale 1 przy opisie flag w vfork()).
  2. Alokujemy miejsce na strukturę task_struct dla nowego procesu przy pomocy funkcji alloc_task_struct(), o której powiem w następnym podrozdziale.
  3. Jeśli proces, który powstaje, spowoduje przekroczenie limitów na ilość procesów dla użytkownika lub ilość procesów w systemie, tworzenie procesu nie udaje się.
  4. Ustawiamy domenę wykonania na domenę procesu macierzystego i zwiększamy licznik odniesień do pliku, którego kod wykonuje proces macierzysty, a więc od zaraz i potomny.
  5. Ustawiamy stan procesu na TASK_UNINTERRUPTIBLE ( uśpiony bez możliwości przerwania sygnałem - nie ma sensu budzenie procesu jeszcze w tej chwili ); uniemożliwiamy zrzut obrazu pamięci ( p->swappable = 0 ) i zaznaczamy, że proces jeszcze nie wykonywał execve() ( p->did_exec = 0 ).
  6. Wywołujemy funkcję copy_flags(), która zajmuje się ustawieniem flag procesu potomnego (p->flags) zgodnie z ustawieniami procesu macierzystego i z argumentem clone_flags. Funkcję copy_flags() omówię później.
  7. Wybieramy numer PID dla nowego procesu ( funkcja get_pid() ).
  8. Ustawiamy wskaźniki na następny i poprzedni proces na liście procesów działających na NULL, wzkaźnik na najmłodsze dziecko nowego procesu także na NULL ( nowo tworzony proces nie ma jeszcze dzieci ).
  9. Inicjalizujemy kolejkę oczekiwania na zakończenie potomków w nowym procesie.
  10. Jeśli była ustawiona flaga CLONE_VFORK, inicjalizujemy mechanizm oczekiwania na zwolnienie pamięci przez potomka ( w momencie wykonania _exit() albo execve() ).
  11. Zerujemy zmienną p->sigpending służącą do przechowywania sygnałów, które nadeszły do procesu i inicjalizujemy mechanizm odbierania sygnałów.
  12. Inicjalizujemy jeszcze kilka zmiennych związanych np. z obsługą czasu, czasem użycia procesora itp., także dla sytuacji, kiedy mamy kilka procesorów. Zerujemy zmienną (p->leader) - nie dziedziczy się bycia liderem sesji - w przeciwnym wypadku mechanizm grupy procesów nie miałby sensu.
  13. Kopiujemy te struktury danych, ktorych procesy macierzysty i nowo tworzony nie będą współdzielić. Zajmują się tym funkcje copy_files, copy_fs, copy_sighand(), copy_mm(). Odpowiednio kopiują deskryptory plików, informację o systemie plików, procedury obsługi sygnałów i deskryptor pamięci i tablice stron. Robią to lub nie w zależności od ustawienia flagi clone_flags. O skutkach współdzielenia zasobów jest troche przy omawianiu flag do clone() w rozdziale 1.
  14. Inicjalizujemy stos trybu jądra procesu potomnego wartościami pobieranymi ze stosu rodzica przy pomocy funkcji copy_thread() ( dalej ).
  15. Zerujemy zmienną p->semundo - nie ma potrzeby zwalniania jakiegoś semafora, kiedy bedziemy się kończyć, bo jak na razie jeszcze nic nie robiliśmy z semaforami.
  16. Proces jest już w duzym stopniu utworzony, teraz umożliwiamy zrzut pamięci (p->swappable=1), ustawiamy sygnał do wysłania do ojca w momencie śmierci dziecka na ten podany w clone_flags ( o ile jest dopuszczalny ); domyślnie nie ustawiamy sygnału wysyłanego do dziecka w momencie śmierci rodzica.
  17. Ustawiamy zmienne związane z grupą wątków, do której należy proces. Inicjalizujemy struktury z tym związane.
  18. Przyznajemy procesowi kwant czasu - proces macierzysty dzieli całkowicie swój kwant aktualny na dwie części - połowę dostaje proces potomny. W razie czego, jeśli rodzicowi już nic nie zostanie z tego podziału, rodzic ustawia flagę need_resched.
  19. Ustawiamy zmienne związane z relacjami rodzicielskimi między procesami.
  20. Dołączamy proces do listy procesów(SET_LINKS(p)), umieszczamy proces w tablicy pidhash ( hash_pid(p) ), zwiększamy zmienną zawierającą ilość procesów w systemie ( nr_threads++ )
  21. Jeśli nowy proces ma być śledzony przez ptrace(), wysyłamy mu sygnał SIGSTOP.
  22. Budzimy proces przy pomocy funkcji wake_up_process() - zostaje przeniesiony do listy procesów gotowych.
  23. Zwiększamy ilość wykonanych forków w systemie i, jeśli potrzeba ( vfork() ), usypiamy rodzica do czasu wykonania przez potomka _exit() lub execve().
  24. Zwracamy PID nowo utworzonego procesu. Wartość ta zostanie zwrócona w procesie current, czyli w procesie macierzystym.

Tak przygotowany proces może się już zacząć wykonywać, może też mieć p->counter == 0. Wtedy zacznie się wykonywać wraz z rozpoczęciem nowej epoki i odnowieniem kwantów czasu.

Niektóre funkcje wywoływane przez do_fork()

W tej sekcji dokumentu omawiam niektóre funkcje, których do_fork() używa do inicjalizacji zmiennych deskryptora nowego procesu.

Makro switch_to()

Treść makra

Treść może się wydać trochę niezrozumiała, dlatego zapiszę algorytm w ''pseudojęzyku'' i w wersji uproszczonej:


\begin{algorithm}
% latex2html id marker 114\caption{switch\_to(prev,next)}\be...
...:
\STATE pop ebp
\STATE pop edi
\STATE pop esi
\end{algorithmic}\end{algorithm}

A __switch_to:


\begin{algorithm}
% latex2html id marker 119\caption{\_\_switch\_to(prev,next)...
...p,next->io\_bitmap)
\STATE END IF
\STATE END IF
\end{algorithmic}\end{algorithm}

Opis działania funkcji

switch_to

Makro switch_to() działa w następujący sposób:
Bierzemy argumenty - prev - aktualny proces, next - proces, na który się przełączamy.

__switch_to

Funkcja __switch_to:

Zachowywanie rejestrów zmiennoprzecinkowych

Ze względów efektywnościowych w przełączaniu kontekstu rezygnujemy z wykonywania jakichkolwiek zbędnych operacji. Jednym ze sposobów przyspieszenia przełączania kontekstu jest próba zmniejszenia ilości operacji zachowywania i przywracania wartości rejestrów zmiennoprzecinkowych. Technika ta działa w ten sposób:

Zadania

Tworzenie procesów

  1. W jaki sposób można uniknąć konieczności kopiowania całej przestrzeni adresowej procesu macierzystego do procesu potomnego w sytuacji, kiedy tworzymy proces jedynie w celu wykonania natychmiast exec() ?
  2. W jaki sposób przydziela się nowo tworzonemu procesowi priorytet dynamiczny ( counter ) ?
  3. Podaj flagi, z którymi należałoby wykonać __clone(), żeby uzyskać fork() i vfork().
  4. Dlaczego nie dziedziczy się bycia liderem sesji ?

Przełączanie kontekstu

  1. W jaki sposób przyspiesza się przełączanie kontekstu dla procesów, które nie używają rejestrów zmiennoprzecinkowych? Czy do tego konieczne jest wsparcie sprzętowe?

Rozwiązania

Tworzenie procesów

  1. W jaki sposób można uniknąć konieczności kopiowania całej przestrzeni adresowej procesu macierzystego do procesu potomnego w sytuacji, kiedy tworzymy proces jedynie w celu wykonania natychmiast exec() ?
    1. Przez używanie funkcji vfork() - procesy dzielą przestrzeń adresową, proces macierzysty śpi do czasu, aż proces potomny wykonw exec(), wtedy się budzi i idzie dalej
    2. Przez clone() i współdzielenie zasobów - można podać zasoby, które się chce dzielić z procesem potomnym
    3. Przez fork(), tak po prostu - w jądrze jest copy-on-write - kopiowane będą tylko te strony fizyczne, które będą modyfikowane.

  2. W jaki sposób przydziela się nowo tworzonemu procesowi priorytet dynamiczny ( counter ) ?

    Proces macierzysty dzieli swój counter na pół i połowę oddaje procesowi potomnemu. Jeśli zostanie mu 0, to ustawia flagę need_resched.

  3. Podaj flagi, z którymi należałoby wykonać __clone(), żeby uzyskać fork() i vfork().

  4. Dlaczego nie dziedziczy się bycia liderem sesji ? Ponieważ wtedy każdy proces byłby liderem sesji, czyli należałby do innej sesji; wtedy mechanizm sesji nie miałby sensu.

Przełączanie kontekstu

  1. W jaki sposób przyspiesza się przełączanie kontekstu dla procesów, które nie używają rejestrów zmiennoprzecinkowych? Czy do tego konieczne jest wsparcie sprzętowe?

Literatura

SO z poprzednich lat
różni autorzy: Referaty z Systemów Operacyjnych z poprzednich lat MIMUW 1998-2000

Linux Kernel Internals
Tigran Aivazian: Linux Kernel 2.4 Internals 2001

Linux Kernel
D.P.Bovet & M.Cesati: Linux Kernel O'Reilly 2001

Źródła jądra
różni autorzy: Źródła jądra linuksa 2.4.7

About this document ...

Procesy w Linuksie - funkcje fork(),vfork() i clone(); przełączanie kontekstu

This document was generated using the LaTeX2HTML translator Version 99.2beta8 (1.43)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -split 0 referat.tex

The translation was initiated by Janina Mincer-Daszkiewicz on 2001-12-27


next_inactive up previous
Janina Mincer-Daszkiewicz 2001-12-27