Obsługa
sygnałów
Tomasz BorzyszkowskiWprowadzenie
2 Zaawansowane systemy operacyjne często realizując duże zadania, wykorzystują do ich realizacji wiele współdziałających ze sobą
programów/procesów. Do dobrego współdziałania procesy te muszą komunikować się ze sobą. W systemach Unixowych istnieje wiele mechanizmów komunikacji międzyprocesowej. Najczęściej używane to: sygnały, potoki i kolejki FIFO. Na bieżącym wykładzie
przyjrzymy się bliżej sygnałom.
Sygnały dostarczają prostej metody przekazywania przerwań
programowych do procesu Unixowego. Z powodu swojej natury,
sygnały są używane raczej do obsługi nietypowych sytuacji, a nie do prostego przesyłania danych pomiędzy procesami. Proces może zrobić z sygnałem następujące rzeczy:
Wybrać sposób reakcji po otrzymaniu sygnału (obsługa sygnału)
Blokować sygnał, tj. pozostawić go na później; dotyczy krytycznych
fragmentów kodu
Nazwy sygnałów
3
Sygnały nie mogą bezpośrednio przenosić informacji, co ogranicza ich użyteczność jako ogólnego mechanizmu komunikacji między
procesami. Jadnak każdy typ sygnału posiada nadaną mnemoniczną nazwę, wskazującą cel w jakim sygnał jest zwykle używany. Nazwy sygnałów są zdefiniowane w pliku nagłówkowym <signal.h>.
Większość sygnałów Unixowych jest przeznaczona do użycia przez jądro, chociaż istnieje również kilka do przesyłania między zwykłymi procesami. Oto kilka wybranych, najczęściej używanych sygnałów:
SIGABRT sygnał przerwania procesu wysyłany przez bieżący proces
za pomocą funkcji abort. Obsługa tego syganłu powinna się
zakończyć tzw. zakończeniem anormalnym. W rzeczywistości następuje zrzut rdzenia (core dump) do pliku w celu dalszej analizy.
SIGALRM tzw. zegar alarmu. Wysyłany do procesu przez jądro, gdy
upłynie ustalony czas. Wszystkie procesy mają w sumie do dyspozycji trzy czasomierze.Czasomierz jest ustawiany przez proces za pomocą funkcji alarm.
4
cd
Nazwy sygnałów
SIGCHLD proces potomny zakończony lub zatrzymany. Zawsze, gdy
proces potomny kończy się lub zatrzymuje, jądro zawiadamia o tym jego proces rodzicielski, wysyłając mu ten sygnał. Domyślnie proces rodzicielski ignoruje sygnał, więc jeżeli chce wiedzieć o
każdym ukończonym procesie, musi jawnie przechwytywać sygnał.
SIGCONT kontynuuj proces, jeżeli był zatrzymany. Jest to sygnał
sterowania pracą, który powinien wznowić proces, jeżeli został
zatrzymany. W przeciwnym przypadku proces powiniem ignorować sygnał. Stanowi on przeciwieństwo sygnału SIGSTOP.
SIGFPE wyjątek zmiennoprzecinkowy. Wysyłany przez jądro, gdy
wystapi błąd obliczeń zmiennoprzecinkowych, np. nadmiar lub niedomiar. Powoduje anormalne zakończenie.
SIGINT przerwanie. Wysyłany przez jądro do wszystkich procesów
powiązanych z sesją terminala, gdy użytkownik wciśnie klawisz przerwania (Ctrl-c). Jest to powszechnie stosowany sposób
5
cd
II
Nazwy sygnałów
SIGHUP sygnał zawieszenia. Wysyłany przez jądro do wszystkich
procesów powiązanych z terminalem sterujacym, jeśli zostaje on odłączony. Wysłany także do wszystkich członków sesji, gdy kończy się proces wiodący sesji, którym zwykle jest proces powłoki, pod warunkiem, że sesja jest powiązana z terminalem sterującym.
Dzięki temu, po wylogowaniu użytkownika, jego procesy dziłające w tle są kończone (o ile nie wyłączono obsługi tego sygnału).
SIGILL nielegalna instrukcja. Wysyłany przez system, gdy proces
próbuje wykonać nielegalna instrukcję. Staje się to możliwe, gdy program uszkodzi swój własny kod lub próbuje wykonać instrukcję zmiennoprzecinkową bez odpowiedniego wsparcia sprzętowego. Sygnał powoduje anormalne zakończenie procesu.
6
cd
III
Nazwy sygnałów
SIGKILL usunięcie. Specjalny sygnał wysyłany do jednego procesu
przez inny, aby usunąć odbiorcę. Jest on także czasem wysyłany przez system, np. podczas zamykania systemu. Jest to jeden z dwóch sygnałów, które nie mogą być zignorowane lub
przechwycone, tj. obsłużone przez procedurę zdefinowaną przez użytkownika.
SIGSTOP zatrzymanie wykonania. Jest to sygnał kontroli zdarzeń,
który zatrzymuje proces. Podobnie jak SIGKILL, nie może zostać
przechwycony lub zignorowany.
SIGTERM programowy sygnał zakończenia. Zwyczajowo jest
używany do kończenia procesu. Programista może użyć tego
sygnału, aby dać procesowi trochę czasu na działania porządkujące przed wysłaniem sygnału SIGKILL.
SIGUSR1 i SIGUSR2 podobnie jak SIGTERM, sygnały te nigdy nie
są wysyłane przez jądro. Mogą być wykorzystane przez użytkownika w dowolnym celu.
7
Obsługa sygnałów
Po otrzymaniu sygnału proces ma do wyboru jeden z trzech sposobów działania:
Podejmij działania domyślne, stosowne do otrzymanego sygnału
Zignoruj sygnał całkowicie i kontynuuj przetwarzanie
Podejmij działania zdefiniowane przez użytkownika
W starszych wersjach Unixa obsługa sygnałów była względnie prosta, choć czasem zawodziła. Nowe procedury, które przedstawimy, są bardziej niezawodne i jednocześnie bardziej złożone.
Jednym z głównych parametrów przekazywanych do funkcji
systemowych obsługujących sygnały są tzw. zestawy sygnałów. Określają one listy sygnałów, z którymi chcemy coś zrobić.
Zestawy sygnałów są definiowane za pomocą typu sigset_t,
zdefiniowanego w pliku nagłówkowym <signal.h>. Typ ten jest
wystarczająco pojemny, aby zapamiętać reprezentację wszystkich zdefiniowanych w systemie sygnałów.
8
Zestawy sygnałów
implementacja
Użytkownik może definiować zestawy potrzebnych mu sygnałów za pomocą następujących funkcji:
int sigemptyset(sigset_t *set); int sigfillset (sigset_t *set);
Pierwsza z funkcji inicjuje zestaw funkcji, wskazywany przez set
tak, że wszystkie sygnały są wyłączone, natomiast druga inicjuje zestaw funkcji, wskazywany przez set tak, że wszystkie sygnały są
włączone.
Kolejne funkcje to:
int sigaddset(sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo);
Funkcje te, odpowiednio, dodają do i usuwają z zestawu sygnałów wskazywajego przez set, sygnał signo. Zalecane jest by parametr signo był nazwą sygnału, taką jak np. SIGINT. Podanie
rzeczywistego numeru sygnału jest możliwe ale niezalecane ze względu na przenośność kodu.
9
Ustalanie działania sygnału
Po zdefiniowaniu zestawu sygnałów można wybrać konkretną metodę obsługi sygnałów używając funkcji:
int sigaction(int signo,
const struct sigaction *act, const struct sigaction *oact);
Parametr signo identyfikuje sygnał, dla którego chcemy określić
działanie. Może to być dowolny sygnał, z wyjątkiem SIGSTOP i
SIGKILL. Drugi parametr określa działanie, jakie chcemy ustawić dla signo. Trzeci parametr jest ustawiany na NULL lub na aktualne
ustawienia.
Struktura sigaction składa się z następujących pól:
void (* sa_handler)(int) identyfikuje działanie, jakie ma
być podjęte po otrzymaniu sygnału. Może przyjmować jedną z następujących wartości: SIG_DFL domyślne działanie systemu, SIG_IGN zignoruj ten sygnał (nie dla SIGSTOP i SIGKILL) albo
adres funkcji wywoływanej po otrzymaniu sygnału. Sygnał zostanie przekazany do funkcji jako jej argument.
10
Struktura
sigaction
sigset_t sa_mask maska sygnałów, które powinny być
blokowane podczas wywoływania funkcji sa_handler. Dodatkowo,
sygnał, który wywołał funkcję będzie zablokowany (dodany do
sa_mask), chyba że użyto flagę SA_NODEFER lub SA_NOMASK.
Blokowanie przekształca sygnały w bardziej, ale nie całkowicie, godny zaufania mechanizm komunikacji.
int sa_flags zbiór flag, które modyfikują zachowanie procesu
obsługi sygnałów. Jest to zbiór wartości połączonych bitowym OR. Zobacz man sigaction.
Struktura sigaction posiada również wskaźnik do dodatkowej funkcji obsługi. Standard POSIX nie przewiduje jeszcze jego wykorzystania. Powyższy opis przedstawia tylko wybrane definicje zachowań jakie może podjąć program po otrzymaniu sygnału (patrz dokumnetacja systemowa). Poniższe programy ilustrują typowe wykorzystanie przechwytywania sygnałów. Zobacz: sygnaly1.c
sygnaly2.c sygnaly3.c
11
Sygnały a funkcje systemowe
W większości przypadków, jeżeli do procesu wysyłany jest sygnał w chwili, gdy wykonuje on funkcję systemową, sygnał nie odnosi
żadnego skutku, dopóki funkcja się nie skończy. Jednak kilka funkcji systemowych zachowuje się inaczej i mogą być przerwane przez
sygnał. Dotyczy to funkcji read, write i open w odniesieniu do
powolnych urządzeń takich, jak: terminal, ale już nie plik dyskowy, oraz funkcji wait i pause. We wszystkich tych przypadkach
przerwana funkcja systemowa zwraca -1 i umieszcza EINTR w
zmiennej errno.
Sygnały systemów Unixowych zwykle nie mogą być odkładane na stosie. Ściślej, dla danego procesu w danej chwili nie może istnieć więcej niż jeden zaległy sygnał danego typu, chociaż może
występować więcej niż jeden zaległy typ sygnałów. Z tego względu nie powinno się używać sygnałów jako godnej zaufania metody
12
sigsetjmp
i
siglongjmp
Czasami w momencie otrzymania sygnału istnieje potrzeba
przeskoczenia do poprzedniej pozycji w programie. Zachowanie takie jest możliwe dzięki podprogramom:
int sigsetjmp (sigjmp_t env, int savemask); void siglongjmp(sigjmp_t env, int val);
Pierwszy zachowuje bieżącą pozycję programu i maskę sygnału przez zapamiętanie środowiska stosu. Drugi przekazuje sterowanie wstecz, do zachowanej pozycji.
Pozycja programu jest zachowana w obiekcie typu sigjmp_buf,
zdefiniowanym w pliku nagłówkowym <setjmp.h>. Jeżeli w
wywołaniu sigsetjmp wartość savemask jest niezerowa, to
zachowana zostanie bieżąca maska sygnału, tj. stan i działania
związane ze wszystkimi sygnałami, oraz środowisko. Mogą one więc być odtworzone przez siglongjmp. Powrót z sigsetjmp zwróci
wartość val, gdy został wywołany z siglongjmp, gdy został
wywołany jako kolejna instrukcja sekwencyjna 0.
13
Blokowanie sygnałów
Jeżeli program wykonuje odpowiedzialne zadanie, np. aktualizuje bazę danych, należałoby go zabezpieczyć przed przerwaniami w kluczowych sytuacjach. Zamiast ignorowania wszystkich
nadchodzących sygnałów proces może blokować sygnały. Oznacza to, że nie będą one obsługiwane, dopóki proces nie zkończy swojej
krytycznej operacji. Funkcją systemową pozwalającą procesowi zablokować konkretny sygnał jest:
int sigprocmask(int how, const sigset_t *set const sigset_t *oset);
Parametr how informuje jakie działanie wykonać, np. SIG_SETMASK
oznacza blokowanie sygnałów ustawionych w drugim parametrze set.
Trzeci parametr jest wypełniany bieżącą maską blokowanych sygnałów.
14
Wysyłanie sygnałów
Do wysyłania sygnałów do procesów służy funkcja:
int kill(pid_t pid, int sig);
Pierwszy parametr pid określa proces albo procesy, do których
będzie wysłany sygnał sig. Ponieważ proces, który wywołuje
funkcja kill musi znać PID procesu, do którego wysyła sygnał,
funkcji kill używa się najczęściej między procesami powiązanymi,
np. procesem rodzicielskim i potomnym. Warto też zauważyć, że proces może wysyłać sygnał do samego siebie.
Proces może wysyłać sygnały tylko do takich procesów, których RIUD lub EUID jest taki sam, co procesu wysyłającego. Jak zwykle, proces administratora może wysyłać sygnały do wszystkich procesów. Jeżeli proces zwykłego użytkownika wysyła sygnał do procesu innego
użytkownika, to funkcja kill zwraca -1 i umieszcza wartość EPERM
w errno. Inne możliwości, to ESRCH: nie ma takiego procesu i EINVAL: sig nie jest ważnym numerem sygnału.
15
Wysyłanie sygnałów
cd
Parametr pid funkcji kill może przybierać następujące wartości:
Jeżeli pid jest równy zero, sygnał będzie wysłany do wszystkich
procesów, które należą do tej samej grupy procesów co nadawca. Dotyczy to również nadawcy.
Jeżeli pid jest równy -1, a efektywny EUID nie jest
administratorem, wtedy sygnał zostanie wysłany do wszystkich procesów z RUID równym EUID nadawcy. Ponownie dotyczy to również nadawcy.
Jeżeli pid jest równy -1, a efektywny EUID jest administratorem,
wtedy sygnał zostanie wysłany do wszystkich procesów z wyjątkiem pewnych specjalnych procesów systemowych.
Jeżeli pid jest mniejszy niż 0, ale różny od -1, wtedy sygnał
zostanie wysłany do wszystkich procesów z GUID równym
bezwzględnej wartości pid. Dotyczy również nadawcy, jeśli jego
16
Wysyłanie sygnałów do siebie
Do wysyłania sygnału do procesu wywołującego służy funkcja:
int raise(int sig);
Sygnał o numerze sig jest wysyłany do procesu wywołującego. W
przypadku powodzenia funkcja zwraca 0.
Do ustawiania zegara alarmu procesu służy funkcja:
int alarm(unsigned int sec);
Parametr sec podaje czas w sekundach do alarmu. Kiedy czas
upłynie, do procesu zostanie wysłany sygnał SIGALRM. Funkcja ta nie
powoduje zawieszenia działania procesu, jak sleep(), proces
kontynuuje wykonanie, przynajmniej do otrzymania sygnału. Aktywny zegar alarmu przedostaje się także przez wywołanie exec(),
natomiast wywołanie funkcji fork() wyłącza zegar alarmu w
procesie potomnym. Alarm może być wyłączony za pomocą
wywołania funkcji alarm(0), ponieważ wywołania tej funkcji nie są
odkładane na stosie, tj. drugie wywołanie zastąpi pierwsze. Jednak zwracany jest wówczas czas pozostający do poprzedniego alarmu.
17
Funkcja systemowa
pause
Systemy Unixowe dostarczają również funkcji systemowej:
int pause(void);
Funkcja ta zawiesza wywołujący proces, aż do otrzymania
dowolnego sygnału. Jeżeli sygnał powoduje normalne zakończenie, wtedy zdarzy się tylko to. Jeżeli sygnał jest ignorowany przez
proces, pause() ignoruje go również. Jeżeli jednak sygnał jest
przechwytywany, to gdy kończy się procedura obsługi przerwania, funkcja zwraca -1 i umieszcza EINTR w errno.
Przykład
:Program w pliku tml.c używa kolejno funkcji alarm() i pause()
do wyświetlenia komunikatu za określoną liczbę sekund. Wywołuje się go następująco:
$ tml 10 koniec pracy