Programowanie współbieżne i rozproszone/PWR Ćwiczenia sygnały: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mengel (dyskusja | edycje)
Nie podano opisu zmian
 
Mengel (dyskusja | edycje)
Nie podano opisu zmian
 
(Nie pokazano 3 pośrednich wersji utworzonych przez tego samego użytkownika)
Linia 48: Linia 48:
systemowej w razie konieczności było zadaniem procesu. Takie podejście choć zapewniało odpowiednią funkcjonalność, to jednak utrudniało pisanie funkcji obsługi sygnałów.   
systemowej w razie konieczności było zadaniem procesu. Takie podejście choć zapewniało odpowiednią funkcjonalność, to jednak utrudniało pisanie funkcji obsługi sygnałów.   


POSIX nie okre¶la wła¶ciwego zachowania, ale wszystkie popularne
Standard POSIX nie określa właściwego zachowania, ale wszystkie popularne systemy zachowują się zgodnie.  Dla każdego sygnału proces może ustawić flagę, która wskazuje, czy funkcja systemowa przerwana przez ten sygnał ma być automatycznie wznawiana przez system.  
systemy zachowuj± się zgodnie.  Dla każdego sygnału proces może ustawić  
flagę, która wskazuje, czy funkcja systemowa przerwana przez ten sygnał  
ma być automatycznie wznawiana przez system.  


==== Funkcje systemowe zwi±zane z sygnałami ====
==== Wysyłanie sygnałów ====
 
Wysyłanie sygnałów


Wysyłanie sygnałów z jednego procesu do innego jest realizowane przez funkcję
Wysyłanie sygnałów z jednego procesu do innego jest realizowane przez funkcję
systemow±
systemową 
        
        
int kill(pid_t pid, int sig);
int kill(pid_t pid, int sig);
 
Obsługa zbiorów sygnałów
 
Funkcje systemowe POSIX do obsługi sygnałów operuj± na zbiorach sygnałów.
Zbiór sygnałów jest reprezentowany przez typ {\tt sigset\_t}
a do operowania na nim służy zbiór makrodefinicji.
POSIX definiuje pięć funkcji do operowania na zbiorze sygnałów.


int sigemptyset(sigset_t *zbior);
Zeruje zbiór sygnałów wskazywany przez parametr zbior.
==== Obsługa zbiorów sygnałów ====


int sigfillset(sigset_t *zbior);
Funkcje systemowe POSIX do obsługi sygnałów operują na zbiorach sygnałów. Zbiór sygnałów jest reprezentowany przez typ <tt> sigset\_t</tt>, a do operowania na nim służy zbiór makrodefinicji.
Dodaje do zbioru wszystkie dostępne sygnały.
Standard POSIX definiuje pięć funkcji do operowania na zbiorze sygnałów.


int sigaddset(sigset_t *zbior, int sygnal);
# int sigemptyset(sigset_t *zbior) --- zeruje zbiór sygnałów wskazywany przez parametr zbior.
Dodaje sygnał sygnal do zbioru.
# int sigfillset(sigset_t *zbior) --- dodaje do zbioru wszystkie dostępne sygnały.
 
# int sigaddset(sigset_t *zbior, int sygnal) --- dodaje sygnał do zbioru.
int sigdelset(sigset_t *zbior, int sygnal);
# int sigdelset(sigset_t *zbior, int sygnal) --- usuwa sygnal ze zbioru.
Usuwa sygnal ze zbioru.
# int sigismember(const sigset_t *zbior, int sygnal) --- przekazuje w wyniku wartość niezerową, jeżeli sygnał jest w zbiorze, a w przeciwnym wypadku 0.  
 
int sigismember(const sigset_t *zbior, int sygnal);
Przekazuje w wyniku warto¶ć niezerow±, jeżeli sygnal jest w zbiorze,
a w przeciwnym wypadku 0.  
 
Obsługa sygnałów


==== Obsługa sygnałów ====
Do zarejestrowania funkcji obsługi sygnału służy funkcja:
Do zarejestrowania funkcji obsługi sygnału służy funkcja:


int * sigaction(int numer_sygnalu, struct sigaction * akcja,
int * sigaction(int numer_sygnalu, struct sigaction * akcja,
                struct sigaction * poprzednia_akcja);
                struct sigaction * poprzednia_akcja);
 
Funkcja ta okre¶la, że sygnał numer_sygnalu będzie obsługiwany w
sposób okre¶lony przez parametr akcja. Jeżeli parametr
poprzednia_akcja jest różny od NULL, to po wykonaniu funkcji będzie
wskazywać obsługę sygnału jaka była przed wywołaniem sigaction. 
Funkcja sigaction przekazuje w wyniku 0 w przypadku pomy¶lnego zakończenia,
a warto¶ć niezerow± w przypadku błędu.
 
Sposób obsługi sygnału przez j±dro jest okre¶lony przez strukturę
 
struct sigaction {
        sighandler_t sa_handler;
        sigset_t sa_mask;
        unsigned long sa_flags;
        void (*sa_restorer)(void);
};
 
Pole sa_handler jest wskaĽnikiem do funkcji o następuj±cej deklaracji:
 
void handler(int numer_sygnalu);
 
gdzie numer_sygnalu jest numerem sygału, który spowodował wywołanie
funkcji. Pole sa_handler może także przyj±ć warto¶ć SIG_IGN lub
SIG_DFL.
 
Pole sa_mask jest zbiorem sygnałów, które powinny być zablokowane w
trakcie wywołania funkcji obsługi. Sygnał, który jest obsługiwany jest
domy¶łnie zablokowany.
 
Pole sa_flags pozwala procesom na zmianę zachowania przy
obsłudze sygnałów. Składa się ona z jednej flagi lub bitowej alternatywy
następuj±cych flag:
 
* SA_NOCLDSTOP
 
Jeżeli dla sygnału SIGCHLD jest podana flaga SA_NOCLDSTOP, to sygnał
jest generowany tylko wtedy, gdy proces zakończył działanie, a
wstrzymanie procesów potomnych nie powoduje wysłania żadnego sygnału.
SA_NOCLDSTOP nie ma wływu na inne sygnały.
 
* SA_NOMASK
 
W trakcie wykonywania funkcji obsługi sygnału, sygnał nie jest
automatycznie blokowany.
 
* SA_ONESHOT
 
Kiedy sygnał jest wysłany, obsługa sygnału jest zerowana
do SIG_DFL.
 
* SA_RESTART
 
Kiedy sygnał jest wysłany do procesu, w czasie, gdy ten
wykonuje woln± funkcję systemow±, funkcja systemowa jest wznawiana po
powrocie z funkcji obsługi sygnału.
 
Ostatnie pole struktury sigaction, sa_restorer, nie jest czę¶ci±
specyfikacji POSIX i jest zarezerwowane na przyszło¶ć.
 
Zmiana maski sygnałów
 
Funkcje opisane w POSIX pozwalaj± procesowi na blokowanie dowolnego
zbioru sygnałów (z wyj. oczywiscie sygnałów, które z definicji s± nieblokowalne, np.: SIGKILL, SIGSTOP) otrzymywanych przez proces. Sygnały nie s± wtedy gubione
- ich dostarczenie jest opóĽnianie dopóki proces poprzez odblokowanie
sygnałów nie wyrazi chęci ich otrzymania.
 
Zbiór sygnałów (typu sigset_t), które proces aktualnie blokuje nosi nazwę
maski sygnałów. Funkcja sigprocmask() pozwala na zmianę aktualnej
maski sygnałów.
 
int sigprocmask(int jak, const sigset_t * zbior, sigset_t * stary_zbior);
 
Pierwszy parametr, opisuje w jaki sposób ma być zmieniana maska
sygnałów i może przyj±ć następuj±ce warto¶ci:


* SIG_BLOCK
Funkcja ta określa, że sygnał <tt>numer_sygnalu</tt> będzie obsługiwany w sposób określony przez parametr <tt>akcja</tt>. Jeżeli parametr poprzednia_akcja jest różny od NULL, to po wykonaniu funkcji będzie wskazywać obsługę sygnału, jaka była przed wywołaniem <tt>sigaction</tt>. Funkcja <tt>sigaction</tt> przekazuje w wyniku 0 w przypadku pomyślnego zakończenia, a wartość niezerową w przypadku błędu.


Sygnały ze zbioru s± dodawane do aktualnej maski sygnałów.
Sposób obsługi sygnału przez jądro jest określony przez strukturę


* SIG_UNBLOCK
struct sigaction {
        sighandler_t sa_handler;
        sigset_t sa_mask;
        unsigned long sa_flags;
        void (*sa_restorer)(void);
};


Sygnały ze zbioru s± usuwane z aktualnej maski sygnałów.
Pole <tt>sa_handler</tt> jest wskaźnikiem do funkcji o następującej deklaracji:


* SIG_SETMASK
void handler(int numer_sygnalu);


Tylko sygnały należ±ce go zbioru s± blokowane.
gdzie <tt>numer_sygnalu</tt> jest numerem sygału, który spowodował wywołanie funkcji. Pole sa_handler może także przyjąć wartość <tt>SIG_IGN</tt> lub <tt>SIG_DFL</tt>.
Pole <tt>sa_mask</tt> jest zbiorem sygnałów, które powinny być zablokowane w trakcie wywołania funkcji obsługi. Sygnał, który jest obsługiwany jest domyślnie zablokowany.
Pole <tt>sa_flags</tt> pozwala procesom na zmianę zachowania przy obsłudze sygnałów. Składa się ona z jednej flagi lub bitowej alternatywy następujących flag:
* SA_NOCLDSTOP. Jeżeli dla sygnału SIGCHLD jest podana flaga SA_NOCLDSTOP, to sygnał
jest generowany tylko wtedy, gdy proces zakończył działanie, a wstrzymanie procesów potomnych nie powoduje wysłania żadnego sygnału. SA_NOCLDSTOP nie ma wływu na inne sygnały.
* SA_NOMASK. W trakcie wykonywania funkcji obsługi sygnału, sygnał nie jest automatycznie blokowany.
* SA_ONESHOT. Kiedy sygnał jest wysłany, obsługa sygnału jest zerowana do SIG_DFL.
* SA_RESTART. Kiedy sygnał jest wysłany do procesu, w czasie, gdy ten wykonuje wolną funkcję systemową, funkcja systemowa jest wznawiana po powrocie z funkcji obsługi sygnału.  


Następuj±ce wywołanie pozwala uzyskać aktualn± maskę sygnałów procesu:
Ostatnie pole struktury <tt>sigaction</tt>, <tt>sa_restorer</tt>, nie jest częścią specyfikacji POSIX i jest zarezerwowane na przyszłość.


sigprocmask(SIG_BLOCK, NULL, &aktualnaMaska);
==== Zmiana maski sygnałów ====
Funkcje opisane w POSIX pozwalają procesowi na blokowanie dowolnego zbioru sygnałów (z wyj. oczywiście sygnałów, które z definicji są nieblokowalne, np.: SIGKILL, SIGSTOP) otrzymywanych przez proces. Sygnały nie są wtedy gubione --- ich dostarczenie jest opóźnianie dopóki proces poprzez odblokowanie sygnałów nie wyrazi chęci ich otrzymania.


Odczytywanie zbioru niezałatwionych sygnałów
Zbiór sygnałów (typu sigset_t), które proces aktualnie blokuje nosi nazwę maski sygnałów. Funkcja sigprocmask() pozwala na zmianę aktualnej maski sygnałów.


Uzyskanie informacji, które z sygnałów s± aktualnie niezałatwione
int sigprocmask(int jak, const sigset_t * zbior, sigset_t * stary_zbior);
jest możliwe dzięki funkcji:


int sigpending(sigset_t * zbior);
Pierwszy parametr, opisuje w jaki sposób ma być zmieniana maska sygnałów i może przyjąć następujące wartości:
* SIG_BLOCK --- sygnały ze zbioru są dodawane do aktualnej maski sygnałów.
* SIG_UNBLOCK --- sygnały ze zbioru są usuwane z aktualnej maski sygnałów.
* SIG_SETMASK --- tylko sygnały należące do zbioru są blokowane.


Po wykonaniu funkcji zbiór wskazywany przez parametr zbior zawiera aktualnie
Następujące wywołanie pozwala uzyskać aktualną maskę sygnałów procesu:
niezałatwione sygnały.
sigprocmask(SIG_BLOCK, NULL, &aktualnaMaska);


Oczekiwanie na sygnał
==== Odczytywanie zbioru niezałatwionych sygnałów ====


Funkcja systemowa {\tt pause()} realizuje oczekiwanie na sygnał.
Uzyskanie informacji, które z sygnałów są aktualnie niezałatwione jest możliwe dzięki funkcji:
int sigpending(sigset_t * zbior);
Po wykonaniu funkcji zbiór wskazywany przez parametr zbior zawiera aktualnie niezałatwione sygnały.  


int pause(void);
==== Oczekiwanie na sygnał ====


Funkcja systemowa <tt>pause()</tt> realizuje oczekiwanie na sygnał.
int pause(void);
Funkcja pause() nie kończy działania dopóki proces nie otrzyma sygnału.
Funkcja pause() nie kończy działania dopóki proces nie otrzyma sygnału.


Funkcja systemowa sigsuspend() także umożliwia oczekiwanie na sygnał.
Funkcja systemowa sigsuspend() także umożliwia oczekiwanie na sygnał.
int sigsuspend(const sigset_t * maska);
Podobnie jak funkcja pause(), sigsuspend() wstrzymuje działanie procesu dopóki proces nie otrzyma sygnału. W odróżnieniu od funkcji pause(), sigsuspend() przed rozpoczęciem  oczekiwania na sygnał tymczasowo zmienia maskę sygnałów procesu na wartość wskazywaną przez parametr maska.


int sigsuspend(const sigset_t * maska);
==== Dobre rady ====
 
Podobnie jak funkcja pause(), sigsuspend() wstrzymuje działanie
procesu dopóki proces nie otrzyma sygnału
 
W odróżnieniu od funkcji pause(), sigsuspend()}
przed rozpoczęciem  oczekiwania na sygnał tymczasowo zmienia
maskę sygnałów procesu na warto¶ć wskazywan± przez parametr maska.
 
Dobre rady:
 
Ponieważ funkcje obsługi sygnałów mog± być wykonywane w dowolnej chwili,
nie należy przy ich pisaniu przyjmować jakichkolwiek
założeń co do tego, co w chwili ich wywołania robi program. Nie mog± one też
dokonywać zmian, które mogłyby zakłócić wykonywanie dalszej czę¶ci programu.
 
Jedn± z najważniejszych rzeczy, na któr± należy zwrócić uwagę,
jest zmiana globalnych danych. Jeżeli nie jest wykonana wystarczaj±co
uważnie, to może doprowadzić do sytuacji wy¶cigu.
Najprostszym sposobem zapewnienia bezpieczeństwa globalnych danych
jest nie stosowanie ich w programie. Inn± metod± jest blokowanie
wszystkich sygnałów, których obsługa modyfikuje struktury danych, na
czas zmieniania struktur przez resztę programu, zapewniaj±c w ten
sposób,  że w danej chwili tylko jeden fragment kodu operuje na tych
strukturach.
 
========================================================================
 
W celu zachowanie zgodno¶ci wstecznej przedstawiamy takżę
pierwotn± implementację sygnałów. W implementacji tej do ustanawiania
w jaki sposób należy reagować na dany sygnał służy funkcja systemowa
 
void * signal(int numer_sygnalu, void * obsluga)
 
Parametr numer_sygnalu jest numerem sygnałem do obsłużenia,
a obsluga definiuje akcję, jaka ma być wykonana, gdy sygnał jest
wysyłany do procesu. Parametr obsluga jest wskaĽnikiem do funkcji,
której jedynym argumentem jest numer odebrnego sygnału i która nie
przekazuje w wyniku żadnej warto¶ci. Kiedy sygnał jest wysyłany do
procesu, j±dro wykonuje funkcję obsługi tak szybko, jak jest to
możliwe. Po powrocie z funkcji obsługi j±dro wznawia wykonanie procesu
w miejscu  jego przerwania.
 
Parametr obsluga może przyj±ć dwie specjalne warto¶ci: SIG_IGN i
SIG_DFL (obydwie zdefiniowane w <signal.h>). Jeżeli podamy SIG_IGN, to
sygnał będzie ignorowany. SIG_DFL informuje j±dro, że ma wykonać
domy¶ln± akcję zwi±zan± z tym sygnałem, która zazwyczaj polega na
zakończeniu działania procesu lub ignorowaniu
sygnału. Dwa sygnały, SIGKILL i SIGSTOP, nie mog± być obsługiwane.
W przypadku tych dwóch sygnałów j±dro zawsze wykonuje akcję domy¶ln±, któr±
jest odpowiednio zakończenie i wstrzymanie działania procesu.


Funkcja signal() przekazuje w wyniku poprzedni sposób obsługi sygnału.
Ponieważ funkcje obsługi sygnałów mogą być wykonywane w dowolnej chwili, nie należy przy ich pisaniu przyjmować jakichkolwiek założeń co do tego, co w chwili ich wywołania robi program. Nie mogą one też dokonywać zmian, które mogłyby zakłócić wykonywanie dalszej części programu.


============================================================================
Jedną z najważniejszych rzeczy, na którą należy zwrócić uwagę, jest zmiana globalnych danych. Jeżeli nie jest wykonana wystarczająco uważnie, to może doprowadzić do sytuacji wyścigu.
Najprostszym sposobem zapewnienia bezpieczeństwa globalnych danych jest nie stosowanie ich w programie. Inną metodą jest blokowanie wszystkich sygnałów, których obsługa modyfikuje struktury danych, na czas zmieniania struktur przez resztę programu, zapewniając w ten
sposób,  że w danej chwili tylko jeden fragment kodu operuje na tych strukturach.


Co można zrobić po otrzymaniu sygnału?
=== Sygnały uniksowe ===


(a) Zignorować go
W celu zachowania zgodności wstecznej przedstawiamy takżę pierwotną implementację sygnałów. W implementacji tej do ustanawiania, w jaki sposób należy reagować na dany sygnał służy funkcja systemowa
=================
void * signal(int numer_sygnalu, void * obsluga)
Parametr numer_sygnalu jest numerem sygnałem do obsłużenia, a obsluga definiuje akcję, jaka ma być wykonana, gdy sygnał jest wysyłany do procesu. Parametr obsluga jest wskaźnikiem do funkcji,
której jedynym argumentem jest numer odebranego sygnału i która nie przekazuje w wyniku żadnej wartości. Kiedy sygnał jest wysyłany do procesu, jądro wykonuje funkcję obsługi tak szybko, jak jest to możliwe. Po powrocie z funkcji obsługi jądro wznawia wykonanie procesu w miejscu  jego przerwania.


main()  
Parametr obsluga może przyjąć dwie specjalne wartości: <tt>SIG_IGN</tt> i <tt>SIG_DFL</tt> (obydwie zdefiniowane w <tt><signal.h></tt>). Jeżeli podamy SIG_IGN, to sygnał będzie ignorowany. SIG_DFL informuje jądro, że ma wykonać domyślną akcję związaną z tym sygnałem, która zazwyczaj polega na zakończeniu działania procesu lub ignorowaniu sygnału. Dwa sygnały, SIGKILL i SIGSTOP, nie mogą być obsługiwane. W przypadku tych dwóch sygnałów jądro zawsze wykonuje akcję domyślną, którą jest odpowiednio zakończenie i wstrzymanie działania procesu.
{
  signal(SIGINT, SIG_IGN);
  signal(SIGQUIT, SIG_IGN);
  /* itd */
}


(b) Posprz±tać i zakończyć działanie programu
Funkcja <tt>signal()</tt> przekazuje w wyniku poprzedni sposób obsługi sygnału.
=============================================


int moje_dzieci;


void porzadki(int typ_syg)
=== Co można zrobić po otrzymaniu sygnału? ===


# Zignorować go
<Source>
main()
{
  signal(SIGINT, SIG_IGN);
  signal(SIGQUIT, SIG_IGN);
  /* itd */
}
</Source>
# Posprzątać i zakończyć działanie programu
<Source>
int moje_dzieci;
void porzadki(int typ_syg)


{
{
   unlink("/tmp/plik_rob");
   unlink("/tmp/plik_rob");
   kill(moje_dzieci, SIGTERM);
   kill(moje_dzieci, SIGTERM);
Linia 284: Linia 174:
   fprintf(stderr, "program konczy dzialanie ...\n");
   fprintf(stderr, "program konczy dzialanie ...\n");
   exit(1);
   exit(1);
}
}


main()  
main()  
{
{
   signal(SIGINT, porzadki);
   signal(SIGINT, porzadki);
   open("/tmp/plik_rob", O_RDWR | O_CREAT, 0644);
   open("/tmp/plik_rob", O_RDWR | O_CREAT, 0644);
   moje_dzieci = fork();
   moje_dzieci = fork();
   /* itd */
   /* itd */
}
}
 
</Source>
(c) Dokonać dynamicznej rekonfiguracji
# Dokonać dynamicznej rekonfiguracji
======================================
<Source>
 
void czytaj_plik_konf (int typ_syg)  
void czytaj_plik_konf (int typ_syg)  
{
{
Linia 315: Linia 204:
   }
   }
}
}
 
</Source>
(d) Przekazać raport nt stanu/dokonać zrzutu wewnętrznych tablic
#Przekazać raport nt stanu/dokonać zrzutu wewnętrznych tablic
================================================================
<Source>
 
int licznik;
int licznik;


Linia 338: Linia 226:
   }
   }
}
}
 
</Source>
(e) Wł±czyć/wył±czyć ¶ledzenie
#Włączyć/wyłączyć śledzenie
==============================
<Source>
 
int flaga;
int flaga;


Linia 362: Linia 249:
   if (flaga) printf("cos uzytecznego\n");
   if (flaga) printf("cos uzytecznego\n");
}
}
</Source>


 
=== Sygnały użytkownika ===
Sygnały SIGUSR1 i SIGUSR2 zdefiniowane jako sygnały do
Sygnały SIGUSR1 i SIGUSR2 zdefiniowane jako sygnały do wykorzystanie przez użytkownika (nigdy nie generowane przez system).
wykorzystanie przez użytkownika (nigdy nie generowane przez system).
 
Polecenia:
 
Do wysłania sygnału z poziomu interpretatora poleceń służy polecenie
kill (informacje man kill). Sygnały mog± też byĽ generowane przez
sekwencje klawiszy np. Ctrl-C generuje sygnał SIGINT, a Ctrl-\ SIGQUIT.

Aktualna wersja na dzień 13:49, 16 paź 2006

Tematyka laboratorium

Sygnały i sygnały POSIX jako prymitywny mechanizm synchronizacyjny.

Materiały w tym module zostały przygotowane przez zespół prowadzących laboratorium Programowania Współbieżnego dla studentów II roku informatyki na Uniwersytecie Warszawskim.

Literatura

  1. M.K. Johnson, E.W. Troan, Oprogramowanie użytkowe w systemie Linux, rozdz. 13
  2. U. Vahalia, Jądro systemu UNIX, rozdz. 4
  3. W.R. Stevens, Programowanie zastosowań sieciowych w systemie Unix, rozdz. 2.4
  4. man do poszczególnych funkcji systemowych

Programy przykładowe

  • Makefile
  • err.c, err.h pliki do obsługi błędów
  • posix/signals.c program wypisujący informacje o wszystkich sygnałach i zmieniający

ich obsługę;

  • posix/cleanup.c program ilustrujący sposób tworzenia funkcji obsługi sygnałów w taki sposób, aby wykonywały one także obsługę standardową
  • posix/ctrl-c.c program ilustrujący trwałe blokowanie obsługi sygnałów
  • posix/child.c program ilustrujący wykorzystanie sygnału zakończenia procesu potomnego.
  • unix/takeover.c program służący do testowania semantyki funkcji signal()
  • unix/sigproc.c program ilustrujący wysyłanie sygnału do grupy procesów
  • unix/sigrace.c program ilustrujący wyścig procesów
  • unix/sigwait.c program ilustrujący zachowanie funkcji systemowej wait przy ignorowaniu sygnału SIGCLD
  • unix/timeout.c program ilustrujący przerywanie funkcji read przez sygnał.

Scenariusz zajęć

Wstęp

Sygnały są najprostszą metodą komunikacji międzyprocesowej opisaną przez standard POSIX. Umożliwiają one asynchroniczne przerwanie działania procesu przez inny proces (lub jądro), dzięki czemu przerwany proces może zareagować na pewne zdarzenia. Po obsłużeniu sygnału przerwany proces kontynuuje działanie od miejsca przerwania. Sygnały używa się do realizacji czynności takich jak kończenie działania procesów, czy informowanie procesów-demonów o tym, że mają odczytać ponownie pliki konfiguracyjne.

Kiedy proces otrzyma sygnał może zrobić jedną z trzech rzeczy:

  • zignorować go
  • spowodować, że jądro wykona specjalną część procesu zanim pozwoli procesowi kontynuować działanie (nazywamy to obsługą sygnału przez proces)
  • pozwolić, aby jądro wykonało domyślną akcję, która zależy od rodzaju wysłanego sygnału

Różne systemy uniksowe definiują różne (czasami niezgodne sematycznie) interfejsy programistyczne dotyczace sygnałów. Ponieważ standard definiowany przez POSIX jest obecnie przestrzegany przez niemal wszystkie wersje systemu Unix (również przez system Linux), dlatego kod powinien być tworzony zgodnie z tym interfejsem.

Sygnały zgodne z POSIX

Oryginalna wersja interfejsu sygnałów borykała się z problemem wielokrotnych sygnałów (nadejście sygnału w trakcie obsługi innego). Rozwiązaniem tego jest wstrzymanie dostarczenia drugiego sygnału dopóki proces nie zakończy obsługi pierwszego. Zapewnia to, że obydwa sygnały będą obsłużone i usuwa ryzyko przepełnienia stosu. Kiedy jądro przechowuje sygnał do późniejszego dostarczenia, o sygnale mówimy, że jest niezałatwiony. Jeżeli do procesu zostanie wysłany sygnał, wówczas gdy jakiś inny tego typu sygnał jest niezałatwiony, to tylko jeden z sygnałów zostanie dostarczony do procesu. Proces nie ma możliwości dowiedzenia się, ile razy dany sygnał był do niego wysyłany, ponieważ wiele sygnałów jest sklejanych w jeden.

Poza blokowaniem sygnałów w trakcie obsługi innych, wprowadzono możliwość jawnego blokowania sygnałów przez proces. Ułatwia to ochronę krytycznych części programu przy zachowaniu obsługi wszystkich wysłanych sygnałów. Taka ochrona pozwala funkcji obsługującej sygnały na operowanie na strukturach danych utrzymywanych przez inne fragmenty kodu przy zachowaniu prostej synchronizacji.

Proces może otrzymać sygnał w czasie oczekiwania na zajęcie zewnętrznego zdarzenia. W pierwotnej implementacji sygnałów wolne funkcje systemowe przekazywały w wyniku błąd EINTR, gdy ich działanie zostało przerwane przez sygnał, a szybkie funkcje systemowe kończyły działanie zanim sygnał został dostarczony. Obsługa błędu EINTR i wznowienie funkcji systemowej w razie konieczności było zadaniem procesu. Takie podejście choć zapewniało odpowiednią funkcjonalność, to jednak utrudniało pisanie funkcji obsługi sygnałów.

Standard POSIX nie określa właściwego zachowania, ale wszystkie popularne systemy zachowują się zgodnie. Dla każdego sygnału proces może ustawić flagę, która wskazuje, czy funkcja systemowa przerwana przez ten sygnał ma być automatycznie wznawiana przez system.

Wysyłanie sygnałów

Wysyłanie sygnałów z jednego procesu do innego jest realizowane przez funkcję systemową

int kill(pid_t pid, int sig);


Obsługa zbiorów sygnałów

Funkcje systemowe POSIX do obsługi sygnałów operują na zbiorach sygnałów. Zbiór sygnałów jest reprezentowany przez typ sigset\_t, a do operowania na nim służy zbiór makrodefinicji. Standard POSIX definiuje pięć funkcji do operowania na zbiorze sygnałów.

  1. int sigemptyset(sigset_t *zbior) --- zeruje zbiór sygnałów wskazywany przez parametr zbior.
  2. int sigfillset(sigset_t *zbior) --- dodaje do zbioru wszystkie dostępne sygnały.
  3. int sigaddset(sigset_t *zbior, int sygnal) --- dodaje sygnał do zbioru.
  4. int sigdelset(sigset_t *zbior, int sygnal) --- usuwa sygnal ze zbioru.
  5. int sigismember(const sigset_t *zbior, int sygnal) --- przekazuje w wyniku wartość niezerową, jeżeli sygnał jest w zbiorze, a w przeciwnym wypadku 0.

Obsługa sygnałów

Do zarejestrowania funkcji obsługi sygnału służy funkcja:

int * sigaction(int numer_sygnalu, struct sigaction * akcja,
                struct sigaction * poprzednia_akcja);

Funkcja ta określa, że sygnał numer_sygnalu będzie obsługiwany w sposób określony przez parametr akcja. Jeżeli parametr poprzednia_akcja jest różny od NULL, to po wykonaniu funkcji będzie wskazywać obsługę sygnału, jaka była przed wywołaniem sigaction. Funkcja sigaction przekazuje w wyniku 0 w przypadku pomyślnego zakończenia, a wartość niezerową w przypadku błędu.

Sposób obsługi sygnału przez jądro jest określony przez strukturę

struct sigaction {
        sighandler_t sa_handler;
        sigset_t sa_mask;
        unsigned long sa_flags;
        void (*sa_restorer)(void);
};

Pole sa_handler jest wskaźnikiem do funkcji o następującej deklaracji:

void handler(int numer_sygnalu);

gdzie numer_sygnalu jest numerem sygału, który spowodował wywołanie funkcji. Pole sa_handler może także przyjąć wartość SIG_IGN lub SIG_DFL. Pole sa_mask jest zbiorem sygnałów, które powinny być zablokowane w trakcie wywołania funkcji obsługi. Sygnał, który jest obsługiwany jest domyślnie zablokowany. Pole sa_flags pozwala procesom na zmianę zachowania przy obsłudze sygnałów. Składa się ona z jednej flagi lub bitowej alternatywy następujących flag:

  • SA_NOCLDSTOP. Jeżeli dla sygnału SIGCHLD jest podana flaga SA_NOCLDSTOP, to sygnał

jest generowany tylko wtedy, gdy proces zakończył działanie, a wstrzymanie procesów potomnych nie powoduje wysłania żadnego sygnału. SA_NOCLDSTOP nie ma wływu na inne sygnały.

  • SA_NOMASK. W trakcie wykonywania funkcji obsługi sygnału, sygnał nie jest automatycznie blokowany.
  • SA_ONESHOT. Kiedy sygnał jest wysłany, obsługa sygnału jest zerowana do SIG_DFL.
  • SA_RESTART. Kiedy sygnał jest wysłany do procesu, w czasie, gdy ten wykonuje wolną funkcję systemową, funkcja systemowa jest wznawiana po powrocie z funkcji obsługi sygnału.

Ostatnie pole struktury sigaction, sa_restorer, nie jest częścią specyfikacji POSIX i jest zarezerwowane na przyszłość.

Zmiana maski sygnałów

Funkcje opisane w POSIX pozwalają procesowi na blokowanie dowolnego zbioru sygnałów (z wyj. oczywiście sygnałów, które z definicji są nieblokowalne, np.: SIGKILL, SIGSTOP) otrzymywanych przez proces. Sygnały nie są wtedy gubione --- ich dostarczenie jest opóźnianie dopóki proces poprzez odblokowanie sygnałów nie wyrazi chęci ich otrzymania.

Zbiór sygnałów (typu sigset_t), które proces aktualnie blokuje nosi nazwę maski sygnałów. Funkcja sigprocmask() pozwala na zmianę aktualnej maski sygnałów.

int sigprocmask(int jak, const sigset_t * zbior, sigset_t * stary_zbior); 

Pierwszy parametr, opisuje w jaki sposób ma być zmieniana maska sygnałów i może przyjąć następujące wartości:

  • SIG_BLOCK --- sygnały ze zbioru są dodawane do aktualnej maski sygnałów.
  • SIG_UNBLOCK --- sygnały ze zbioru są usuwane z aktualnej maski sygnałów.
  • SIG_SETMASK --- tylko sygnały należące do zbioru są blokowane.

Następujące wywołanie pozwala uzyskać aktualną maskę sygnałów procesu:

sigprocmask(SIG_BLOCK, NULL, &aktualnaMaska); 

Odczytywanie zbioru niezałatwionych sygnałów

Uzyskanie informacji, które z sygnałów są aktualnie niezałatwione jest możliwe dzięki funkcji:

int sigpending(sigset_t * zbior);

Po wykonaniu funkcji zbiór wskazywany przez parametr zbior zawiera aktualnie niezałatwione sygnały.

Oczekiwanie na sygnał

Funkcja systemowa pause() realizuje oczekiwanie na sygnał.

int pause(void);

Funkcja pause() nie kończy działania dopóki proces nie otrzyma sygnału.

Funkcja systemowa sigsuspend() także umożliwia oczekiwanie na sygnał.

int sigsuspend(const sigset_t * maska);

Podobnie jak funkcja pause(), sigsuspend() wstrzymuje działanie procesu dopóki proces nie otrzyma sygnału. W odróżnieniu od funkcji pause(), sigsuspend() przed rozpoczęciem oczekiwania na sygnał tymczasowo zmienia maskę sygnałów procesu na wartość wskazywaną przez parametr maska.

Dobre rady

Ponieważ funkcje obsługi sygnałów mogą być wykonywane w dowolnej chwili, nie należy przy ich pisaniu przyjmować jakichkolwiek założeń co do tego, co w chwili ich wywołania robi program. Nie mogą one też dokonywać zmian, które mogłyby zakłócić wykonywanie dalszej części programu.

Jedną z najważniejszych rzeczy, na którą należy zwrócić uwagę, jest zmiana globalnych danych. Jeżeli nie jest wykonana wystarczająco uważnie, to może doprowadzić do sytuacji wyścigu. Najprostszym sposobem zapewnienia bezpieczeństwa globalnych danych jest nie stosowanie ich w programie. Inną metodą jest blokowanie wszystkich sygnałów, których obsługa modyfikuje struktury danych, na czas zmieniania struktur przez resztę programu, zapewniając w ten sposób, że w danej chwili tylko jeden fragment kodu operuje na tych strukturach.

Sygnały uniksowe

W celu zachowania zgodności wstecznej przedstawiamy takżę pierwotną implementację sygnałów. W implementacji tej do ustanawiania, w jaki sposób należy reagować na dany sygnał służy funkcja systemowa

void * signal(int numer_sygnalu, void * obsluga)

Parametr numer_sygnalu jest numerem sygnałem do obsłużenia, a obsluga definiuje akcję, jaka ma być wykonana, gdy sygnał jest wysyłany do procesu. Parametr obsluga jest wskaźnikiem do funkcji, której jedynym argumentem jest numer odebranego sygnału i która nie przekazuje w wyniku żadnej wartości. Kiedy sygnał jest wysyłany do procesu, jądro wykonuje funkcję obsługi tak szybko, jak jest to możliwe. Po powrocie z funkcji obsługi jądro wznawia wykonanie procesu w miejscu jego przerwania.

Parametr obsluga może przyjąć dwie specjalne wartości: SIG_IGN i SIG_DFL (obydwie zdefiniowane w <signal.h>). Jeżeli podamy SIG_IGN, to sygnał będzie ignorowany. SIG_DFL informuje jądro, że ma wykonać domyślną akcję związaną z tym sygnałem, która zazwyczaj polega na zakończeniu działania procesu lub ignorowaniu sygnału. Dwa sygnały, SIGKILL i SIGSTOP, nie mogą być obsługiwane. W przypadku tych dwóch sygnałów jądro zawsze wykonuje akcję domyślną, którą jest odpowiednio zakończenie i wstrzymanie działania procesu.

Funkcja signal() przekazuje w wyniku poprzedni sposób obsługi sygnału.


Co można zrobić po otrzymaniu sygnału?

  1. Zignorować go
 main() 
 {
   signal(SIGINT, SIG_IGN);
   signal(SIGQUIT, SIG_IGN);
   /* itd */
 }
  1. Posprzątać i zakończyć działanie programu
 
 int moje_dzieci;
 void porzadki(int typ_syg)

 {
  unlink("/tmp/plik_rob");
  kill(moje_dzieci, SIGTERM);
  wait(0);
  fprintf(stderr, "program konczy dzialanie ...\n");
  exit(1);
 }

 main() 
 {
  signal(SIGINT, porzadki);
  open("/tmp/plik_rob", O_RDWR | O_CREAT, 0644);
  moje_dzieci = fork();
  /* itd */
 }
  1. Dokonać dynamicznej rekonfiguracji
void czytaj_plik_konf (int typ_syg) 
{
  int fd;
  fd = open("moj_plik_konf", O_RDONLY);

  /* czytanie parametrow konfiguracyjnych */
  close(fd);
  signal(SIGUSR1, czytaj_plik_konf);
}

main() 
{
  czytaj_plik_konf();  /* inicjalna konfiguracja */

  while(1) {   /* obsluga w petli */
  ...
  }
}
  1. Przekazać raport nt stanu/dokonać zrzutu wewnętrznych tablic
int licznik;

void drukuj_info(int typ_syg)
{
  /* drukuj info o stanie */
  printf("liczba skopiowanych blokow: %d\n", licznik);
  signal(SIGUSR1, drukuj_info);
}

main () 
{
  signal(SIGUSR1, drukuj_info);
  for (licznik=0; licznik<DUZA_LICZBA; licznik++) {
    /* czytaj blok z tasmy wejsciowej */
    ...
    /* pisz blok na tasme wyjsciowa */
    ...
  }
}
  1. Włączyć/wyłączyć śledzenie
int flaga;

void przelacz_flage(int typ_syg) 
{
  flaga ^= 1;
  signal(SIGUSR1, przelacz_flage);


}

main() 
{
  /* inicjalnie wylacz sledzenie */
  flaga = 0;
  signal(SIGUSR1, przelacz_flage);
  /* zrob cos uzytecznego */
  /* wewnatrz kodu instrukcje implementujace sledzenie 
     powinny wygladac nastepujaco: */
  if (flaga) printf("cos uzytecznego\n");
}

Sygnały użytkownika

Sygnały SIGUSR1 i SIGUSR2 są zdefiniowane jako sygnały do wykorzystanie przez użytkownika (nigdy nie są generowane przez system).