Komunikacja między procesami: sygnały
Wprowadzenie
Sygnały są formą komunikacji między procesami w systemie Unix. Sygnał jest to pewien komunikat wysyłany do procesu, oznaczający że proces ma chwilowo przerwać wykonywanie. Pojawia się on asynchronicznie, tzn. w dowolnym momencie w czasie pracy procesu, niezależnie od tego co dany proces akurat robi. Proces nie wie z góry że otrzyma sygnał.
Otrzymanie sygnału zazwyczaj znaczy, że nastąpiło jakieś zdarzenie wyjątkowe, które wymaga od procesu natychmiastowej reakcji. Sygnały są zazwyczaj używane do takich zadań jak kończenie działania procesów, czy informowanie demonów, że mają ponownie odczytać pliki konfiguracyjne.
Proces może otrzymać sygnał od:
jądra - np. w wyniku zdarzenia związanego ze sprzętem
innego procesu - za pomocą funkcji opisanych dalej
użytkownika - użycie polecenia kill, albo naciśnięcie kombinacji klawiszy (np.ctrl-c)
W zależności od rodzaju zdarzenia i żądanej reakcji, wysyłane może być wiele różnych typów sygnałów. Każdy sygnał ma przypisany numer oraz nazwę zaczynającą się od "SIG". Nazwy te są zdefiniowane w pliku <signal.h> (zobacz man 7 signal).
W momencie otrzymania sygnału proces wstrzymuje aktualnie wykonywaną pracę, i reaguje na sygnał na jeden z trzech sposobów (tzw. akcja lub dyspozycja dla sygnału):
przechwycenie sygnału, tzn. wykonanie zdefiniowanej w programie akcji
zignorowanie sygnału
wykonanie akcji domyślnej, skojarzonej z danym typem sygnału - najczęściej zakończenie procesu w pewien sposób
Po obsłużeniu sygnału przerwany proces kontynuuje działanie od miejsca przerwania (o ile nie został zakończony). Dwa sygnały - SIGKILL i SIGSTOP - nie mogą zostać przechwycone ani zignorowane. Po otrzymaniu któregoś z nich proces musi wykonać akcję domyślną. Daje nam to możliwość bezwarunkowego zatrzymania/zakończenia dowolnego procesu, jeżeli zajdzie taka potrzeba. Nie powinno się ignorować również niektórych sygnałów związanych z błędami sprzętowymi (np. dzielenie przez 0, czy niepoprawne odwołanie do pamięci).
Jeśli chcemy przechwytywać jakiś sygnał, musimy zdefiniować funkcję która będzie wykonywana po otrzymaniu takiego sygnału, oraz powiadomić jądro za pomocą funkcji signal(), która to funkcja. Dzięki temu możemy np. sprawić że po naciśnięciu przez użytkownika ctrl+c, nasz program zdąży jeszcze zamknąć połączenia sieciowe czy pliki, usunąć pliki tymczasowe itd.
Rodzaje sygnałów:
(Są to wybrane sygnały, lista wszystkich sygnałów jest nieco dłuższa. Zestawy sygnałów, numery im przypisane i sposób ich obsługi mogą się nieco różnić w różnych wersjach Unixów.)
SIGHUP (1) - zerwanie łączności z terminalem; jest wysyłany wtedy, gdy użytkownik rozłączy się z terminalem z którym związany jest dany proces (np. przez wylogowanie); służy do zakończenia wszystkich programów w momencie zakończenia sesji na terminalu; inny sposób wykorzystania tego sygnału to powiadomienie procesów demonów, że powinny przeładować pliki konfiguracyjne (wybrano ten sygnał, ponieważ rozłączenie z terminalem nie jest istotne dla demonów)
[standardowa akcja: zakończenie procesu]
SIGINT (2) - sygnał przerwania, generowany w momencie naciśnięcia przez użytkownika specjalnej kombinacji klawiszy (zwykle ctrl+c lub delete); służy np. do przerwania pracy procesu który wyprowadza na ekran dużą ilość niepotrzebnych danych
[standardowa akcja: zakończenie procesu]
SIGQUIT (3) - generowany po naciśnięciu klawisza zakończenia, najczęściej ctrl+\.
[standardowa akcja: zakończenie procesu i zrzut core]
(zrzut core oznacza że w bieżącym katalogu jest tworzony plik "core", a w nim zapisywany jest obraz pamięci procesu. Plik ten może być wykorzystany np. przez debuggery)
SIGILL (4) - proces wykonał nieprawidłową instrukcję sprzętową
[standardowa akcja: zakończenie procesu i zrzut core]
SIGTRAP (5), SIGIOT (6), SIGEMT (7) - błąd sprzętowy, zależny od implementacji systemu
[standardowa akcja: zakończenie procesu i zrzut core]
SIGFPE (8) - wyjątek zmiennopozycyjny (np. dzielenie przez zero lub inny błąd operacji arytmetycznej)
[standardowa akcja: zakończenie procesu i zrzut core]
SIGKILL (9) - sygnał bezwarunkowego zakończenia procesu, którego nie da się przechwycić ani zignorować
[standardowa akcja: zakończenie procesu]
SIGBUS (10) - błąd magistrali (błąd sprzętowy)
[standardowa akcja: zakończenie procesu i zrzut core]
SIGSEGV (11) - odwołanie do nieprawidłowego adresu w pamięci (segmentation violation czyli naruszenie zasad segmentacji)
[standardowa akcja: zakończenie procesu i zrzut core]
SIGSYS (12) - próba wykonania nieprawidłowej funkcji systemowej (nieprawidłowe argumenty)
[standardowa akcja: zakończenie procesu i zrzut core]
SIGPIPE (13) - próba zapisu do łącza komunikacyjnego, kiedy proces czytający z tego łącza zakończył już pracę (dotyczy także komunikacji przez gniazda sieciowe)
[standardowa akcja: zakończenie procesu]
SIGALRM (14) - alarm (pobudka); wysyłany po upłynięciu odpowiedniego okresu czasu, ustalonego przez funkcję alarm() ub setitimer()
[standardowa akcja: zakończenie procesu]
SIGTERM (15) - sygnał zakończenia procesu; podobny do SIGKILL, ale można go przechwycić albo zignorować; jest to domyślny sygnał polecenia "kill"
[standardowa akcja: zakończenie procesu]
SIGUSR1 (16), SIGUSR2 (17) - sygnały zdefiniowane przez użytkownika
[standardowa akcja: zakończenie procesu]
SIGCLD / SIGCHLD (18) - zmiana stanu (zakończenie lub zatrzymanie) procesu potomnego
[standardowa akcja: zignorowanie sygnału]
SIGPWR / SIGINFO (19) - niedobór mocy; sygnał jest wysyłany kiedy komputer pracuje na zasilaniu awaryjnym (UPS lub bateria), którego napięcie spadło do krytycznego poziomu i komputer musi zostać zamknięty w ciągu kilkudziesięciu sekund
[standardowa akcja: zakończenie procesu]
SIGSTOP - nakaz wstrzymania pracy procesu; nie może zostać przechwycony ani zignorowany
[standardowa akcja: zatrzymanie procesu]
SIGCONT - nakaz kontunuacji pracy, wysyłany po wstrzymaniu przez SIGSTOP
[standardowa akcja: kontynuacja lub zignorowanie sygnału]
SIGTSTP - sygnał zatrzymania; podobny do SIGSTOP, ale może zostać zignorowany lub przechwycony; można go wysłać z terminalu przez naciśnięcie kombinacji ctrl+z
[standardowa akcja: zatrzymanie procesu]
SIGABRT - awaryjne zakończenie (wysyłany za pomocą funkcji abort())
[standardowa akcja: zakończenie procesu]
Polecenia dotyczące sygnałów:
Z poziomu powłoki do obsługi sygnałów służą polecenia:
trap (wbudowane polecenie shella) - służy do określania reakcji shella na odebranie przez niego poszczególnych sygnałów
kill - wysłanie do procesu lub grupy procesów określonego sygnału; polecenie to jest interfejsem do opisanej niżej funkcji systemowej kill(); można podać numer sygnału lub jego nazwę; sposób interpretacji podanego PID'a przez polecenie jest analogiczne jak w funkcji kill()
Funkcje ANSI C i systemowe dotyczące sygnałów:
int kill(pid_t pid, int signo);
Funkcja służy do wysyłania sygnałów do innych procesów. Signo to numer sygnału, a pid może zostać podany na kilka sposobów:
jeżeli pid > 0, to sygnał jest wysyłany do jednego procesu o podanym identyfikatorze pid
jeżeli pid < 0, to jest wysyłany do grupy procesów o identyfikatorze pgid=-pid. czyli podanie pid -48 spowoduje wysłanie sygnału do wszystkich procesów należących do grupy 48
jeżeli pid == 0, to sygnał jest wysyłany do wszystkich procesów należących do tej samej grupy, co aktualny proces
akcja dla pid == -1 jest różna na różnych systemach, może to być np. wysłanie sygnału do wszystkich procesów należących do tego samego użytkownika co proces wysyłający
Wysłanie sygnału następuje tylko pod warunkiem, że dany proces ma uprawnienia do wysyłania sygnałów podanym innym procesom, oraz że procesy docelowe nie należą do grupy procesów systemowych (najczęściej: proces wymiany (0), init (1) i demon stronicowania (2)). Jeżeli w polu signo podamy 0 (tzw. sygnał pusty), to funkcja kill nie wysyła żadnego sygnału, a tylko sprawdza czy docelowy proces istnieje.
int raise(int signo);
Służy do wysyłania sygnałów przez proces do siebie samego.
void (*signal (int signo, void (*func)(int)))(int);
Funkcja ta służy do ustalenia przechwytywania podanego sygnału signo. W momencie otrzymania przez proces takiego sygnału, o ile jest on przechwytywalny, zostanie wykonana funkcja, do której wskaźnik przekazujemy jako parametr func. Możemy w to miejsce wstawić także stałą SIG_IGN (sygnał będzie wtedy ignorowany) lub SIG_DFL (zostanie wykonana akcja domyślna). Funkcja przechwytująca sygnał musi przyjmować jeden parametr typu int (numer sygnału) i nic nie zwracać.
unsigned int alarm(unsigned int seconds);
Służy do ustawienia licznika czasu na podaną liczbę sekund. Po upłynięciu zadanego czasu, jądro wyśle do procesu sygnał SIGALRM. Może on zostać przechwycony, w przeciwnym wypadku zostanie wykonana akcja domyślna czyli zakończenie. Jeżeli seconds==0, to poprzednio ustawiony licznik jest wyłączany.
int pause(void);
Wykonywanie procesu jest wstrzymywane, do czasu otrzymania jakiegokolwiek sygnału (np. SIGALRM ustawionego wcześniej funkcją alarm()). Może to być wykorzystane np. do wstrzymania procesu na określony czas, jak w funkcji sleep().
void abort(void);
Wywołanie tej funkcji powoduje awaryjne zakończenie procesu. Jest to realizowane przez wysłanie do procesu sygnału SIGABRT, którego nie może on zignorować. Może go przechwycić, ale tylko w celu wykonania tzw. czynności porządkujących, później program i tak zostanie zakończony.
Blokowanie sygnałów:
Może się zdarzyć, że będziemy chcieli poinformować jądro o tym, że nie chcemy żeby przekazywał bezpośrednio do danego procesu niektóre sygnały. Możemy na przykład mieć w kodzie jakiś krytyczny fragment, którego wykonanie nie powinno zostać przerwane przez sygnały. Robi się to przez ustalenie tzw. maski sygnałów. Maska sygnałów jest zbiorem sygnałów, tzn. pewną strukturą w której każdemu sygnałowi jest przypisany jeden bit (prawda lub fałsz). Struktura taka nosi nazwę sigset_t, i najczęściej jest implementowana w postaci czterobajtowej liczby całkowitej.
Do obsługi struktur sigset_t służą następujące funkcje:
int sigemptyset(sigset_t *set); Inicjuje zbiór sygnałów set, wyłączając w nim wszystkie sygnały (czyli ustala wartość reprezentującej je liczby na 0).
int sigfillset(sigset_t *set); Inicjuje zbiór sygnałów, włączając wszystkie sygnały.
Przed użyciem struktury trzeba ją zawsze zainicjwać przez sigemptyset lub sigfillset.
int sigaddset(sigset_t *set, int signo); Dodaje do zbioru sygnałów pojedynczy sygnał.
int sigdelset(sigset_t *set, int signo); Usuwa ze zbioru sygnałów pojedynczy sygnał.
int sigismember(sigset_t *set, int signo); Sprawdza czy podany sygnał jest włączony w danym zbiorze.
Kiedy już ustawimy zbiór sygnałów powyższymi metodami, możemy użyć funkcji sigprocmask():
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
Pierwszy parametr wskazuje sposób uaktualnienia maski sygnałów:
SIG_BLOCK - nowa maska będzie połączeniem starej maski i maski podanej w set (maska podana w set zawiera zbiór sygnałów, które chcemy zablokować)
SIG_UNBLOCK - maska podana w set zawiera zbiór sygnałów, które chcemy odblokować
SIG_SETMASK - stara maska jest nadpisywana maską zawartą w set
Do parametru oset zostanie wpisana zawartość starej maski.
Kiedy jądro próbuje przekazać do procesu sygnał, a ten aktualnie jest zablokowany (nie dotyczy sygnałów SIGKILL i SIGSTOP - tych nie można blokować), to sygnał jest przechowywany do czasu, kiedy proces albo ustawi akcję dla tego typu sygnału na ignorowanie, albo odblokuje dany sygnał (w tym przypadku sygnał jest mu przekazywany w momencie odblokowania).
int sigpending(sigset_t *set);
Za pomocą tej funkcji możemy z wnętrza procesu odczytać listę sygnałów, które oczekują na odblokowanie przez niego. Do zmiennej set jest wpisywany zbiór oczekujących sygnałów, które można sprawdzić funkcją sigismember().
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
Struktura sigaction jest zdefiniowana następująco:
struct sigaction
{
void (*sa_handler)();
sigset_t sa_mask;
int sa_flags;
};
Jest to rozszerzona wersja funkcji signal, służąca do tego samego czyli do zmiany dyspozycji sygnału. Zmienna sa_handler zawiera wskaźnik do funkcji obsługi sygnału. sa_mask zawiera zbiór sygnałów, które mają być zablokowane na czas wykonania tej funkcji. W ten sposób możemy się zabezpieczyć przed odebraniem jakiegoś sygnału (i w konsekwencji wykonaniem jego funkcji) w czasie, kiedy jeszcze wykonuje się funkcja obsługująca inny sygnał. W szczególności drugie obsłużenie tego samego sygnału podczas obsługiwania pierwszego jego egzemplarza jest zawsze blokowane.
int sigsuspend(const sigset_t *sigmask);
Funkcja ta działa następująco: podmienia tymczasowo maskę sygnałów procesu na tą wskazaną w sigmask, a następnie wstrzymuje wykonanie programu do czasu otrzymania jakiegoś sygnału. Przydaje się to np. w sytuacji kiedy chcielibyśmy wykonać funkcję pause() aby zaczekać na jakiś sygnał, a jest on w danej chwili zablokowany.