Obsługa sygnałów zgodna ze
standardem Posix cz.2
Blokowanie sygnałów
Może się zdarzyć, że będziemy chcieli poinformować jądro o
tym, że nie chcemy żeby niektóre sygnały przekazywane były
bezpośrednio do danego procesu.
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.
Funkcje obsługi struktury
sigset_t
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.
Funkcje obsługi struktury
sigset_t
Kiedy jest już ustawiony zbiór sygnałów powyższymi
metodami, można 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.
Funkcje obsługi struktury
sigset_t
uwaga
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).
Funkcje obsługi struktury
sigset_t
int sigpending(sigset_t *set);
Za pomocą tej funkcji można 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().
Funkcja Sigaction
int sigaction(int signo, const struct sigaction *act, struct sigaction
*oact);
Pierwszy argument, signum, to numer sygnału, którego nadejście spowoduje
wywołanie procedury obsługi, określonej w drugim argumencie. Można podać
dowolny numer, za wyjątkiem SIGKILL i SIGSTOP, ponieważ obsługi tych
sygnałów nie da się przechwycić.
Numeracja sygnałów nie musi być przenośna, dlatego zamiast wartości
liczbowych zaleca się skorzystanie z symboli SIG* zdefiniowanych w
<signal.h>.
Drugi i trzeci argument wywołania, to wskaźniki do struktury sigaction,
określającej sposób obsługi danego sygnału. Drugi argument określa żądany
nowy sposób obsługi, za pomocą trzeciego funkcja zwraca informację o
dotychczasowym sposobie obsługi.
Wartość zwracana
Funkcja sigaction zwraca 0 z przypadku sukcesu i -1 w razie niepowodzenia. W tym
drugim przypadku wartość errno ustawiana jest na jedną z: EINVAL, EFAULT albo EINTR.
Funkcja Sigaction
W obu przypadkach wolno podać wskaźnik NULL zamiast argumentu, np. w celu
ustawienia nowej procedury obsługi sygnału SIGINT bez pobierania informacji o
dotychczasowej należy użyć:
#include <signal.h>
struct sigaction sa_new;
/* ... */
sigaction(SIGINT, &sa_new, 0);
a w celu pobrania informacji o dotychczasowej obsłudze sygnału (np. w celu jej
modyfikacji lub dla późniejszego odtworzenia) bez ustanawiania nowej:
#include <signal.h>
struct sigaction sa_old;
sigaction(SIGINT, 0, &sa_old);
Podanie obu wskaźników jako NULL jest również dozwolone (i sensowne), pozwala
bowiem sprawdzić, czy dany numer sygnału jest wspierany w danym systemie.
Funkcja Sigaction
Funkcja sigaction posługuje się strukturą sigaction do pobierania i
odsyłania informacji o handlerze obsługi sygnału. Struktura ta była w
przeszłości kilkakrotnie zmieniana, jednak podstawowe składowe
pozostały takie same.
struct sigaction
{
void (*sa_handler)();
sigset_t sa_mask;
int sa_flags;
};
void (*sa_handler)(int);
W składowej sa_handler należy umieścić adres funkcji, która zostanie wywołana
po nadejściu sygnału. Można również nadać jej jedną z dwóch wartości
predefiniowanych: SIG_DFT, co powoduje przywrócenie domyślnego sposobu
obsługi sygnału albo SIG_IGN, co spowoduje zignorowanie sygnału.
Funkcja Sigaction
void (*sa_sigaction)(int, siginfo_t *, void *);
Zamiast sa_handler można użyć składowej sa_sigaction, podając
równocześnie flagę SA_SIGINFO w składowej sa_flags. Wówczas po
nadejściu sygnału do handlera obsługi zostaną przekazane dodatkowe
informacje o przyczynach wystąpienia sygnału. Nie można używać
obu tych składowych równocześnie (w niektórych implementacjach
mogą być one zadeklarowane jako unia).
sigset_t sa_mask;
Składowa sa_mask zawiera informacje o sygnałach, których obsługa
zostanie na czas wykonania handlera wstrzymana (wskazane sygnały
będą blokowane). Do manipulacji tą składową służą osobne funkcje.
int sa_flags;
Składowa sa_flags zawiera dodatkowe flagi. Flagi te w większości są
specyficzne dla danego systemu operacyjnego, a standardowe to:
SA_NOCLDSTOP (zdefiniowana w POSIX) oraz wspomniana już
SA_SIGINFO (dodana w POSIX.1b).
Funkcja Sigaction
Składowa sa_mask
Składowa ta zawiera zestaw sygnałów, których obsługa będzie
zawieszona na czas wykonywania przez proces procedury obsługi
sygnału. Do manipulacji zestawami sygnałów służą funkcje:
int sigemptyset(sigset_t *set);
Wypełnia set tak, by wszystkie sygnały były z niego wyłączone.
int sigfillset(sigset_t *set);
Wypełnia set tak, by wszystkie sygnały były w nim zawarte.
int sigaddset(sigset_t *set, int signum);
Dodaje sygnał signum do zestawu.
int sigdelset(sigset_t *set, int signum);
Usuwa sygnał signum z zestawu.
Funkcja Sigaction
Włączenie blokowania wszystkich sygnałów (zazwyczaj tego właśnie
chcemy):
#include <signal.h>
struct sigaction sa;
sigfillset(&(sa.sa_mask));
/* ... */
sigaction(SIGINT, &sa_new, 0);
Włączenie blokowania wszystkich sygnałów oprócz SIGTSTP:
#include <signal.h>
struct sigaction sa;
sigfillset(&(sa.sa_mask));
/* pełny zestaw sygnałów */
sigdelset(&(sa.sa_mask), SIGTSTP);
/* wyklucz sygnał z zestawu */
/* ... */
sigaction(SIGINT, &sa_new, 0);
Funkcja Sigaction
Włączenie blokowania tylko SIGTSTP i SIGALRM:
#include <signal.h>
struct sigaction sa;
sigemptyset(&(sa.sa_mask));
/* pusty zestaw sygnałów */
sigaddset(&(sa.sa_mask), SIGTSTP);
/* dodaj SIGTSTP */
sigaddset(&(sa.sa_mask), SIGALRM);
/* dodaj SIGALRM */
/* ... */
sigaction(SIGINT, &sa_new, 0);Włączenie blokowania wszystkich sygnałów
oprócz SIGTSTP:
Należy podkreslić , że sygnał, który przechwytujemy, jest „z urzędu” blokowany na
czas obsługi.
Funkcja Sigaction
Składowa sa_flags
W pełni standardowa i przenośna jest tylko flaga SA_NOCLDSTOP.
Działanie kilku wybranych flag, które wydają się przydatne:
SA_ONESHOT lub SA_RESETHAND
Przywraca domyślną obsługę sygnałów po wywołaniu procedury obsługi. Historycznie
rzecz biorąc, było to typowe zachowanie systemów UNIX po zainstalowaniu handlera
za pomocą funkcji signal i źródło nieustannych kłopotów.
SA_RESTART
Powoduje zmianę zachowania przerwanych nadejściem sygnału niektórych wywołań
funkcji systemowych: zamiast zwracać błąd i ustawiać errno na wartość EINTR,
funkcje te będą wywołane ponownie. Praktyczne (w sposób typowy dla BSD, skąd
zaczerpnięto koncept) rozwiązanie dla takich operacji jak np. read.
SA_NOMASK lub SA_NODEFER
Nie blokuj sygnałów podczas działania handlera (jak przy signal).
SA_SIGINFO
Użyj adresu podanego w składowej sa_sigaction, a nie sa_handler do zainstalowania
handlera.
Funkcja Sigaction
Składowa sa_flags powinna być sumą bitową ustawionych flag. Jeśli
nie ustawiamy żadnej, sa_flags trzeba koniecznie wyzerować:
#include <signal.h>
struct sigaction sa;
sigfillset(&(sa.sa_mask));
sa.sa_flags = 0;
/* ... */
sigaction(SIGINT, &sa_new, 0);
Przykłady cz.1
obsługa sygnału SIGINT
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void int_handler(int signum)
{
fprintf(stderr, "-- Signal %i catched\n", signum);
}
int main(void)
{
int i;
struct sigaction sa;
sa.sa_handler = int_handler;
sigfillset(&(sa.sa_mask));
sa.sa_flags = 0;
sigaction(SIGINT, &sa, 0);
for(i = 0; i < 20; ++i) {
printf("Running (%i)\n", i);
sleep(1);
}
return 0;
}
Przykłady cz.1
sa.sa_handler = int_handler;
ustala nazwę (w istocie adres) funkcji wywoływanej, gdy przyjdzie
sygnał. Wywołanie
sigfillset(&(sa.sa_mask));
ustawia blokowanie wszystkich sygnałów na czas wykonywania
handlera. Następnie zerowane są flagi:
sa.sa_flags = 0;
i handler zostaje zainstalowany:
sigaction(SIGINT, &sa, 0);
Program powyższy po uruchomieniu co sekundę wyświetla komunikat
Running[…]. Po wysłaniu SIGINT (zazwyczaj jest to kombinacja
klawiszy Ctrl+C) sterowanie trafia do funkcji obsługi sygnału, która
wypisuje na stderr komunikat -- Signal 2 catched
W tym przykładzie raz zainstalowany handler działa aż do zakończenia
procesu, co łatwo sprawdzić w praktyce, naciskając kilkakrotnie
Ctrl+C.
Przykłady cz.2
flaga SA_ONESHOT
W poprzednim przykładzie wprowadzono jedną tylko, drobną zmianę.
Instrukcję:
sa.sa_flags = 0;
zastępiono przez:
sa.sa_flags = SA_ONESHOT;
Tak zainstalowany handler obsługi sygnału zadziała jeden raz, po
czym zostanie przywrócona obsługa domyślna. W taki sposób działały
handlery sygnałów instalowane za pomocą funkcji signal
Przykłady cz.3
przywracanie obsługi
Do przykładu cz. 2 wprowadzamy kolejną zmianę: w funkcji
int_handler będziemy przywracali obsługę sygnału za pomocą
handlera.
Struktura sigaction zostanie zmienną globalną, żeby handler mógł
skorzystać z wartości ustawionych w funkcji main:
Przykłady cz.3
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
struct sigaction sa;
void int_handler(int signum)
{
sigaction(SIGINT, &sa, 0);
fprintf(stderr, "-- Signal %i catched\n", signum);
}
int main(void)
{
int i;
sa.sa_handler = int_handler;
sigfillset(&(sa.sa_mask));
sa.sa_flags = SA_ONESHOT;
sigaction(SIGINT, &sa, 0);
for(i = 0; i < 20; ++i) {
printf("Running (%i)\n", i);
sleep(1);
}
return 0;
}
Przykłady cz.3
Kod ten jest podobny do tego, który należało dawniej zastosować, by
uzyskać wielokrotne użycie handlera zainstalowanego za pomocą
funkcji signal. Jest to jednak rozwiązanie niepewne: pomiędzy
wywołaniem handlera i przywróceniem obsługi sygnału istnieje krótki
odcinek czasu, kiedy sygnał obsługiwany jest w sposób domyślny.
Przykłady cz.4
funkcja raise
Funkcja raise, to po prostu okrojona wersja funkcji kill, wysyłająca
sygnał do tego samego procesu, który ją uruchomił. Po co wysyłać
sygnał do samego siebie?
Załóżmy, że program otrzymujący sygnał przerwania SIGINT musi
najpierw posprzątać, na przykład usunąć utworzone pliki
tymczasowe. W takim przypadku najlepiej przechwycić sygnał, zrobić
porządki, a następnie (przy przywróconej domyślnej obsłudze
sygnału) ponownie wysłać go do siebie.
Przykłady cz.4
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void int_handler(int signum)
{
fprintf(stderr, "Final cleanup.\n");
raise(SIGINT);
fprintf(stderr, "Preparing to die...\n");
}
int main(void)
{
int i;
struct sigaction sa;
sa.sa_handler = int_handler;
sigfillset(&(sa.sa_mask));
sa.sa_flags = SA_ONESHOT;
sigaction(SIGINT, &sa, 0);
for(i = 0; i < 20; ++i) {
printf("Running (%i)\n", i);
sleep(1);
}
return 0;
}
Funkcja sigsuspend
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.
Uwagi dotyczące własnej obsługi
sygnałów
Dziedziczenie obsługi sygnałów
Proces potomny, utworzony za pomocą exec dziedziczy obsługę sygnałów,
wszelako z pewnymi zmianami:
Obsługa sygnałów SIG_DFL i SIG_IGN dziedziczona jest bez zmian,
Obsługa sygnałów, dla których zainstalowane zostały handlery zmieniana jest
na SIG_DFL. Jest to konieczne, ponieważ po wykonaniu exec handlerów już
nie ma w pamięci.
Ograniczenia procedur obsługi sygnałów
W procedurze obsługującej sygnały nie wolno używać funkcji bibliotecznych,
które zmieniają wartości zmiennych globalnych lub statycznych. Ogólnie,
pamięci innej, niż na stosie. Zatem funkcji oznaczonych w angielskojęzycznej
dokumentacji jako non-reentrant należy używać z rozwagą.
Ze zmienną errno akurat łatwo sobie poradzić, zachowując jej wartość przed
wywołaniem funkcji bibliotecznej, a następnie ją odtwarzając. Ogólnie, jeśli
jakaś funkcja zmienia zmienną globalną, do której mamy dostęp, można
zapobiec kłopotom, tworząc kopię tej zmiennej w celu odtworzenia wartości
po wywołaniu.
Uwagi dotyczące własnej obsługi
sygnałów
Samo tylko odczytywanie wartości zmiennych globalnych programu
jest bezpieczne, jednak jeśli taka zmienna nie jest atomic, można
trafić z odczytem akurat na moment jej modyfikacji i otrzymać
niepoprawną wartość.
Natomiast użycie takich funkcji jak malloc, które utrzymują gdzieś
własne dane, nie jest na ogół bezpieczne. Z malloc mogą z kolei
korzystać niejawnie rozmaite funkcje biblioteczne (jak choćby użyta
w przykładach funkcja printf).
Podobnie, korzystanie w handlerze sygnałów z tych samych
deskryptorów plików, których używa główny program nie jest
wskazane. (W przykładach częściowo zapobiegłem kłopotom,
używając stderr w obsłudze sygnałów i stdout w programie głównym,
jednak skierowania strumieni łatwo mogą zostać zmienione z
zewnątrz.)
I tak dalej. Sprawdź w dokumentacji, które funkcje biblioteczne są
reentrant. Sprawdź też napisane przez siebie funkcje pod tym kątem.
Z drugiej strony, jeśli program główny nie używa w ogóle jakiejś
funkcji (albo używa jej wyłącznie w sytucjach, gdy sygnały sa
ignorowane), można jej użyć w procedurze obsługi sygnałów
Uwagi dotyczące własnej obsługi
sygnałów
Ogólne zasady tworzenia handlerów
Nie są to niepodważalne prawa, a bardziej zalecenia, którymi należy się
kierować:
O ile to możliwe, procedura obsługi sygnału powinna tylko ustawiać flagę
informującą o nadejściu sygnału i powracać, a program główny powinien
okresowo sprawdzać stan tej flagi. Zmienna pełniąca funkcję flagi
powinna mieć atrybut volatile.
Jeśli trzeba wywołać jakąś funkcję, powinna to być funkcja reentrant. W
przeciwnym przypadku trzeba zapewnić, że wykonanie tej funkcji w
programie głównym nie może zostać przerwane nadejściem sygnału.
Wykonanie procedury obsługi sygnału powinno trwać krótko, ponieważ
zazwyczaj na ten czas obsługa nadchodzących sygnałów jest
zawieszona.