¿Cómo evitar el uso de printf en un controlador de señal?

16 minutos de lectura

avatar de usuario
yuhao

Ya que printf no es reentrante, no se supone que sea seguro usarlo en un controlador de señales. Pero he visto muchos códigos de ejemplo que usan printf Por aquí.

Así que mi pregunta es: ¿cuándo debemos evitar usar printf en un controlador de señal, y hay un reemplazo recomendado?

  • Una respuesta simple y no muy útil a la pregunta en su título: Vea eso printf llamar a ese manipulador de señales? Bórralo.

    –Keith Thompson

    6 de junio de 2013 a las 18:04

  • ¡Hola YuHao! Creo que encontrará un enlace muy interesante para leer. “Utilice funciones reentrantes para un manejo de señales más seguro” Lo he leído después de tanto tiempo, me gustaría compartir lo artificial aquí con ustedes. Espero que lo disfrutes.

    –Grijesh Chauhan

    26 de abril de 2014 a las 9:18

  • Consulte también Print int desde el controlador de señales usando funciones seguras de escritura o asíncronas.

    – jww

    28 de enero de 2018 a las 1:34


avatar de usuario
jonathan leffler

El problema principal es que si la señal interrumpe malloc() o alguna función similar, el estado interno puede ser temporalmente inconsistente mientras mueve bloques de memoria entre la lista libre y usada, u otras operaciones similares. Si el código en el controlador de señales llama a una función que luego invoca malloc()esto puede arruinar por completo la gestión de la memoria.

El estándar C tiene una visión muy conservadora de lo que puede hacer en un controlador de señales:

ISO/IEC 9899:2011 §7.14.1.1 El signal función

¶5 Si la señal no se produce como resultado de llamar al abort o raise función, el comportamiento no está definido si el controlador de señal se refiere a cualquier objeto con duración de almacenamiento estático o de subprocesos que no es un objeto atómico libre de bloqueo que no sea mediante la asignación de un valor a un objeto declarado como volatile sig_atomic_to el controlador de señal llama a cualquier función en la biblioteca estándar que no sea la abort función, la _Exit función, la
quick_exit función, o la signal función con el primer argumento igual al número de señal correspondiente a la señal que provocó la invocación del controlador. Además, si tal llamado a la signal la función da como resultado una SIG_ERR devolución, el valor de errno es indeterminado.252)

252) Si cualquier señal es generada por un controlador de señal asíncrono, el comportamiento no está definido.

POSIX es mucho más generoso con lo que puede hacer en un controlador de señales.

Conceptos de señales en la edición POSIX 2008 dice:

Si el proceso es de varios subprocesos, o si el proceso es de un solo subproceso y se ejecuta un controlador de señal que no sea como resultado de:

  • El proceso llamando abort(), raise(), kill(), pthread_kill()o sigqueue() para generar una señal que no esté bloqueada

  • Una señal pendiente que se desbloquea y se entrega antes de que regrese la llamada que la desbloqueó

el comportamiento no está definido si el controlador de señal se refiere a cualquier objeto que no sea errno con una duración de almacenamiento estática distinta de la asignación de un valor a un objeto declarado como volatile sig_atomic_to si el manejador de señales llama a cualquier función definida en este estándar que no sea una de las funciones enumeradas en la siguiente tabla.

La siguiente tabla define un conjunto de funciones que serán seguras para señales asíncronas. Por lo tanto, las aplicaciones pueden invocarlos, sin restricciones, desde funciones de captura de señales:

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

Todas las funciones que no están en la tabla anterior se consideran inseguras con respecto a las señales. En presencia de señales, todas las funciones definidas por este volumen de POSIX.1-2008 se comportarán como se define cuando son llamadas o interrumpidas por una función de captura de señales, con una sola excepción: cuando una señal interrumpe una función insegura y la señal- La función de captura llama a una función insegura, el comportamiento no está definido.

Operaciones que obtienen el valor de errno y operaciones que asignan un valor a errno será seguro para la señal asíncrona.

Cuando se envía una señal a un subproceso, si la acción de esa señal especifica terminación, detención o continuación, todo el proceso se terminará, detendrá o continuará, respectivamente.

sin embargo, el printf() La familia de funciones está notablemente ausente de esa lista y no se puede llamar de forma segura desde un controlador de señales.

Él POSIX 2016 update amplía la lista de funciones seguras para incluir, en particular, un gran número de funciones de <string.h>, que es una adición particularmente valiosa (o fue un descuido particularmente frustrante). La lista es ahora:

_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, o terminas usando write() sin el soporte de formato proporcionado por printf() et al, o termina configurando una bandera que prueba (periódicamente) en lugares apropiados en su código. Esta técnica se demuestra hábilmente en la respuesta de Grijesh Chauhan.


Funciones C estándar y seguridad de señales

chqrlie hace una pregunta interesante, a la que no tengo más que una respuesta parcial:

¿Cómo es que la mayoría de las funciones de cadena de <string.h> o las funciones de la clase de caracteres de <ctype.h> y muchas más funciones de la biblioteca estándar de C no están en la lista anterior? Una implementación tendría que ser deliberadamente mala para hacer strlen() no es seguro llamar desde un controlador de señal.

Para muchas de las funciones de <string.h>es difícil ver por qué no se declararon seguros para señales asíncronas, y estoy de acuerdo en que strlen() es un buen ejemplo, junto con strchr(), strstr()etc. Por otro lado, otras funciones como strtok(), strcoll() y strxfrm() son bastante complejos y no es probable que sean seguros para la señal asíncrona. Porque strtok() retiene el estado entre llamadas, y el manejador de señales no podría decir fácilmente si alguna parte del código que está usando strtok() estaría desordenado. Él strcoll() y strxfrm() Las funciones funcionan con datos sensibles a la configuración regional, y cargar la configuración regional implica todo tipo de configuración de estado.

Las funciones (macros) de <ctype.h> son sensibles a la configuración regional y, por lo tanto, podrían encontrarse con los mismos problemas que strcoll() y strxfrm().

Encuentro difícil ver por qué las funciones matemáticas de <math.h> no son seguros para la señal asíncrona, a menos que sea porque podrían verse afectados por un SIGFPE (excepción de coma flotante), aunque la única vez que veo uno de esos en estos días es para entero división por cero. Una incertidumbre similar surge de <complex.h>, <fenv.h> y <tgmath.h>.

Algunas de las funciones en <stdlib.h> podría estar exento – abs() por ejemplo. Otros son específicamente problemáticos: malloc() y la familia son excelentes ejemplos.

Se podría realizar una evaluación similar para los otros encabezados en el Estándar C (2011) utilizados en un entorno POSIX. (El estándar C es tan restrictivo que no hay interés en analizarlos en un entorno estándar C puro). Los marcados como ‘dependientes de la configuración regional’ no son seguros porque la manipulación de configuraciones regionales puede requerir asignación de memoria, etc.

  • <assert.h>Probablemente no sea seguro
  • <complex.h>Posiblemente seguro
  • <ctype.h> – No es seguro
  • <errno.h> – A salvo
  • <fenv.h>Probablemente no sea seguro
  • <float.h> — Sin funciones
  • <inttypes.h> — Funciones sensibles a la configuración regional (no seguras)
  • <iso646.h> — Sin funciones
  • <limits.h> — Sin funciones
  • <locale.h> — Funciones sensibles a la configuración regional (no seguras)
  • <math.h>Posiblemente seguro
  • <setjmp.h> – No es seguro
  • <signal.h> – Permitió
  • <stdalign.h> — Sin funciones
  • <stdarg.h> — Sin funciones
  • <stdatomic.h>Posiblemente seguro, probablemente no seguro
  • <stdbool.h> — Sin funciones
  • <stddef.h> — Sin funciones
  • <stdint.h> — Sin funciones
  • <stdio.h> – No es seguro
  • <stdlib.h> — No todos son seguros (algunos están permitidos, otros no)
  • <stdnoreturn.h> — Sin funciones
  • <string.h> — No todo es seguro
  • <tgmath.h>Posiblemente seguro
  • <threads.h>Probablemente no sea seguro
  • <time.h> — Dependiente de la configuración regional (pero time() está explícitamente permitido)
  • <uchar.h> — Dependiente de la configuración regional
  • <wchar.h> — Dependiente de la configuración regional
  • <wctype.h> — Dependiente de la configuración regional

Analizar los encabezados POSIX sería… más difícil porque hay muchos de ellos, y algunas funciones pueden ser seguras pero muchas no lo serán… pero también más simple porque POSIX dice qué funciones son seguras para señales asíncronas (no muchas de ellas). Tenga en cuenta que un encabezado como <pthread.h> tiene tres funciones seguras y muchas funciones inseguras.

NÓTESE BIEN: Casi toda la evaluación de las funciones y los encabezados de C en un entorno POSIX son conjeturas a medias. No tiene sentido una declaración definitiva de un organismo de normalización.

  • ¿Cómo es que la mayoría de las funciones de cadena de <string.h> o las funciones de la clase de caracteres de <ctype.h> y muchas más funciones de la biblioteca estándar de C no están en la lista anterior? Una implementación tendría que ser deliberadamente mala para hacer strlen() no es seguro llamar desde un controlador de señal.

    – chqrlie

    10 de diciembre de 2016 a las 15:18

  • @chqrlie: pregunta interesante: vea la actualización (no había forma de incluir tanto en los comentarios con sensatez).

    –Jonathan Leffler

    10 dic 2016 a las 19:32

  • Gracias por tu profundo análisis. Con respecto a <ctype.h> cosas, es específico de la configuración regional y podría causar problemas si la señal interrumpe una función de configuración de la configuración regional, pero una vez que se carga la configuración regional, su uso debería ser seguro. Supongo que, en algunas situaciones complejas, la carga de los datos locales podría hacerse de forma incremental, haciendo así que las funciones de <ctype.h> inseguro. La conclusión sigue siendo: En caso de duda, abstenerse.

    – chqrlie

    10 dic 2016 a las 19:57


  • @chqrlie: Estoy de acuerdo en que la moraleja de la historia debería ser En caso de duda, abstenerse. Es un buen resumen.

    –Jonathan Leffler

    10 dic 2016 a las 19:59

  • Se considera una mejor práctica declarar volatile sigatomic_t alarm_fired;

    – Basile Starynkevitch

    3 de junio de 2013 a las 9:03

  • Agregar un enlace “¿Qué sucede durante este programa de manejo de señales?”

    –Grijesh Chauhan

    17 de abril de 2014 a las 11:09

  • @GrijeshChauhan: si estamos trabajando en un código de producto, entonces no podemos llamar a la función de pausa, el flujo puede estar en cualquier lugar cuando se produce la señal, por lo que en ese caso realmente no sabemos dónde guardar “if (alarm_fired) printf(“Ding! \norte”);” en codigo.

    – pankaj kushwaha

    25 de octubre de 2016 a las 6:44

  • @pankajkushwaha sí, tienes razón, sufre de condición de carrera

    –Grijesh Chauhan

    26 de octubre de 2016 a las 6:59

  • @GrijeshChauhan, Hay dos cosas que no pude entender. 1. ¿Cómo sabes cuándo revisar la bandera? Entonces, habrá múltiples puntos de verificación en el código en casi todos los puntos para imprimir. 2. Definitivamente habrá condiciones de carrera en las que la señal podría llamarse antes del registro de la señal o la señal podría ocurrir después del punto de control. Creo que esto solo ayudará a la impresión en algunas condiciones, pero no resuelve completamente el problema.

    – Darshan b

    5 de marzo de 2019 a las 4:01


Implemente su propia seguridad de señal asíncrona snprintf("%d y use write

No es tan malo como pensaba, ¿Cómo convertir un int a string en C? tiene varias implementaciones.

Dado que solo hay dos tipos interesantes de datos a los que pueden acceder los controladores de señales:

  • sig_atomic_t globales
  • int argumento de la señal

esto básicamente cubre todos los casos de uso interesantes.

El hecho de que strcpy La seguridad de la señal también hace que las cosas sean aún mejores.

El programa POSIX a continuación imprime para calcular la cantidad de veces que recibió SIGINT hasta el momento, que puede activar con Ctrl + Cy el e ID de la señal.

Puede salir del programa con Ctrl + \ (SIGQUIT).

C Principal:

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

Compilar y ejecutar:

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

Después de presionar Ctrl + C quince veces, la terminal muestra:

^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

donde 2 es el número de señal para SIGINT.

Probado en Ubuntu 18.04. GitHub ascendente.

avatar de usuario
Juan Hascall

Una técnica que es especialmente útil en programas que tienen un seleccionar bucle es escribir un solo byte en una tubería al recibir una señal y luego manejar la señal en el ciclo de selección. Algo a lo largo de estas líneas (manejo de errores y otros detalles omitidos por brevedad):

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

Si te importa cual señal que era, entonces el byte de la tubería puede ser el número de señal.

avatar de usuario
Etéreo

Para fines de depuración, escribí una herramienta que verifica que, de hecho, solo está llamando funciones en el async-signal-safe list e imprime un mensaje de advertencia para cada función insegura llamada dentro de un contexto de señal. Si bien no resuelve el problema de querer llamar a funciones que no son asíncronas desde un contexto de señal, al menos lo ayuda a encontrar casos en los que lo haya hecho accidentalmente.

El código fuente es en GitHub. Funciona por sobrecarga signal/sigactionluego secuestrando temporalmente el PLT entradas de funciones no seguras; esto hace que las llamadas a funciones no seguras se redirijan a un contenedor.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad