Programowanie współbieżne i rozproszone/PWR Ćwiczenia 2: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
(Nie pokazano 3 wersji utworzonych przez 2 użytkowników) | |||
Linia 3: | Linia 3: | ||
== Literatura uzupełniająca == | == Literatura uzupełniająca == | ||
# M. K. Johnson, E. W. Troan, ''Oprogramowanie użytkowe w systemie Linux'', rozdz. 9.2.1 i 9.4.1-9.4.5 | # M. K. Johnson, E. W. Troan, ''Oprogramowanie użytkowe w systemie Linux'', rozdz. 9.2.1 i 9.4.1-9.4.5. | ||
# W. R. Stevens, ''Programowanie zastowań sieciowych w systemie | # W. R. Stevens, ''Programowanie zastowań sieciowych w systemie UNIX'', rozdz. 2.5.1-2.5.4. | ||
# M. J. Bach, ''Budowa systemu operacyjnego UNIX'', rozdz. 7.1, 7.3-7.5 | # M. J. Bach, ''Budowa systemu operacyjnego UNIX'', rozdz. 7.1, 7.3-7.5. | ||
# ''man'' do poszczególnych funkcji systemowych | # ''man'' do poszczególnych funkcji systemowych | ||
Linia 50: | Linia 50: | ||
*itd | *itd | ||
|} | |} | ||
Funkcja systemowa <tt>fork</tt> tworzy nowy proces i kopiuje do niego wszystkie powyższe elementy, zmieniając jedynie te elementy, | Funkcja systemowa <tt>fork</tt> tworzy nowy proces i kopiuje do niego wszystkie powyższe elementy, zmieniając jedynie te elementy, które muszą zostać zmienione (na przykład PID). Zatem nowy proces potomny: | ||
* wykonuje taki sam kod jak proces macierzysty; | * wykonuje taki sam kod jak proces macierzysty; | ||
* dziedziczy po procesie macierzystym całą historię wykonania, bo stos wykonania jest także kopiowany; oznacza to w szczególności, że | * dziedziczy po procesie macierzystym całą historię wykonania, bo stos wykonania jest także kopiowany; oznacza to w szczególności, że | ||
wykonanie w procesie potomnym zaczyna się od następnej instrukcji po <tt>fork()</tt>. | wykonanie w procesie potomnym zaczyna się od następnej instrukcji po <tt>fork()</tt>. | ||
* ma te same zmienne co proces macierzysty i do tego zmienne te mają te same wartości co w procesie macierzystym. Jednak przestrzenie adresowe tych procesów są rozłączne: każdy ma swoją kopię zmiennych. Oznacza to m.in. | * ma te same zmienne co proces macierzysty i do tego zmienne te mają te same wartości co w procesie macierzystym. Jednak przestrzenie adresowe tych procesów są rozłączne: każdy ma swoją kopię zmiennych. Oznacza to m.in., że zmiana wartości zmiennej w procesie potomnym nie jest odzwierciedlana w procesie macierzystym i na odwrót. | ||
* ma te same uprawnienia, te same otwarte pliki itd. (tym zajmiemy się w kolejnym laboratorium) | * ma te same uprawnienia, te same otwarte pliki itd. (tym zajmiemy się w kolejnym laboratorium). | ||
A oto przykład ilustrujący wykorzystanie funkcji <tt>fork()</tt> do tworzenia nowego procesu. Przykład ten możesz znaleźć w plikach przygotowanych do zajęć pod nazwą <tt>proc_fork.c</tt>. | A oto przykład ilustrujący wykorzystanie funkcji <tt>fork()</tt> do tworzenia nowego procesu. Przykład ten możesz znaleźć w plikach przygotowanych do zajęć pod nazwą <tt>proc_fork.c</tt>. | ||
Linia 97: | Linia 97: | ||
#W wierszu '''2''' program wypisuje swój [[PID]] pobrany za pomocą funkcji systemowej <tt>getpid</tt>. | #W wierszu '''2''' program wypisuje swój [[PID]] pobrany za pomocą funkcji systemowej <tt>getpid</tt>. | ||
#W wierszu '''3''' następuje wywołanie funkcji systemowej <tt>fork</tt> i dochodzi do "rozwidlenia" procesu. Tworzy się nowy proces i w chwili powrotu z funkcji systemowej mamy już dwa procesy. Oba mają w tym momencie jeszcze tę samą wartość zmiennej <tt>pid</tt>, ale natychmiast po powrocie zmienna ta otrzymuje wartość przekazaną przez funkcję systemową <tt>fork()</tt>. Będzie to zatem 0 w procesie potomnym oraz wartość większa od 0 w procesie macierzystym. | #W wierszu '''3''' następuje wywołanie funkcji systemowej <tt>fork</tt> i dochodzi do "rozwidlenia" procesu. Tworzy się nowy proces i w chwili powrotu z funkcji systemowej mamy już dwa procesy. Oba mają w tym momencie jeszcze tę samą wartość zmiennej <tt>pid</tt>, ale natychmiast po powrocie zmienna ta otrzymuje wartość przekazaną przez funkcję systemową <tt>fork()</tt>. Będzie to zatem 0 w procesie potomnym oraz wartość większa od 0 w procesie macierzystym. | ||
# Wiersz '''4''' zawiera bardzo ważny element programu --- kontrolę błędów. System operacyjny może nie utworzyć nowego procesu z wielu powodów (np. brak pamięci, brak miejsca w tablicy procesów, przekroczenie limitu procesów na użytkownika itp). Każda funkcja systemowa przekazuje swój kod zakończenia. Jest to wartość >= 0, jeżeli funkcja zakończyła się pomyślnie lub liczba ujemna oznaczająca kod błędu | # Wiersz '''4''' zawiera bardzo ważny element programu --- kontrolę błędów. System operacyjny może nie utworzyć nowego procesu z wielu powodów (np. brak pamięci, brak miejsca w tablicy procesów, przekroczenie limitu procesów na użytkownika itp). Każda funkcja systemowa przekazuje swój kod zakończenia. Jest to wartość >= 0, jeżeli funkcja zakończyła się pomyślnie, lub w przeciwnym wypadku liczba ujemna oznaczająca kod błędu. Ponadto jeżeli wystąpił błąd, to kod błędu jest przypisywany na globalną zmienną <tt>errno</tt>. Dzięki kodowi błędu uzyskujemy więcej informacji o powodach wystąpienia danego błędu. Przykład wykorzystania zmiennej <tt>errno</tt> można znaleźć w funkcji <tt>syserr</tt> znajdującej się w pliku <tt>err.c</tt>, która korzysta z globalnej tablicy <tt>sys_errlist</tt> zawierającej opisy wszystkich kodów błędów.'''Uwaga! Sprawdzanie poprawności wykonania wszystkich funkcji systemowych jest absolutnym obowiązkiem programisty!''' | ||
# Fragment programu od wiersza '''5''' jest wykonywany jedynie przez proces potomny, który wypisuje swój [[PID]] i wartość przekazaną mu przez funkcję systemową <tt>fork()</tt>, po czym kończy się. | # Fragment programu od wiersza '''5''' jest wykonywany jedynie przez proces potomny, który wypisuje swój [[PID]] i wartość przekazaną mu przez funkcję systemową <tt>fork()</tt>, po czym kończy się. | ||
# Fragment programu od wiersza '''6''' jest wykonywany jedynie przez proces macierzysty, który wypisuje swój [[PID]] i wartość przekazaną mu przez funkcję systemową <tt>fork()</tt> | # Fragment programu od wiersza '''6''' jest wykonywany jedynie przez proces macierzysty, który wypisuje swój [[PID]] i wartość przekazaną mu przez funkcję systemową <tt>fork()</tt> | ||
Linia 126: | Linia 126: | ||
<!--(lub <tt>wait3(), wait4(), waitpid()) --> | <!--(lub <tt>wait3(), wait4(), waitpid()) --> | ||
:<tt>pid_t wait(int *stan);</tt> | :<tt>pid_t wait(int *stan);</tt> | ||
Funkcja przekazuje w wyniku [[PID]] zakończonego procesu. Parametr <tt>stan</tt> jest wskaźnikiem do zmiennej zawierającej kod zakończonego procesu. Funkcja jest blokująca, co oznacza, że proces macierzysty, który ją wywoła zostanie wstrzymany | Funkcja przekazuje w wyniku [[PID]] zakończonego procesu. Parametr <tt>stan</tt> jest wskaźnikiem do zmiennej zawierającej kod zakończonego procesu. Funkcja jest blokująca, co oznacza, że proces macierzysty, który ją wywoła, zostanie wstrzymany aż do zakończenia któregoś z jego procesów potomnych. Jeżeli proces nie miał potomków, to funkcja przekazuje błąd (-1). Jeżeli potomek zakończy się zanim jego rodzic wywoła <tt>wait</tt>, to rodzic nie będzie czekał i wykona się poprawnie od razu dając w wyniku [[PID]] zakończonego potomka. | ||
Po co w ogóle oczekiwać na zakończenie swoich potomków? Otóż czasem proces macierzysty chce otrzymać jakieś informacje od kończących się potomków. Jednak nawet jeśli nie ma potrzeby przekazania żadnych informacji, to i tak warto wywołać funkcję systemową <tt>wait</tt> i poczekać na zakończenie potomków. Dlaczego? System operacyjny przechowuje kody zakończenia procesów potomnych, aż do chwili odebrania ich przez ich procesy macierzyste. Proces potomny, który się zakończył, a którego kod nie został odebrany przez rodzica to tzw. [[zombi]]. System operacyjny nie może usunąć po prostu informacji o zombie, bo być może w przyszłości informacja o jego kodzie zakończenia będzie potrzebna w procesie macierzystym. Z tego powodu zombie zajmują miejsce w tablicach systemowych. Oczywiście system operacyjny ma pewien mechanizm usuwania zombie, nawet jeśli ich proces macierzysty nie wywoła funkcji <tt>wait</tt> (za zadanie to odpowiada proces <tt>init</tt>, który po zainicjowaniu systemu "adoptuje" wszystkie osierocone procesy i wykonuje funkcję <tt>wait</tt>), ale wiąże się to z pewnym narzutem. Dlatego ważne jest wywoływanie funkcji <tt>wait</tt>, aby uniknąć niepotrzebnego zajmowania miejsca w tablicy procesów. | Po co w ogóle oczekiwać na zakończenie swoich potomków? Otóż czasem proces macierzysty chce otrzymać jakieś informacje od kończących się potomków. Jednak nawet jeśli nie ma potrzeby przekazania żadnych informacji, to i tak warto wywołać funkcję systemową <tt>wait</tt> i poczekać na zakończenie potomków. Dlaczego? System operacyjny przechowuje kody zakończenia procesów potomnych, aż do chwili odebrania ich przez ich procesy macierzyste. Proces potomny, który się zakończył, a którego kod nie został odebrany przez rodzica to tzw. [[zombi]]. System operacyjny nie może usunąć po prostu informacji o zombie, bo być może w przyszłości informacja o jego kodzie zakończenia będzie potrzebna w procesie macierzystym. Z tego powodu zombie zajmują miejsce w tablicach systemowych. Oczywiście system operacyjny ma pewien mechanizm usuwania zombie, nawet jeśli ich proces macierzysty nie wywoła funkcji <tt>wait</tt> (za zadanie to odpowiada proces <tt>init</tt>, który po zainicjowaniu systemu "adoptuje" wszystkie osierocone procesy i wykonuje funkcję <tt>wait</tt>), ale wiąże się to z pewnym narzutem. Dlatego ważne jest wywoływanie funkcji <tt>wait</tt>, aby uniknąć niepotrzebnego zajmowania miejsca w tablicy procesów. | ||
Linia 133: | Linia 133: | ||
: Zmodyfikuj poprzedni proces tak, aby można było zaobserwować [[zombie]]. | : Zmodyfikuj poprzedni proces tak, aby można było zaobserwować [[zombie]]. | ||
Aby zabaczyć [[zombi]] (polecenie <tt>ps</tt> wyświetla je jako <tt><defunct></tt> wpisz do pliku <tt>proc_fork.c</tt> przed wykonaniem | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź | ||
<div class="mw-collapsible-content" style="display:none">Aby zabaczyć [[zombi]] (polecenie <tt>ps</tt> wyświetla je jako <tt><defunct></tt> wpisz do pliku <tt>proc_fork.c</tt> przed wykonaniem | |||
<tt>wait</tt> w procesie macierzystym polecenie zawieszenia procesu na przykład na 10 sekund: <tt>sleep(10)</tt>. Przekompiluj program, a następnie wywołaj go w tle (<tt>./proc_fork &</tt>) (w tle) i wykonaj (w ciągu 10 sekund) polecenie <tt>ps</tt>. | <tt>wait</tt> w procesie macierzystym polecenie zawieszenia procesu na przykład na 10 sekund: <tt>sleep(10)</tt>. Przekompiluj program, a następnie wywołaj go w tle (<tt>./proc_fork &</tt>) (w tle) i wykonaj (w ciągu 10 sekund) polecenie <tt>ps</tt>. | ||
</div></div> | |||
; Ćwiczenie sprawdzające. | ; Ćwiczenie sprawdzające. | ||
Linia 182: | Linia 183: | ||
; Ćwiczenie | ; Ćwiczenie | ||
: Przypuśćmy, że usunięto instrukcję <tt>return 0</tt> w wierszu. Co się stanie? Ile procesów powstanie? | : Przypuśćmy, że usunięto instrukcję <tt>return 0</tt> w wierszu. Co się stanie? Ile procesów powstanie? | ||
: Przypuśćmy, że powyższy fragment jest jedynie początkiem dużego programu. Chcemy w nim jedynie utworzyć pięć procesów potomnych, z których każdy ma opuścić pętlę i wykonywać własny kod umieszczony poza nią. Jak zmodyfikować pętlę? Ile procesów opuszcza pętlę? Jak je odróżnić od siebie? | : Przypuśćmy, że powyższy fragment jest jedynie początkiem dużego programu. Chcemy w nim jedynie utworzyć pięć procesów potomnych, z których każdy ma opuścić pętlę i wykonywać własny kod umieszczony poza nią. Jak zmodyfikować pętlę? Ile procesów opuszcza pętlę? Jak je odróżnić od siebie? | ||
=== Podmiana wykonywanego programu === | === Podmiana wykonywanego programu === | ||
Linia 207: | Linia 207: | ||
* kolejne argumenty zawierają właściwe argumenty wywoływanego programu. | * kolejne argumenty zawierają właściwe argumenty wywoływanego programu. | ||
Jeśli przykładowo | Jeśli przykładowo chcemy wywołać program, który wykona polecenie <tt>ls -l f*</tt>, to użyjemy funkcji systemowej w następującej postaci: | ||
:<tt> execlp ("ls", "ls", "-l", "f*", 0) | :<tt> execlp ("ls", "ls", "-l", "f*", 0) | ||
Jeżeli wykonanie funkcji <tt>exec()</tt> się powiedzie, to nigdy nie nastapi | Jeżeli wykonanie funkcji <tt>exec()</tt> się powiedzie, to nigdy nie nastapi powrót z jej wywołania. Funkcje <tt>exec()</tt> najczęściej wywołuje się zaraz po wykonaniu <tt>fork()</tt> w procesie potomnym. Jak zobaczymy w kolejnym laboratorium, czasem jednak między <tt>fork</tt> a <tt>exec</tt> umieszczamy fragment kodu, który wykonuje pewne czynności przygotowawcze. | ||
A oto pełny przykład: | A oto pełny przykład: | ||
Linia 255: | Linia 255: | ||
Proces może spowodować zakończenie samego siebie przez wywołanie funkcji: | Proces może spowodować zakończenie samego siebie przez wywołanie funkcji: | ||
:<tt>void exit(int kod_zakonczenia);</tt> | :<tt>void exit(int kod_zakonczenia);</tt> | ||
W przypadku poprawnego zakończenia kod zakończenia powinien byc równy 0. Jeśli chcemy zasygnalizować błędne zakończenie programu, to używamy wartości różnych od 0. | W przypadku poprawnego zakończenia, kod zakończenia powinien byc równy 0. Jeśli chcemy zasygnalizować błędne zakończenie programu, to używamy wartości różnych od 0. | ||
=== Funkcja <tt>system()</tt> === | === Funkcja <tt>system()</tt> === |
Aktualna wersja na dzień 15:58, 2 paź 2006
Tematyka laboratorium
- Procesy w systemie Unix: tworzenie, kończenie, podmiana programy, oczekiwanie na potomków
Literatura uzupełniająca
- M. K. Johnson, E. W. Troan, Oprogramowanie użytkowe w systemie Linux, rozdz. 9.2.1 i 9.4.1-9.4.5.
- W. R. Stevens, Programowanie zastowań sieciowych w systemie UNIX, rozdz. 2.5.1-2.5.4.
- M. J. Bach, Budowa systemu operacyjnego UNIX, rozdz. 7.1, 7.3-7.5.
- man do poszczególnych funkcji systemowych
Pliki z programami przykładowymi
- Makefile
- err.h plik nagłówkowy biblioteki obsługującej błędy
- err.c biblioteka do obsługi błędów
- proc_fork.c program ilustrujący tworzenie nowego procesu
- proc_tree.c program tworzący drzewo procesów
- proc_exec.c program tworzący nowy proces, ktory wykona nowe polecenie
Scenariusz zajęć
Identyfikator procesu
Każdy proces w systemie ma jednoznaczny identyfikator nazywany potocznie PID (od angielskiego: Process ID). Identyfikatory aktualnie wykonujących się procesów możesz poznać wykonując w Linuksie polecenie ps.
- Ćwiczenie
- Wykonaj polecenie ps. Zobaczysz wszystkie uruchomione przez Ciebie procesy w tej sesji. Znajdzie się wsród nich proces ps i bash (lub inny stosowany przez Ciebie interpreter poleceń), który analizuje i wykonuje Twoje polecenia. Pierwsza kolumna to PID procesu, a ostatnia to polecenie, które dany proces wykonuje. Więcej informacji na temat polecenia ps uzyskasz wywołując man ps.
Z poziomu programisty, proces może poznać swój PID wywołując funkcję systemową:
- pid_t getpid();
Wartości typu pid_t reprezentują PIDy procesów. Najczęściej jest to długa liczba całkowita (long int), ale w zależności od wariantu systemu operacyjnego definicja ta może być inna. Dlatego lepiej posługiwać się nazwą pid_t.
Tworzenie nowego procesu
W Linuksie, tak jak we wszystkich systemach uniksowych, istnieje hierarchia procesów. Każdy proces poza pierwszym procesem w systemie (procesem init o PIDzie 1) jest tworzony przez inny proces. Nowy proces nazywamy procesem potomnym, a proces który go stworzył nosi nazwę procesu macierzystego.
Do tworzenia procesów służy funkcja systemowa:
- pid_t fork();
Powrót z wywołania tej funkcji następuje dwa razy:
- w procesie macierzystym, w którym wartością przekazywaną przez funkcję fork jest PID nowo utworzonego potomka,
- w procesie potomnym, w którym funkcja przekazuje w wyniku 0.
Jak dokładnie działa funkcja systemowa fork()? Proces w systemie Unix jest wygodnie wyobrażać sobie jako obiekt składający się z trzech części:
Wykonywany kod | Dane:
|
Dane systemowe:
|
Funkcja systemowa fork tworzy nowy proces i kopiuje do niego wszystkie powyższe elementy, zmieniając jedynie te elementy, które muszą zostać zmienione (na przykład PID). Zatem nowy proces potomny:
- wykonuje taki sam kod jak proces macierzysty;
- dziedziczy po procesie macierzystym całą historię wykonania, bo stos wykonania jest także kopiowany; oznacza to w szczególności, że
wykonanie w procesie potomnym zaczyna się od następnej instrukcji po fork().
- ma te same zmienne co proces macierzysty i do tego zmienne te mają te same wartości co w procesie macierzystym. Jednak przestrzenie adresowe tych procesów są rozłączne: każdy ma swoją kopię zmiennych. Oznacza to m.in., że zmiana wartości zmiennej w procesie potomnym nie jest odzwierciedlana w procesie macierzystym i na odwrót.
- ma te same uprawnienia, te same otwarte pliki itd. (tym zajmiemy się w kolejnym laboratorium).
A oto przykład ilustrujący wykorzystanie funkcji fork() do tworzenia nowego procesu. Przykład ten możesz znaleźć w plikach przygotowanych do zajęć pod nazwą proc_fork.c.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include "err.h" int main () { 1: pid_t pid; 2: /* wypisuje identyfikator procesu */ printf("Moj PID = %d\n", getpid()); /* tworzy nowy proces */ 3: switch (pid = fork()) { 4: case -1: /* błąd */ syserr("Error in fork\n"); 5: case 0: /* proces potomny */ printf("Jestem procesem potomnym. Mój PID = %d\n", getpid()); printf("Jestem procesem potomnym. Wartość przekazana przez fork() = %d\n", pid); return 0; 6: default: /* proces macierzysty */ printf("Jestem procesem macierzystym. Mój PID = %d\n", getpid()); printf("Jestem procesem macierzystym. Wartość przekazana przez fork() = %d\n", pid); 7: /* czeka na zakończenie procesu potomnego */ if(wait(0) == -1) syserr("Error in wait\n"); return 0; } /*switch*/ }
Przeanalizujmy ten program.
- Część z dyrektywami #include. Tutaj umieszczamy niezbędne pliki nagłówkowe:
- standardową bibliotekę wejścia/wyjścia stdio.h
- plik nagłówkowy unistd.h zawierający deklaracje standardowych funkcji uniksowych (fork(), write() itd.); Należy go dołączyć do programu, w przeciwnym razie kompilator generuje ostrzeżenia takie jak implicit declaration of function fork
- plik nagłówkowy z deklaracją funkcji wait() z sys/wait.h
- plik nagłówkowy z deklaracją funkcji syserr do obsługi błędów. Więcej na temat błędów za chwilę.
- Kod programu umieszczamy jak zwykle w funkcji main. Wewnątrz tej funkcji w wierszu 1 znajduje się deklaracja zmiennej pid, na której będziemy pamiętać PID procesu.
- W wierszu 2 program wypisuje swój PID pobrany za pomocą funkcji systemowej getpid.
- W wierszu 3 następuje wywołanie funkcji systemowej fork i dochodzi do "rozwidlenia" procesu. Tworzy się nowy proces i w chwili powrotu z funkcji systemowej mamy już dwa procesy. Oba mają w tym momencie jeszcze tę samą wartość zmiennej pid, ale natychmiast po powrocie zmienna ta otrzymuje wartość przekazaną przez funkcję systemową fork(). Będzie to zatem 0 w procesie potomnym oraz wartość większa od 0 w procesie macierzystym.
- Wiersz 4 zawiera bardzo ważny element programu --- kontrolę błędów. System operacyjny może nie utworzyć nowego procesu z wielu powodów (np. brak pamięci, brak miejsca w tablicy procesów, przekroczenie limitu procesów na użytkownika itp). Każda funkcja systemowa przekazuje swój kod zakończenia. Jest to wartość >= 0, jeżeli funkcja zakończyła się pomyślnie, lub w przeciwnym wypadku liczba ujemna oznaczająca kod błędu. Ponadto jeżeli wystąpił błąd, to kod błędu jest przypisywany na globalną zmienną errno. Dzięki kodowi błędu uzyskujemy więcej informacji o powodach wystąpienia danego błędu. Przykład wykorzystania zmiennej errno można znaleźć w funkcji syserr znajdującej się w pliku err.c, która korzysta z globalnej tablicy sys_errlist zawierającej opisy wszystkich kodów błędów.Uwaga! Sprawdzanie poprawności wykonania wszystkich funkcji systemowych jest absolutnym obowiązkiem programisty!
- Fragment programu od wiersza 5 jest wykonywany jedynie przez proces potomny, który wypisuje swój PID i wartość przekazaną mu przez funkcję systemową fork(), po czym kończy się.
- Fragment programu od wiersza 6 jest wykonywany jedynie przez proces macierzysty, który wypisuje swój PID i wartość przekazaną mu przez funkcję systemową fork()
- Wiersz 7 zawiera wywołanie funkcji systemowej wait, którą omówimy za chwilę. W skrócie powoduje ona zatrzymanie wykonania procesu w oczekiwaniu na zakończenie jego potomka.
Uwagi:
- Jeszcze raz podkreślić należy, jak ważne jest wychwytywanie sytuacji błędnych i reakcja na nie.
- Po udanym wykonaniu funkcji fork() powyższy program jest wykonywany przez dwa procesy "jednocześnie". Oba wypisują pewne komunikaty na ekranie. Funkcja printf daje gwarancję niepodzielności komunikatów wypisywanych na ekran na poziomie pojedynczych wierszy. Oznacza to, że jeśli rozpoczniemy wypisywanie na ekranie, to zanim nie skończymy wiersza, nikt inny nie będzie po ekranie pisał.
- Konstrukcja
switch (pid = fork()) { case -1: /* błąd */ syserr("Error in fork\n"); case 0: /* proces potomny */ ... return 0; default: /* proces macierzysty */ ...
jest dość charakterystycznym sposobem korzystania z funkcji fork i warto ją zapamiętać.
- Ćwiczenia
- Skompiluj i uruchom powyższy program
- Czy jesteś w stanie odróżnić wiersze wypisywane przez proces potomny od wierszy wypisywanych przez proces macierzysty?
- W jakiej kolejności są wypisywane komunikaty na ekranie? Czy jest to jedyna możliwa kolejność?
Oczekiwanie na zakończenie procesu potomnego
Proces macierzysty może zaczekać na zakończenie procesu potomnego za pomocą funkcji:
- pid_t wait(int *stan);
Funkcja przekazuje w wyniku PID zakończonego procesu. Parametr stan jest wskaźnikiem do zmiennej zawierającej kod zakończonego procesu. Funkcja jest blokująca, co oznacza, że proces macierzysty, który ją wywoła, zostanie wstrzymany aż do zakończenia któregoś z jego procesów potomnych. Jeżeli proces nie miał potomków, to funkcja przekazuje błąd (-1). Jeżeli potomek zakończy się zanim jego rodzic wywoła wait, to rodzic nie będzie czekał i wykona się poprawnie od razu dając w wyniku PID zakończonego potomka.
Po co w ogóle oczekiwać na zakończenie swoich potomków? Otóż czasem proces macierzysty chce otrzymać jakieś informacje od kończących się potomków. Jednak nawet jeśli nie ma potrzeby przekazania żadnych informacji, to i tak warto wywołać funkcję systemową wait i poczekać na zakończenie potomków. Dlaczego? System operacyjny przechowuje kody zakończenia procesów potomnych, aż do chwili odebrania ich przez ich procesy macierzyste. Proces potomny, który się zakończył, a którego kod nie został odebrany przez rodzica to tzw. zombi. System operacyjny nie może usunąć po prostu informacji o zombie, bo być może w przyszłości informacja o jego kodzie zakończenia będzie potrzebna w procesie macierzystym. Z tego powodu zombie zajmują miejsce w tablicach systemowych. Oczywiście system operacyjny ma pewien mechanizm usuwania zombie, nawet jeśli ich proces macierzysty nie wywoła funkcji wait (za zadanie to odpowiada proces init, który po zainicjowaniu systemu "adoptuje" wszystkie osierocone procesy i wykonuje funkcję wait), ale wiąże się to z pewnym narzutem. Dlatego ważne jest wywoływanie funkcji wait, aby uniknąć niepotrzebnego zajmowania miejsca w tablicy procesów.
- Ćwiczenie (ukrywajka)
- Zmodyfikuj poprzedni proces tak, aby można było zaobserwować zombie.
- Ćwiczenie sprawdzające.
- Za chwilę omówimy program tworzący drzewo procesów. Zanim jednak przeczytasz to omówienie spróbuj napisać go sam. Chodzi o stworzenie procesu, który utworzy zadaną z góry (na przykład w postaci stałej) liczbę procesów potomnych. Każdy proces potomny powinien wypisać na ekran jakiś komunikat kontrolny, a następnie zakończyć się. Proces macierzysty ma przed zakończeniem poczekać na zakończenie wszystkich swoich potomków.
A oto rozwiązanie powyższego ćwiczenia.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include "err.h" #define NR_PROC 5 int main () { pid_t pid; int i; /* wypisuje identyfikator procesu */ printf("Moj PID = %d\n", getpid()); /* tworzy nowe procesy */ for (i = 1; i <= NR_PROC; i++) switch (pid = fork()) { case -1: /* blad */ syserr("Error in fork\n"); case 0: /* proces potomny */ printf("Jestem procesem potomnym. Moj PID = %d\n", getpid()); printf("Jestem procesem potomnym. Wartosc przekazana przez fork() = %d\n", pid); return 0; default: /* proces macierzysty */ printf("Jestem procesem macierzystym. Moj PID = %d\n", getpid()); printf("Jestem procesem macierzystym. Wartosc przekazana przez fork() = %d\n", pid); } /*switch*/ /* czeka na zakonczenie procesow potomnych */ for (i = 1; i <= NR_PROC; i++) if (wait(0) == -1) syserr("Error in wait\n"); return 0;
}
- Ćwiczenie
- Skompiluj i uruchom kilkakrotnie ten program. Przeanalizuj PIDY i zwroć uwagę na kolejność wypisywania informacji.
- Ćwiczenie
- Przypuśćmy, że usunięto instrukcję return 0 w wierszu. Co się stanie? Ile procesów powstanie?
- Przypuśćmy, że powyższy fragment jest jedynie początkiem dużego programu. Chcemy w nim jedynie utworzyć pięć procesów potomnych, z których każdy ma opuścić pętlę i wykonywać własny kod umieszczony poza nią. Jak zmodyfikować pętlę? Ile procesów opuszcza pętlę? Jak je odróżnić od siebie?
Podmiana wykonywanego programu
Procesowi możemy zlecić wykonanie innego programu. Jak widzieliśmy poprzednio, jedną z części procesu jest kod. Po wykonaniu funkcji fork()</tt) jest on dziedziczony po procesie potomnym, ale w dowolnej chwili można go podmienić. Służy do tego rodzina funkcji exec. Jest to tak naprawdę ta sama funkcja, ale mająca sześć różnych postaci wywołania i przekazywanych argumentów:
- int execl (const char * sciezka, const char * arg0, ...)
- int execlp(const char * plik, const char * arg0, ...)
- int execle(const char * sciezka, const char * arg0, ..., const char ** envp)
- int execv (const char * sciezka, const char ** argv)
- int execvp(const char * plik, const char ** argv)
- int execve(const char * sciezka, const ** char argv, const char ** envp)
Dokładny opis wszystkich postaci znajdziesz w man. Teraz tylko przedstawimy znaczenie poszczególnych literek w nazwach funkcji i omówimy dwie postaci wywołania tej funkcji:
- l oznacza, że argumenty wywołania programu są w postaci listy napisów zakończonej zerem (NULL)
- v oznacza, że argumenty wywołania programu są w postaci tablicy napisow (tak jak argument argv funkcji main)
- p oznacza, że plik z programem do wykonania musi się znajdować na ścieżce przeszukiwania ze zmiennej środowiskowej PATH
- e oznacza, że środowisko jest przekazywane ręcznie jako ostatni argument
Przyjrzyjmy się przykładowej postaci tej funkcji:
- int execlp (const char * sciezka, const char * arg0, ...)
Poszczególne argumenty mają następujące znaczenie:
- scieżka to pełna scieżka do pliku zawierającego wykonywalny program lub po prostu nazwa pliku z programem. W tym drugim przypadku plik będzie przeszukiwany w katalogach, które znajdują się na zmiennej środowiskowej PATH
- arg0 to nazwa pliku zawierającego program (już bez ścieżki)
- kolejne argumenty zawierają właściwe argumenty wywoływanego programu.
Jeśli przykładowo chcemy wywołać program, który wykona polecenie ls -l f*, to użyjemy funkcji systemowej w następującej postaci:
- execlp ("ls", "ls", "-l", "f*", 0)
Jeżeli wykonanie funkcji exec() się powiedzie, to nigdy nie nastapi powrót z jej wywołania. Funkcje exec() najczęściej wywołuje się zaraz po wykonaniu fork() w procesie potomnym. Jak zobaczymy w kolejnym laboratorium, czasem jednak między fork a exec umieszczamy fragment kodu, który wykonuje pewne czynności przygotowawcze.
A oto pełny przykład:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include "err.h" int main () { pid_t pid; /* wypisuje identyfikator procesu */ printf("Moj PID = %d\n", getpid()); /* tworzy nowy proces */ switch (pid = fork()) { case -1: /* blad */ syserr("Error in fork\n"); case 0: /* proces potomny */ printf("Jestem procesem potomnym. Moj PID = %d\n", getpid()); printf("Jestem procesem potomnym. Wartosc przekazana przez fork() = "%d\n", pid); /* wykonuje program ps */ execlp("ps", "ps", 0); syserr("Error in execlp\n"); default: /* proces macierzysty */ printf("Jestem procesem macierzystym. Moj PID = %d\n", getpid()); printf("Jestem procesem macierzystym. Wartosc przekazana przez fork() = %d\n", pid); /* czeka na zakonczenie procesu potomnego */ if (wait(0) == -1) syserr("Error in wait\n"); return 0; } /*switch*/ }
- Ćwiczenie
- Skompiluj proc_exec.c i wykonaj go. Spróbuj zmienić ps na inny program wywoływany z argumentami.
- Zwróć uwagę na sposób obsługi błędow funkcji exec. Dlaczego wywołanie funkcji syserr jest bezwarunkowe?
- Ćwiczenie
- Wykonaj powyższy program przekierowując wyjście do pliku. Obejrzyj plik z wynikami. Czy zawiera on wszystkie komunikaty wypisywane przez program? Czego nie ma? Dlaczego?
Kończenie procesu
Proces może spowodować zakończenie samego siebie przez wywołanie funkcji:
- void exit(int kod_zakonczenia);
W przypadku poprawnego zakończenia, kod zakończenia powinien byc równy 0. Jeśli chcemy zasygnalizować błędne zakończenie programu, to używamy wartości różnych od 0.
Funkcja system()
Oprócz pary funkcji fork-exec można użyć funkcji
- system(),
która powoduje wywołanie interpretera poleceń (np. /bin/bash) i przekazanie do niego jako argumentu argumentu funkcji system. Jest to mniej efektywne niż para funkcji fork-exec, bo powoduje powstanie dodatkowego procesu (interpretera poleceń).
Zadanie zaliczeniowe
- Zadanie
- Napisz program tworzący "linię" 5 procesów, w której każdy proces potomny jest przodkiem następnego procesu (a więc pierwszy proces jest ojcem drugiego, dziadkiem trzeciego itd). Każdy proces macierzysty przed zakończeniem swojego działania powinien zaczekać na zakończenie swojego potomka. Pamiętaj o obsłudze błędów! Możesz wykorzystać do tego celu funkcje z pliku err.c