Ponieważ printf
nie jest ponownie wprowadzany, używanie go w programie obsługi sygnału nie powinno być bezpieczne. Ale widziałem wiele przykładowych kodów, które używają printf
tego sposobu.
Więc moje pytanie brzmi: kiedy powinniśmy unikać używania printf
w module obsługi sygnału i czy istnieje zalecany zamiennik?
printf
wezwanie w tym sygnalizatorze? Usuń to.Odpowiedzi:
Możesz użyć jakiejś zmiennej flag, ustawić tę flagę wewnątrz funkcji obsługi sygnału i na podstawie tej
printf()
funkcji wywołania flagi w main () lub innej części programu podczas normalnej pracy.Zauważ, że w poniższym przykładzie program obsługi sygnału ding () ustawia flagę
alarm_fired
na 1 jako przechwycony SIGALRM, aw funkcji głównejalarm_fired
wartość jest sprawdzana, aby warunkowo poprawnie wywołać printf.static int alarm_fired = 0; void ding(int sig) // can be called asynchronously { alarm_fired = 1; // set flag } int main() { pid_t pid; printf("alarm application starting\n"); pid = fork(); switch(pid) { case -1: /* Failure */ perror("fork failed"); exit(1); case 0: /* child */ sleep(5); kill(getppid(), SIGALRM); exit(0); } /* if we get here we are the parent process */ printf("waiting for alarm to go off\n"); (void) signal(SIGALRM, ding); pause(); if (alarm_fired) // check flag to call printf printf("Ding!\n"); printf("done\n"); exit(0); }
Źródła: Beginning Linux Programming, 4th Edition , W tej książce wyjaśniono dokładnie Twój kod (czego chcesz), Rozdział 11: Processes and Signals, strona 484
Ponadto należy zachować szczególną ostrożność podczas pisania funkcji obsługi, ponieważ można je wywoływać asynchronicznie. Oznacza to, że procedura obsługi może zostać wywołana w dowolnym momencie programu w nieprzewidywalny sposób. Jeśli dwa sygnały nadejdą w bardzo krótkim czasie, jeden program obsługi może działać w drugim. Uważa się, że lepszą praktyką jest stwierdzenie
volatile sigatomic_t
, że dostęp do tego typu jest zawsze atomowy, co pozwala uniknąć niepewności co do przerwania dostępu do zmiennej. (czytaj: Atomic Data Access and Signal Handling for detail expiation).Przeczytaj Definiowanie obsługi sygnałów : aby dowiedzieć się, jak napisać funkcję obsługi sygnału, która może być ustanowiona za pomocą funkcji
signal()
lubsigaction()
.Lista autoryzowanych funkcji na stronie podręcznika , wywołanie tej funkcji w programie obsługi sygnału jest bezpieczne.
źródło
volatile sigatomic_t alarm_fired;
Podstawowym problemem jest to, że jeśli sygnał przerwie się
malloc()
lub zostanie wykonana podobna funkcja, stan wewnętrzny może być tymczasowo niespójny podczas przenoszenia bloków pamięci między listą wolnych i używanych lub innych podobnych operacji. Jeśli kod w module obsługi sygnału wywołuje funkcję, która następnie wywołujemalloc()
, może to całkowicie zepsuć zarządzanie pamięcią.Standard C przyjmuje bardzo konserwatywne podejście do tego, co można zrobić w module obsługi sygnału:
POSIX jest o wiele bardziej szczodry, jeśli chodzi o to, co możesz zrobić w obsłudze sygnału.
Signal Concepts w edycji POSIX 2008 mówi:
Jednak
printf()
rodzina funkcji jest wyraźnie nieobecna na tej liście i może nie być wywoływana bezpiecznie z obsługi sygnału.POSIX 2016 zmiana rozszerza listę bezpiecznych funkcji, obejmujących w szczególności duża liczba funkcji z
<string.h>
, co jest szczególnie cenne uzupełnienie (lub był to szczególnie frustrujące niedopatrzenie). Lista jest teraz:_Exit() getppid() sendmsg() tcgetpgrp() _exit() getsockname() sendto() tcsendbreak() abort() getsockopt() setgid() tcsetattr() accept() getuid() setpgid() tcsetpgrp() access() htonl() setsid() time() aio_error() htons() setsockopt() timer_getoverrun() aio_return() kill() setuid() timer_gettime() aio_suspend() link() shutdown() timer_settime() alarm() linkat() sigaction() times() bind() listen() sigaddset() umask() cfgetispeed() longjmp() sigdelset() uname() cfgetospeed() lseek() sigemptyset() unlink() cfsetispeed() lstat() sigfillset() unlinkat() cfsetospeed() memccpy() sigismember() utime() chdir() memchr() siglongjmp() utimensat() chmod() memcmp() signal() utimes() chown() memcpy() sigpause() wait() clock_gettime() memmove() sigpending() waitpid() close() memset() sigprocmask() wcpcpy() connect() mkdir() sigqueue() wcpncpy() creat() mkdirat() sigset() wcscat() dup() mkfifo() sigsuspend() wcschr() dup2() mkfifoat() sleep() wcscmp() execl() mknod() sockatmark() wcscpy() execle() mknodat() socket() wcscspn() execv() ntohl() socketpair() wcslen() execve() ntohs() stat() wcsncat() faccessat() open() stpcpy() wcsncmp() fchdir() openat() stpncpy() wcsncpy() fchmod() pause() strcat() wcsnlen() fchmodat() pipe() strchr() wcspbrk() fchown() poll() strcmp() wcsrchr() fchownat() posix_trace_event() strcpy() wcsspn() fcntl() pselect() strcspn() wcsstr() fdatasync() pthread_kill() strlen() wcstok() fexecve() pthread_self() strncat() wmemchr() ffs() pthread_sigmask() strncmp() wmemcmp() fork() raise() strncpy() wmemcpy() fstat() read() strnlen() wmemmove() fstatat() readlink() strpbrk() wmemset() fsync() readlinkat() strrchr() write() ftruncate() recv() strspn() futimens() recvfrom() strstr() getegid() recvmsg() strtok_r() geteuid() rename() symlink() getgid() renameat() symlinkat() getgroups() rmdir() tcdrain() getpeername() select() tcflow() getpgrp() sem_post() tcflush() getpid() send() tcgetattr()
W rezultacie albo kończysz używanie
write()
bez obsługi formatowania zapewnianej przezprintf()
et al, albo ustawisz flagę, którą testujesz (okresowo) w odpowiednich miejscach w kodzie. Technika ta jest sprawnie wykazano w odpowiedzi przez Grijesh Chauhan .Standardowe funkcje C i bezpieczeństwo sygnału
chqrlie zadaje interesujące pytanie, na które mam tylko częściową odpowiedź:
Dla wielu z tych funkcji w
<string.h>
, trudno jest zrozumieć, dlaczego nie zostały one uznane za bezpieczne asynchroniczny sygnał, a ja zgadza się,strlen()
jest najlepszym przykładem, wraz zstrchr()
,strstr()
itd Z drugiej strony, inne funkcje, takie jakstrtok()
,strcoll()
istrxfrm()
są raczej złożone i prawdopodobnie nie będą bezpieczne dla sygnału asynchronicznego. Ponieważstrtok()
zachowuje stan między wywołaniami, a program obsługi sygnału nie może łatwo stwierdzić, czy jakaś część używanego kodustrtok()
byłaby pomieszana. Funkcjestrcoll()
istrxfrm()
działają z danymi zależnymi od ustawień regionalnych, a ładowanie ustawień regionalnych obejmuje różnego rodzaju ustawienia stanu.Wszystkie funkcje (makra) z
<ctype.h>
są wrażliwe na ustawienia regionalne i dlatego mogą napotkać te same problemy, costrcoll()
istrxfrm()
.Trudno mi zrozumieć, dlaczego funkcje matematyczne z
<math.h>
nie są bezpieczne dla sygnału asynchronicznego, chyba że dzieje się tak dlatego, że może na nie wpływać SIGFPE (wyjątek zmiennoprzecinkowy), chociaż mniej więcej jedyny raz, kiedy widzę jeden z tych dni, dotyczy liczby całkowitej dzielenie przez zero. Podobna niepewność powstaje<complex.h>
,<fenv.h>
i<tgmath.h>
.Niektóre funkcje
<stdlib.h>
mogą być wyłączone -abs()
na przykład. Inne są szczególnie problematyczne:malloc()
a rodzina jest najlepszym przykładem.Podobną ocenę można przeprowadzić dla innych nagłówków w Standardzie C (2011) używanych w środowisku POSIX. (Standard C jest tak restrykcyjny, że nie interesuje go analizowanie w czystym środowisku Standard C). Te oznaczone jako „zależne od ustawień regionalnych” są niebezpieczne, ponieważ manipulowanie lokalizacjami może wymagać alokacji pamięci itp.
<assert.h>
- Prawdopodobnie nie jest bezpieczny<complex.h>
- Prawdopodobnie bezpieczny<ctype.h>
- Nie jest bezpieczne<errno.h>
- Bezpieczny<fenv.h>
- Prawdopodobnie nie jest bezpieczny<float.h>
- Brak funkcji<inttypes.h>
- Funkcje zależne od lokalizacji (niebezpieczne)<iso646.h>
- Brak funkcji<limits.h>
- Brak funkcji<locale.h>
- Funkcje zależne od lokalizacji (niebezpieczne)<math.h>
- Prawdopodobnie bezpieczny<setjmp.h>
- Nie jest bezpieczne<signal.h>
- Dozwolony<stdalign.h>
- Brak funkcji<stdarg.h>
- Brak funkcji<stdatomic.h>
- Prawdopodobnie bezpieczne, prawdopodobnie niebezpieczne<stdbool.h>
- Brak funkcji<stddef.h>
- Brak funkcji<stdint.h>
- Brak funkcji<stdio.h>
- Nie jest bezpieczne<stdlib.h>
- Nie wszystkie są bezpieczne (niektóre są dozwolone, inne nie)<stdnoreturn.h>
- Brak funkcji<string.h>
- Nie wszystko jest bezpieczne<tgmath.h>
- Prawdopodobnie bezpieczny<threads.h>
- Prawdopodobnie nie jest bezpieczny<time.h>
- Zależne od lokalizacji (aletime()
jest wyraźnie dozwolone)<uchar.h>
- Zależne od lokalizacji<wchar.h>
- Zależne od lokalizacji<wctype.h>
- Zależne od lokalizacjiAnaliza nagłówków POSIX byłaby… trudniejsza, ponieważ jest ich dużo, a niektóre funkcje mogą być bezpieczne, ale wiele nie będzie… ale także prostsze, ponieważ POSIX mówi, które funkcje są bezpieczne dla sygnału asynchronicznego (niewiele z nich). Zwróć uwagę, że nagłówek taki jak
<pthread.h>
ma trzy bezpieczne funkcje i wiele niebezpiecznych funkcji.Uwaga: Prawie cała ocena funkcji i nagłówków C w środowisku POSIX to częściowo wyuczone domysły. Nie ma sensu ostateczne oświadczenie organu normalizacyjnego.
źródło
<string.h>
lub funkcje klas znaków z<ctype.h>
i wiele innych funkcji biblioteki standardowej C nie znajduje się na powyższej liście? Implementacja musiałaby być celowo zła, abystrlen()
wywołanie z modułu obsługi sygnału było niebezpieczne.<ctype.h>
rzeczy, jest to specyficzne dla lokalizacji i może powodować problemy, jeśli sygnał przerywa funkcję ustawiania ustawień regionalnych, ale po załadowaniu ustawień regionalnych używanie ich powinno być bezpieczne. Wydaje mi się, że w niektórych złożonych sytuacjach ładowanie danych regionalnych może odbywać się przyrostowo, przez co funkcje<ctype.h>
stają się niebezpieczne. Wniosek pozostaje: jeśli masz wątpliwości, wstrzymaj się od głosu.Zawsze tego unikaj, powie: Po prostu nie używaj
printf()
w obsłudze sygnałów.Przynajmniej w systemach zgodnych z POSIX możesz używać
write(STDOUT_FILENO, ...)
zamiastprintf()
. Jednak formatowanie może nie być łatwe: Wydrukuj int z programu obsługi sygnału przy użyciu funkcji zapisu lub funkcji asynchronicznychźródło
Always avoid it.
znaczy? Unikaćprintf()
?printf()
w obsłudze sygnałów.2
punkt, sprawdź OP z pytaniem Jak unikać używaniaprintf()
w obsłudze sygnału?Do celów debugowania napisałem narzędzie, które sprawdza, czy w rzeczywistości wywołujesz tylko funkcje z
async-signal-safe
listy i wyświetla komunikat ostrzegawczy dla każdej niebezpiecznej funkcji wywoływanej w kontekście sygnału. Chociaż nie rozwiązuje to problemu chęci wywoływania funkcji niezabezpieczonych asynchronicznie z kontekstu sygnału, pomaga przynajmniej znaleźć przypadki, w których zrobiłeś to przypadkowo.Kod źródłowy znajduje się na GitHub . Działa poprzez przeciążenie
signal/sigaction
, a następnie tymczasowe przejmowaniePLT
wpisów niebezpiecznych funkcji; Powoduje to przekierowanie wywołań niebezpiecznych funkcji do opakowania.źródło
Zaimplementuj własny bezpieczny sygnał asynchroniczny
snprintf("%d
i korzystaj z niegowrite
Nie jest tak źle, jak myślałem, jak przekonwertować int na string w C? ma kilka implementacji.
Ponieważ istnieją tylko dwa interesujące typy danych, do których mają dostęp procedury obsługi sygnałów:
sig_atomic_t
globalneint
argument sygnałuto w zasadzie obejmuje wszystkie interesujące przypadki użycia.
Fakt, że
strcpy
jest również bezpieczny dla sygnału, czyni wszystko jeszcze lepszym.Poniższy program POSIX drukuje na standardowe wyjście, ile razy otrzymał SIGINT do tej pory, którą można wyzwolić
Ctrl + C
, oraz identyfikator sygnału i.Możesz wyjść z programu za pomocą
Ctrl + \
(SIGQUIT).main.c:
#define _XOPEN_SOURCE 700 #include <assert.h> #include <limits.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> /* Calculate the minimal buffer size for a given type. * * Here we overestimate and reserve 8 chars per byte. * * With this size we could even print a binary string. * * - +1 for NULL terminator * - +1 for '-' sign * * A tight limit for base 10 can be found at: * /programming/8257714/how-to-convert-an-int-to-string-in-c/32871108#32871108 * * TODO: get tight limits for all bases, possibly by looking into * glibc's atoi: /programming/190229/where-is-the-itoa-function-in-linux/52127877#52127877 */ #define ITOA_SAFE_STRLEN(type) sizeof(type) * CHAR_BIT + 2 /* async-signal-safe implementation of integer to string conversion. * * Null terminates the output string. * * The input buffer size must be large enough to contain the output, * the caller must calculate it properly. * * @param[out] value Input integer value to convert. * @param[out] result Buffer to output to. * @param[in] base Base to convert to. * @return Pointer to the end of the written string. */ char *itoa_safe(intmax_t value, char *result, int base) { intmax_t tmp_value; char *ptr, *ptr2, tmp_char; if (base < 2 || base > 36) { return NULL; } ptr = result; do { tmp_value = value; value /= base; *ptr++ = "ZYXWVUTSRQPONMLKJIHGFEDCBA9876543210123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[35 + (tmp_value - value * base)]; } while (value); if (tmp_value < 0) *ptr++ = '-'; ptr2 = result; result = ptr; *ptr-- = '\0'; while (ptr2 < ptr) { tmp_char = *ptr; *ptr--= *ptr2; *ptr2++ = tmp_char; } return result; } volatile sig_atomic_t global = 0; void signal_handler(int sig) { char key_str[] = "count, sigid: "; /* This is exact: * - the null after the first int will contain the space * - the null after the second int will contain the newline */ char buf[2 * ITOA_SAFE_STRLEN(sig_atomic_t) + sizeof(key_str)]; enum { base = 10 }; char *end; end = buf; strcpy(end, key_str); end += sizeof(key_str); end = itoa_safe(global, end, base); *end++ = ' '; end = itoa_safe(sig, end, base); *end++ = '\n'; write(STDOUT_FILENO, buf, end - buf); global += 1; signal(sig, signal_handler); } int main(int argc, char **argv) { /* Unit test itoa_safe. */ { typedef struct { intmax_t n; int base; char out[1024]; } InOut; char result[1024]; size_t i; InOut io; InOut ios[] = { /* Base 10. */ {0, 10, "0"}, {1, 10, "1"}, {9, 10, "9"}, {10, 10, "10"}, {100, 10, "100"}, {-1, 10, "-1"}, {-9, 10, "-9"}, {-10, 10, "-10"}, {-100, 10, "-100"}, /* Base 2. */ {0, 2, "0"}, {1, 2, "1"}, {10, 2, "1010"}, {100, 2, "1100100"}, {-1, 2, "-1"}, {-100, 2, "-1100100"}, /* Base 35. */ {0, 35, "0"}, {1, 35, "1"}, {34, 35, "Y"}, {35, 35, "10"}, {100, 35, "2U"}, {-1, 35, "-1"}, {-34, 35, "-Y"}, {-35, 35, "-10"}, {-100, 35, "-2U"}, }; for (i = 0; i < sizeof(ios)/sizeof(ios[0]); ++i) { io = ios[i]; itoa_safe(io.n, result, io.base); if (strcmp(result, io.out)) { printf("%ju %d %s\n", io.n, io.base, io.out); assert(0); } } } /* Handle the signals. */ if (argc > 1 && !strcmp(argv[1], "1")) { signal(SIGINT, signal_handler); while(1); } return EXIT_SUCCESS; }
Skompiluj i uruchom:
gcc -std=c99 -Wall -Wextra -o main main.c ./main 1
Po piętnastokrotnym naciśnięciu Ctrl + C terminal wyświetla:
^Ccount, sigid: 0 2 ^Ccount, sigid: 1 2 ^Ccount, sigid: 2 2 ^Ccount, sigid: 3 2 ^Ccount, sigid: 4 2 ^Ccount, sigid: 5 2 ^Ccount, sigid: 6 2 ^Ccount, sigid: 7 2 ^Ccount, sigid: 8 2 ^Ccount, sigid: 9 2 ^Ccount, sigid: 10 2 ^Ccount, sigid: 11 2 ^Ccount, sigid: 12 2 ^Ccount, sigid: 13 2 ^Ccount, sigid: 14 2
gdzie
2
jest numer sygnału dlaSIGINT
.Testowane na Ubuntu 18.04. GitHub upstream .
źródło
Jedną z technik, która jest szczególnie użyteczna w programach, które mają pętlę wyboru, jest zapisanie pojedynczego bajtu potokiem po odebraniu sygnału, a następnie obsługa sygnału w pętli wyboru. Coś w tym kierunku (obsługa błędów i inne szczegóły pominięte dla zwięzłości) :
static int sigPipe[2]; static void gotSig ( int num ) { write(sigPipe[1], "!", 1); } int main ( void ) { pipe(sigPipe); /* use sigaction to point signal(s) at gotSig() */ FD_SET(sigPipe[0], &readFDs); for (;;) { n = select(nFDs, &readFDs, ...); if (FD_ISSET(sigPipe[0], &readFDs)) { read(sigPipe[0], ch, 1); /* do something about the signal here */ } /* ... the rest of your select loop */ } }
Jeśli obchodzi Cię, który to był sygnał, bajt w dół potoku może być numerem sygnału.
źródło
Możesz używać printf w programach obsługi sygnałów, jeśli używasz biblioteki pthread. unix / posix określa, że printf jest atomowy dla wątków, patrz odpowiedź Dave'a Butenhof tutaj: https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw Zauważ, że aby uzyskać wyraźniejszy obraz wyjścia printf, powinieneś uruchomić swoją aplikację w konsoli (w Linuksie użyj ctl + alt + f1, aby uruchomić konsolę 1), zamiast pseudo-tty utworzonego przez GUI.
źródło