[LinuxFocus-icon]
Strona Główna  |  Mapa Serwisu  |  Indeks  |  Szukaj

Nowości | Archiwum | Linki | O Nas
Ten dokument jest dostępny w następujących językach: English  Castellano  ChineseGB  Deutsch  Francais  Italiano  Nederlands  Russian  Turkce  Polish  

[Leonardo]
Leonardo Giordani
<leo.giordani(at)libero.it>

O Autorze:

Student Telecomunikacji na Politechnice w Mediolanie, pracuje jako administrator sieciowy, interesuje się programowaniem. (przeważnie Assembler i C/C++). Od 1999 pracuje tylko na Linux/Unix.

Translated to English by:
Leonardo Giordani <leo.giordani(at)libero.it>

Zawartość:


 

Programowanie współbieżne - wprowadzenie i zasady.

[run in paralell]

Notka:

Ta seria artykółów wprowadza czytelnika w pojęcia multitaskingu w systemie Linux. Rozpoczynając od teoretycznego wprowadzenia do multitaskingu i kończąc na napisaniu pełnej aplikacji ukazującej w pełni komunikację pomiędzy procesami z prostą, ale wydajną komunikacją protokołów. Potrzebną wiedzą do zrozumienia artykułu jest :

Wszystkie wyjaśnienia można znaleźć w manualach których umiejscowienie jest pokazane w nawiasach. Wszystkie biblioteki glibc są udokumentowane na stronach informacji gnu (info Libc, albo info:/libc/Top w konquerorze).
_________________ _________________ _________________

 

Wprowadzenie

Punktem zwrotnym w historii systemów operacyjnych było wprowadzenie pojęcia multiprogramingu(programowanie wielowątkowe), czyli techniki, która daje możliwość wykonywania wielu programów w odpowiednim porządku by uzyskać większą stabilność zasobów systemu. Wezmy pod uwagę na przykład prostą stacje roboczą, na której użytkownik może korzystać z procesora tekstu, odtwarzacza audio, drukarki, przeglądarki internetowej i wielu innych. Jak wiemy to jest tylko mała część długiej listy programów, które możemy włączyć na naszym komputerze.  

Pojęcie procesu

Gdy programy są uruchomione w jednym czasie to wówczas występują komplikacje w systemie operacyjnym. W celu uniknięcia konfliktów pomiędzy uruchomionymi programami należy kapsułkować każdy z nich z informacjami potrzebnymi do ich wykonania.

Przed tym jak poznamy co się dokładnie dzieje w Linuksie, opanujmy trochę technicznej nomenklatury: mamy URUCHOMIONY PROGRAM, czyli zbiór instrukcji KOD PROGRAMU które są wykonywane w danym czasie, PRZESTRZEŃ PAMIĘCI czyli część pamięci, która została zarezerwowana przez dane i STATUS PROCESORA który jest wartością parametru procesora, tak jak flaga albo licznik programu (adres instrukcji, która zostanie wykonana w następnej kolejności).

Zdefiniowaliśmy pojęcie URUCHOMIONEGO PROGRAMU jako obiektu posiadającego KOD, PRZESTRZEŃ PAMIĘCI i STATUS PROCESORA. Jeśli w pewnym czasie podczas wykonywania programu zapamiętamy te informacje i zamienimy je z tymi samym zestawem informacji, który wezmiemy z innego uruchomionego programu i robiąc tak dalej, będziemy przeplatać te czynności tak opisane czynności możemy nazwać PROCESEM (albo ZADANIEM), czyli pojęcie PROCES jest używane do opisu uruchomionego programu.

Wytłumaczmy co się działo z naszą stacją roboczą, mówiliśmy o tym wcześniej : w każdym momencie jest wykonywane tylko jedno zadanie (mikroprocesor jest w stanie obsłużyć tylko jedno zadanie) i maszyna wykonuje część kodu programu, następnie po jakimś czasie (czas ten nazywamy kwantem ang. QUANTUM) nasz proces zostaje zawieszony i informacje dotyczące jego są zapisane i zamienione z informacjami dotyczącymi innego czekającego na wykonanie procesu. Ten kod zostanie wykonany ponownie po upłynięciu kwantu czasu i tak dalej. I cała ta procedura jest nazywana multitaskingiem lub wielozadaniowością.

Sam proces multitaskingu ukazuje nam wiele problemów i większość z nich nie jest trywialna wiezmy pod uwagę proces kolejkowania oczekujących procesów (tzw. SCHEDULING, czyli harmonogram), problemy te są rozwiązywane inaczej w każdym systemie (uzależnione jest to od architektury systemu operacyjnego). Możliwe, że to będzie tematem przyszłego artykułu, który wprowadzi was w tematykę kodu Kernela.  

Procesy w Linuksie i Uniksie

Powiedzmy coœ o procesach uruchamianych na naszej maszynie. Komendą dającą nam trochę informacji jest ps(1), która jest skrótem od (process status) status procesu. Uruchamiając w shelu naszą komendę na ekranie zostanie wyświetlone coś w rodzaju :

  PID TTY          TIME CMD
 2241 ttyp4    00:00:00 bash
 2346 ttyp4    00:00:00 ps

Ta lista nie jest kompletna, ale skoncentrujmy się na tym co mamy : ps daje nam listę procesów, które są uruchomione na naszym terminalu. W ostatniej kolumnie jest wykazane jak wygląda nazwa procesu, który został uruchomiony (na przykład "mozila" dla Mozilla Web Browser (przeglądarka internetowa) albo "gcc" dla GNU Compiler Collection)). Niestety ps jest na liście procesów ponieważ lista ta pokazuje procesy już uruchomione. Innym wypisanym procesem jest Bourne Again Shell, shell który jest uruchomiony na moim terminalu.

Zostawmy na chwile informacje jakie przekazuje nam TIME i TTY i spojżmy na PID Process IDentifier. Numer pid jest unikalnym dodatnim numerem (różnym od zera), który jest przypisany do każdego uruchomionego procesu, gdy proces zostanie skasowany, to pid nie może być użyty ponownie z innym procesem, ale gdy proces trwa to pid zawsze jest przyporządkowany do tego samego procesu. Wszystkie te wiadomości jakie uzyskaliśmy dzięki poleceniu ps, będą prawdopodobnie inne gdy uruchomimy to polecenie ponownie. Można to przetestować uruchamiajšc innego shella, bez zamykania tego w którym jesteśmy teraz i uruchommy ps, zostanie nam wypisana ta sama lista procesów ale z innymi numerami pid, ten test pokazuje man że chociaż programy są te same, to nie są to już te same procesy.

Możemy także uzyskać listę uruchomionych procesów w inny sposób, manual do ps mówi, że gdy uruchomimy ps z argumentem -e który znaczy "wybierz wszystkie procesy", to zostanie nam pokazana długa lista procesów przedstawioną jak powyżej. Najlepiej jest przekierować naszą listę do pliku np. ps.log :

ps -e > ps.log

Teraz możemy czytać i edytować nasz plik w edytorze który preferujemy (albo prościej komendą less), jak zaczęliœmy na początku artykułu nasze numery pid były większe niż się spodziewaliśmy. Teraz wypisaliœmy listę nie tylko procesów uruchomionych przez nas (bezpośrednio z wiersza poleceń) ale także wiele innych, wiele z nich ma dziwne nazwy : numery i ich tożsamość zależą od konfiguracji twojego systemu, ale zawsze są jakieś wspólne rzeczy. Po pierwsze, nie ważne jaką konfiguracje systemu masz to zawsze procesem którego pid jest równy 1 to proces "init", ojciec wszystkich procesów. Dla niego jest przyporządkowany jest numer 1 dlatego, bo on zawsze jest uruchamiany jako pierwszy przez system operacyjny. Następną rzeczą, którą możemy z łatwością zauważyć, jest to, że nazwy wielu procesów kończą się na "d", nazywane one są demonami i są to jedne z najważniejszych procesów w systemie. Będziemy się zajmować procesem "init" i demonami w przyszłych artykułach.  

Wielozadaniowość w libc

Teraz mamy pojęcie znaczenia pojęcia proces i jego wagi dla systemu. Teraz będziemy zajmować się napisaniem kodu, który będzie wykorzystywał wielozadaniowość. Od prostej sytuacji uruchomienia procesu będziemy się przesuwać w kierunku nowych problemów: komunikacji pomiędzy procesami i ich synchronizacją. Znajdziemy dwa bardzo dobre rozwiązania tego problemu: komunikaty i semafory - które zostanš pózniej szczegółowo opisane w jednym z następnych artykułów dotyczącym wątków. Po "komunikatach" zabierzemy się za pisanie naszej aplikacji bazując na wszystkim o czym była do tej pory mowa.

Standardowa biblioteka C (libc, zaimplementowana w Linux'ie jako glibc) używa narzędzi z systemu Unix V, system Unix V (teraz SysV) jest komercyjną implementacją Unixa i także zapoczątkował jedną z najważniejszych rodzin Unixa, inne stanowią rodzinę BSD Unix.

W libc typ danych pid_t jest zdefiniowany jako integer zawierający numer pid. Od teraz będziemy używać tego do uzyskania numeru pid, ale tylko dla jasności, używanie integera to samo.

Pokażmy teraz funkcję, która daje nam wiedze na temat numeru pid naszego programu.

pid_t getpid (void)

(funkcja ta jest zdefiniowana razem z pid_t w unistd.h i sys/types.h). Napiszemy program, który zademonstruje nam działanie funkcji getpid() i wypisze nasz pid na standardowym wyjściu (na ekranie).

#include <unistd.h>;
#include <sys/types.h>;
#include <stdio.h>;

int main()
{
  pid_t pid;

  pid = getpid();
  printf("The pid assigned to the process is %d\n", pid);

  return 0;
}
Zapisz program jako print_pid.c i skompiluj to.
gcc -Wall -o print_pid print_pid.c
Stworzyliśmy plik wykonywalny print_pid. Przypominam, że jeśli katalog bieżący nie jest w ścieżce to wymagane jest uruchomienie programu jako "./print_pid". Wynik naszego programu nie będzie wielkim zaskoczeniem : wypisze dodatnią liczbę i jeśli uruchomimy ten program więcej niż raz, zobaczymy, że ten numer będzie zwiększony o jeden raz za razem, wzrost ten nie musi być zawsze taki sam, ponieważ może wystąpić przypadek, że pomiędzy dwoma uruchomieniami naszego programu mogą zostać uruchomione inne procesy co spowoduje zwiększenie się naszego numery o inną wartość. Na przykład możemy uruchomić ps pomiędzy dwoma uruchomieniami print_pid.

Teraz nadszedł czas aby nauczyć się tworzyć procesy. Kiedy program (zawierający się w procesie A) tworzy inny proces (B) oba są identyczne, to znaczy oba mają ten sam kod, oba wykorzystują w tym samym stopniu pamięć i oba mają ten sam status procesora. Od tego momentu oba mogą być wykonywane w dwa różne sposoby, na przykład w zależności od wyjścia użytkownika albo innych zdarzeń losowych. Proces "A" jest "procesem nadrzędnym", a proces "B" jest "procesem podrzędnym". Teraz możemy lepiej zrozumieć nazwę "ojciec wszystkich procesów", czyli procesu nadrzędnego do wszystkich co się odnosiło do procesu init. Funkcją, która tworzy nam inne procesy jest funkcja:

pid_t fork(void)
nazwa ta pochodzi od własności procesów jaką jest rozwidlanie. Funkcja ta zwraca numer pid, ale zasługuje na szczególną uwagę. Powiedzieliśmy sobie, że nasz proces duplikuje się na na proces nadrzędny i podrzędny, które są wykonywane poprzez przeplatanie z innymi uruchomionymi procesami, robiąc inną pracę. Nasuwa się pytanie, który z procesów będzie wykonywany jako pierwszy: nadrzędny czy podrzędny. Odpowiedz jest prosta: pierwszy albo drugi. Decyzja, który proces będzie uruchomiony jako pierwszy, jest brana z harmonogramu systemu operacyjnego i nie ma znaczenia czy jest to proces nadrzędny czy podrzędny, kolejność zależy od algorytmu zależnego od wielu parametrów.

Jednakże istotne jest żeby wiedzieć, który proces się właśnie wykonuje ponieważ kod jest taki sam, oba procesy będą zawierać kod procesu nadrzędnego i podrzędnego ale każdy z procesów będzie wykonywał jedna wybrana część. Najlepiej przedstawi to poniższy algorytm :

- FORK
- IF YOU ARE THE SON EXECUTE (...)
- IF YOU ARE THE FATHER EXECUTE (...)
co reprezentuje w pewnym abstrakcyjny sposób kod naszego programu. Funkcja fork zwraca '0' do procesu podrzędnego i pid procesu podrzędnego do procesu nadrzędnego. Jeœli zwróconym pid jest zero to wiemy który proces będzie wykonywany. W języku C wygląda to tak :
int main()
{
  pid_t pid;

  pid = fork();
  if (pid == 0)
  {
    CODE OF THE SON PROCESS
  }
  CODE OF THE FATHER PROCESS
}
Nadszedł czas na napisanie pierwszego przykładu wykorzystania wielozadaniowości, nasz kod możemy zapisać jako fork_demo.c i skompilować jak było to robione poprzednio. Ponumerowane linie wstawiłem żeby ułatwić omawianie programu. Program ten rozdzieli proces na nadrzędny i podrzędny, oba procesy wypiszą coś na ekranie (jeśli wszystko pójdzie dobrze):
(01) #include <unistd.h>
(02) #include <sys/types.h>
(03) #include <stdio.h>

(04) int main()
(05) {
(05)   pid_t pid;
(06)   int i;

(07)   pid = fork();

(08)   if (pid == 0){
(09)     for (i = 0; i  < 8; i++){
(10)       printf("-SON-\n");
(11)     }
(12)     return(0);
(13)   }

(14)   for (i = 0; i < 8; i++){
(15)     printf("+FATHER+\n");
(16)   }

(17)   return(0);
(18) }

Linie (01)-(03) zawierają informacje o plikach nagłówkowych, z których korzysta nasz program (standard I/O, multitasking).
Funkcja main (jak zawsze w GNU), zwraca integera, który normalnie jest równy zero jeśli wszystko poszło dobrze i program wykonał się w całości bez żadnych błędów albo coś poszło nie tak i wystąpiły błędy. Przypuśćmy, że wszystko poszło dobrze, bez błędów (kontrole błędów można dodać jak wszystko będzie dobrze w podstawowym programie). W linii (05) definiujemy typ danych pid i zmiennš i (06) potrzebną nam do obsługi pętli. Te dwa typy są takie same ale dla jasności są zdefiniowane inaczej.
W linii (07) wywołujemy funkcję fork, która gdy zwróci zero spowoduje wykonanie procesu podrzędnego da nam pid procesu podrzędnego. Sprawdzane to jest w linii (08). Następnie wykonywany jest kod programu w liniach (09)-(13) należšcy do procesu podrzędnego następnie reszta dotyczące procesu nadrzędnego,czyli linie (09)-(13).
Dwie części po prostu wypisują 8 razy na standardowym wyjściu słowo "-SON-" albo "+FATHER+", w zależności od tego który proces jest wykonywany i na końcu zwraca zero. To jest bardzo ważne, ponieważ bez tego ostatniego "return" proces podrzędny kończąc swojš pętle mógłby wykonywać kod przeznaczony dla procesu nadrzędnego (można tego spróbować, to nie zniszczy ci komputera, ale po prostu my tego nie chcemy). Taki błąd będzie naprawdę bardzo ciężko znalezć, ponowne uruchamianie programu (zwłaszcza bardziej złożonego) będzie dawać nam inne wyniki, w wyniku czego usunięcie błędów będzie prawie całkiem niemożliwe.

Uruchamiając ten program możliwe, że będziesz nie usatysfakcjonowany. Ja nie mogę zapewnić, że rezultat będzie mieszaniną tych dwóch łańcuchów , zależne to będzie od prędkości wykonania tej krótkiej pętli. Prawdopodobnie twój ekran będzie wypisywał łańcuchy "+FATHER+" i "-SON-", albo przeciwnie. Spróbuj uruchomić program więcej niż jeden raz i zobaczysz że rezultaty mogą ulec zmianie.

Wstawiając losowe przerwy pomiędzy każdym printf możemy zauważyć więcej wizualnych efektów. Użyjemy do tego funkcji sleep i random.

sleep(rand()%4)
ta linijka sprawia, że program "usypia" na losowo wygenerowaną liczbę sekund pomiędzy 0 i 3 (operator % zwraca resztę z dzielenia liczby integer), teraz kod wygląda następująco:
(09)  for (i = 0; i < 8; i++){
(->)    sleep (rand()%4);
(10)    printf("-FIGLIO-\n");
(11)  }
i robimy to samo dla kody procesu nadrzędnego. Zapisujemy jako fork_demo2.c, kompilujemy i uruchamiamy. Teraz program wykonuje się wolniej, ale także widzimy różnice w tym co otrzymujemy na wyjściu:
[leo@mobile ipc2]$ ./fork_demo2
-SON-
+FATHER+
+FATHER+
-SON-
-SON-
+FATHER+
+FATHER+
-SON-
-FIGLIO-
+FATHER+
+FATHER+
-SON-
-SON-
-SON-
+FATHER+
+FATHER+
[leo@mobile ipc2]$

Teraz przyjrzyjmy się problemowi, jaki tu mamy. Tworzymy pewną ilość procesów podrzędnych tak ,że wykonują operacje inne niż te wykonywane przez proces nadrzędny w równoczesnym środowisku obliczeniowym, czasami proces nadrzędny potrzebuje skomunikować się z procesem podrzędnym albo przynajmniej zsynchronizować się z nim aby wykonać operacje w odpowiednim czasie. Pierwszym sposobem aby uzyskać taką synchronizację jak funkcja wait:

pid_t waitpid (pid_t PID, int *STATUS_PTR, int OPTIONS)
gdzie PID jest numerem PID procesu który ma czekać, STATUS_PTR jest wskaznikiem na liczbę integer, która określa status procesu podrzędnego (NULL jest wtedy jeśli informacje nie są wymagane) i OPTIPNS jest zbiorem opcji, o które na razie nie musimy się martwić. To jest przykład programu w którym proces nadrzędny tworzy proces podrzędny i czeka aż on się skończy :
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main()
{
  pid_t pid;
  int i;

  pid = fork();

  if (pid == 0){
    for (i = 0; i < 14; i++){
      sleep (rand()%4);
      printf("-SON-\n");
    }
    return 0;
  }

  sleep (rand()%4);
  printf("+FATHER+ Waiting for son's termination...\n");
  waitpid (pid, NULL, 0);
  printf("+FATHER+ ...ended\n");

  return 0;
}
Funkcja sleep w kodzie procesu nadrzędnego została wstawiona aby rozróżnić wykonanie. Zapiszmy kod jako fork_demo3.c, skompilujmy i wykonajmy. Właśnie napisaliśmy nasz pierwszą aplikację wykorzystujšcą wielozadaniowość systemu linux!

W następnym artykule nauczymy się więcej na temat synchronizacji i komunikacji pomiędzy procesami. Teraz napisz swój program używajšc poznanych funkcji i jak chcesz to wyślij mi a ja sprawdzę czy dobrze ich użyłeś i jeśli będą błędy to je poprawie. Wyślij mi plik z kodem oraz mały plik tekstowy opisujący program, twoje Imię i e-mail. Powodzenia!  

Warto przeczytać

 

Dyskusja dotycząca tego artykułu

Komentarze do dyskusji:
 Strona talkback 

Strona prowadzona przez redakcję LinuxFocus
© Leonardo Giordani, FDL
LinuxFocus.org
tłumaczenie:
it --> -- : Leonardo Giordani <leo.giordani(at)libero.it>
en --> pl: Przemyslaw Pozniak <malinaz(at)poczta.wp.pl>

2002-11-24, generated by lfparser version 2.33