GNU/Linux >> Linux Esercitazione >  >> Linux

Come evitare di usare printf in un gestore di segnale?

Il problema principale è che se il segnale interrompe malloc() o qualche funzione simile, lo stato interno potrebbe essere temporaneamente incoerente durante lo spostamento di blocchi di memoria tra l'elenco disponibile e utilizzato o altre operazioni simili. Se il codice nel gestore di segnale chiama una funzione che quindi invoca malloc() , questo potrebbe rovinare completamente la gestione della memoria.

Lo standard C ha una visione molto prudente di ciò che puoi fare in un gestore di segnali:

ISO/IEC 9899:2011 §7.14.1.1 Il signal funzione

¶5 Se il segnale si verifica in modo diverso dal risultato della chiamata al abort o raise funzione, il comportamento è indefinito se il gestore del segnale fa riferimento a qualsiasi oggetto con durata statica o threadstorage che non sia un oggetto atomico privo di lock se non assegnando un valore a un oggetto dichiarato come volatile sig_atomic_t , oppure il gestore del segnale chiama qualsiasi funzione nella libreria standard diversa da abort funzione, il _Exit funzione, il quick_exit funzione o signal funzione con il primo argomento uguale al numero di segnale corrispondente al segnale che ha causato l'invocazione dell'handler. Inoltre, se tale chiamata al signal funzione risulta in un SIG_ERR return, il valore di errno è indeterminato.

Se un segnale è generato da un gestore di segnale asincrono, il comportamento è indefinito.

POSIX è molto più generoso su ciò che puoi fare in un gestore di segnali.

Signal Concepts nell'edizione POSIX 2008 dice:

Se il processo è multi-thread o se il processo è single-thread e viene eseguito un gestore di segnale diverso da come risultato di:

  • Il processo che chiama abort() , raise() , kill() , pthread_kill() o sigqueue() per generare un segnale che non sia bloccato

  • Un segnale in attesa di essere sbloccato e consegnato prima che ritorni la chiamata che lo ha sbloccato

il comportamento non è definito se il gestore del segnale fa riferimento a qualsiasi oggetto diverso da errno con durata di archiviazione statica diversa dall'assegnazione di un valore a un oggetto dichiarato come volatile sig_atomic_t , o se il gestore del segnale chiama qualsiasi funzione definita in questo standard diversa da una delle funzioni elencate nella tabella seguente.

La tabella seguente definisce un insieme di funzioni che devono essere async-signal-safe. Pertanto, le applicazioni possono invocarli, senza restrizioni, dalle funzioni di cattura del segnale:

_Exit()             fexecve()           posix_trace_event() sigprocmask()
_exit()             fork()              pselect()           sigqueue()
…
fcntl()             pipe()              sigpause()          write()
fdatasync()         poll()              sigpending()

Tutte le funzioni non presenti nella tabella precedente sono considerate non sicure rispetto ai segnali. In presenza di segnali, tutte le funzioni definite da questo volume di POSIX.1-2008 devono comportarsi come definito quando chiamate da o interrotte da una funzione di cattura del segnale, con una sola eccezione:quando un segnale interrompe una funzione non sicura e il segnale- catching chiama una funzione unsafe, il comportamento non è definito.

Operazioni che ottengono il valore di errno e operazioni che assegnano un valore a errno sarà async-signal-safe.

Quando un segnale viene consegnato a un thread, se l'azione di quel segnale specifica la terminazione, l'arresto o la continuazione, l'intero processo deve essere terminato, interrotto o continuato, rispettivamente.

Tuttavia, il printf() famiglia di funzioni è notevolmente assente da tale elenco e potrebbe non essere chiamata in modo sicuro da un gestore di segnale.

Il POSIX 2016 update estende l'elenco delle funzioni sicure per includere, in particolare, un gran numero di funzioni da <string.h> , che è un'aggiunta particolarmente preziosa (o è stata una svista particolarmente frustrante). L'elenco ora è:

_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()

Di conseguenza, finisci per usare write() senza il supporto per la formattazione fornito da printf() et al, oppure si finisce per impostare un flag che si verifica (periodicamente) in punti appropriati nel codice. Questa tecnica è abilmente dimostrata nella risposta di Grijesh Chauhan.

Funzioni C standard e sicurezza del segnale

chqrlie pone una domanda interessante, alla quale non ho che una risposta parziale:

Come mai la maggior parte delle funzioni di stringa da <string.h> o le funzioni della classe di caratteri da <ctype.h> e molte altre funzioni della libreria standard C non sono nell'elenco sopra? Un'implementazione dovrebbe essere volutamente malvagia per rendere strlen() non sicuro da chiamare da un gestore di segnali.

Per molte delle funzioni in <string.h> , è difficile capire perché non siano stati dichiarati sicuri per il segnale asincrono e concordo con il strlen() è un ottimo esempio, insieme a strchr() , strstr() , ecc. D'altra parte, altre funzioni come strtok() , strcoll() e strxfrm() sono piuttosto complessi e probabilmente non sono sicuri per il segnale asincrono. Perché strtok() mantiene lo stato tra le chiamate e il gestore del segnale non è in grado di stabilire facilmente se una parte del codice che utilizza strtok() sarebbe incasinato. Il strcoll() e strxfrm() le funzioni funzionano con dati sensibili alla locale e il caricamento della locale implica ogni tipo di impostazione dello stato.

Le funzioni (macro) da <ctype.h> sono tutti sensibili alle impostazioni locali e pertanto potrebbero incorrere negli stessi problemi di strcoll() e strxfrm() .

Trovo difficile capire perché le funzioni matematiche da <math.h> non sono sicuri per il segnale asincrono, a meno che non sia perché potrebbero essere influenzati da un SIGFPE (eccezione a virgola mobile), anche se l'unica volta che ne vedo uno in questi giorni è per integer divisione per zero. Un'incertezza simile deriva da <complex.h> , <fenv.h> e <tgmath.h> .

Alcune delle funzioni in <stdlib.h> potrebbe essere esentato — abs() Per esempio. Altri sono specificamente problematici:malloc() e la famiglia sono ottimi esempi.

Analoga valutazione potrebbe essere fatta per le altre intestazioni dello Standard C (2011) utilizzate in ambiente POSIX. (Lo standard C è così restrittivo che non c'è alcun interesse ad analizzarli in un ambiente C standard puro.) Quelli contrassegnati come "dipendenti dalle impostazioni locali" non sono sicuri perché la manipolazione delle impostazioni locali potrebbe richiedere l'allocazione di memoria, ecc.

  • <assert.h>Probabilmente non sicuro
  • <complex.h>Possibilmente al sicuro
  • <ctype.h> — Non sicuro
  • <errno.h> — Sicuro
  • <fenv.h>Probabilmente non sicuro
  • <float.h> — Nessuna funzione
  • <inttypes.h> — Funzioni sensibili alla localizzazione (non sicure)
  • <iso646.h> — Nessuna funzione
  • <limits.h> — Nessuna funzione
  • <locale.h> — Funzioni sensibili alla localizzazione (non sicure)
  • <math.h>Possibilmente al sicuro
  • <setjmp.h> — Non sicuro
  • <signal.h> — Consentito
  • <stdalign.h> — Nessuna funzione
  • <stdarg.h> — Nessuna funzione
  • <stdatomic.h>Forse sicuro, probabilmente non sicuro
  • <stdbool.h> — Nessuna funzione
  • <stddef.h> — Nessuna funzione
  • <stdint.h> — Nessuna funzione
  • <stdio.h> — Non sicuro
  • <stdlib.h> — Non tutti sicuri (alcuni sono consentiti, altri no)
  • <stdnoreturn.h> — Nessuna funzione
  • <string.h> — Non tutti sicuri
  • <tgmath.h>Possibilmente al sicuro
  • <threads.h>Probabilmente non sicuro
  • <time.h> — Dipendente dalla localizzazione (ma time() è esplicitamente consentito)
  • <uchar.h> — Dipendente dalle impostazioni locali
  • <wchar.h> — Dipendente dalle impostazioni locali
  • <wctype.h> — Dipendente dalle impostazioni locali

Analizzare le intestazioni POSIX sarebbe... più difficile in quanto ce ne sono molte e alcune funzioni potrebbero essere sicure ma molte non lo saranno... ma anche più semplice perché POSIX dice quali funzioni sono sicure per il segnale asincrono (non molte di esse). Nota che un'intestazione come <pthread.h> ha tre funzioni sicure e molte funzioni non sicure.

NB: Quasi tutta la valutazione delle funzioni e delle intestazioni C in un ambiente POSIX è un'ipotesi semi-istruita. Non ha senso una dichiarazione definitiva da parte di un ente normativo.


Come evitare di usare printf in un gestore di segnali?

  1. Evitalo sempre, dirà:Basta non usare printf() nei gestori di segnale.

  2. Almeno su sistemi conformi a POSIX, puoi usare write(STDOUT_FILENO, ...) invece di printf() . Tuttavia, la formattazione potrebbe non essere semplice:stampa int dal gestore di segnale utilizzando le funzioni write o async-safe


Puoi usare qualche variabile flag, impostare quel flag all'interno del gestore del segnale e, in base a quel flag, chiamare printf() funzione in main() o altra parte del programma durante il normale funzionamento.

Non è sicuro chiamare tutte le funzioni, come printf , dall'interno di un gestore di segnale. Una tecnica utile consiste nell'utilizzare un gestore di segnale per impostare un flag e poi controlla che flag dal programma principale e stampare un messaggio se richiesto.

Si noti nell'esempio seguente, il gestore di segnale ding() imposta un flag alarm_fired a 1 come SIGALRM rilevato e nella funzione principale alarm_fired value viene esaminato per chiamare correttamente printf in modo condizionale.

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);
}

Riferimento:Beginning Linux Programming, 4th Edition, In questo libro viene spiegato esattamente il tuo codice (quello che vuoi), Capitolo 11:Processi e segnali, pagina 484

Inoltre, è necessario prestare particolare attenzione nella scrittura delle funzioni del gestore perché possono essere chiamate in modo asincrono. Cioè, un gestore potrebbe essere chiamato in qualsiasi punto del programma, in modo imprevedibile. Se due segnali arrivano durante un intervallo molto breve, un gestore può essere eseguito all'interno di un altro. Ed è considerata una pratica migliore dichiarare volatile sigatomic_t , questo tipo è sempre accessibile in modo atomico, evita l'incertezza sull'interruzione dell'accesso a una variabile. (leggi:Accesso ai dati atomici e gestione del segnale per l'espiazione dei dettagli).

Leggi Defining Signal Handlers :per imparare a scrivere una funzione di signal handler che può essere stabilita con signal() o sigaction() funzioni.
Elenco delle funzioni autorizzate nella pagina di manuale, chiamare questa funzione all'interno del gestore del segnale è sicuro.


Linux
  1. Come scrivo caratteri non ASCII usando echo?

  2. Come scrivere un gestore di segnale per catturare SIGSEGV?

  3. Accodamento del segnale in C

  4. Durante l'utilizzo di printf come sfuggire ai caratteri speciali nello script di shell?

  5. Come riempire un file con FF usando dd?

Come creare un server CS:GO su VPS Linux

Come passare da un TTY all'altro senza utilizzare i tasti funzione in Linux

Procedura:un'introduzione all'uso di Git

Come connettersi a MySQL usando PHP

Come posso evitare i prompt durante l'utilizzo di azcopy su Linux in uno script?

Come rimuovere un file senza usare rm?