Das Hauptproblem besteht darin, dass das Signal malloc()
unterbricht oder einer ähnlichen Funktion, kann der interne Zustand vorübergehend inkonsistent sein, während er Speicherblöcke zwischen der freien und der belegten Liste oder anderen ähnlichen Operationen verschiebt. Wenn der Code im Signalhandler eine Funktion aufruft, die dann malloc()
aufruft , kann dies die Speicherverwaltung komplett ruinieren.
Der C-Standard betrachtet das, was Sie in einem Signal-Handler tun können, sehr konservativ:
ISO/IEC 9899:2011 §7.14.1.1 Der signal
Funktion
¶5 Wenn das Signal anders als als Ergebnis des Aufrufs von abort
auftritt oder raise
-Funktion ist das Verhalten undefiniert, wenn sich der Signal-Handler auf ein beliebiges Objekt mit statischer oder Thread-Speicherdauer bezieht, das kein lock-freies atomares Objekt ist, außer durch Zuweisen eines Werts zu einem als volatile sig_atomic_t
deklarierten Objekt , oder der Signal-Handler ruft eine andere Funktion in der Standardbibliothek als abort
auf Funktion, die _Exit
Funktion, diequick_exit
Funktion oder die signal
Funktion, wobei das erste Argument gleich der Signalnummer ist, die dem Signal entspricht, das den Aufruf des Handlers verursacht hat. Außerdem, wenn ein solcher Aufruf an signal
Funktion ergibt einen SIG_ERR
zurück, der Wert von errno
ist unbestimmt.
Wenn ein Signal von einem asynchronen Signalhandler generiert wird, ist das Verhalten undefiniert.
POSIX ist viel großzügiger, was Sie in einem Signal-Handler tun können.
Signal Concepts in der Ausgabe von POSIX 2008 sagt:
Wenn der Prozess multithreaded ist oder wenn der Prozess singlethreaded ist und ein Signalhandler ausgeführt wird, außer als Ergebnis von:
-
Der Prozess, der
abort()
aufruft ,raise()
,kill()
,pthread_kill()
, odersigqueue()
um ein nicht blockiertes Signal zu erzeugen -
Ein anstehendes Signal, das entsperrt und geliefert wird, bevor der Anruf, der es entsperrt hat, zurückkehrt
das Verhalten ist undefiniert, wenn der Signal-Handler auf ein anderes Objekt als errno
verweist mit statischer Speicherdauer anders als durch Zuweisen eines Werts zu einem als volatile sig_atomic_t
deklarierten Objekt , oder wenn der Signal-Handler eine andere in diesem Standard definierte Funktion als eine der in der folgenden Tabelle aufgeführten Funktionen aufruft.
Die folgende Tabelle definiert eine Reihe von Funktionen, die asynchronsignalsicher sein sollen. Daher können Anwendungen sie ohne Einschränkung von signalfangenden Funktionen aufrufen:
_Exit() fexecve() posix_trace_event() sigprocmask()
_exit() fork() pselect() sigqueue()
…
fcntl() pipe() sigpause() write()
fdatasync() poll() sigpending()
Alle Funktionen, die nicht in der obigen Tabelle enthalten sind, gelten als unsicher in Bezug auf Signale. In Anwesenheit von Signalen müssen sich alle in diesem Band von POSIX.1-2008 definierten Funktionen wie definiert verhalten, wenn sie von einer signalfangenden Funktion aufgerufen oder durch diese unterbrochen werden, mit einer einzigen Ausnahme:wenn ein Signal eine unsichere Funktion unterbricht und das Signal- Fangfunktion ruft eine unsichere Funktion auf, das Verhalten ist undefiniert.
Operationen, die den Wert von errno
erhalten und Operationen, die errno
einen Wert zuweisen soll async-signalsicher sein.
Wenn ein Signal an einen Thread geliefert wird und die Aktion dieses Signals Beendigung, Stopp oder Fortsetzen angibt, soll der gesamte Prozess beendet, gestoppt bzw. fortgesetzt werden.
Allerdings ist die printf()
Familie von Funktionen fehlt in dieser Liste und kann nicht sicher von einem Signal-Handler aufgerufen werden.
Die POSIX 2016 update erweitert die Liste der sicheren Funktionen insbesondere um eine Vielzahl der Funktionen aus <string.h>
, was eine besonders wertvolle Ergänzung ist (oder ein besonders frustrierendes Versehen war). Die Liste ist jetzt:
_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()
Als Ergebnis verwenden Sie entweder write()
ohne die von printf()
bereitgestellte Formatierungsunterstützung et al, oder Sie setzen am Ende ein Flag, das Sie (regelmäßig) an geeigneten Stellen in Ihrem Code testen. Diese Technik wird in der Antwort von Grijesh Chauhan gekonnt demonstriert.
Standard-C-Funktionen und Signalsicherheit
chqrlie stellt eine interessante Frage, auf die ich nur eine teilweise Antwort habe:
Wie kommen die meisten String-Funktionen von <string.h>
oder die Zeichenklassenfunktionen von <ctype.h>
und viele weitere Funktionen der C-Standardbibliothek sind nicht in der Liste oben? Eine Implementierung müsste absichtlich böse sein, um strlen()
zu machen Es ist nicht sicher, von einem Signal-Handler aus aufgerufen zu werden.
Für viele der Funktionen in <string.h>
, ist es schwer zu verstehen, warum sie nicht als asynchrones Signal sicher erklärt wurden, und ich würde dem strlen()
zustimmen ist zusammen mit strchr()
ein Paradebeispiel , strstr()
, usw. Andererseits andere Funktionen wie strtok()
, strcoll()
und strxfrm()
sind ziemlich komplex und wahrscheinlich nicht asynchronsignalsicher. Weil strtok()
behält den Zustand zwischen Aufrufen bei, und der Signal-Handler konnte nicht leicht feststellen, ob ein Teil des Codes strtok()
verwendet wäre durcheinander. Die strcoll()
und strxfrm()
Funktionen arbeiten mit Locale-sensiblen Daten, und das Laden des Locales beinhaltet alle möglichen Statuseinstellungen.
Die Funktionen (Makros) von <ctype.h>
sind alle gebietsschemaabhängig und könnten daher auf die gleichen Probleme wie strcoll()
stoßen und strxfrm()
.
Ich finde es schwer zu verstehen, warum die mathematischen Funktionen von <math.h>
sind nicht sicher für asynchrone Signale, es sei denn, sie könnten von einer SIGFPE (Gleitkomma-Ausnahme) betroffen sein, obwohl ich heutzutage nur noch eine für Integer sehe Durch Null teilen. Eine ähnliche Unsicherheit ergibt sich aus <complex.h>
, <fenv.h>
und <tgmath.h>
.
Einige der Funktionen in <stdlib.h>
ausgenommen werden könnte – abs()
zum Beispiel. Andere sind besonders problematisch:malloc()
und Familie sind Paradebeispiele.
Eine ähnliche Bewertung könnte für die anderen Header in Standard C (2011) vorgenommen werden, die in einer POSIX-Umgebung verwendet werden. (Standard-C ist so restriktiv, dass es kein Interesse daran gibt, sie in einer reinen Standard-C-Umgebung zu analysieren.) Diejenigen, die als „gebietsschemaabhängig“ gekennzeichnet sind, sind unsicher, weil das Manipulieren von Gebietsschemas eine Speicherzuweisung usw. erfordern könnte.
<assert.h>
— Wahrscheinlich nicht sicher<complex.h>
— Möglicherweise sicher<ctype.h>
— Nicht sicher<errno.h>
— Sicher<fenv.h>
— Wahrscheinlich nicht sicher<float.h>
— Keine Funktionen<inttypes.h>
— Locale-sensitive Funktionen (unsicher)<iso646.h>
— Keine Funktionen<limits.h>
— Keine Funktionen<locale.h>
— Locale-sensitive Funktionen (unsicher)<math.h>
— Möglicherweise sicher<setjmp.h>
— Nicht sicher<signal.h>
— Erlaubt<stdalign.h>
— Keine Funktionen<stdarg.h>
— Keine Funktionen<stdatomic.h>
— Möglicherweise sicher, wahrscheinlich nicht sicher<stdbool.h>
— Keine Funktionen<stddef.h>
— Keine Funktionen<stdint.h>
— Keine Funktionen<stdio.h>
— Nicht sicher<stdlib.h>
— Nicht alle sicher (einige sind erlaubt, andere nicht)<stdnoreturn.h>
— Keine Funktionen<string.h>
— Nicht alle sicher<tgmath.h>
— Möglicherweise sicher<threads.h>
— Wahrscheinlich nicht sicher<time.h>
— Abhängig vom Gebietsschema (abertime()
ist ausdrücklich erlaubt)<uchar.h>
— Gebietsschemaabhängig<wchar.h>
— Gebietsschemaabhängig<wctype.h>
— Gebietsschemaabhängig
Die Analyse der POSIX-Header wäre … schwieriger, da es viele davon gibt, und einige Funktionen sicher sein könnten, aber viele nicht … aber auch einfacher, weil POSIX sagt, welche Funktionen asynchrones Signal sicher sind (nicht viele von ihnen). Beachten Sie, dass ein Header wie <pthread.h>
hat drei sichere Funktionen und viele unsichere Funktionen.
Hinweis: Fast die gesamte Bewertung von C-Funktionen und -Headern in einer POSIX-Umgebung ist halbwegs fundierte Vermutung. Es macht keinen Sinn, eine endgültige Aussage einer Normungsorganisation zu machen.
So vermeiden Sie die Verwendung von printf
in einem Signalhandler?
-
Vermeiden Sie es immer, wird sagen:Verwenden Sie einfach nicht
printf()
in Signalhandlern. -
Zumindest auf POSIX-konformen Systemen können Sie
write(STDOUT_FILENO, ...)
verwenden stattprintf()
. Die Formatierung ist jedoch möglicherweise nicht einfach:Drucken Sie int vom Signalhandler mit schreib- oder asynchronsicheren Funktionen
Sie können eine Flag-Variable verwenden, dieses Flag innerhalb des Signal-Handlers setzen und basierend auf diesem Flag printf()
aufrufen Funktion in main() oder einem anderen Teil des Programms während des normalen Betriebs.
Es ist nicht sicher, alle Funktionen aufzurufen, wie z. B. printf
, innerhalb eines Signalhandlers. Eine nützliche Technik besteht darin, einen Signalhandler zu verwenden, um einen flag
zu setzen und überprüfen Sie dann flag
aus dem Hauptprogramm und drucken Sie bei Bedarf eine Nachricht aus.
Beachten Sie im Beispiel unten, dass der Signalhandler ding() ein Flag alarm_fired
setzt auf 1 als SIGALRM abgefangen und in Hauptfunktion alarm_fired
value wird untersucht, um printf bedingt korrekt aufzurufen.
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);
}
Referenz:Beginning Linux Programming, 4th Edition, In diesem Buch wird genau Ihr Code erklärt (was Sie wollen), Kapitel 11:Prozesse und Signale, Seite 484
Darüber hinaus müssen Sie beim Schreiben von Handlerfunktionen besondere Sorgfalt walten lassen, da sie asynchron aufgerufen werden können. Das heißt, ein Handler kann unvorhersehbar an jedem Punkt im Programm aufgerufen werden. Wenn zwei Signale in einem sehr kurzen Intervall eintreffen, kann ein Handler in einem anderen laufen. Und es wird als bessere Vorgehensweise angesehen, volatile sigatomic_t
zu deklarieren , auf diesen Typ wird immer atomar zugegriffen, vermeiden Sie Ungewissheit über das Unterbrechen des Zugriffs auf eine Variable. (Lesen Sie:Atomic Data Access and Signal Handling für detaillierte Sühne).
Lesen Sie Signal-Handler definieren:um zu lernen, wie man eine Signal-Handler-Funktion schreibt, die mit signal()
eingerichtet werden kann oder sigaction()
Funktionen.
Liste der autorisierten Funktionen in der Handbuchseite, der Aufruf dieser Funktion innerhalb des Signalhandlers ist sicher.