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 forkvfork 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_filescopy_fscopy_sighandcopy_mmdup_mmap, która kopiuje
strony pamięci procesu macierzystego do przestrzeni potomka
na zasadzie copy-on-write. do_forkdo_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) execdo_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_forkdo_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_processvfork 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.
exitexit 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_mmOdpowiedzialna 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_exitZaimplementowana 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_filesCelem 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_fsCelem 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_sighandCelem 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_notifyFunkcja 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_exitacct_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.