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 31 wersji utworzonych przez 2 użytkowników)
Linia 1: Linia 1:
=== Literatura ===
== Tematyka laboratorium ==
# M. K. Johnson, E. W. Troan, ''Oprogramowanie użytkowe w systemie Linux'', rozdz. 9.2.1 i 9.4.1-9.4.5
# Procesy w systemie Unix: tworzenie, kończenie, podmiana programy, oczekiwanie na potomków
# W. R. Stevens, ''Programowanie zastowań sieciowych w systemie UNI'', rozdz. 2.5.1-2.5.4
 
# M. J. Bach, ''Budowa systemu operacyjnego UNIX'', rozdz. 7.1, 7.3-7.5
== 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
# ''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.
 
-->
=== Tematyka laboratorium ===
# Przypomnienie zasad tworzenia plików [[Makefile]]
# Kompilacja programów w środowisku Unix
# Procesy w systemie Unix: tworzenie, kończenie,


== Scenariusz zajęć ==
=== Identyfikator procesu ===
=== Identyfikator procesu ===
Każdy proces w systemie ma jednoznaczny identyfikator nazywany potocznie
Każdy proces w systemie ma jednoznaczny identyfikator nazywany potocznie
Linia 42: Linia 23:


; Ćwiczenie
; Ćwiczenie
: Wykonaj polecenie <tt>ps</tt>. Zobaczysz wszystkie uruchomione przez Ciebie procesy
: Wykonaj polecenie <tt>ps</tt>. Zobaczysz wszystkie uruchomione przez Ciebie procesy w tej sesji. Znajdzie się wsród nich proces <tt>ps</tt> i <tt>bash</tt> (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 <tt>ps</tt> uzyskasz wywołując <tt>man ps</tt>.
w tej sesji. Znajdzie się wsród nich proces <tt>ps</tt> i <tt>bash</tt> (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 <tt>ps</tt> uzyskasz
wywołując <tt>man ps</tt>.


<!--
Z poziomu programisty, proces może poznać swój [[PID]] wywołując funkcję systemową:
  Proces mozne poznac swoj PID wywolujac funkcje systemowa:
:<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>.


        pid_t getpid();
=== 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 [[proces potomny|procesem potomnym]], a proces który go stworzył nosi nazwę [[proces macierzysty|procesu macierzystego]].


  Wartosci typu pid_t reprezentuja PIDy procesow. Najczesciej jest
Do tworzenia procesów służy funkcja systemowa:
  to dluga liczba calkowita, ale w zaleznosci od wariantu systemu
:<tt>pid_t fork();</tt>
  definicja ta moze byc inna. Dlatego lepiej poslugiwac sie
Powrót z wywołania tej funkcji następuje dwa razy:
  typem pid_t.
* 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.


3. Tworzenie nowego procesu
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:
  ------------------------
{| border="1"
  W Linuksie, tak jak we wszystkich systemach uniksowych, istnieje
|+ Proces Uniksowy. Podział na logiczne części.
  hierarchia procesow. Kazdy proces poza pierwszym procesem w systemie
| Wykonywany kod
  (procesem init o PIDzie 1) jest tworzony przez inny proces. Nowy proces
| Dane:
  nazywamy procesem potomnym, a proces ktory go stworzyl procesem
*w szczególności wszystkie zmienne procesu  
  macierzystym.
| 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).


  Do tworzenia procesow sluzy funkcja systemowa:
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>.


        pid_t fork();
#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*/
}


  Powrot z wywolania tej funkcji nastepuje dwa razy: w procesie macierzystym
Przeanalizujmy ten program.
  i w procesie potomnym. Dla potomka funkcja przekazuje w wyniku 0, a dla
#Część z dyrektywami <tt>#include</tt>. Tutaj umieszczamy niezbędne pliki nagłówkowe:
  procesu macierzystego PID nowo utworzonego potomka.
#*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.  


  Proces potomny wykonuje taki sam kod jak proces macierzysty - zaczyna
'''Uwagi:'''
  od wykonania nastepnej instrukcji po fork(). Jednak przestrzenie adresowe
# Jeszcze raz podkreślić należy, jak ważne jest wychwytywanie sytuacji błędnych i reakcja na nie.
   tych procesow sa rozlaczne. Kazdy ma swoja kopie zmiennych. Wartosci
# 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ł.
   zmiennych w procesie potomnym sa poczatkowo takie same jak w procesie
# Konstrukcja
  macierzystym w momencie utworzenia nowego procesu. Procesy maja te same
'''switch''' (pid = fork()) {
  uprawnienia, te same otwarte pliki itd.
  '''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ć.


* Tworzenie nowego procesu ilustruje przyklad proc_fork.c, ktory nalezy
; Ćwiczenia
  teraz przeczytac i uruchomic ./proc_fork (o funkcji wait bedzie za chwile).
: 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ść?


* Wykonaj polecenie ps -l. W 4-tej kolumnie znajduje sie PID, a w 5-tej
  PPID, czyli PID procesu macierzystego (Parent PID). Jaki proces jest
  procesem macierzystym dla procesu wykonujacego ps?


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


        pid_t wait(int *stan);
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.


  Funkcja przekazuje w wyniku PID zakonczonego procesu. Parametr stan jest
; Ćwiczenie (ukrywajka)
  wskaznikiem do zmiennej zawierajacej kod zakonczonego procesu.
: Zmodyfikuj poprzedni proces tak, aby można było zaobserwować [[zombie]].
  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
<div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź 
  odebrania ich przez ich procesy macierzyste. Proces potomny, ktorego
<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
  kod nie zostal odebrany przez rodzica to tzw. zombi (proces, ktory
<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>.
  sie zakonczyl, ale informacje o nim sa przechowywane przez system).
</div></div>
  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
; Ćwiczenie sprawdzające.
  wait w procesie macierzystym: "sleep(10)" (zawieszenie procesu na 10
: 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.  
  sekund), wykonaj make a nastepnie ./proc_fork & (w tle) i ps.


* Przeczytaj proc_tree.c i wykonaj kilkukrotnie ./proc_tree.
A oto rozwiązanie powyższego ćwiczenia.  
  Przeanalizuj PIDY i zwroc uwage na kolejnosc wypisywania informacji.
5. Uruchamianie nowych programow
  -----------------------------


  Procesowi mozemy zlecic wykonanie innego programu - aktualnie
  wykonywany program zostanie wtedy zastapiony innym.
  Sluza do tego funkcje execXXX() - szesc postaci rozniacych sie sposobem
  przekazywania argumentow.


  int execl (const char * sciezka, const char * arg0, ...)
#include <stdio.h>
  int execlp(const char * plik, const char * arg0, ...)
#include <stdlib.h>
   int execle(const char * sciezka, const char * arg0, ..., const char ** envp)
#include <unistd.h>
  int execv (const char * sciezka, const char ** argv)
#include <sys/types.h>
  int execvp(const char * plik, const char ** argv)
#include <sys/wait.h>
  int execve(const char * sciezka, const ** char argv, const char ** envp)
#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;
}


  Krotkie wyjasnienie (wiecej w man 3 exec):
; Ćwiczenie
: Skompiluj i uruchom kilkakrotnie ten program. Przeanalizuj PIDY i zwroć uwagę na kolejność wypisywania informacji.


  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:
; Ćwiczenie
: 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?


  - sciezka, to pelna sciezka do wykonywalnego programu,
=== Podmiana wykonywanego programu ===
  - plik, to nazwa pliku z programem (tylko z p),
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:
  - arg0 i argv[0] sa nazwa pliku zawierajacego program, a nastepne argumenty
:<tt>int execl (const char * sciezka, const char * arg0, ...)</tt>
    zawieraja wlasciwe argumenty programu.
:<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>


  Jezeli wykonanie funkcji sie powiedzie, to nigdy nie nastapi powrot z jej
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:
  wywolania.
* l oznacza, że argumenty wywołania programu są w postaci listy napisów zakończonej zerem (<tt>NULL</tt>)
* v oznacza, że argumenty wywołania programu są w postaci tablicy napisow (tak jak argument <tt>argv</tt> funkcji <tt>main</tt>)
* p oznacza, że plik z programem do wykonania musi się znajdować na ścieżce przeszukiwania ze [[zmiennej środowiskowej]] <tt>PATH</tt>
* e oznacza, że środowisko jest przekazywane ręcznie jako ostatni argument


  Funkcje exec() najczesciej wywoluje sie zaraz po wykonaniu fork() w
Przyjrzyjmy się przykładowej postaci tej funkcji:
  procesie potomnym.
:<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.


* Przeanalizuj proc_exec.c i wykonaj ./proc_exec.
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:
  Sprobuj zmienic ps na hello z pierwszych zajec, a nastepnie
:<tt> execlp ("ls", "ls", "-l", "f*", 0)
  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.
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.
  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
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*/
}


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


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