it-swarm.com.de

Wie vermeide ich die Verwendung von printf in einem Signalhandler?

Da printf nicht wiedereintrittsfähig ist, sollte sie in einem Signalhandler nicht verwendet werden. Ich habe jedoch viele Beispielcodes gesehen, die printf auf diese Weise verwenden.

Meine Frage ist also: Wann müssen wir die Verwendung von printf in einem Signalhandler vermeiden und gibt es einen empfohlenen Ersatz?

73
Yu Hao

Sie können eine Flagvariable verwenden, dieses Flag innerhalb des Signalhandlers setzen und während des normalen Betriebs die printf()-Funktion in main () oder einem anderen Programmteil aufrufen. 

Es ist nicht sicher, alle Funktionen wie printf von einem Signalhandler aus aufzurufen. Eine nützliche Technik ist die Verwendung einer Signalbehandlungsroutine, um eine flag festzulegen und diese flag.__ zu überprüfen. aus dem Hauptprogramm und drucken Sie ggf. eine Meldung aus. 

Beachten Sie im nachstehenden Beispiel: Signalhandler Ding () setzt ein Flag alarm_fired auf 1, wenn SIGALRM aufgefangen wird, und in der Hauptfunktion alarm_fired wird geprüft, ob printf ordnungsgemäß aufgerufen wird. 

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: Beginn der Linux-Programmierung, 4. Ausgabe , In diesem Buch wird Ihr Code genau erklärt (was Sie möchten), Kapitel 11: Prozesse und Signale, Seite 484 

Darüber hinaus müssen Sie beim Schreiben von Handler-Funktionen besondere Vorsicht walten lassen, da sie asynchron aufgerufen werden können. Das heißt, ein Handler kann unvorhersehbar an jeder Stelle des Programms aufgerufen werden. Wenn zwei Signale in sehr kurzer Zeit eintreffen, kann ein Handler in einem anderen laufen. Es wird als bessere Praxis betrachtet, volatile sigatomic_t zu deklarieren. Auf diesen Typ wird immer atomar zugegriffen. Vermeiden Sie die Unsicherheit, wenn Sie den Zugriff auf eine Variable unterbrechen. (lese: Atomischer Datenzugriff und Signalverarbeitung für Detailauswertung). 

Lesen Definieren von Signal-Handlern : um zu lernen, wie eine Signal-Handler-Funktion geschrieben wird, die mit den Funktionen signal() oder sigaction() eingerichtet werden kann.
Liste der autorisierten Funktionen in manual page Der Aufruf dieser Funktion im Signalhandler ist sicher.

52
Grijesh Chauhan

Das Hauptproblem besteht darin, dass der interne Zustand vorübergehend inkonsistent sein kann, wenn das Signal malloc() oder eine ähnliche Funktion unterbricht, während Speicherblöcke zwischen der freien und der verwendeten Liste oder anderen ähnlichen Vorgängen verschoben werden. Wenn der Code im Signalhandler eine Funktion aufruft, die dann malloc() aufruft, kann dies die Speicherverwaltung vollständig ruinieren.

Der C-Standard sieht sehr konservativ aus, was Sie in einem Signal-Handler tun können:

ISO/IEC 9899: 2011 §7.14.1.1 Die Funktion signal

¶5 Wenn das Signal nicht als Ergebnis des Aufrufs der Funktion abort oder raise auftritt, ist das Verhalten undefiniert, wenn der Signalhandler auf ein Objekt mit statischer oder Thread-Speicherdauer verweist, das keine Sperre ist -freies atomares Objekt außer durch Zuweisen eines Werts zu einem als volatile sig_atomic_t deklarierten Objekt oder durch Aufrufen einer anderen Funktion in der Standardbibliothek als der Funktion abort, der Funktion _Exit, die Funktion quick_exit oder die Funktion signal, wobei das erste Argument der Signalnummer entspricht, die dem Signal entspricht, das den Aufruf des Handlers verursacht hat. Wenn ein solcher Aufruf der Funktion signal zu einer Rückgabe von SIG_ERR führt, ist der Wert von errno unbestimmt.252)

252) Wenn ein Signal von einem asynchronen Signalhandler generiert wird, ist das Verhalten undefiniert.

POSIX ist viel großzügiger in Bezug darauf, was Sie in einem Signal-Handler tun können.

Signalkonzepte In der POSIX 2008-Ausgabe heißt es:

Wenn es sich um einen Multithread-Prozess handelt oder wenn es sich um einen Single-Thread-Prozess handelt und ein Signal-Handler ausgeführt wird, der sich nicht aus folgenden Gründen ergibt:

  • Der Prozess, der abort(), raise(), kill(), pthread_kill() oder sigqueue() aufruft, um ein Signal zu generieren, das nicht blockiert ist

  • Ein anstehendes Signal wird entsperrt und zugestellt, bevor der entsperrende Anruf zurückkommt

das Verhalten ist undefiniert, wenn der Signalhandler auf ein anderes Objekt als errno mit einer anderen statischen Speicherdauer verweist, als indem einem als volatile sig_atomic_t deklarierten Objekt ein Wert zugewiesen wird, oder wenn der Signalhandler eine in definierte Funktion aufruft dieser Standard ist nicht eine der in der folgenden Tabelle aufgeführten Funktionen.

Die folgende Tabelle definiert eine Reihe von Funktionen, die asynchron signalsicher sein sollen. Daher können Anwendungen sie ohne Einschränkung über Signalfangfunktionen 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 aufgeführt sind, gelten in Bezug auf Signale als unsicher. Bei Vorhandensein von Signalen müssen sich alle Funktionen, die in dieser Ausgabe von POSIX.1-2008 definiert sind, wie definiert verhalten, wenn sie von einer Signalfangfunktion aufgerufen oder unterbrochen werden, mit einer einzigen Ausnahme: Wenn ein Signal eine unsichere Funktion unterbricht und das Signal catching function ruft eine unsichere Funktion auf, das Verhalten ist undefiniert.

Operationen, die den Wert errno erhalten, und Operationen, die errno einen Wert zuweisen, müssen asynchron signalsicher sein.

Wenn ein Signal an einen Thread übergeben wird und die Aktion dieses Signals das Beenden, Stoppen oder Fortsetzen angibt, muss der gesamte Prozess beendet, gestoppt oder fortgesetzt werden.

Die Funktionsfamilie printf() fehlt jedoch in dieser Liste und kann von einem Signalhandler möglicherweise nicht sicher aufgerufen werden.

Das POSIX 2016-Update erweitert die Liste der sicheren Funktionen, um insbesondere eine große Anzahl der Funktionen aus <string.h> zu berücksichtigen, bei dem es sich um a handelt besonders wertvolle Ergänzung (oder war ein besonders frustrierendes Versehen). 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()

Infolgedessen verwenden Sie entweder write() ohne die von printf() ua bereitgestellte Formatierungsunterstützung, oder Sie setzen ein Flag, das Sie (regelmäßig) an geeigneten Stellen im Code testen. Diese Technik wird in der Antwort von Grijesh Chauhan geschickt demonstriert.


Standard C-Funktionen und Signalsicherheit

chqrliefragt eine interessante Frage, auf die ich nur eine Teilantwort habe:

Wie kommt es, dass die meisten Zeichenfolgenfunktionen von <string.h> oder die Zeichenklassenfunktionen von <ctype.h> und viele andere C-Standardbibliotheksfunktionen nicht in der obigen Liste enthalten sind? Eine Implementierung muss absichtlich böse sein, um strlen() für den Aufruf von einem Signalhandler unsicher zu machen.

Für viele der Funktionen in <string.h> ist es schwer zu erkennen, warum sie nicht als asynchron signal sicher deklariert wurden, und ich würde zustimmen, dass die strlen() ein erstklassiges Beispiel ist, zusammen mit strchr(), strstr(), etc. Auf der anderen Seite Andererseits sind andere Funktionen wie strtok(), strcoll() und strxfrm() ziemlich komplex und sind wahrscheinlich nicht asynchron signalsicher. Da strtok() den Status zwischen Aufrufen beibehält und der Signalhandler nicht leicht erkennen konnte, ob ein Teil des Codes, der strtok() verwendet, fehlerhaft ist. Die Funktionen strcoll() und strxfrm() arbeiten mit länderspezifischen Daten, und das Laden der Ländereinstellung umfasst alle Arten von Statuseinstellungen.

Die Funktionen (Makros) von <ctype.h> sind alle vom Gebietsschema abhängig und können daher auf die gleichen Probleme wie strcoll() und strxfrm() stoßen.

Ich finde es schwer zu verstehen, warum die mathematischen Funktionen von <math.h> nicht für asynchrone Signale geeignet sind, es sei denn, sie könnten von einer SIGFPE-Ausnahme (Gleitkomma-Ausnahme) betroffen sein, obwohl ich nur eine davon sehe Diese Tage sind für ganzzahlige Division durch Null. Eine ähnliche Unsicherheit ergibt sich aus <complex.h>, <fenv.h> und <tgmath.h>.

Einige der Funktionen in <stdlib.h> könnten ausgenommen sein - zum Beispiel abs(). Andere sind besonders problematisch: malloc() und family sind Paradebeispiele.

Eine ähnliche Bewertung könnte für die anderen Überschriften in Standard C (2011) vorgenommen werden, die in einer POSIX-Umgebung verwendet werden. (Standard C ist so restriktiv, dass es kein Interesse gibt, sie in einer reinen Standard C-Umgebung zu analysieren.) Die als "Gebietsschema-abhängig" gekennzeichneten Bereiche sind unsicher, da die Manipulation von Gebietsschemata möglicherweise eine Speicherzuweisung usw. erfordert.

  • <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> - Gebietsschemasensitive Funktionen (unsicher)
  • <iso646.h> - Keine Funktionen
  • <limits.h> - Keine Funktionen
  • <locale.h> - Gebietsschemasensitive Funktionen (unsicher)
  • <math.h> - Möglicherweise sicher
  • <setjmp.h> - Nicht sicher
  • <signal.h> - Zulässig
  • <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> - Gebietsschema-abhängig (aber time() ist explizit erlaubt)
  • <uchar.h> - abhängig vom Gebietsschema
  • <wchar.h> - abhängig vom Gebietsschema
  • <wctype.h> - abhängig vom Gebietsschema

Das Analysieren der POSIX-Header wäre… schwieriger, da es viele gibt und einige Funktionen sicher sind, aber viele nicht…, aber auch einfacher, da POSIX angibt, welche Funktionen asynchron signalisiert sicher sind (nicht viele von ihnen). Beachten Sie, dass ein Header wie <pthread.h> drei sichere Funktionen und viele unsichere Funktionen hat.

NB: Fast die gesamte Bewertung von C-Funktionen und -Kopfzeilen in einer POSIX-Umgebung beruht auf halbherzigen Vermutungen. Es macht keinen Sinn, eine endgültige Aussage einer Normungsorganisation zu treffen.

49

Wie vermeiden Sie die Verwendung von printf in einem Signalhandler?

  1. Vermeide es immer, werde sagen: Benutze printf() nicht in Signalhandlern.

  2. Zumindest auf POSIX-konformen Systemen können Sie write(STDOUT_FILENO, ...) anstelle von printf() verwenden. Die Formatierung ist jedoch möglicherweise nicht einfach: Drucken von int aus dem Signalhandler mit schreibgeschützten oder asynchronen Funktionen

13
alk

Zu Debugging-Zwecken habe ich ein Tool geschrieben, das überprüft, ob Sie tatsächlich nur Funktionen in der Liste async-signal-safe aufrufen, und eine Warnmeldung für jede unsichere Funktion ausgibt, die innerhalb eines Signalkontexts aufgerufen wird. Es löst zwar nicht das Problem, nicht-asynchrone sichere Funktionen aus einem Signalkontext heraus aufrufen zu wollen, aber es hilft Ihnen zumindest, Fälle zu finden, in denen Sie dies versehentlich getan haben.

Der Quellcode ist auf GitHub . Es funktioniert, indem Sie signal/sigaction überladen und dann die PLT-Einträge unsicherer Funktionen temporär entführen. Dies führt dazu, dass Aufrufe an unsichere Funktionen an einen Wrapper umgeleitet werden.

6
dwks

Eine Technik, die besonders in Programmen mit Auswahlschleife nützlich ist, besteht darin, beim Empfang eines Signals ein einzelnes Byte in eine Pipe zu schreiben und dann das Signal in der Auswahlschleife zu behandeln. Etwas in dieser Richtung (Fehlerbehandlung und andere Details wurden der Kürze halber weggelassen)} _:

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 */
    }
}

Wenn Sie das Signal welches interessieren, dann kann das Byte in der Pipe die Signalnummer sein.

0
John Hascall

Sie können printf in Signalhandlern verwenden, wenn Sie die pthread-Bibliothek verwenden. unix/posix gibt an, dass printf für Threads atomar ist. Dave Butenhof antwortet hier: https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw Hinweis: Um ein klareres Bild der printf-Ausgabe zu erhalten, sollten Sie Ihre Anwendung in einer Konsole ausführen (unter Linux verwenden Sie ctl + alt + f1, um Konsole 1 zu starten), .__ Pseudo-tty erstellt von der GUI. 

0
drlolly

Implementiere deinen eigenen async-signal-sicheren snprintf("%d und verwende write

Es ist nicht so schlimm wie ich dachte, Wie konvertiere ich ein int in einen String in C? hat mehrere Implementierungen.

Da es nur zwei interessante Datentypen gibt, auf die Signalbehandler zugreifen können:

  • sig_atomic_t globals
  • int Signalargument

dies deckt grundsätzlich alle interessanten Anwendungsfälle ab.

Die Tatsache, dass strcpy auch Signalsicher ist, macht die Sache noch besser.

Das untenstehende POSIX-Programm gibt aus, wie oft SIGINT bisher empfangen wurde, was Sie mit Ctrl + C und der Signal-ID und auslösen können.

Sie können das Programm mit Ctrl + \ (SIGQUIT) beenden.

haupt 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:
 * https://stackoverflow.com/questions/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: https://stackoverflow.com/questions/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;
}

Kompilieren und ausführen:

gcc -std=c99 -Wall -Wextra -o main main.c
./main 1

Nach fünfzehnmaligem Drücken von Strg + C zeigt das Terminal:

^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

dabei ist 2 die Signalnummer für SIGINT.

Getestet auf Ubuntu 18.04. GitHub Upstream .