Procesy stanowią najbardziej zasadniczy element koncepcyjny architektury
wieloprogramowych systemów operacyjnych, do których niewątpliwie należy Linux.
Pojęcie "proces" zwykle definiuje się jako "wykonujący się egzemplarz programu".
Jest oczywiste, że musi istnieć jakiś mechanizm w systemie, który umożliwi
tworzenie procesów. Takim mechanizmem jest wywołanie systemowe fork
.
Funkcja fork
jest wywoływana w programie w następujący sposób:
pid_t fork(void);
fork
nazywa się procesem macierzystym, rodzicem lub przodkiem,
nowo utworzony natomiast nazywa się procesem potomnym, potomkiem lub dzieckiem.
Funkcja fork
ma (jeśli zadziała bezbłędnie) nietypową własność: wywołana raz wraca dwa razy i
zwraca dwie wartości, tj. dla procesu wywołującego: PID potomka, dla nowo utworzonego
procesu: 0. W przypadku błędu zwraca się: -1.
Wykorzystując to, funkcji fork
w programie najczęściej używa się używa się według
następującego schematu wywołania:
switch(pid = fork()) {
case -1:
//obsługa błędu przy wykonaniu funkcji fork
case 0:
// Tutaj wstawia się kod potomka
default:
// Tu wstawia się kod ojca
}
Wywołania systemowe
Aby utworzyć nowy proces trzeba skorzystać z jednej z trzech funkcji:
fork
, vfork
, clone
. Powodują one wywołanie funkcji systemowych odpowiednio: sys_fork
,
sys_vfork
oraz sys_clone
zdefiniowanych w pliku process.c. Wszystkie one są
interfejsem do funkcji do_fork
, która, wywoływana z różnymi flagami
tworzy nowy proces. Różnice między tymi funkcjami są następujące:
wywołania
i fork
vfork
są bardzo do siebie
podobne dzięki technice kopiowania
przy zapisie (copy-on-write) polegającej na pozwoleniu na czytanie przez rodzica
i przez potomka tych samych stron fizycznych i kopiowaniu ich dopiero
gdy któryś z nich zdecyduje się na zapis. Różnica jest taka, że vfork
tworzy proces, który współdzieli przestrzeń pamięci rodzica i, aby zapobiec
nadpisywaniu przez rodzica danych potrzebnych potomkowi, rodzic jest
zawieszany do momentu zakończenia się potomka lub wykonania przez niego
nowego programu. clone
natomiast służy jako wsparcie dla
tworzenia lekkich procesów posiadających ten sam PID i nie jest domyślnie
dostępne w Linuxie. Jądro musi być skompilowane z odpowiednią opcją.
Żeby gładko przebrnąć przez opisy funkcji i algorytmów zamieszczonych dalej proponuję krótkie przypomnienie podstawowych struktur wykorzystywanych przy tworzeniu procesu. Opiszę tylko te pola, które dla rozpatrywanego zagadnienia mają znaczenie
do_fork
- określają które zasoby mają być współdzielone
przez rodzica i potomka:
ptrace()
to potomek także ma być śledzonymm_release
wywoływanej przy kończeniu wykonywania
się dziecka na danych ojca (czyli po exec lub exit wykonanych przez owo dziecko)do_fork
używa się jeszcze następujących flag
(zdefiniowanych również w sched.h)
exec
struct task_struct { volatile long state; /* pole to opisuje co aktualnie dzieje się * * z procesem ustawiana jest jedna i tylko * * jedna flaga z następujących: TASK_ZOMBIE * * TASK_STOPPED, TASK_RUNNING, * * TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE*/ unsigned long flags; /* flagi zdefiniowane powyżej */ ... volatile long need_resched; /* jeśli jest ustawione na jeden oznacza * * procesowi skończył się czas procesora */ unsigned long ptrace; /* pole określające czy proces ma być śledzony */ ... long counter; /* przechowuje informację o ilości czasu jaka pozostała * * jeszcze procesowi do chwili wywłaszczenia go przez jądro */ ... struct mm_struct *mm; /*struktura dokładnie będzie opisana w temacie * * dotyczącym pamięci. Tu wystarczy powiedzieć, ża zawiera ona * * wskaźniki do deskryptorów pamięci oraz licznik ilości odwołań* * do nich */ ... struct task_struct *next_task, *prev_task; /* wskaźniki do sąsiednich * * procesów na dwukierunkowej liście wszystkich * procesów */ ... int exit_code, exit_signal; int pdeath_signal; /* Sygnał wysyłany kiedy umiera ojciec */ ... int did_exec:1; pid_t pid; /* Numer PID */ ... int leader; /* ustawiony na jeden jeśli proces jest * * leaderem sesji */ struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; /* wzkaźniki do (oryginalnego) ojca, najmłodszego dziecka, * * młodszego i straszego rodzeństwa */ ... struct completion *vfork_done; /* dla vfork(), zdefiniowana w * * pliku completion.h - jest wykorzystywana do zawieszania * * ojca procesu */ ... int swappable:1; ... struct rlimit rlim[RLIM_NLIMITS]; /* w tej tablicy określone są * * limity zasobów dostępnych dla procesu* * np maksymalny czas CPU, rozmiar stosu* * lub danych oraz dopuszczalna liczba * * procesów danego użytkownika, czy też * * ilość otwartych plików dla procesu */ ... struct tty_struct *tty; /* NULL jeśli proces wykonuje się w tle */ ... struct sem_undo *semundo; /* do cofania operacji semaforowych */ ... struct fs_struct *fs; struct files_struct *files; struct signal_struct *sig; };
fs_struct
(zdefiniowana w fs_struct.h) -
przechowuje informacje o masce praw dostępu dla nowo tworzonych plików
przez użytkownika (umask), wskaźniki do struktur dotyczących dwu katalogów:
głównego oraz aktualnego katalogu roboczego. Zawiera także licznik odwołań
do siebie, co umożliwia współdzielenie jej przez wiele procesów.
files_struct
(zdefiniowana w sched.h) -
przechowuje informacje o deskryptorach plików otwartych przez proces.
Również zawiera licznik odwołań do siebie.
signal_struct
(zdefiniowana w sched.h) - w niej, w tablicy
przechowywane są informacje o akcjach jakie mają być podjęte w przypadku otrzymania
konkretnego sygnału. Zawiera także licznik odwołań do siebie.do_fork
Funkcje opisane poniżej wywoływane są w funkcji do_fork
. I tak jak do_fork
nie są dostępne dla programisty. W większości zostały zaimplementowane w pliku
fork.c. Opisuję je w kolejności w jakiej wystąpiły w poniżej opisanym algorytmie do_fork
.
pid_t get_pid(long clone_flags)
do_fork
).
Algorytm szukania nowego pidu jest następujący: zwiększa się ostatnio przydzielony
pid. Jeśli otrzymana wartość jest równa lub przekracza stałą PID_MAX, to za
ostatnio przydzielony pid podstawia się wartosc 300 (omija się pidy demonów).
Następnie sprawdza się, czy nowy pid nie koliduje z jakimkolwiek innym pidem,
lub polem session. Jeśli koliduje brany jest kolejny pid i wszystkie kroki
ponownie zostają wykonane.
copy_files
copy_fs
copy_sighand
copy_mm
dup_mmap
, która kopiuje
strony pamięci procesu macierzystego do przestrzeni potomka
na zasadzie copy-on-write. do_fork
do_fork
została zaimplementowana w pliku fork.c.
Jest wywoływana z następującymi parametrami: do_fork
:
do_fork
z błędem EPERM (brak uprawnień) - tylko proces 0 może klonować PID. alloc_task_struct()
, która rezerwuje 8kb pamięci
na strukturę TASK_STRUCT. W przypadku niepowodzenia do_fork
kończy działanie z błędem ENOMEM (brak dostępnej pamięci) exec
do_fork
znalazła się CLONE_PTRACE get_pid(clone_flags)
wait4()
copy_files
),
(copy_fs
),
(copy_sighand
),
(copy_mm
). copy_thread
, w której następuje zainicjowanie stosu trybu jądra u
dziecka - tu następuje rozwidlenie: dziecku się będzie się wydawać, że to
ono wykonało fork i będzie miało 0 w polu ret_from_forkdo_fork
do_fork
pozostał jeszcze
czas procesora - jeśli nie to ustawia się mu pole need_resched na 1 SET_LINKS
ustawia się resztę powiązań rodzinnych nowemu
procesowi oraz jego rodzeństwu i ojcu oraz wstawia się nowy proces do kolejki
procesówwake_up_process
vfork
to zawiesza
się ojca nowego procesu do czasu gdy nowy albo skończy działanie, albo wykona exec
Proces w Linuxie może zakończyć się z kilku powodów np gdy otrzyma sygnał, jednakże
najczęstszą przyczyną jest napotkanie wywołania systemowego exit
lub
osiągnięcie ostatniej instrukcji w procedurze głównej wykonywanego programu, co na jedno wychodzi.
Funkcja exit, która służy do kończenia procesów w Linuxie zwalnia zasoby zajmowane dotychczas
przez wywołujący proces, zmienia jego stan na ZOMBIE.
exit
exit kończy normalnie proces. Interfejs użytkownika jest następujący:
void exit(int status);
sys_exit
będącą opakowaniem dla do_exit
wywołanej
z parametrem error_code.
Ważniejsze funkcje wywoływane w do_exit
__exit_mm
Odpowiedzialna jest za zwalnianie zasobów związanych z procesem, a ściśle z polem mm
struktury task_struct. Wykonywana jest funkcja mm_release()
istotna dla
dla procesu macierzystego jeśli proces kończący się powstał w wyniku wywołania
vfork()
. Jeżeli struktura mm jest współdzielona przez kilka
procesów, to proces wykonujacy exit
tylko zmniejsza licznik odwołań do niej,
w przeciwnym razie jądro odzyskuje wcześniej przydzieloną pamięć.
sem_exit
Zaimplementowana w celu zapobieżenia sytuacji gdy proces wykona operacje semaforowe blokujące inne procesy i zakończy się. Problem ten jest rozwiązany poprzez cofnięcie wszystkich operacji semaforowych wykonanych przez proces zapisanych w polu semundo deskryptora procesu.
Funkcja__exit_files
Celem jest zamknięcie otwartych plików. Jeśli struktura files z deskryptora procesu wykonujacego exit jest współdzielona przez kilka procesów zmniejsza się licznik odwołań do tej struktury w przeciwnym razie zamyka się deskryptory plików i zwalnia się pamięć przydzieloną strukturze files.
Funkcja__exit_fs
Celem jest zwolnienie i-wezłów katalogów głównego i roboczego wykorzystywanych przez niszczony proces. Jeśli struktura fs z deskryptora procesu nie jest współdzielona usuwa się ją z pamięci wpp. zmniejsza się licznik odwołań do tej struktury.
Funkcjaexit_sighand
Celem jest usunięcie tablicy funkcji obsługujących sygnały. Tak jak powyżej jeśli żaden proces nie współdzieli obsługi sygnałów jest ona usuwana z pamięci, wpp. zmniejsza się licznik odwołań do niej.
Funkcjaexit_notify
Funkcja ta zmienia rodzica dzieciom procesu (próbuje się je oddać innym wątkom z danej grupy procesów, jeśli się to nie uda oddaje się je procesowi init), następnie sprawdza się czy jakaś grupa procesów nie stała się sieroca przez zakończenie się procesu (tzn ojciec umierającego procesu należy do innej grupy) : jeśli tak wysyła się sygnały SIGHUP i SIGCONT, dalej informuje się ojca o śmierci potomka i zmienia się stan procesu na ZOMBIE.
Algorytmdo_exit
Funkcja do_exit
obsługuje wszystkie zakończenia procesów w Linuxie. Jest
zdefiniowana jest w pliku kernel/exit.c. Jako parametr przyjmuje
wartość typu long, natomiast sama niczego nie zwraca.
W do_exit
wykonywane są następujące kroki:
do_exit
:
jeśli proces jest w trakcie obsługi przerwania, jest procesem idle (pid=0)
lub procesem init (pid=1) do_exit
zakończy się niepowodzeniem -
wywołana zostanie funkcja panic
, po której system jest
zwykle rebootowany.do_exit
acct_process(code);
.
Rekord zawiera informacje o działaniu procesu takie jak: nazwę wykonanego programu,
czas zużyty w trybie użytkownika i w trybie jadra, czas pracy w sekundach,
czas rozpoczecia pracy, numer ID użykownika i grupy, identyfikator
terminala, informacje o zakończonym procesie, błędy obsługi stron,
kod zakończenia programu . __exit_mm
), wykonanymi operacjami
semaforowymi (sem_exit
),
otwartymi deskryptorami plików (__exit_files
),
systemem plików (__exit_fs
),
obsługą sygnałów (exit_sighand
),
wątkami (exit_thread). exit_notify
)schedule
służącą do przeszeregowania
procesow (musi zostać wybrany nowy proces do wykonania przez procesor).
Stan ustawiony na ZOMBIE powoduje, że funkcja szeregująca nie wybierze już tego
procesu do wykonania. Zatem sterowanie nie powróci do funkcji exit
.
bug
umieszona jest na końcu do_exit
na wszelki wypadek jeśli coś się wydarzy i sterowanie jednak wróci do exit
- do_exit
wykonuje się raz jeszcze.