Como evitar o uso de printf em um manipulador de sinal?

86

Como printfnão é reentrante, não é seguro usá-lo em um manipulador de sinal. Mas eu vi muitos códigos de exemplo que usam printfessa maneira.

Portanto, minha pergunta é: quando devemos evitar o uso printfem um manipulador de sinal e há uma substituição recomendada?

Yu Hao
fonte
12
Uma resposta simples e não muito útil para a pergunta em seu título: vê aquela printfchamada naquele manipulador de sinais? Delete isso.
Keith Thompson
6
Olá Yu Hao! Acho que você achará o link muito interessante de ler. "Use funções reentrantes para um manuseio mais seguro de sinais" Eu li depois de tanto tempo, gostaria de compartilhar o artificial aqui com você. Espero que goste.
Grijesh Chauhan

Respostas:

58

Você pode usar alguma variável de sinalizador, definir esse sinalizador dentro do manipulador de sinal e com base nessa printf()função de chamada de sinalizador em main () ou outra parte do programa durante a operação normal.

Não é seguro chamar todas as funções, como printf, de dentro de um manipulador de sinal. Uma técnica útil é usar um manipulador de sinal para definir um flage, em seguida, verificar flag no programa principal e imprimir uma mensagem, se necessário.

Observe no exemplo abaixo, o manipulador de sinal ding () define um sinalizador alarm_firedcomo 1 quando SIGALRM é capturado e o alarm_firedvalor da função principal é examinado para chamar condicionalmente printf corretamente.

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

Referência: Iniciando a Programação do Linux, 4ª Edição , Neste livro exatamente o seu código é explicado (o que você deseja), Capítulo 11: Processos e Sinais, página 484

Além disso, você precisa ter cuidado especial ao escrever funções de manipulador, pois elas podem ser chamadas de forma assíncrona. Ou seja, um manipulador pode ser chamado em qualquer ponto do programa, de forma imprevisível. Se dois sinais chegarem durante um intervalo muito curto, um manipulador pode funcionar dentro de outro. E é considerada a melhor prática declarar volatile sigatomic_t, este tipo sempre é acessado atomicamente, evita incertezas sobre a interrupção do acesso a uma variável. (leia: Acesso Atômico a Dados e Manuseio de Sinais para expiação de detalhes).

Leia Definindo Manipuladores de Sinal : para aprender como escrever uma função de manipulador de sinal que pode ser estabelecida com as funções signal()ou sigaction().
Lista de funções autorizadas na página de manual , chamar esta função dentro do manipulador de sinal é seguro.

Grijesh Chauhan
fonte
18
É considerada uma prática recomendada declararvolatile sigatomic_t alarm_fired;
Basile Starynkevitch
1
@GrijeshChauhan: se estamos trabalhando em um código de produto, então não podemos chamar a função de pausa, o fluxo pode estar em qualquer lugar quando o sinal ocorrer, então nesse caso realmente não sabemos onde manter "if (alarm_fired) printf (" Ding! \ n ");" em código.
pankaj kushwaha
@pankajkushwaha sim, você está correto, ele está sofrendo de condição racial
Grijesh Chauhan
@GrijeshChauhan, há duas coisas que eu não conseguia entender. 1. Como saber quando verificar a bandeira? Portanto, haverá vários pontos de verificação no código em quase todos os pontos a serem impressos. 2. Definitivamente haverá condições de corrida em que o sinal pode ser chamado antes do registro do sinal ou o sinal pode ocorrer após o ponto de verificação. Acho que isso só vai ajudar na impressão em algumas condições, mas não resolve completamente o problema.
Darshan b
52

O principal problema é que se o sinal for interrompido malloc()ou alguma função semelhante, o estado interno pode ficar temporariamente inconsistente enquanto move blocos de memória entre a lista livre e a lista usada ou outras operações semelhantes. Se o código no manipulador de sinal chamar uma função que então invoca malloc(), isso pode destruir completamente o gerenciamento de memória.

O padrão C tem uma visão muito conservadora do que você pode fazer em um manipulador de sinal:

ISO / IEC 9899: 2011 §7.14.1.1 A signalfunção

¶5 Se o sinal ocorrer diferente do resultado da chamada da função abortou raise, o comportamento será indefinido se o manipulador de sinal se referir a qualquer objeto com duração de armazenamento estático ou thread que não seja um objeto atômico livre de bloqueio, exceto por atribuição de um valor a um objeto declarado como volatile sig_atomic_t, ou o manipulador de sinal chama qualquer função na biblioteca padrão diferente da abortfunção, a _Exitfunção, a quick_exitfunção ou a signalfunção com o primeiro argumento igual ao número do sinal correspondente ao sinal que causou a invocação do manipulador. Além disso, se essa chamada para a signalfunção resultar em um SIG_ERRretorno, o valor de errnoé indeterminado. 252)

252) Se algum sinal for gerado por um manipulador de sinal assíncrono, o comportamento é indefinido.

POSIX é muito mais generoso sobre o que você pode fazer em um manipulador de sinal.

Signal Concepts na edição POSIX 2008 diz:

Se o processo for multi-threaded ou se o processo for single-threaded e um manipulador de sinal for executado diferente do resultado de:

  • O chamado processo abort(), raise(), kill(), pthread_kill(), ou sigqueue()para gerar um sinal de que não está bloqueado

  • Um sinal pendente sendo desbloqueado e sendo entregue antes da chamada que o desbloqueou retorna

o comportamento é indefinido se o manipulador de sinal se referir a qualquer objeto que não seja errnocom duração de armazenamento estático diferente de atribuir um valor a um objeto declarado como volatile sig_atomic_t, ou se o manipulador de sinal chamar qualquer função definida neste padrão diferente de uma das funções listadas em a seguinte tabela.

A tabela a seguir define um conjunto de funções que devem ser seguras para sinais assíncronos. Portanto, os aplicativos podem invocá-los, sem restrição, a partir de funções de captura de sinal:

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

Todas as funções que não estão na tabela acima são consideradas inseguras em relação aos sinais. Na presença de sinais, todas as funções definidas por este volume de POSIX.1-2008 devem se comportar conforme definido quando chamadas de ou interrompidas por uma função de captura de sinal, com uma única exceção: quando um sinal interrompe uma função insegura e o sinal- a função de captura chama uma função não segura, o comportamento é indefinido.

As operações que obtêm o valor de errnoe as operações que atribuem um valor a errnodevem ser assíncronas sem sinal.

Quando um sinal é entregue a uma thread, se a ação desse sinal especifica término, parada ou continuação, todo o processo deve ser finalizado, interrompido ou continuado, respectivamente.

No entanto, a printf()família de funções está notavelmente ausente dessa lista e não pode ser chamada com segurança de um manipulador de sinal.

A atualização POSIX 2016 estende a lista de funções seguras para incluir, em particular, um grande número de funções de <string.h>, o que é uma adição particularmente valiosa (ou foi um descuido particularmente frustrante). A lista agora é:

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

Como resultado, você acaba usando write()sem o suporte de formatação fornecido por printf()et al, ou acaba definindo um sinalizador que testa (periodicamente) em locais apropriados em seu código. Essa técnica é habilmente demonstrada na resposta de Grijesh Chauhan .


Funções C padrão e segurança de sinal

chqrlie faz uma pergunta interessante, para a qual não tenho mais do que uma resposta parcial:

Por que a maioria das funções de string de <string.h>ou de funções de classe de caractere <ctype.h>e muitas outras funções de biblioteca padrão C não estão na lista acima? Uma implementação precisaria ser propositalmente má para tornar strlen()insegura a chamada de um manipulador de sinal.

Para muitas das funções em <string.h>, é difícil ver por que eles não foram declaradas seguras sinal assíncrono, e eu concordo o strlen()é um excelente exemplo, juntamente com strchr(), strstr(), etc. Por outro lado, outras funções como strtok(), strcoll()e strxfrm()são bastante complexos e provavelmente não são seguros para sinais assíncronos. Porque strtok()retém o estado entre as chamadas, o manipulador de sinais não poderia dizer facilmente se alguma parte do código que está usando strtok()seria bagunçada. As funções strcoll()e strxfrm()trabalham com dados sensíveis à localidade, e o carregamento da localidade envolve todos os tipos de configuração de estado.

As funções (macros) de <ctype.h>são todas sensíveis à localidade e, portanto, podem ter os mesmos problemas que strcoll()e strxfrm().

Acho difícil ver por que as funções matemáticas de <math.h>não são seguras para sinais assíncronos, a menos que seja porque elas podem ser afetadas por um SIGFPE (exceção de ponto flutuante), embora seja a única vez que vejo um desses hoje em dia para inteiros divisão por zero. Incerteza semelhante surge de <complex.h>, <fenv.h>e <tgmath.h>.

Algumas das funções em <stdlib.h>podem ser isentas - abs()por exemplo. Outros são especificamente problemáticos: malloc()e a família são os principais exemplos.

Uma avaliação semelhante poderia ser feita para os outros cabeçalhos na Norma C (2011) usados ​​em um ambiente POSIX. (O padrão C é tão restritivo que não há interesse em analisá-los em um ambiente puro do padrão C). Aqueles marcados como 'dependentes de localidade' não são seguros porque a manipulação de localidades pode exigir alocação de memória, etc.

  • <assert.h>- Provavelmente não é seguro
  • <complex.h>- Possivelmente seguro
  • <ctype.h> - Não é seguro
  • <errno.h> - Seguro
  • <fenv.h>- Provavelmente não é seguro
  • <float.h> - Sem funções
  • <inttypes.h> - Funções sensíveis à localidade (inseguro)
  • <iso646.h> - Sem funções
  • <limits.h> - Sem funções
  • <locale.h> - Funções sensíveis à localidade (inseguro)
  • <math.h>- Possivelmente seguro
  • <setjmp.h> - Não é seguro
  • <signal.h> - Permitido
  • <stdalign.h> - Sem funções
  • <stdarg.h> - Sem funções
  • <stdatomic.h>- Possivelmente seguro, provavelmente não seguro
  • <stdbool.h> - Sem funções
  • <stddef.h> - Sem funções
  • <stdint.h> - Sem funções
  • <stdio.h> - Não é seguro
  • <stdlib.h> - Nem todos são seguros (alguns são permitidos; outros não)
  • <stdnoreturn.h> - Sem funções
  • <string.h> - Nem tudo seguro
  • <tgmath.h>- Possivelmente seguro
  • <threads.h>- Provavelmente não é seguro
  • <time.h>- Depende da localidade (mas time()é explicitamente permitido)
  • <uchar.h> - Depende da localidade
  • <wchar.h> - Depende da localidade
  • <wctype.h> - Depende da localidade

Analisar os cabeçalhos POSIX seria ... mais difícil porque há muitos deles, e algumas funções podem ser seguras, mas muitas não ... mas também mais simples porque o POSIX diz quais funções são seguras para sinais assíncronos (não muitas delas). Observe que um cabeçalho como <pthread.h>tem três funções seguras e muitas funções não seguras.

NB: Quase toda a avaliação das funções C e cabeçalhos em um ambiente POSIX são suposições semi-educadas. Não faz sentido uma declaração definitiva de um organismo de padrões.

Jonathan Leffler
fonte
Por que a maioria das funções de string de <string.h>ou de funções de classe de caractere <ctype.h>e muitas outras funções de biblioteca padrão C não estão na lista acima? Uma implementação precisaria ser propositalmente má para tornar strlen()insegura a chamada de um manipulador de sinal.
chqrlie
@chqrlie: pergunta interessante - veja a atualização (não havia como encaixar muito nos comentários de forma sensata).
Jonathan Leffler
Obrigado por sua análise aprofundada. Em relação às <ctype.h>coisas, é específico do locale e pode causar problemas se o sinal interromper uma função de configuração do locale, mas uma vez que o local é carregado, usá-los deve ser seguro. Eu acho que, em algumas situações complexas, o carregamento dos dados de localidade pode ser feito de forma incremental, tornando as funções <ctype.h>inseguras. A conclusão permanece: na dúvida, abstenha-se.
chqrlie
@chqrlie: Eu concordo que a moral da história deve ser. Na dúvida, se abstenha . É um bom resumo.
Jonathan Leffler
13

Como evitar o uso printfem um manipulador de sinal?

  1. Sempre evite, dirá: Apenas não use printf()em manipuladores de sinal.

  2. Pelo menos em sistemas em conformidade com POSIX, você pode usar em write(STDOUT_FILENO, ...)vez de printf(). A formatação pode não ser fácil, no entanto: Imprimir int do manipulador de sinal usando funções de gravação ou de segurança assíncrona

alk
fonte
1
Alk Always avoid it.significa? Evitar printf()?
Grijesh Chauhan
2
@GrijeshChauhan: Sim, já que o OP estava perguntando quando evitar o uso de printf()manipuladores de sinal.
alk
Alk +1 para 2ponto, verifique OP perguntando como evitar o uso printf()em manipuladores de sinal?
Grijesh Chauhan
7

Para fins de depuração, escrevi uma ferramenta que verifica se você está, na verdade, apenas chamando funções da async-signal-safelista e imprime uma mensagem de aviso para cada função não segura chamada em um contexto de sinal. Embora não resolva o problema de querer chamar funções não seguras de forma assíncrona a partir de um contexto de sinal, pelo menos ajuda a encontrar casos em que você fez isso acidentalmente.

O código-fonte está no GitHub . Ele funciona sobrecarregando e signal/sigaction, em seguida, sequestrando temporariamente as PLTentradas de funções não seguras; isso faz com que as chamadas para funções não seguras sejam redirecionadas para um wrapper.

dwks
fonte
Solicitação de recurso GCC: gcc.gnu.org/ml/gcc-help/2012-03/msg00210.html
Ciro Santilli 郝海东 冠状 病 六四 六四 事件 法轮功
1

Implemente seu próprio sinal assíncrono seguro snprintf("%de usewrite

Não é tão ruim quanto eu pensava, como converter um int em string em C? tem várias implementações.

Uma vez que existem apenas dois tipos interessantes de dados que os manipuladores de sinais podem acessar:

  • sig_atomic_t globais
  • int argumento de sinal

isso basicamente cobre todos os casos de uso interessantes.

O fato de strcpytambém ser seguro para sinais torna as coisas ainda melhores.

O programa POSIX abaixo imprime para padrão o número de vezes que recebeu SIGINT até agora, com o qual você pode disparar Ctrl + C, e o ID do sinal e.

Você pode sair do programa com 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;
}

Compile e execute:

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

Depois de pressionar Ctrl + C quinze vezes, o terminal mostra:

^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

onde 2é o número do sinal para SIGINT.

Testado no Ubuntu 18.04. GitHub upstream .

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fonte
0

Uma técnica que é especialmente útil em programas que possuem um loop de seleção é escrever um único byte em um tubo ao receber um sinal e, em seguida, tratar o sinal no loop de seleção. Algo nessa linha (tratamento de erros e outros detalhes omitidos por questões de brevidade) :

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

Se você se importa com qual sinal era, então o byte no cano pode ser o número do sinal.

John Hascall
fonte
-1

Você pode usar printf em manipuladores de sinal se estiver usando a biblioteca pthread. unix / posix especifica que printf é atômico para tópicos cf Dave Butenhof responda aqui: https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw Observe que, para obter uma imagem mais clara da saída de printf, você deve executar seu aplicativo em um console (no linux, use ctl + alt + f1 para iniciar o console 1), em vez de um pseudo-tty criado pela GUI.

drlolly
fonte
3
Os manipuladores de sinal não são executados em algum segmento separado, eles são executados no contexto do segmento que estava em execução quando ocorreu a interrupção do sinal. Esta resposta está completamente errada.
itaych