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

Resuelto Yu Hao asked hace 11 años • 8 respuestas

Dado que printfno es reentrante, no se supone que sea seguro usarlo en un controlador de señales. Pero he visto muchos códigos de ejemplo que se utilizan printfde esta manera.

Entonces mi pregunta es: ¿cuándo debemos evitar su uso printfen un controlador de señales? ¿Existe un reemplazo recomendado?

Yu Hao avatar Jun 03 '13 13:06 Yu Hao
Aceptado

El principal problema es que si la señal se 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 administración de la memoria.

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

ISO/IEC 9899:2011 §7.14.1.1 La signalfunción

¶5 Si la señal ocurre de otra manera que no sea el resultado de llamar a la función aborto raise, el comportamiento no está definido si el controlador de señales se refiere a cualquier objeto con duración de almacenamiento estático o de subprocesos que no sea un objeto atómico sin bloqueo, excepto mediante la asignación de un valor. a un objeto declarado como volatile sig_atomic_t, o el controlador de señales llama a cualquier función en la biblioteca estándar que no sea la abortfunción, la _Exitfunción, la quick_exitfunción o la signalfunción con el primer argumento igual al número de señal correspondiente a la señal que provocó la invocación del manipulador. Además, si dicha llamada a la signalfunción da como resultado una SIG_ERRdevolución, el valor de errnoes indeterminado. 252)

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

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

Signal Concepts en la edición POSIX 2008 dice:

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

  • El proceso que llama a abort(), raise(), , o para generar una kill()señal que no está bloqueadapthread_kill()sigqueue()

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

el comportamiento no está definido si el controlador de señales se refiere a cualquier objeto que no errnotenga una duración de almacenamiento estático que no sea asignando un valor a un objeto declarado como volatile sig_atomic_t, o si el controlador 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 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 figuran 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 según lo definido cuando sean llamadas o interrumpidas por una función de captura de señales, con una única 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.

Las operaciones que obtienen el valor de errnoy las operaciones que asignan un valor errnoserán seguras para la señal asíncrona.

Cuando se entrega una señal a un hilo, si la acción de esa señal especifica terminación, parada o continuación, todo el proceso deberá terminar, detenerse o continuar, respectivamente.

Sin embargo, la printf()familia de funciones está notablemente ausente de esa lista y es posible que no se pueda llamar de forma segura desde un controlador de señales.

La actualización POSIX 2016 amplía la lista de funciones seguras para incluir, en particular, una gran cantidad de funciones de <string.h>, lo cual es una adición particularmente valiosa (o fue un descuido particularmente frustrante). La lista ahora es:

_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, terminas usándolo write()sin el soporte de formato proporcionado por printf()et al, o terminas configurando una marca que pruebas (periódicamente) en los lugares apropiados de tu código. Esta técnica queda hábilmente demostrada en la respuesta de Grijesh Chauhan .


Funciones C estándar y seguridad de señal

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

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

Para muchas de las funciones en <string.h>, es difícil ver por qué no fueron declaradas seguras para señales asíncronas, y estoy de acuerdo en que strlen()es un excelente 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 señales asíncronas. Porque strtok()retiene el estado entre llamadas y el controlador de señales no puede determinar fácilmente si alguna parte del código que se está utilizando strtok()estaría en mal estado. Las funciones strcoll()y strxfrm()funcionan con datos sensibles a la configuración regional, y cargar la configuración regional implica todo tipo de configuración de estado.

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

Me resulta difícil ver por qué las funciones matemáticas de <math.h>no son seguras para señales asíncronas, a menos que sea porque podrían verse afectadas por una SIGFPE (excepción de punto flotante), aunque la única vez que veo una de esas en estos días es para números enteros. división por cero. Una incertidumbre similar surge de <complex.h>, <fenv.h>y <tgmath.h>.

Algunas funciones <stdlib.h>podrían quedar exentas, abs()por ejemplo. Otros son específicamente problemáticos: malloc()y la familia son buenos ejemplos.

Se podría hacer una evaluación similar para los demás encabezados del 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). Aquellos 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>- Seguro
  • <fenv.h>Probablemente no sea seguro
  • <float.h>— Sin funciones
  • <inttypes.h>— Funciones sensibles a la configuración regional (inseguras)
  • <iso646.h>— Sin funciones
  • <limits.h>— Sin funciones
  • <locale.h>— Funciones sensibles a la configuración regional (inseguras)
  • <math.h>Posiblemente seguro
  • <setjmp.h>- No es seguro
  • <signal.h>- Permitido
  • <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>— Depende 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... 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.

NB: Casi toda la evaluación de funciones y encabezados de C en un entorno POSIX son conjeturas semi-educadas. No tiene sentido una declaración definitiva de un organismo de normalización.

Jonathan Leffler avatar Jun 03 '2013 07:06 Jonathan Leffler

Puede usar alguna variable de indicador, establecer ese indicador dentro del controlador de señales y, en función de ese indicador, llamar a la printf()función en main() u otra parte del programa durante el funcionamiento normal.

No es seguro llamar a todas las funciones, como printf, desde un controlador de señales. Una técnica útil es utilizar un controlador de señales para configurar flagy luego verificarlo flag desde el programa principal e imprimir un mensaje si es necesario.

Observe que en el ejemplo siguiente, el controlador de señales ding() establece un indicador alarm_fireden 1 cuando SIGALRM detecta y en la función principal alarm_firedse examina el valor para llamar condicionalmente a printf correctamente.

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

Referencia: Beginning Linux Programming, 4ta edición . En este libro se explica exactamente su código (lo que desea), Capítulo 11: Procesos y señales, página 484

Además, debe tener especial cuidado al escribir funciones de controlador porque se pueden llamar de forma asincrónica. Es decir, se puede llamar a un controlador en cualquier punto del programa, de forma impredecible. Si llegan dos señales durante un intervalo muy corto, un controlador puede ejecutarse dentro de otro. Y se considera una mejor práctica declarar volatile sigatomic_tque siempre se accede a este tipo de forma atómica, lo que evita la incertidumbre sobre la interrupción del acceso a una variable. (lea: Acceso a datos atómicos y manejo de señales para obtener información detallada sobre la expiación).

Lea Definición de controladores de señales : para aprender a escribir una función de controlador de señales que se pueda establecer con las funciones signal()o sigaction().
Lista de funciones autorizadas en la página del manual . Llamar a esta función dentro del controlador de señales es seguro.

Grijesh Chauhan avatar Jun 03 '2013 06:06 Grijesh Chauhan

¿ Cómo evitar el uso printfen un manejador de señales?

  1. Evítelo siempre, dirá: Simplemente no lo use printf()en manejadores de señales.

  2. Al menos en sistemas compatibles con POSIX, puede utilizar write(STDOUT_FILENO, ...)en lugar de printf(). Sin embargo, el formateo puede no ser fácil: imprima int desde el controlador de señales usando funciones seguras de escritura o asincrónicas.

alk avatar Jun 03 '2013 08:06 alk

Para fines de depuración, escribí una herramienta que verifica que, de hecho, solo estás llamando a funciones de la async-signal-safelista 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 no asíncronas seguras desde un contexto de señal, al menos le ayuda a encontrar casos en los que lo ha hecho accidentalmente.

El código fuente está en GitHub . Funciona sobrecargando signal/sigactiony luego secuestrando temporalmente las PLTentradas de funciones inseguras; esto hace que las llamadas a funciones inseguras se redirijan a un contenedor.

dwks avatar Mar 27 '2016 00:03 dwks