fork INGDGQOBTLT3EN54O4V6SR5MNFCMO2OEGNGMO6Q





2.1.1 Tworzenie procesu - algorytm fork




Do spisu tresci tematu 2
2.1.1 Tworzenie procesu - algorytm fork




Spis tresci

Wprowadzenie
Struktury danych:
wprowadznie,
task_struct,
files_struct,
fs_struct,
mm_struct,
signal_struct
Procedury pomocnicze:
wprowadzenie,
find_empty_process(),
get_pid(),
dup_mmap(),
copy_mm(),
copy_fs(),
copy_files(),
copy_sighand(),
makro SET_LINKS()
Funkcje systemowe:
wprowadzenie,
fork() oraz
vfork(),
clone() - wsparcie dla watkow,
implementacja - do_fork(),
Bibliografia
Pytania i odpowiedzi




Wprowadzenie


Funkcja systemowa fork() jest jedna z najwazniejszych funkcji
w systemach unixowych, takze w Linuxie. Za jej sprawa, i tylko za
jej sprawa, moga sie pojawiac w systemie nowe procesy. Jedynym procesem,
ktory jest powolywany do zycia w inny sposob jest proces o numerze
(identyfikatorze) 1, czyli init. Jak to sie dzieje dokladnie,
dowiesz sie w omowieniu tematu 10.


Z punktu widzenie programisty, fork() jest bardzo prosta
funkcja. Wywoluje sie go tak:

pid = fork();

i w wyniku dostaje dwie rozne wartosci. Proces macierzysty, ktory
wowolal forka, dostaje identyfikator dziecka. Dziecko dostaje
w wyniku 0. Dzieki temu procesy moga poznac "ktory jest ktory" bez uciekania
sie do sztuczek. Dziecko jest prawie dokladna kopia rodzica - rozni sie
tylko tym, co zwraca fork(). Oczywiscie w przypadku
niepowodzenia dziecko nie jest tworzone a rodzic otrzymuje informacje o
bledzie.


Struktury danych

Wprowadzenie


Wlasciwie wszystkie ponizsze struktury powinny zostac omowione w
temacie 1, ale przyda sie spojrzenie na nie
z "naszej", forkowej, strony.

fork() wykorzystuje do swoich dzialan glownie systemowa
tablice task[NR_TASKS], zadeklarowana w pliku
include/linux/sched.h. Jest to "potrojna" struktura danych,
bowiem na tablicy tej zaimplementowane sa dwukierunkowe listy (m. in. dla
procesow gotowych do wykonania i wszystkich procesow) oraz drzewo genealogiczne.
Dzialanie omawianej funkcji fork()sprowadza sie do wpisania
nowych informacji do powyzszej tablicy i zapewnienia spojnosci danych po
dokonaniu wpisu.


W Linuxie moze byc uruchomionych maksymalnie NR_TASKS
procesow. Root moze wszystko - nie ma nakladanych na niego zadnych
ograniczen, oprocz powyzszej liczby. Zwykly uzytkownik moze uruchomic
maksymalnie MAX_TASKS_PER_USER procesow, ale w systemie
musi zawsze zostac przynajmniej MIN_TASKS_LEFT_FOR_ROOT
pozycji w tablicy procesow. Odpowiednie wartosci wynosza standardowo: 512,
256, 4 i sa zdefiniowane w pliku
include/linux/tasks.h.


Struktura task_struct


Ta struktura to "metryczka" procesu. Zawiera wszystkie informacje o
procesie.
Oto interesujace nas fragmenty:

struct task_struct {
volatile long state;
/* -1 zombie, 0 spiacy badz gotowy, >0 zatrzymany */

...

struct task_struct *next_task, *prev_task;
struct task_struct *next_run, *prev_run;
/* wskazniki sluzace do implementacji dwukierunkowych kolejek
procesow gotowych do wykonania i wszystkich procesow */

...

int did_exec:1; /* czy po forku wykonano exec? */
int pid;

...

struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
/* wskazniki do: (oryginalnego) ojca, najmlodszego dziecka,
starszego i mlodszego rodzenstwa */
...

struct fs_struct *fs;
struct files_struct *files;
struct mm_struct *mm;
struct signal_struct *sig;
/* wskazniki do struktur, ktorych znaczenie
opisane jest nizej */
};


Pelna definicja
task_struct znajduje sie w
include/linux/sched.h.

Struktura files_struct


Struktura files_struct dotyczy informacji o plikach uzywanych
przez pojedynczy proces. Jak prawie kazda struktura dotyczaca informacji
o procesie, tak i ta zawiera licznik odwolan do niej, umozliwiajac
wspoldzielenie struktury przez kilka procesow. Oto pelna definicja:

struct files_struct {
int count;
/* licznik odwolan do struktury */
fd_set close_on_exec;
/* zbior deksryptorow plikow, ktore beda automatycznie
zamkniete przy wykonywaniu funkcji exec */
struct file * fd[NR_OPEN];
/* tablica otwartych plikow procesu */
};


Tutaj
mozna obejrzec sobie oryginalna definicje.

Struktura fs_struct


Ta stuktura zawiera informacje o masce praw dostepu dla nowo tworzonych
przez uzytkownika plikow (umask), oraz wskazniki do dwoch i-wezlow:
wezla naszego katalogu glownego (ktory mozemy zmieniac przez
chroot()) oraz katalogu, w ktorym sie aktualnie znajdujemy.
Dzieki temu system wie, gdzie jestesmy i nie pozwoli innemu procesowi usunac
naszego katalogu aktualnego. Definicja jest krotka:

struct fs_struct {
int count;
/* licznik odwolan do struktury */
unsigned short umask;
/* umask uzytkownika */
struct inode * root, * pwd;
/* wskazniki do i-wezlow katalogu glownego i aktualnego */
};


a jej oryginalny wyglad mozna znalezc
tutaj.


Struktura mm_struct


Ta struktura jest bardzo skomplikowana i powinna byc w calosci omowiona w
temacie 4. Dosc powiedziec, ze struktura ta
zawiera ona m. in. nastepujace
informacje: o aktualnym kontekscie procesu, poczatku i koncu jego segmentu
kodu, danych, parametrow wywolania, przekazanego srodowiska shellowego,
informacje o segmencie stosu, poczatku tablicy stron, oraz informacje o
pamieci wirtualnej procesu. Jedyna interesujaca
nas informacja jest to, ze ta struktura takze moze byc wspoldzielona
przez kilka procesow (zob. pole count w
definicji
stuktury).


Struktura signal_struct


Struktura signal_struct to wlasciwie tablica funkcji
obslugujacych sygnaly, uzupelniona o dodatkowe, nieistotne dla nas,
informacje.
Jej pelna
definicja jest bardzo krotka:

struct signal_struct {
int count;
/* licznik odwolan do struktury */
struct sigaction action[32];
/* tablica akcji podejmowanych po otrzymaniu sygnalu */
};

Strukture sigaction mozna obejrzec
tutaj.



Procedury pomocnicze

Wprowadzenie

Ponizsze procedury nie sa bezposrednio dostepne dla programisty. Sa to
wlasciwie czesci procedury do_fork() i zdefiniowane sa
w pliku kernel/fork.c,
oprocz makra SET_LINKS, ktorego definicje mozna znalezc w
pliku
include/linux/sched.h. Omowione zostana krotka w takiej
kolejnosci, w jakiej wystepuja w fork.c. Ich dokladna
definicje, czyli typy parametrow i typ zwracanej wartosci, mozna
zobaczyc ogladajac zrodla. Funkcje o prefiksie copy_ zwracaja
0 w przypadku sukcesu, -1 w przypadku bledu (najczesciej blad braku
pamieci). Wszystkie one umozliwiaja klonowanie - wtedy jest zwykle tylko
zwiekszany licznik odwolan do danej struktury.


Funkcja

find_empty_process()

Funkcja najpierw sprawdza, czy mozna jeszcze w gole uruchomic jakis
nowy proces. Sa zadeklarowane dwie wartosci w pliku

include/linux/tasks.h: NR_TASKS okreslajaca liczbe
zadan w systemie i MAX_TASKS_LEFT_FOR_ROOT. Byla o nic mowa we
wprowadzeniu do struktur danych.
Jesli nie jestes
rootem, a liczba zadan w systemie jest wieksza niz roznica miedzy powyzszymi
wartosciami, to funkcja zwraca -EAGAIN. Nastepnie, jesli nie jestes
rootem, funkcja sprawdza, czy nie przekroczyles ustalonej dla ciebie liczby
zadan. Jesli tak, to zwracane jest -EAGAIN. Na koniec przeszukiwana
jest globalna tablica zadan task i zwracany indeks pozycji, ktora
ma wartosc NULL. Jesli nie ma wolnej pozycji, to zwracany jest
-EAGAIN.

Funkcja

get_pid()

Funkcja zwraca unikatowy identyfikator procesu (o ile nie zazadamy
klonowania). Nowy pid jest szukany w ten sposob, ze zwiekszamy ostatnio
przydzielony pid. Jesli pid ma wartosc wieksza lub rowna 0x8000, czyli
32768, to nowy pid przyjmuje wartosc 1. Nastepnie jest sprawdzane, czy nowy
pid nie koliduje z jakimkolwiek pidem, pgrp-idem lub polem
session opisu zadania. W tym przypadku brany jest kolejny pid.
Przyklad na zastosowanie goto w C.
Jesli zadamy klonowania pid-u, to zwracany jest nasz pid (tj. pid ojca).

Funkcja

dup_mmap()

Jest wywolywana przez copy_mm(). Kopiuje strony pamieci rodzica do
dziecka. Kopiowanie odbywa sie za zasadzie copy-on-write, czyli
jest ustawiany odpowiedni bit na stronach i fizyczne kopiowanie stron odbywa
sie tylko w przypadku proby zapisu przez ojca lub dziecko czegos na te
strony.

Uwaga! Nie mylic tego faktu ze wspoldzieleniem pamieci, kiedy
to strony nigdy nie sa kopiowane, a zmiany dokonane w pamieci przez jeden
proces sa widoczne dla innego procesu.

Funkcja

copy_mm()

Zwykle po przydzieleniu miejsca na strukture opisujace pamiec wywoluje
dup_mmap(). Zerowane sa dane statystyczne dotyczace dostepu
do pamieci (minor and major page faults).
Jesli zadamy klonowania pamieci, to segmenty nie sa
kopiowane, ale procesy moga wspoldzielic pamiec. Struktura wtedy nie jest
kopiowana, tylko zwieksza sie licznik odwolan do niej.

Funkcja

copy_fs()

Kopiuje strukture fs_struct. Zwieksza liczniki i-wezlow
katalogu glownego oraz aktualnego. W przypadku braku pamieci, zwraca -1. W
przypadku klonowania tylko zwieksza licznik odwolan do struktury.

Funkcja

copy_files()

Kopiuje prywatna tablice deskryptorow plikow procesu, zwieksza liczniki
odwolan do i-wezlow plikow opisywanych przez deskryptory. Jezeli zadamy klonowania,
to tablica deskryptorow nie jest kopiowana, ale wspoldzielona (zwieksza sie
licznik odwolan do niej). Liczniki i-wezlow zwiekszaja sie zawsze.

Funkcja

copy_sighand()

Kopiuje procedury obslugi poszczegolnych sygnalow. Mozliwe jest
takze wspoldzielenie tablicy obslugi sygnalow - wtedy zwieksza sie tylko
licznik odwolan do tej tablicy.

Funkcja

SET_LINKS()

Makro zapisuje nowy proces na koncu dwukierunkowej listy cyklicznej.
Wskaznik na mlodsze rodzenstwo jest ustawiany na NULL,
natomiast modyfikowany jest wskaznik na starsze rodzenstwo i wskaznik
na mlodsze rodzenstwo w starszym rodzenstwie (to tylko tak zawile napisane).
Wskaznik na dziecko w naszym rodzicu jest ustawiany na nas.




Funkcje systemowe

Wprowadzenie

Glowna funkcja jest do_fork(). Istnieje do niej interfejs
w postaci trzech funkcji: fork(), vfork() oraz
clone(). Biblioteka libc zapewnia, ze
fork() z poziomu C wywoluje funkcje sys_fork(),
vfork()w Linuxie jest po prostu inna nazwa
fork-a, natomiast clone() powoduje wolanie
funkcji sys_clone() w jadrze. Obie te funkcje mozna obejrzec
tutaj,
ich tresc sprowadza sie do odpowiedniego wywolania funkcji
do_fork().

Funkcje

fork() i vfork()


Funkcja tworzy proces-potomka, ktory rozni sie od rodzica tylko
pid-em i ppid-em i faktem, ze informacje
statystyczne sa wyzerowane.

DEFINICJE: pid_t fork(void)
pid_t vfork(void)
WYNIK: identyfikator nowego procesu w procesie-rodzicu,
0 w procesie-dziecku
-1, gdy blad: errno = EAGAIN (brak pamieci)


Funkcja jest bezargumentowa. Funkcja nie zwraca nigdy
ENOMEM.


Intencja, jaka przyswiecala wprowadzeniu funkcji vfork()
byly oszczednosci czasowe i pamieciowe. Funkcja ta pojawila sie
w systemie BSD, gdy nie bylo mozliwe stosowanie techniki copy on
write, a fork() kopiowal fizycznie pamiec procesu
macierzystego. vfork()nie kopiowal pamieci i dziecko dzialalo
w przestrzeni adresowej rodzica. Projektanci zakladali, ze bezposrednio po
vfork() zostanie wykonana jedna z funkcji exec.
W Linuxie fork() nie kopiuje fizycznie segmentow
pamieci, zaznacza tylko, ze przy modyfikacji beda musialy byc skopiowane.
Dokladniej to zagadnienie jest omowione w rodziale o
zarzadzaniu pamiecia. Zatem
fork() ogranicza sie jedynie do skopiowania kilku struktur
systemowych i dzieki temu jest wykonywany szybko. W Linuxie
vfork() jest tym samym co fork().

Wsparcie dla watkow - funkcja

clone()


Funkcja ta umozliwia w pelni wykorzystanie mocy do_fork-a.
Umozliwia klonowanie, czyli wspoldzielenie, przez ojca i potomka pewnych
zasobow (jednego lub wielu) - pamieci, tablicy deskryptorow, tablicy
obslugi sygnalow a nawet - uwaga! - idetyfikatora. W ten sposob, trzeba
przyznac - bardzo sprytny, sa wspomagane w Linuxie watki. Jednak
aby funkcje mozna bylo wywolac (domyslnie jest ona niedostepna), trzeba
skompilowac jadro ze zdefiniowanym symbolem
CLONE_ACTUALLY_WORKS_OK.

DEFINICJA: pid_t clone(void *sp, unsigned long flags)
WYNIK: identyfikator nowego procesu w procesie-rodzicu,
0 w procesie-dziecku
-1, gdy blad: errno = EAGAIN (brak pamieci)
ENOSYS (nie ma wkompilowanej
tej funkcji w jadro)


Jesli sp jest rozne od NULL, to wskaznik ten
jest uzywany jako poczatkowy wskaznik stosu dziecka. Pole flags
okresla, co chcemy klonwac, a najmlodszy bajt mowi, jakie sygnaly wyslemy do
ojca podczas naszej smierci. Wywolanie clone(0,SIGCHLD|COPYVM)
jest rownowazne fork-owi.

Implementacja - funkcja

do_fork()


Funkcja jest 'porzadna', tj. sprzata po sobie w razie jakiegokolwiek
niepowodzenia. Efekt ten jest uzyskany poprzez skoki do odpowiednich
fragmentow kodu. Zwalnianie pamieci odbywa sie w kolejnosci odwrotnej do jej
przydzialu, co nie tylko upraszcza implementacje sprzatanie, ale jest takze,
tak sie przynajmnie wydaje, bezpieczniejsze.


Najpierw alokowana jest pamiec na strukture
task_struct,
do ktorej wskaznik zostanie umieszczony potem w tablicy procesow. Nastepnie
jest przydzielana pamiec na stos jadra, a potem wyszukiwany za pomoca

find_empty_process() wolny indeks w tablicy procesow. Potem
fork() inicjuje wartosci struktury nowego procesu. Zaznaczany jest
miedzy innymi fakt, ze proces jest nowy i nie wykonal jeszcze
exec-a, oraz, ze procesu nie mozna wyrzucic z pamieci oraz go
przerwac. Przyznawany jest pid procesu i inicjowany zegar. Po takiej
wstepnej inicjacji proces jest wstawiany do tablicy procesow, a za pomoca makra

SET_LINKS tworzone sa dowiazania do przodkow i sasiadow w
tablicy procesow.

W nastepnej kolejnosci kopiowana jest informacja o deskryptorach plikow
(wywolanie

copy_files()) i modyfikowane informacje systemowe. Potem
wywolywana jest funkcja

copy_fs(), nastepnie kopiowane za pomoca

copy_signhand() handlery sygnalow i wreszcie segmenty pamieci
(
copy_mm()). Nastepnie dzialajaca na bardzo niskim poziomie funkcja

copy_thread() powoduje 'fizyczne rozdzielenie' procesow
macierzystego i nowo tworzonego, po czym proces oznaczany jest jako
'swappable' i budzony przez

wake_up_process(). copy_thread() powoduje, ze proces potomny
ma wrazenie, jakby wykonywal funkcje systemowa fork()
i nastepna instrukcja bedzie instrukcja powrotu z wykonania funkcji
systemowej z kodem 0.
Dzieki temu fork() zwraca w
przypadku procesu macierzystego pid dziecka, a w przypadku dziecka - 0.


Kod wake_up_process ogranicza sie do zmiany flagi procesu na
TASK_RUNNING oraz ewentualnym dodaniu do kolejki procesow
oczekujacych na wykonanie.

W pseudokodzie wyglada to nastepujaco:

{
w razie jakichkolwiek niepowodzen zwolnij pamiec i zwroc blad EAGAIN;
przydziel pamiec na strukture task_struct;
przydziel pamiec na stos kontekstu jadra;
znajdz wolne miejsce w tablicy procesow sprawdzajac, czy nie sa
przekraczane limity;
skopiuj informacje aktualnego procesu do task_struct nowego procesu
i w nastepnych krokach zmieniaj te, ktore wymagaja zmian;
zanotuj informacje, ze nowy proces nie wykonywal exec;
przydziel identyfikator nowemu procesowi;
wstaw proces do tablicy procesow, dokonujac odpowiednich manipulacji
wskaznikami;
kopiuj informacje o plikach, systemie plikow, handlerach sygnalow
i pamieci;
dokonaj odpowiedniego wpisu na stos jadra dziecka, aby symulowal
powrot z wywolania funkcji systemowej z wynikiem 0;
obudz nowy proces-dziecko - wstaw go do kolejki procesow gotowych
do wykonania;
wroc z funkcji zwracajac identyfikator dziecka;
}




Bibliografia


Pliki zrodlowe Linuxa:


include/linux/tasks.h
- definicje stalych

include/linux/sched.h
- definicje stalych, struktur i makra SET_LINKS

include/asm-i386/signal.h
- struktura sigaction

kernel/sched.c
- funkcja wake_up_process()

kernel/fork.c
- glowny plik z do_fork() i funkcjami pomocniczymi

arch/i386/kernel/process.c
- interfejs: funkcje sys_fork() oraz
sys_clone()

Maurice J. Bach: Budowa systemu operacyjnego UNIX, wyd. II,
WNT 1995, ISBN 83-204-2015-6
Michael K. Johnson: The Linux Kernel Hackers' Guide, Alpha
version 0.6




Pytania i odpowiedzi


Jakie znalezione w plikach zrodlowych rozwiazanie programistyczne
uwazaja Panstwo za najciekawsze? 
Moim zdaniem ciekawym i wartym zapamietania rozwiazaniem programistycznym
jest sposob przydzielania i zwalniania pamieci w razie bledu w funkcji
do_fork().
Spojrzmy na koniec tej funkcji - sa tam etykiety, do ktorych skacze sie
w razie bledu. Zwalnianie pamieci odbywa sie w kolejnosci odwrotnej do
jej przydzielania. W ten sposob kod zwalniajacy pamiec jest krotki,
przejrzysty i latwy do modyfikacji. Standardowo rzadko stosuje sie tak
dokladne 'sprzatanie po sobie', bo Linux zapewnia odlaczanie segmentow
pamieci w przypadku zakonczenia programu, jednak do dobrego tonu nalezy
zwalnianie wszystkich zasobow. Wymuszal to np. system operacyjny komputera
Amiga - pamiec musiala byc zwalniana przez program, inaczej po jakims czasie
zaczynalo w systemie brakowac pamieci. Powyzsza procedura to takze przyklad
na eleganckie (i przejrzyste!) zastosowanie goto do obslugi
sytuacji awaryjnych.



Pytaniem cichym, jak sie domyslam, i przez dlugi czas pozostajacym
bez odpowiedzi, bylo westchnienie wspolprowadzacego ze mna cwiczenia
Grzeska, ktory pytal sam siebie: "Kiedy ta gadula wreszcie skonczy?".
Innych zapytan nie bylo, ale mozna mi je zadawac via e-mail. :-)

Autor: Tomasz Blaszczyk





Wyszukiwarka