Programowanie współbieżne i rozproszone/PWR Ćwiczenia sygnały

Z Studia Informatyczne
< Programowanie współbieżne i rozproszone
Wersja z dnia 13:12, 16 paź 2006 autorstwa Mengel (dyskusja | edycje)
(różn.) ← poprzednia wersja | przejdź do aktualnej wersji (różn.) | następna wersja → (różn.)
Przejdź do nawigacjiPrzejdź do wyszukiwania

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.

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.

Funkcje systemowe zwi±zane z sygnałami

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 {\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.

int sigfillset(sigset_t *zbior); Dodaje do zbioru wszystkie dostępne sygnały.

int sigaddset(sigset_t *zbior, int sygnal); Dodaje sygnał sygnal do zbioru.

int sigdelset(sigset_t *zbior, int sygnal); Usuwa sygnal ze zbioru.

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

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¶ł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

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 go 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 {\tt 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.

============================================================

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.

================================================================

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

(a) Zignorować go

=====

main() {

 signal(SIGINT, SIG_IGN);
 signal(SIGQUIT, SIG_IGN);
 /* itd */

}

(b) 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 */

}

(c) 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 */
 ...
 }

}

(d) 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 */
   ...
 }

}

(e) 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 SIGUSR1 i SIGUSR2 s± zdefiniowane jako sygnały do wykorzystanie przez użytkownika (nigdy nie s± 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.