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 21 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 71: 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, który muszą zostać zmienione (na przykład PID). Zatem nowy proces potomny:
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. to, że zmiana wartości zmiennej w procesie potomnym nie jest odzwierciedlana w procesie macierzystym i na odwrót.  
* 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:
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>.


  #include <stdio.h>
  #include <stdio.h>
Linia 86: Linia 65:
  #include <sys/wait.h>
  #include <sys/wait.h>
  #include "err.h"
  #include "err.h"
  int main ()
  int main ()
  {
  {
  pid_t pid;
'''1:'''  pid_t pid;
 
'''2:'''  /* wypisuje identyfikator procesu */
  /* wypisuje identyfikator procesu */
    printf("Moj PID = %d\n", getpid());
  printf("Moj PID = %d\n", getpid());
    /* tworzy nowy proces */
 
'''3:'''  '''switch''' (pid = fork()) {
  /* tworzy nowy proces */
'''4:'''    '''case''' -1: /* błąd */
  '''switch''' (pid = fork()) {
        syserr("Error in fork\n");
    '''case''' -1: /* blad */
'''5:'''    '''case''' 0: /* proces potomny */
      syserr("Error in fork\n");
        printf("Jestem procesem potomnym. Mój PID = %d\n", getpid());
 
        printf("Jestem procesem potomnym. Wartość przekazana przez fork() = %d\n", pid);
    '''case''' 0: /* proces potomny */
        '''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*/
}


      printf("Jestem procesem potomnym. Moj PID = %d\n", getpid());
Przeanalizujmy ten program.
      printf("Jestem procesem potomnym. Wartosc przekazana przez fork() = %d\n", pid);
#Część z dyrektywami <tt>#include</tt>. Tutaj umieszczamy niezbędne pliki nagłówkowe:
#*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.


'''Uwagi:'''
# Jeszcze raz podkreślić należy, jak ważne jest wychwytywanie sytuacji błędnych i reakcja na nie.
# 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ł.
# Konstrukcja
'''switch''' (pid = fork()) {
  '''case''' -1: /* błąd */
        syserr("Error in fork\n");
  '''case''' 0: /* proces potomny */
      ... 
       '''return''' 0;
       '''return''' 0;
  '''default''': /* proces macierzysty */
        ...
jest dość charakterystycznym sposobem korzystania z funkcji <tt>fork</tt> i warto ją zapamiętać.


    '''default''': /* proces macierzysty */
; Ć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ść?


      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 */
=== Oczekiwanie na zakończenie procesu potomnego ===
Proces macierzysty może zaczekać na zakończenie procesu potomnego za pomocą funkcji:
<!--(lub <tt>wait3(), wait4(), waitpid()) -->
:<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 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.


      '''if'''(wait(0) == -1)
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.
        syserr("Error in wait\n");


      '''return''' 0;
; Ćwiczenie (ukrywajka)
  } /*switch*/
: Zmodyfikuj poprzedni proces tak, aby można było zaobserwować [[zombie]].
}


<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>.
</div></div>


<!--
; Ćwiczenie sprawdzające.
* Wykonaj polecenie ps -l. W 4-tej kolumnie znajduje sie PID, a w 5-tej
: 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.
  PPID, czyli PID procesu macierzystego (Parent PID). Jaki proces jest
  procesem macierzystym dla procesu wykonujacego ps?


4. Oczekiwanie na zakonczenie procesu potomnego
A oto rozwiązanie powyższego ćwiczenia.  
  --------------------------------------------
  Proces macierzysty moze zaczekac na zakonczenie procesu potomnego za pomoca
  funkcji wait() (lub wait3(), wait4(), waitpid()).


        pid_t wait(int *stan);


   Funkcja przekazuje w wyniku PID zakonczonego procesu. Parametr stan jest
#include <stdio.h>
   wskaznikiem do zmiennej zawierajacej kod zakonczonego procesu.
#include <stdlib.h>
   Funkcja jest blokujaca, co oznacza, ze proces macierzysty, ktory ja wywola
#include <unistd.h>
   zostanie wstrzymany, az do zakonczenia jakiegos procesu potomnego.
#include <sys/types.h>
   Jezeli proces nie mial potomkow funkcja zwroci blad (-1). Jezeli potomek
#include <sys/wait.h>
  zakonczy sie zanim rodzic wywola wait, to wait nie zablokuje procesu i
#include "err.h"
   wykona sie poprawnie dajac w wyniku PID potomka.
#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;
}


  System przechowuje kody zakonczenia procesow potomnych, az do chwili
; Ćwiczenie
  odebrania ich przez ich procesy macierzyste. Proces potomny, ktorego
: Skompiluj i uruchom kilkakrotnie ten program. Przeanalizuj PIDY i zwroć uwagę na kolejność wypisywania informacji.
  kod nie zostal odebrany przez rodzica to tzw. zombi (proces, ktory
  sie zakonczyl, ale informacje o nim sa przechowywane przez system).
  Dlatego bardzo wazne jest odbieranie kodow zakonczenia (wywolywanie wait),
  aby uniknac niepotrzebnego zajmowania miejsca w tablicy procesow.


* Aby zabaczyc zombi (<defunct>) wpisz do pliku proc_fork.c przed wykonaniem
  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
  Przeanalizuj PIDY i zwroc uwage na kolejnosc wypisywania informacji.
: Przypuśćmy, że usunięto instrukcję <tt>return 0</tt> w wierszu. Co się stanie? Ile procesów powstanie?
5. Uruchamianie nowych programow
: 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?
  -----------------------------


  Procesowi mozemy zlecic wykonanie innego programu - aktualnie
=== Podmiana wykonywanego programu ===
  wykonywany program zostanie wtedy zastapiony innym.
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:
  Sluza do tego funkcje execXXX() - szesc postaci rozniacych sie sposobem
:<tt>int execl (const char * sciezka, const char * arg0, ...)</tt>
  przekazywania argumentow.
:<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>


  int execl (const char * sciezka, const char * arg0, ...)
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:
  int execlp(const char * plik, const char * arg0, ...)
* l oznacza, że argumenty wywołania programu są w postaci listy napisów zakończonej zerem (<tt>NULL</tt>)
  int execle(const char * sciezka, const char * arg0, ..., const char ** envp)
* v oznacza, że argumenty wywołania programu są w postaci tablicy napisow (tak jak argument <tt>argv</tt> funkcji <tt>main</tt>)
  int execv (const char * sciezka, const char ** argv)
* p oznacza, że plik z programem do wykonania musi się znajdować na ścieżce przeszukiwania ze [[zmiennej środowiskowej]] <tt>PATH</tt>
  int execvp(const char * plik, const char ** argv)
* e oznacza, że środowisko jest przekazywane ręcznie jako ostatni argument
  int execve(const char * sciezka, const ** char argv, const char ** envp)


  Krotkie wyjasnienie (wiecej w man 3 exec):
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.


  l - argumenty programu w postaci listy napisow zakonczonej 0 (NULL),
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:
  v - argumenty programu w postaci tablicy napisow (tak jak argv dla funkcji
:<tt> execlp ("ls", "ls", "-l", "f*", 0)
      main),
  p - sciezka przeszukiwania ze zmiennej srodowiskowej PATH,
  e - srodowisko przekazywane recznie jako ostatni parametr
      (raczej nie uzywane).


  Parametry:
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.


   - sciezka, to pelna sciezka do wykonywalnego programu,
A oto pełny przykład:
   - plik, to nazwa pliku z programem (tylko z p),
#include <stdio.h>
  - arg0 i argv[0] sa nazwa pliku zawierajacego program, a nastepne argumenty
#include <stdlib.h>
     zawieraja wlasciwe argumenty programu.
#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*/
}


  Jezeli wykonanie funkcji sie powiedzie, to nigdy nie nastapi powrot z jej
  wywolania.
; Ć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?


  Funkcje exec() najczesciej wywoluje sie zaraz po wykonaniu fork() w
; Ćwiczenie
  procesie potomnym.
: 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?
 
* Przeanalizuj proc_exec.c i wykonaj ./proc_exec.
  Sprobuj zmienic ps na hello z pierwszych zajec, a nastepnie
  na inny program wywolany z argumentami. Zwroc uwage na sposob obslugi
  bledow funkcji exec. Dlaczego wywolanie funkcji syserr jest bezwarunkowe?
 
* Przeanalizuj simple_shell.c i wykonaj ./simple_shell.
  Uwaga: wyjscie z programu przez Ctrl-D. Jest to prosty interpretator
  polecen. Potrafi wykonac tylko polecenia zewnetrzne, czyli takie,
  ktore moze zlecic innemu procesowi.
  Sprobuj dodac do niego mozliwosc wykonywania procesow w tle, czyli
  takich procesow, na ktore nie czeka interpretator polecen.
 
6. Konczenie procesu
  -----------------
 
  Proces moze spowodowac zakonczenie samego siebie przez wywolanie funkcji:
 
      void exit(int kod_zakonczenia);
 
  W przypadku poprawnego zakonczenia kod zakonczenia powinien byc rowny 0,
  a rozny od 0, jezeli nastapil blad.
 
7. Funkcja system()
  ----------------
 
  Oprocz pary funkcji fork-exec mozna uzyc funkcji system(), ktora powoduje
  wywolanie /bin/bash z argumentem tej funkcji.  Jest to drozsze niz para
  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