10. PROSTE MECHANIZMY KOORDYNACJI DOSTĘPNE W JĘZYKU C
W systemie Unix użytkownikowi (nie będącemu administratorem) nie wolno
wykonywać
bezpośrednio żadnych operacji na zasobach. Operacje te wykonywane są przez jądro
systemu na
zlecenie użytkownika wydane poprzez wywołanie funkcji systemowej. Użytkownik
dostrzega
system poprzez zbiór funkcji systemowych i struktur logicznych, na których one
operują, jako
tak zwaną maszynę wirtualną (niezależną od szczegółów realizacji sprzętowej).
Funkcje systemowe są dostępne dla użytkownika za pośrednictwem języka, w którym
użytkownik
porozumiewa się z systemem (na przykład języka komend shella lub języka
programowania C).
W każdym języku funkcje są obudowane w pewien interfejs (nazwa, postać
parametrów itd.).
Uwaga.
Wiele komend shella i funkcji dostępnych w języku C wywołuje nie pojedyncze funkcje
systemowe,
ale stanowi podprogramy wywołujące wiele różnych funkcji systemowych.
W języku C każda funkcja jest scharakteryzowana przez:
- nazwę;
- liczbę, kolejność i typy argumentów;
- typ wyniku;
- specyfikację efektu wykonania funkcji;
- wykaz możliwych sytuacji błędnych i odpowiadających im wartości zmiennej
globalnej errno.
Dla niektórych funkcji argumenty mogą tworzyć listę o niezdeterminowanej
długości (zakończoną
znakiem pustym).
Jedyną funkcją systemową, dla której nie są przewidziane żadne sytuacje
błędne, jest funkcja
exit (powodująca zakończenie procesu).
Uwaga.
W profesjonalnych programach wszystkie wywołania jakichkolwiek funkcji,
które mogą zwrócić
kod błędu, powinny być testowane pod kątem takiej możliwości !
Funkcje operujące na identyfikatorach.
Każdy proces poza procesem o numerze 0 powstaje wskutek utworzenia przez
inny proces. Numery
procesów są liczbami naturalnymi przydzielanymi rosnąco modulo rozmiar
tablicy procesów (zwykle
32 K) z pominięciem numerów aktualnie używanych. Każdy proces pamięta swój
PID i PPID, ale
nie zapamiętuje w sposób automatyczny identyfikatorów tworzonych potomków
(programista może
spowodować przechowywanie ich w zmiennych). Jeśli proces kończy działanie
wcześniej, niż jego
(niektóre) procesy potomne, wszystkie procesy potomne otrzymują PPID=1 (jest
to PID procesu Init)
i kontynuują działanie.
int getpid(void); - zwraca PID procesu
int getppid(void); - zwraca PPID procesu
int getpgrp(void); - zwraca PGRP procesu
int setpgrp(void); - odłącza proces od dotychczasowej grupy i
ustanawia go przywódcą
nowej grupy (PGRP = PID)
Uwaga. Istnieją też odpowiednie funkcje dla identyfikatorów użytkowników i ich
grup.
Funkcje związane z tworzeniem i kończeniem procesów.
Tworzenie nowego procesu:
int fork(void); zwraca -1 w przypadku niepowodzenia (na przykład brak
zasobów)
zwraca 0 utworzonemu procesowi potomnemu
zwraca PID utworzonego potomka procesowi
rodzicielskiemu
Wykonanie funkcji fork przez jądro systemu wiąże się z szeregiem
skomplikowanych czynności
(przydział zasobów, wpisanie do tablicy procesów, kopiowanie środowiska itp.) i
jest czasochłonne.
Segment instrukcji nie jest kopiowany, segment danych jest zwykle kopiowany
dopiero w przypadku
próby dokonania zapisu przez nowy proces.
Zamiana kontekstu procesu:
Funkcja systemowa exec ma sześć interfejsów w języku C (różniących się
sposobem przekazywania
parametrów i zmiennych środowiska). Jej zadaniem jest zamiana kontekstu
procesu (przy zachowaniu
tożsamości procesu), to jest spowodowanie, żeby proces zaczął wykonywać inny
program.
int execl (char ścieżka, char arg0, char arg1, ... , char argn, NULL);
ścieżka - pełna nazwa ścieżkowa pliku z nowym programem;
arg0 - powtórzona sama nazwa pliku z nowym programem;
arg1 ... argn - lista parametrów dla nowego programu zakończona znakiem
pustym (NULL).
int execv (char ścieżka, char argv [ ] );
int execle (...);
int execve (...);
int execlp (...);
int execvp (...);
Funkcje fork i exec zazwyczaj współpracują ze sobą.
Kończenie wykonywania procesu:
void exit (int kod);
Kończy działanie procesu, wysyła sygnał do procesu rodzicielskiego oraz
jednobajtowy kod wyjścia.
Oczekiwanie na zakończenie działania potomka:
int wait (int wsk);
Zawiesza proces w oczekiwaniu na zakończenie któregokolwiek procesu
potomnego. Zwraca PID
zakończonego potomka lub -1 w przypadku błędu. wsk zwraca dwa bajty:
- jeśli prawy bajt ma wartość 0, to lewy bajt zwraca kod wyjścia potomka;
- jeśli prawy bajt ma wartość niezerową, to określa, jaki sygnał spowodował
zakończenie potomka,
oraz czy nastąpił zrzut pamięci do pliku core.
Uwaga. Obecnie istnieje też funkcja pozwalająca czekać na zakończenie
określonego potomka.
Funkcje związane z operowaniem na sygnałach.
Wysłanie sygnału:
int kill (int pid, int sig);
Umożliwia wysłanie określonego sygnału do określonego procesu / grupy
procesów.
Przechwycenie sygnału:
void (signal (int sig, void (func) (int))) (int);
Umożliwia przechwycenie określonego sygnału (jeśli to możliwe) i wykonanie
wskazanej funkcji
obsługi.
Polecenia shella kill i trap są obudowami funkcji systemowych kill i signal.
Funkcje związane z operowaniem na łączach nienazwanych.
Pierwotnie łącza nienazwane mogły być używane jedynie jako jednokierunkowe:
P Q
zapis
odczyt
kolejka prosta
Funkcja tworząca łącze:
int pipe (int fd [2] );
fd [0] - deskryptor pliku służący do odczytu z łącza
fd [1] - deskryptor pliku służący do zapisu do łącza
Do zapisów / odczytów stosujemy funkcje systemowe write i read (są
wykonywane niepodzielnie).
Łącze ma pojemność zależną od ustawień systemowych (co najmniej pół KB,
zazwyczaj 4 KB).
Zazwyczaj bezpośrednio po wywołaniu funkcji pipe wywoływana jest funkcja
fork (proces potomny
dziedziczy deskryptory plików), a następnie, w zależności od zamierzonego
kierunku przesyłania,
zamykane są niepotrzebne deskryptory (po jednym w każdym procesie).
...
pipe (fd);
if (fork ( ) = = 0) fd [1] fd
[1]
{
close (fd [0] );
...
} fd [0] łącze fd
[0]
else
{ proces
proces
close (fd [1] ); potomny
rodzicielski
...
}
Główną wadą łącz nienazwanych jest to, że mogą łączyć tylko procesy
spokrewnione (zazwyczaj
pary rodzic - potomek, ale mogą też być dziadek - wnuk, dwóch potomków itp.).
W nowszych wersjach Unixa łącza są implementowane jako dwukierunkowe
(full
duplex)
.
W starszych mogły być tylko jednokierunkowe
(half-duplex)
- chcąc uzyskać
łączność
dwukierunkową należało skorzystać z dwóch par deskryptorów i dwukrotnie
wywołać funkcję pipe.
Uwaga.
1) W przypadku próby odczytu z pustego łącza lub próby zapisu do pełnego łącza
procesy są czasowo
zawieszane.
2) W przypadku łącz dwukierunkowych może być potrzebna synchronizacja
operacji zapisu i odczytu
po obu stronach łącza (na przykład za pomocą semaforów).
3) Na zakończenie działania programu należy pozamykać wszystkie otwarte
deskryptory.
Funkcje związane z operowaniem na łączach nazwanych (FIFO).
Łącza nazwane są uwidoczniane w systemie plików jako specjalny rodzaj plików o
zerowym
rozmiarze. Mogą być tworzone i usuwane zarówno w programach, jak i przy użyciu
komend shella.
Z łączami nazwanymi mogą współpracować dowolne procesy (niekoniecznie
spokrewnione), które
posiadają odpowiednie prawa dostępu.
Funkcja tworząca kolejkę FIFO:
int mknod (const char ścieżka, int tryb);
ścieżka - pełna nazwa ścieżkowa kolejki FIFO
tryb - słowo trybu, którego bity informują między innymi o prawach dostępu do
kolejki
Przed użyciem łącze nazwane musi być otwarte (open), a przed zakończeniem
wykonywania programu
zamknięte (close) przez każdy proces współpracujący z łączem. Jest wymuszona
synchronizacja
otwarcia łącza do zapisu i otwarcia łącza do odczytu przez dwa procesy chcące
korzystać z łącza.
Samo korzystanie z łącza wygląda podobnie, jak w przypadku łącz nienazwanych
(funkcje write i read).