background image

 

 

04 

Metodyka i Techniki Programowania 

Procesy i wątki 
dr inż. Rafał Stankiewicz

 

2008-03-10 

 

Krótkie wprowadzenie. 

 
Procesem jest każdy program, który został uruchomiony i jest wykonywany. Każdy proces ma odrębne niezależne 
struktury danych, kodu, rejestry, stosy itd. Proces jest niezależnym bytem w systemie i może działać niezależnie od 
innych procesów (z wyjątkiem sytuacji, gdy celowo zaprogramowano mechanizmy współpracy między procesami). 
Wątek jest tworzony jest wewnątrz procesu i wykorzystuje jego zasoby. Dla wątku tworzone są na nowo tylko 
niezbędne struktury resztę współdzieli z innymi wątkami/procesem. Przykładowo, zmiany wykonane na zmiennych 
globalnych są widoczne w innych wątkach, ze wszystkimi tego skutkami. 
 
PROCESY 
Każdy proces ma swój unikalny identyfikator w systemie – PID (ang. Process identifier). Procesy w systemie UNIX 
powstają poprzez mechanizm rozwidlania procesów. Każdy nowy proces jest tworzony przez inny proces. Proces 
tworzący zwany jest procesem macierzystym, a proces nowy – potomnym. Dla każdego procesu można więc podać 
identyfikator procesu, który go utworzył – PPID (ang. Parent PID). Więcej o rozwidlaniu procesów poniżej. 
 
Wyświetlenie listy procesów w systemie umożliwia polecenie ps. Poszczególne opcje pozwalają na wyświetlenie 
różnorodnych informacji o procesie. Szczegółowo o możliwościach polecenia ps, opcjach i wyświetlanych 
informacjach można przeczytać np. w manualu (man ps). 
 
Proces może zostać uruchomiony

 

na pierwszym planie (np.: ./program), wówczas przez czas działania programu nie ma dostępu do 
wiersza poleceń terminala. Proces przejmuje kontrolę nad standardowym wejściem (stdin). 

 

w tle (np.: ./program &), wówczas proces działa niejako na drugim planie nie blokując dostępu do 
terminala (można wykonywać inne polecenia, uruchamiać inne procesy). 

 
Możliwe jest wymuszenie przerwania działania procesu w dowolnym momencie przez wysłanie do niego 
odpowiedniego sygnału. Do wysyłania sygnałów służą:  

 

komenda kill [-signal] pid 

 

pewne kombinacje klawiszy np.:  ^C – wysłanie sygnału INT (interrupt) do procesu – przerywa działanie 
bieżącego procesu działającego na pierwszym planie 

 
Działający proces może zostać zatrzymany (uśpiony) na pewien czas. Proces wówczas nie jest wykonywany (nie 
jest mu przydzielany czas procesora). Zatrzymanie procesu następuje poprzez wysłanie do niego sygnału TSTP lub 
STOP. Sygnał TSTP można wysłać do procesu przez naciśnięcie kombinacji klawiszy ^Z (pod warunkiem, że proces 
działa na pierwszym planie). 
 
Uśpiony proces może zostać ponownie przywrócony do działania. Można tego dokonać wysyłając do procesu sygnał 
CONT. Oprócz komendy kill można też użyć jedno z dwóch poleceń: 

 

fg – przywrócenie do działania na pierwszy plan 

 

bg – przywrócenie do działania w tle. 

Ponadto, poleceniem fg można przenieść na pierwszy plan proces działający w tle. 
 
Użyteczne jest polecenie jobs które wyświetla listę procesów uśpionych oraz działających w tle. 
 
Jak  już  wspomniano,  procesy  tworzone  są  przy  pomocy  tego  samego  mechanizmu  –  rozwidlania  procesów 
(wywołanie  funkcji  fork).  Jeżeli  proces  wywoła  funkcję  fork,  wówczas  w  systemie  powstanie  nowy  proces 
(potomny)  będący  jego  kopią.  Dla  każdego  procesu  zawsze  można  określić,  który  inny  proces  go  utworzył  (z 
wyjątkiem procesu init). W momencie utworzenia proces otrzymuje swój unikalny identyfikator PID przydzielony 
przez system. Nie da się przewidzieć wartości PID. Proces init jest pierwszym procesem powstającym przy starcie 

background image

 

 

systemu.  Jedynie  proces  init  ma  zawsze  taki  sam  numer  PID  równy  1.  Każdy  proces  dziedziczy  też  od  swojego 
rodzica jego identyfikator jako PPID. 
 
Do uzyskania wartości PID oraz PPID procesu służą następujące funkcje języka C: 
pid_t getpid(); 
pid_t getppid(); 
Do rozwidlania procesów służy funkcja: 
pid_t fork(); 
 
Funkcja  fork  zwraca  wartość  zero  w  procesie  potomnym,  natomiast  w  procesie  tworzącym  (macierzystym) 
zwracana  jest
  wartość  identyfikatora  PID  utworzonego  procesu  (w  ten  sposób  proces  macierzysty  zna  PID 
swojego potomka). W przypadku, gdy nie udało się utworzyć procesu potomnego zwracana jest wartość –1. 
  
 
Przykład 
 
Przykład przedstawia mechanizm rozwidlania procesów i przydzielania identyfikatorów PID, PPID.  
Załóżmy, że kod programu rower.c  jest następujący (pominięto biblioteki): 

main(){ 

int a; 

 

a=fork(); 

 
 

sleep(2); 

 

printf(”%d\n”,a); 

 

return 0; 

Kolejne etapy wykonania procesu wyglądają nastepująco: 

./rower

PID=1202
PPID=1097

a=fork();

./rower

PID=1298
PPID=1202

a=0

a=1298

1

2

3

proces macierzysty
(parent, “rodzic”)

proces potomny
(child, “dziecko”)

sleep(2);
printf(”%d\n”,a);
return 0;

sleep(2);
printf(”%d\n”,a);
return 0;

 

 

 

W chwili (1) uruchomiono  program  ./rower. Interpreter  poleceń (shell) miał PID równy 1097. 

Procesowi  ./rower  przydzielony  został  PID=1202,  zaś  jego  PPID  jest  równy  PID-owi  procesu 
shell’a – 1097. 

 

W chwili (2) proces ./rower wykonał funkcję fork() i utworzył w systemie swoją nową kopię. 

Nowy  proces  zwany  jest  procesem  potomnym,  zaś  proces,  który  wywołał  funkcję  fork() 
procesem macierzystym. Nowo powstałemu procesowi został przydzielony PID równy 1298. Jego 
PPID jest równy PID-owi procesu macierzystego, czyli 1202.  

 

Funkcja fork() zwróciła w procesie macierzystym wartość 1298 (PID potomka), zaś w procesie 

potomnym  wartość  0.  Oznacza  to,  że  w  chwili  (3)  wartości  zmiennej  a  w  obu  procesach  będą 
odpowiednio 1298 i 0. 

background image

 

 

Obydwa  procesy  mają  ten  sam  kod  i  są  niejako  na  tym  samym  etapie  wykonywania  swojego  kodu. 
Kolejną  operacją,  jaką  wykonają  obydwa  procesy  będzie  funkcja  sleep(2).  Następnie  oba  procesy 
wykonają funkcję printf i zakończą swoje działanie. 

 
 
 
Mechanizm rozwidlania procesów powoduje utworzenie tylko kopii procesu, co daje ograniczone możliwości 
tworzenia różnorodnych procesów. Aby było możliwe uruchamianie innych programów konieczny jest jeszcze 
mechanizm podmiany kodu procesu
 
Przykładowo, gdy uruchamiamy program ls, w pierwszej chwili powstaje kopia procesu naszego shell’a, której kod 
jest następnie podmieniany na kod programu ls. 
 
Każdy proces może w dowolnym momencie podmienić swój kod na inny (czyli zupełnie zmienić swoje własności i 
funkcje). W momencie podmiany kodu nie zmieniają się identyfikatory procesu. Kod procesu, który „decyduje się” na 
podmianę swojego kodu (oraz jego zestaw zmiennch) są bezpowrotnie tracone i zastępowane nowym kodem  i 
nowymi strukturami danych. 
 
Dostępna jest rodzina funkcji w języku C służących do podmiany kodu. Jedną z nich jest funkcja  
int execl(const char *path, const char  *arg0, ..., const char *argn, char * 
/*NULL*/); 
 
Oczekiwanie na zakończenie procesów potomnych 
Procesy  macierzysty  i  potomny  powinny  kończyć  swoje  działanie  w  kolejności  odwrotnej  niż  powstawały.,  tzn. 
proces macierzysty nie powinien zakończyć się wcześniej niż jego procesy potomne. Jednakże jest to możliwe. Jeśliby 
się  tak  stało,  PPID  w  procesie  potomnym  utraci  ważność  (nie  ma  już  procesu  o  takim  identyfikatorze).  Istnieje 
niebezpieczeństwo,  że  zwolniony  identyfikator  procesu  zostanie  przydzielony  przez  system  innemu  procesowi. 
Dlatego też „osierocony” proces zostaje przejęty przez proces  init, a jego PPID ustawiony na 1. Sytuacja taka jest 
jednak nienormalna (w niektórych systemach osierocone procesy nie mogą się poprawnie zakończyć i pozostają w 
systemie jako tzw. procesy-duchy, zajmujące niepotrzebnie zasoby komputera). 
 
Należy  zapewnić,  aby  proces  macierzysty  poczekał  na  zakończenie  swoich  procesów  potomnych  i  odebrał  od  nich 
kod  zakończenia  procesu.  W  tym  celu  proces  macierzysty  powinien  wywołać  funkcję  wait  tyle  razy  ile  utworzył 
procesów potomnych. Funkcja ta ma następującą składnię: 
 
#include <sys/types.h> 
#include <sys/wait.h> 
 
int wait(int *stat_loc); 
 
Funkcja  zwraca  identyfikator  zakończonego  procesu  potomnego  lub  -1  jeżeli  wszystkie  procesy  potomne  się  już 
zakończyły.  Jedno  wywołanie  funkcji  wait  oczekuje  tylko  na  zakończenie  jednego  procesu  potomnego.  Jeżeli  nie 
wiemy  ile  mamy  procesów  potomnych,  należy  wykonywać  w  pętli  funkcję  wait,  aż  do  momentu,  kiedy  zwróci 
wartość -1 (nie ma już procesów potomnych). 
W zmiennej wskazywanej przez  stat_loc zapisywana jest liczba szesnastkowa w  postaci: XXYY,  gdzie  XX to kod 
zakończenia procesu potomnego, zaś YY to numer sygnału, który spowodował zakończenie procesu potomnego lub 
0, jeśli proces zakończył się samodzielnie.  
 
WĄTKI 
 
Technicznie,  wątek  to  niezależny  strumień  instrukcji,  który  może  być  wykonywany  jednocześnie  z  innym 
strumieniem instrukcji danego procesu.  Jest to jak gdyby „procedura”, która może zostać wykonana niezależnie od 
głównego części danego procesu. 
 
Czym jest wątek i czym różni się od procesu można zobrazować w następujący sposób: 
 

background image

 

 

Pojedynczy proces (bez  wątków) wykonując swoją  główną cześć kodu (funkcja main) może  wywoływać 
inne  funkcje.  Opuszcza  wówczas  wykonywanie  programu  głównego  i  „przechodzi”  do  wykonania  kodu 
wywołanej  funkcji.  Po  skończeniu  wykonywania  funkcji  wraca  do  wykonywania  głównej  części  kodu. 
Wątek  można  sobie  wyobrazić  jako  wywołanie  funkcji,  przy  czym  program  główny  jest  wykonywany 
dalej,  równolegle  z  wątkiem.  Proces  wywołujący  wątek  i  sam  wątek  (wywołana  funkcja)  wykonują  się 
równocześnie.  
Z  kolei  nowy  proces  (powstały  w  wyniku  rozwidlania  procesów)  jest  natomiast  zupełnie  niezależny  od 
procesu,  który  go  utworzył.  Wszystkie  struktury  danych,  kodu,  rejestry  itp.  ma  niezależne  i  odrębne. 
Stwarza to szereg nowych możliwości, z których najważniejsze jest tworzenie procesów o różnym kodzie 
(dzięki mechanizmowi podmiany kodu).  

 
Jak  już  wspomniano,  wątek  tworzony  jest  wewnątrz  procesu  i  wykorzystuje  jego  zasoby.  Dla  wątku  tworzone  są 
tylko niezbędne struktury takie jak: wskaźnik stosu, rejestry, ustawienia planowania CPU,  zestaw obsługi sygnałów, 
dane specyficzne dla wątku (ID wątki, itd.) Resztę  struktur procesu wątek współdzieli z innymi wątkami/procesem. 
Wątek  istnieje  dopóki  istnieje  proces  go  tworzący  lub  do  momentu  kiedy  sam  się  zakończy  lub  jego  działanie 
zostanie  przerwane  z  zewnątrz  (np.  przez  proces  tworzący  lub  przez  sygnał  wysłany  przez  zupełnie  inny  proces). 
Realizacja  wątków  i  możliwość  programowania  ich  w  języku  C  jest  wspierana  (pod  UNIX-em)  przez  bibliotekę 
pthreads.h, która definiuje około 60 funkcji niezbędnych do tworzenia i obsługi wątków. 
 
Podstawową  motywacją  do  wykorzystania  wątków  jest  zwiększenie  potencjalnej  efektywności  programu.  W 
porównaniu  do  procesów  (aplikacji  wieloprocesowych)  wątki  mają  mniejsze  wymagania  związane  z  ich  obsługą  i 
tworzeniem,  przez  co  są  szybsze  i  wydajniejsze  (korzystają  ze  wspólnych  obszarów  pamięci  współdzielą  część 
zmiennych,  pliki  itp.)  Wątki  mają  zastosowanie  w  tworzeniu  dużych  aplikacji  wielowątkowych,  w  których  wiele 
operacji musi wykonywać się jednocześnie. Aplikacje takie mogą być tworzone również jako wieloprocesowe (przy 
użyciu współpracujących procesów) jednak zakres możliwości jest inny.  
 
Przykładem sytuacji, w której rozwidlania procesów (wieloporcesowości) nie da się zastąpić wielowątkowością  jest 
chociażby  uruchamianie    programów  w  interpretera  poleceń  (shell-a).  Używany  shell  najpierw  wykonuje  funkcję 
fork  (tworzy  swoją  kopię)  a  następnie  podmienia  kod  tej  kopii  na  kod  programu,  który  uruchamiamy.  Bez 
rozwidlania  procesów  byłoby  to  niemożliwe  (wszystkie  programy  musiałyby  być  niejako  z  góry  wbudowane  we 
„wszechpotężny” wielowątkowy system operacyjny). 
 
 
SYGNAŁY 
Sygnał to informacja dla procesu, że wystąpiło jakieś zdarzenie. Sygnały są wysyłane przez:  

 

jądro do procesu, 

 

proces do innego procesu. 

Sygnały są zwykle asynchroniczne tzn. nie da się przewidzieć momentu ich pojawienia się. Proces może w dowolnym 
momencie  otrzymać  sygnał.  Winien  wówczas  przerwać  pracę  i  zareagować  na  otrzymany  sygnał  (wykonać 
odpowiednie operacje). Dlatego też sygnały nazywa się inaczej przerwaniami programowymi
 
W systemie UNIX do każdego typu sygnału  przypisane są określone czynności domyślne, które powinien wykonać 
proces po otrzymaniu danego sygnału. Mogą to być: 

 

zatrzymanie procesu 

 

zakończenie procesu 

 

zakończenie procesu z zapisaniem obrazu pamięci (utworzenie pliku core) 

 

ignorowanie 

 

Sygnał można wysłać: 

 

poleceniem kill 

kill -signal pid 
np.: kill -INT 2367 
Listę sygnałów można wypisać poleceniem kill -l 

 

funkcją kill 

int kill(int pid, int sig); 

 

naciskając klawisz terminala (patrz tabela) 

background image

 

 

 

przez jądro: błędy operacji, adresacji, arytmetyczne, pojawienie się wysokopriorytetowych danych w gnieździe 

itp. 

 
Każdy  proces  może  zawierać  swoje  funkcje  do  obsługi  sygnałów  (nie  będziemy  się  tym  jednak  zajmować).  Proces 
może  w  związku  z  tym  również  ignorować  sygnały.  Istnieją  dwa  sygnały,  tzw.  niezawodne,  które  działają  zawsze: 
SIGKILL i SIGSTOP
 
Przykłady sygnałów: 

 

SIGNAL 

ID 
 

Action  
 

Event 

Command 

 

Key 

SIGHUP 

Exit 

Hangup 

kill -HUP pid 

 

SIGINT 

Exit 

Interrupt 

kill -INT pid 

^C 

SIGQUIT 

Core 

Quit 

kill -QUIT pid 

^\ 

SIGKILL 

Exit 

Killed 

kill -9 pid 

 

SIGPIPE 

13  Exit 

Broken Pipe 

kill -PIPE pid 

 

SIGTERM 

15  Exit 

Terminated 

kill -TERM pid 

 

SIGSTOP 

23  Stop 

Stopped (signal) 

kill -STOP pid 

 

SIGTSTP 

24  Stop 

Stopped (user) 

kill -TSTP pid 

^Z ^Y