Programowanie współbieżne i rozproszone/PWR Ćwiczenia 2: Różnice pomiędzy wersjami
mNie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 118: | Linia 118: | ||
#W wierszu '''2''' program wypisuje swój [[PID]] pobrany za pomocą funkcji systemowej <tt>getpid</tt>. | #W wierszu '''2''' program wypisuje swój [[PID]] pobrany za pomocą funkcji systemowej <tt>getpid</tt>. | ||
#W wierszu '''3''' następuje wywołanie funkcji systemowej <tt>fork</tt> i dochodzi do "rozwidlenia" procesu. Tworzy się nowy proces i w chwili powrotu z funkcji systemowej mamy już dwa procesy. Oba mają w tym momencie jeszcze tę samą wartość zmiennej <tt>pid</tt>, ale natychmiast po powrocie zmienna ta otrzymuje wartość przekazaną przez funkcję systemową <tt>fork()</tt>. Będzie to zatem 0 w procesie potomnym oraz wartość większa od 0 w procesie macierzystym. | #W wierszu '''3''' następuje wywołanie funkcji systemowej <tt>fork</tt> i dochodzi do "rozwidlenia" procesu. Tworzy się nowy proces i w chwili powrotu z funkcji systemowej mamy już dwa procesy. Oba mają w tym momencie jeszcze tę samą wartość zmiennej <tt>pid</tt>, ale natychmiast po powrocie zmienna ta otrzymuje wartość przekazaną przez funkcję systemową <tt>fork()</tt>. Będzie to zatem 0 w procesie potomnym oraz wartość większa od 0 w procesie macierzystym. | ||
# Wiersz '''4''' zawiera bardzo ważny element programu --- kontrolę błędów. System operacyjny może nie utworzyć nowego procesu z wielu powodów (np. brak pamięci, brak miejsca w tablicy procesów, przekroczenie limitu procesów na użytkownika itp). Każda funkcja systemowa przekazuje swój kod zakończenia. Jest to wartość >= 0, jeżeli funkcja zakończyła się pomyślnie lub liczba ujemna oznaczająca kod błędu w przeciwnym razie. 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. | # Wiersz '''4''' zawiera bardzo ważny element programu --- kontrolę błędów. System operacyjny może nie utworzyć nowego procesu z wielu powodów (np. brak pamięci, brak miejsca w tablicy procesów, przekroczenie limitu procesów na użytkownika itp). Każda funkcja systemowa przekazuje swój kod zakończenia. Jest to wartość >= 0, jeżeli funkcja zakończyła się pomyślnie lub liczba ujemna oznaczająca kod błędu w przeciwnym razie. 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>. | |||
<!-- | <!-- |
Wersja z 09:33, 12 cze 2006
Tematyka laboratorium
- Przypomnienie zasad tworzenia plików Makefile
- Kompilacja programów w środowisku Unix
- Procesy w systemie Unix: tworzenie, kończenie,
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 UNI, 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
Scenariusz zajęć
Identyfikator procesu
Każdy proces w systemie ma jednoznaczny identyfikator nazywany potocznie PID (od angielskiego: Process ID). Identyfikatory aktualnie wykonujących się procesów możesz poznać wykonując w Linuksie polecenie ps.
- Ćwiczenie
- Wykonaj polecenie ps. Zobaczysz wszystkie uruchomione przez Ciebie procesy w tej sesji. Znajdzie się wsród nich proces ps i bash (lub inny stosowany przez Ciebie interpreter poleceń), który analizuje i wykonuje Twoje polecenia. Pierwsza kolumna to PID procesu, a ostatnia to polecenie, które dany proces wykonuje. Więcej informacji na temat polecenia ps uzyskasz wywołując man ps.
Z poziomu programisty, proces może poznać swój PID wywołując funkcję systemową:
- pid_t getpid();
Wartości typu pid_t reprezentują PIDy procesów. Najczęściej jest to długa liczba całkowita (long int), ale w zależności od wariantu systemu operacyjnego definicja ta może być inna. Dlatego lepiej posługiwać się nazwą pid_t.
Tworzenie nowego procesu
W Linuksie, tak jak we wszystkich systemach uniksowych, istnieje hierarchia procesów. Każdy proces poza pierwszym procesem w systemie (procesem init o PIDzie 1) jest tworzony przez inny proces. Nowy proces nazywamy procesem potomnym, a proces który go stworzył nosi nazwę procesu macierzystego.
Do tworzenia procesów służy funkcja systemowa:
- pid_t fork();
Powrót z wywołania tej funkcji następuje dwa razy:
- w procesie macierzystym, w którym wartością przekazywaną przez funkcję fork jest PID nowo utworzonego potomka,
- w procesie potomnym, w którym funkcja przekazuje w wyniku 0.
Jak dokładnie działa funkcja systemowa fork()? Proces w systemie Unix jest wygodnie wyobrażać sobie jako obiekt składający się z trzech części:
Wykonywany kod | Dane:
|
Dane systemowe:
|
Funkcja systemowa fork tworzy nowy proces i kopiuje do niego wszystkie powyższe elementy, zmieniając jedynie te elementy, który 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. to, że zmiana wartości zmiennej w procesie potomnym nie jest odzwierciedlana w procesie macierzystym i na odwrót.
- ma te same uprawnienia, te same otwarte pliki itd. (tym zajmiemy się w kolejnym laboratorium)
A oto przykład ilustrujący wykorzystanie funkcji fork() do tworzenia nowego procesu. Przykład ten możesz znaleźć w plikach przygotowanych do zajęć pod nazwą proc_fork.c.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include "err.h" int main () { 1: pid_t pid; 2: /* wypisuje identyfikator procesu */ printf("Moj PID = %d\n", getpid()); /* tworzy nowy proces */ 3: switch (pid = fork()) { 4: case -1: /* błąd */ syserr("Error in fork\n"); 5: case 0: /* proces potomny */ printf("Jestem procesem potomnym. Mój PID = %d\n", getpid()); printf("Jestem procesem potomnym. Wartość przekazana przez fork() = %d\n", pid); return 0; 6: default: /* proces macierzysty */ printf("Jestem procesem macierzystym. Mój PID = %d\n", getpid()); printf("Jestem procesem macierzystym. Wartość przekazana przez fork() = %d\n", pid); 7: /* czeka na zakończenie procesu potomnego */ if(wait(0) == -1) syserr("Error in wait\n"); return 0; } /*switch*/ }
Przeanalizujmy ten program.
- Część z dyrektywami #include. Tutaj umieszczamy niezbędne pliki nagłówkowe:
- standardową bibliotekę wejścia/wyjścia stdio.h
- plik nagłówkowy unistd.h zawierający deklaracje standardowych funkcji uniksowych (fork(), write() itd.); Należy go dołączyć do programu, w przeciwnym razie kompilator generuje ostrzeżenia takie jak implicit declaration of function fork
- plik nagłówkowy z deklaracją funkcji wait() z sys/wait.h
- plik nagłówkowy z deklaracją funkcji syserr do obsługi błędów. Więcej na temat błędów za chwilę.
- Kod programu umieszczamy jak zwykle w funkcji main. Wewnątrz tej funkcji w wierszu 1 znajduje się deklaracja zmiennej pid, na której będziemy pamiętać PID procesu.
- W wierszu 2 program wypisuje swój PID pobrany za pomocą funkcji systemowej getpid.
- W wierszu 3 następuje wywołanie funkcji systemowej fork i dochodzi do "rozwidlenia" procesu. Tworzy się nowy proces i w chwili powrotu z funkcji systemowej mamy już dwa procesy. Oba mają w tym momencie jeszcze tę samą wartość zmiennej pid, ale natychmiast po powrocie zmienna ta otrzymuje wartość przekazaną przez funkcję systemową fork(). Będzie to zatem 0 w procesie potomnym oraz wartość większa od 0 w procesie macierzystym.
- Wiersz 4 zawiera bardzo ważny element programu --- kontrolę błędów. System operacyjny może nie utworzyć nowego procesu z wielu powodów (np. brak pamięci, brak miejsca w tablicy procesów, przekroczenie limitu procesów na użytkownika itp). Każda funkcja systemowa przekazuje swój kod zakończenia. Jest to wartość >= 0, jeżeli funkcja zakończyła się pomyślnie lub liczba ujemna oznaczająca kod błędu w przeciwnym razie. Ponadto jeżeli wystąpił błąd, to kod błędu jest przypisywany na globalną zmienną errno. Dzięki kodowi błędu uzyskujemy więcej informacji o powodach wystąpienia danego błędu. Przykład wykorzystania zmiennej errno można znaleźć w funkcji syserr znajdującej się w pliku err.c, która korzysta z globalnej tablicy sys_errlist zawierającej opisy wszystkich kodów błędów.Uwaga! Sprawdzanie poprawności wykonania wszystkich funkcji systemowych jest absolutnym obowiązkiem programisty!
- Fragment programu od wiersza 5 jest wykonywany jedynie przez proces potomny, który wypisuje swój PID i wartość przekazaną mu przez funkcję systemową fork().