fork
task_struct
,
files_struct
,
fs_struct
,
mm_struct
,
signal_struct
find_empty_process()
,
get_pid()
,
dup_mmap()
,
copy_mm()
,
copy_fs()
,
copy_files()
,
copy_sighand()
,
makro SET_LINKS()
fork()
oraz
vfork()
,
clone()
- wsparcie dla watkow,
implementacja - do_fork()
,
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.
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.
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
.
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.
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.
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).
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.
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.
find_empty_process()
get_pid()
dup_mmap()
copy_mm()
copy_fs()
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.
copy_files()
copy_sighand()
SET_LINKS()
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.
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()
.
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()
.
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.
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; }
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()
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. :-)