Programowanie współbieżne i rozproszone/PWR Ćwiczenia 2: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mengel (dyskusja | edycje)
Nie podano opisu zmian
Mengel (dyskusja | edycje)
 
(Nie pokazano 25 wersji utworzonych przez 2 użytkowników)
Linia 1: Linia 1:
== Tematyka laboratorium ==
== Tematyka laboratorium ==
# Przypomnienie zasad tworzenia plików [[Makefile]]
# Procesy w systemie Unix: tworzenie, kończenie, podmiana programy, oczekiwanie na potomków
# Kompilacja programów w środowisku Unix
# Procesy w systemie Unix: tworzenie, kończenie,  
 


== 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 UNI'', rozdz. 2.5.1-2.5.4
# 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


<!--
== Pliki z programami przykładowymi ==
Pliki, z ktorych bedziemy korzystac
*<tt>Makefile</tt>
-----------------------------------
*<tt>err.h</tt> plik nagłówkowy biblioteki obsługującej błędy
  Makefile
*<tt>err.c</tt> biblioteka do obsługi błędów
        Plik Makefile.
*<tt>proc_fork.c</tt> program ilustrujący tworzenie nowego procesu
 
*<tt>proc_tree.c</tt> program tworzący drzewo procesów
  err.h
*<tt>proc_exec.c</tt> program tworzący nowy proces, ktory wykona nowe polecenie
        Plik naglowkowy biblioteki obslugujacej bledy.
 
  err.c
        Biblioteka obslugujaca bledy.
 
  proc_fork.c
        Program ilustrujacy tworzenie nowego procesu.
 
  proc_tree.c
        Program tworzacy drzewo procesow - 5 procesow majacych wspolnego
        przodka.
 
  proc_exec.c
        Program tworzacy nowy proces, ktory wykona polecenie "ps".


  simple_shell.c
        Przykladowa implementacja bardzo prostego interpretatora polecen.
-->
== Scenariusz zajęć ==
== Scenariusz zajęć ==
=== Identyfikator procesu ===
=== Identyfikator procesu ===
Linia 48: Linia 27:
Z poziomu programisty, proces może poznać swój [[PID]] wywołując funkcję systemową:
Z poziomu programisty, proces może poznać swój [[PID]] wywołując funkcję systemową:
:<tt>pid_t getpid(); </tt>
:<tt>pid_t getpid(); </tt>
Wartości typu <tt>pid_t</tt> reprezentują [[PID]]y procesów. Najczęściej jest to długa liczba całkowita (<tt>long int</tt>), ale w zależności od wariantu systemu operacyjnego definicja ta może być inna. Dlatego lepiej posługiwać się nazwą <tt>pid_t</tt>.
Wartości typu <tt>pid_t</tt> reprezentują [[PID]]y procesów. Najczęściej jest to długa liczba całkowita (<tt>long int</tt>), ale w zależności od wariantu systemu operacyjnego definicja ta może być inna. Dlatego lepiej posługiwać się nazwą <tt>pid_t</tt>.


=== Tworzenie nowego procesu ===
=== Tworzenie nowego procesu ===
W Linuksie, tak jak we wszystkich systemach uniksowych, istnieje hierarchia procesów. Każdy proces poza pierwszym procesem w systemie (procesem <tt>init</tt> o [[PID]]zie 1) jest tworzony przez inny proces. Nowy proces nazywamy [[procesem potomnym|proces potomny]], a proces który go stworzył nosi nazwę [[procesu macierzystego]].
W Linuksie, tak jak we wszystkich systemach uniksowych, istnieje hierarchia procesów. Każdy proces poza pierwszym procesem w systemie (procesem <tt>init</tt> o [[PID]]zie 1) jest tworzony przez inny proces. Nowy proces nazywamy [[proces potomny|procesem potomnym]], a proces który go stworzył nosi nazwę [[proces macierzysty|procesu macierzystego]].
<!--
  Do tworzenia procesow sluzy funkcja systemowa:


        pid_t fork();
Do tworzenia procesów służy funkcja systemowa:
:<tt>pid_t fork();</tt>
Powrót z wywołania tej funkcji następuje dwa razy:
* w procesie macierzystym, w którym wartością przekazywaną przez funkcję <tt>fork</tt> jest [[PID]] nowo utworzonego potomka,
* w procesie potomnym, w którym funkcja przekazuje w wyniku 0.


  Powrot z wywolania tej funkcji nastepuje dwa razy: w procesie macierzystym
Jak dokładnie działa funkcja systemowa <tt>fork()</tt>? Proces w systemie Unix jest wygodnie wyobrażać sobie jako obiekt składający się z trzech części:
  i w procesie potomnym. Dla potomka funkcja przekazuje w wyniku 0, a dla
{| border="1"
  procesu macierzystego PID nowo utworzonego potomka.
|+ Proces Uniksowy. Podział na logiczne części.
| Wykonywany kod
| Dane:
*w szczególności wszystkie zmienne procesu
| Dane systemowe:
*PID,
*PID ojca
*otwarte pliki
*itd
|}
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;
* 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>.
* 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).


  Proces potomny wykonuje taki sam kod jak proces macierzysty - zaczyna
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>.
  od wykonania nastepnej instrukcji po fork(). Jednak przestrzenie adresowe
  tych procesow sa rozlaczne. Kazdy ma swoja kopie zmiennych. Wartosci
  zmiennych w procesie potomnym sa poczatkowo takie same jak w procesie
  macierzystym w momencie utworzenia nowego procesu. Procesy maja te same
  uprawnienia, te same otwarte pliki itd.


  * Tworzenie nowego procesu ilustruje przyklad proc_fork.c, ktory nalezy
  #include <stdio.h>
   teraz przeczytac i uruchomic ./proc_fork (o funkcji wait bedzie za chwile).
#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*/
}


* Wykonaj polecenie ps -l. W 4-tej kolumnie znajduje sie PID, a w 5-tej
Przeanalizujmy ten program.
  PPID, czyli PID procesu macierzystego (Parent PID). Jaki proces jest
#Część z dyrektywami <tt>#include</tt>. Tutaj umieszczamy niezbędne pliki nagłówkowe:
  procesem macierzystym dla procesu wykonujacego ps?
#*standardową bibliotekę wejścia/wyjścia <tt>stdio.h</tt>
#*plik nagłówkowy <tt>unistd.h</tt> zawierający deklaracje standardowych funkcji uniksowych (<tt>fork()</tt>, <tt>write()</tt> itd.); Należy go dołączyć do programu, w przeciwnym razie kompilator generuje ostrzeżenia takie jak <tt>implicit declaration of function fork</tt>
#*plik nagłówkowy z deklaracją funkcji <tt>wait()</tt> z <tt>sys/wait.h</tt>
#*plik nagłówkowy z deklaracją funkcji <tt>syserr</tt> do obsługi błędów. Więcej na temat błędów za chwilę.
#Kod programu umieszczamy jak zwykle w funkcji <tt>main</tt>. Wewnątrz tej funkcji w wierszu '''1''' znajduje się deklaracja zmiennej <tt>pid</tt>, na której będziemy pamiętać [[PID]] procesu.
#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.
# 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 '''6''' jest wykonywany jedynie przez proces macierzysty, który wypisuje swój [[PID]] i wartość przekazaną mu przez funkcję systemową <tt>fork()</tt>
# Wiersz '''7''' zawiera wywołanie funkcji systemowej <tt>wait</tt>, którą omówimy za chwilę. W skrócie powoduje ona zatrzymanie wykonania procesu w oczekiwaniu na zakończenie jego potomka.


4. Oczekiwanie na zakonczenie procesu potomnego
'''Uwagi:'''
  --------------------------------------------
# Jeszcze raz podkreślić należy, jak ważne jest wychwytywanie sytuacji błędnych i reakcja na nie.
  Proces macierzysty moze zaczekac na zakonczenie procesu potomnego za pomoca
# Po udanym wykonaniu funkcji <tt>fork()</tt> powyższy program jest wykonywany przez dwa procesy "jednocześnie". Oba wypisują pewne komunikaty na ekranie. Funkcja <tt>printf</tt> 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ł.
  funkcji wait() (lub wait3(), wait4(), waitpid()).
# 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 <tt>fork</tt> i warto ją zapamiętać.


        pid_t wait(int *stan);
; Ć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ść?


  Funkcja przekazuje w wyniku PID zakonczonego procesu. Parametr stan jest
  wskaznikiem do zmiennej zawierajacej kod zakonczonego procesu.
  Funkcja jest blokujaca, co oznacza, ze proces macierzysty, ktory ja wywola
  zostanie wstrzymany, az do zakonczenia jakiegos procesu potomnego.
  Jezeli proces nie mial potomkow funkcja zwroci blad (-1). Jezeli potomek
  zakonczy sie zanim rodzic wywola wait, to wait nie zablokuje procesu i
  wykona sie poprawnie dajac w wyniku PID potomka.


  System przechowuje kody zakonczenia procesow potomnych, az do chwili
=== Oczekiwanie na zakończenie procesu potomnego ===
  odebrania ich przez ich procesy macierzyste. Proces potomny, ktorego
Proces macierzysty może zaczekać na zakończenie procesu potomnego za pomocą funkcji:
  kod nie zostal odebrany przez rodzica to tzw. zombi (proces, ktory
<!--(lub <tt>wait3(), wait4(), waitpid()) -->
  sie zakonczyl, ale informacje o nim sa przechowywane przez system).
:<tt>pid_t wait(int *stan);</tt>
  Dlatego bardzo wazne jest odbieranie kodow zakonczenia (wywolywanie wait),
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.
  aby uniknac niepotrzebnego zajmowania miejsca w tablicy procesow.


* Aby zabaczyc zombi (<defunct>) wpisz do pliku proc_fork.c przed wykonaniem
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.
  wait w procesie macierzystym: "sleep(10)" (zawieszenie procesu na 10
  sekund), wykonaj make a nastepnie ./proc_fork & (w tle) i ps.


* Przeczytaj proc_tree.c i wykonaj kilkukrotnie ./proc_tree.
; Ćwiczenie (ukrywajka)
  Przeanalizuj PIDY i zwroc uwage na kolejnosc wypisywania informacji.
: Zmodyfikuj poprzedni proces tak, aby można było zaobserwować [[zombie]].
5. Uruchamianie nowych programow
  -----------------------------


  Procesowi mozemy zlecic wykonanie innego programu - aktualnie
<div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź 
  wykonywany program zostanie wtedy zastapiony innym.
<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
  Sluza do tego funkcje execXXX() - szesc postaci rozniacych sie sposobem
<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>.
  przekazywania argumentow.
</div></div>


  int execl (const char * sciezka, const char * arg0, ...)
; Ćwiczenie sprawdzające.
  int execlp(const char * plik, const char * arg0, ...)
: 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.  
  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)


  Krotkie wyjasnienie (wiecej w man 3 exec):
A oto rozwiązanie powyższego ćwiczenia.


  l - argumenty programu w postaci listy napisow zakonczonej 0 (NULL),
  v - argumenty programu w postaci tablicy napisow (tak jak argv dla funkcji
      main),
  p - sciezka przeszukiwania ze zmiennej srodowiskowej PATH,
  e - srodowisko przekazywane recznie jako ostatni parametr
      (raczej nie uzywane).


   Parametry:
#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;
}


  - sciezka, to pelna sciezka do wykonywalnego programu,
; Ćwiczenie
  - plik, to nazwa pliku z programem (tylko z p),
: Skompiluj i uruchom kilkakrotnie ten program. Przeanalizuj PIDY i zwroć uwagę na kolejność wypisywania informacji.
  - arg0 i argv[0] sa nazwa pliku zawierajacego program, a nastepne argumenty
    zawieraja wlasciwe argumenty programu.


  Jezeli wykonanie funkcji sie powiedzie, to nigdy nie nastapi powrot z jej
  wywolania.


  Funkcje exec() najczesciej wywoluje sie zaraz po wykonaniu fork() w
; Ćwiczenie
  procesie potomnym.
: 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?


* Przeanalizuj proc_exec.c i wykonaj ./proc_exec.
=== Podmiana wykonywanego programu ===
  Sprobuj zmienic ps na hello z pierwszych zajec, a nastepnie
Procesowi możemy zlecić wykonanie innego programu. Jak widzieliśmy poprzednio, jedną z części procesu jest kod. Po wykonaniu funkcji <tt>fork()</tt) jest on dziedziczony po procesie potomnym, ale w dowolnej chwili można go podmienić. Służy do tego rodzina funkcji <tt>exec</tt>. Jest to tak naprawdę ta sama funkcja, ale mająca sześć różnych postaci wywołania i przekazywanych argumentów:
  na inny program wywolany z argumentami. Zwroc uwage na sposob obslugi
:<tt>int execl (const char * sciezka, const char * arg0, ...)</tt>
  bledow funkcji exec. Dlaczego wywolanie funkcji syserr jest bezwarunkowe?
:<tt>int execlp(const char * plik, const char * arg0, ...)</tt>
:<tt>int execle(const char * sciezka, const char * arg0, ..., const char ** envp)</tt>
:<tt>int execv (const char * sciezka, const char ** argv)</tt>
:<tt>int execvp(const char * plik, const char ** argv)</tt>
:<tt>int execve(const char * sciezka, const ** char argv, const char ** envp)</tt>


  * Przeanalizuj simple_shell.c i wykonaj ./simple_shell.
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:
  Uwaga: wyjscie z programu przez Ctrl-D. Jest to prosty interpretator
* l oznacza, że argumenty wywołania programu są w postaci listy napisów zakończonej zerem (<tt>NULL</tt>)
  polecen. Potrafi wykonac tylko polecenia zewnetrzne, czyli takie,
* v oznacza, że argumenty wywołania programu są w postaci tablicy napisow (tak jak argument <tt>argv</tt> funkcji <tt>main</tt>)
  ktore moze zlecic innemu procesowi.
* p oznacza, że plik z programem do wykonania musi się znajdować na ścieżce przeszukiwania ze [[zmiennej środowiskowej]] <tt>PATH</tt>
  Sprobuj dodac do niego mozliwosc wykonywania procesow w tle, czyli
* e oznacza, że środowisko jest przekazywane ręcznie jako ostatni argument
  takich procesow, na ktore nie czeka interpretator polecen.


6. Konczenie procesu
Przyjrzyjmy się przykładowej postaci tej funkcji:
  -----------------
:<tt>int execlp (const char * sciezka, const char * arg0, ...)</tt>
Poszczególne argumenty mają następujące znaczenie:
*<tt>scieżka</tt> 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 <tt>PATH</tt>
*<tt>arg0</tt> to nazwa pliku zawierającego program (już bez ścieżki)
* kolejne argumenty zawierają właściwe argumenty wywoływanego programu.


  Proces moze spowodowac zakonczenie samego siebie przez wywolanie funkcji:
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)


      void exit(int kod_zakonczenia);
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.


   W przypadku poprawnego zakonczenia kod zakonczenia powinien byc rowny 0,
A oto pełny przykład:
  a rozny od 0, jezeli nastapil blad.
#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*/
}


7. Funkcja system()
  ----------------
; Ćwiczenie
: Skompiluj proc_exec.c i wykonaj go. Spróbuj zmienić <tt>ps</tt> na inny program wywoływany z argumentami.  
: Zwróć uwagę na sposób obsługi błędow funkcji <tt>exec</tt>. Dlaczego wywołanie funkcji <tt>syserr</tt> jest bezwarunkowe?


  Oprocz pary funkcji fork-exec mozna uzyc funkcji system(), ktora powoduje
; Ćwiczenie
  wywolanie /bin/bash z argumentem tej funkcji.  Jest to drozsze niz para
: 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?
  fork-exec, bo powoduje powstanie dodatkowego procesu (interpretatora
  polecen).
 
 
INFORMACJE DODATKOWE
--------------------
 
O obsludze bledow funkcji systemowych
-------------------------------------
Kazda z funkcji, ktore tutaj omawiamy wymaga pewnych dzialan systemu
operacyjnego, a dokladniej - wykonania funkcji systemowych.
Piszac program nie uzywamy bezposrednio funkcji systemowych,
ale odpowiadajacych im funkcji bibliotecznych, ktore wywoluja
wlasciwa funkcje i wykonuje pewne dodatkowe czynnosci.
 
Kazda funkcja systemowa przekazuje swoj kod zakonczenia. Jest to 0, jezeli
funkcja zakonczyla sie pomyslnie lub liczba ujemna oznaczajaca kod bledu
w przeciwnym przypadku. Funkcja z biblioteki C, ktora wywoluje funkcje
systemowa sprawdza, czy nie nastapil blad i jezeli tak, to przypisuje
wartosc bledu na globalna zmienna errno i przekazuje w wyniku -1.
Dzieki kodowi bledu uzyskujemy wiecej informacji o powodach wystapienia
danego bledu. Przyklad wykorzystania zmiennej errno mozna znalezc w funkcji
syserr w pliku err.c, ktora korzysta z globalnej tablicy sys_errlist
zawierajacej opisy wszystkich kodow bledow.
 
W dalszym ciagu zajec bedziemy uzywac pewnego skrotu myslowego, a mianowicie
bedziemy nazywac funkcja systemowa odpowiednia funkcje biblioteczna
- na przyklad funkcja systemowa fork().
 
Zalecamy uzywania funkcji syserr do obslugi bledow funkcji systemowych.
 
O plikach naglowkowych
----------------------
 
  a) Plik naglowkowy `unistd.h' zawiera deklaracje standardowych funkcji
    uniksowych (fork(), write(), etc.).  Warto go dolaczyc do programu, aby
    kompilator nie generowal ostrzezen takich jak, "implicit declaration of
    function `fork'".
  b) Deklaracja funkcji wait() znajduje sie w `sys/wait.h'.


=== Kończenie procesu ===
Proces może spowodować zakończenie samego siebie przez wywołanie funkcji:
:<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.


ZADANIE 2
=== Funkcja <tt>system()</tt> ===
---------
Oprócz pary funkcji <tt>fork-exec</tt> można użyć funkcji
:<tt>system()</tt>,
która powoduje wywołanie interpretera poleceń (np. <tt>/bin/bash</tt>) i przekazanie do niego jako argumentu argumentu funkcji <tt>system</tt>.  Jest to mniej efektywne niż para funkcji <tt>fork-exec</tt>, bo powoduje powstanie dodatkowego procesu
(interpretera poleceń).


  ------------------------------------------------------------------
== Zadanie zaliczeniowe ==
  |                                                                  |
  |  Napisz program tworzacy "linie" 5 procesow, gdzie kazdy        |
  |  proces potomny jest przodkiem nastepnego procesu.              |
  |  Kazdy proces macierzysty powinien zaczekac na zakonczenie      |
  |  swojego potomka.                                              |
  |                                                                  |
  |  Do obsugi bledow nalezy wykorzystac funkcje z biblioteki err.  |
  |                                                                  |
  ------------------------------------------------------------------


-->
; 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 <tt>err.c</tt>

Aktualna wersja na dzień 15:58, 2 paź 2006

Tematyka laboratorium

  1. Procesy w systemie Unix: tworzenie, kończenie, podmiana programy, oczekiwanie na potomków

Literatura uzupełniająca

  1. M. K. Johnson, E. W. Troan, Oprogramowanie użytkowe w systemie Linux, rozdz. 9.2.1 i 9.4.1-9.4.5.
  2. W. R. Stevens, Programowanie zastowań sieciowych w systemie UNIX, rozdz. 2.5.1-2.5.4.
  3. M. J. Bach, Budowa systemu operacyjnego UNIX, rozdz. 7.1, 7.3-7.5.
  4. 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:

Proces Uniksowy. Podział na logiczne części.
Wykonywany kod Dane:
  • w szczególności wszystkie zmienne procesu
Dane systemowe:
  • PID,
  • PID ojca
  • otwarte pliki
  • itd

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.

  1. 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ę.
  2. 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.
  3. W wierszu 2 program wypisuje swój PID pobrany za pomocą funkcji systemowej getpid.
  4. 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.
  5. 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!
  6. 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ę.
  7. 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()
  8. 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:

  1. Jeszcze raz podkreślić należy, jak ważne jest wychwytywanie sytuacji błędnych i reakcja na nie.
  2. 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ł.
  3. 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.
Odpowiedź
Ć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