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

Como printf no se vuelve a ingresar, se supone que no es seguro usarlo en un manejador de señal. Pero he visto muchos códigos de ejemplo que usan printf esta manera.

Entonces mi pregunta es: ¿cuándo debemos evitar el uso de printf en un manejador de señal, y hay un reemplazo recomendado?

Puede usar alguna variable de indicador, establecer esa bandera dentro del controlador de señal y, basándose en esa printf() llamar a la printf() en main () u otra parte del progtwig durante el funcionamiento normal.

No es seguro llamar a todas las funciones, como printf , desde un manejador de señal. Una técnica útil es usar un manejador de señal para establecer un flag y luego verificar ese flag desde el progtwig principal e imprimir un mensaje si es necesario.

Observe en el ejemplo a continuación, el gestor de señal ding () establece un indicador alarm_fired en 1 como SIGALRM capturado y en la función principal el valor de alarm_fired se examina para llamar condicionalmente 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, 4th Edition , 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 las funciones del manejador porque pueden llamarse de forma asíncrona. Es decir, se podría llamar a un controlador en cualquier punto del progtwig, 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_t , este tipo siempre se accede atómicamente, evitar la incertidumbre sobre la interrupción del acceso a una variable. (léase: Acceso a datos atómicos y gestión de señales para la expiación detallada).

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

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 utilizada u otras operaciones similares. Si el código en el controlador de señal llama a una función que invoca malloc() , esto puede arruinar completamente la gestión de memoria.

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

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

¶5 Si la señal no es el resultado de llamar a la función abort o raise , el comportamiento no está definido si el manejador de señal se refiere a cualquier objeto con duración estática o de almacenamiento de subproceso que no sea un objeto atómico sin locking que no sea la asignación un valor para un objeto declarado como volatile sig_atomic_t , o el manejador de señal llama a cualquier función en la biblioteca estándar distinta de la función abort , la función _Exit , la función quick_exit o la función de signal 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 llamada a la función de signal resulta en un retorno SIG_ERR , el valor de errno es indeterminado. 252)

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

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

Signal Concepts en la edición POSIX 2008 dice:

Si el proceso es de subprocesos múltiples o si el proceso es de subproceso único y se ejecuta un controlador de señal que no sea el resultado de:

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

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

el comportamiento no está definido si el manejador de señal se refiere a cualquier objeto que no sea errno con duración de almacenamiento estático diferente a la asignación de un valor a un objeto declarado como volatile sig_atomic_t , o si el manejador de señal 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 asíncronas-señal-segura. Por lo tanto, las aplicaciones pueden invocarlas, sin restricción, desde las 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 se encuentran 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 definen cuando son llamadas desde o interrumpidas por una función de captura de señal, 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.

Las operaciones que obtienen el valor de errno y las operaciones que asignan un valor a errno serán async-signal-safe.

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

Sin embargo, la familia de funciones printf() está notablemente ausente de esa lista y no se puede llamar con seguridad desde un manejador de señal.

La actualización POSIX 2016 amplía la lista de funciones seguras para incluir, en particular, una gran cantidad de funciones de , que es una adición particularmente valiosa (o fue una supervisión 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, terminas usando write() sin el soporte de formato provisto por printf() et al, o terminas configurando un indicador que pruebas (periódicamente) en lugares apropiados en tu código. Esta técnica se demuestra hábilmente en la respuesta de Grijesh Chauhan .


Funciones estándar C y seguridad de la señal

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

¿Cómo es que la mayoría de las funciones de cadena de o las funciones de clase de carácter de y muchas más funciones de biblioteca estándar de C no están en la lista anterior? Una implementación necesitaría ser intencionalmente mala para hacer que strlen() no sea seguro para llamar desde un manejador de señal.

Para muchas de las funciones en , es difícil ver por qué no se declararon como señales asíncronas seguras, y estoy de acuerdo con que strlen() es un buen ejemplo, junto con strchr() , strstr() , etc. Por otro lado, otras funciones como strtok() , strcoll() y strxfrm() son bastante complejas y es probable que no sean señal asíncrona segura. Debido a que strtok() conserva el estado entre las llamadas, y el manejador de señal no podría decir fácilmente si alguna parte del código que está usando strtok() se estropearía. Las 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.

Las funciones (macros) de son todas 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

no son seguras para señal asíncrona, a menos que sea porque podrían verse afectadas por un SIGFPE (excepción de punto flotante), aunque es la única vez que veo uno de esos estos días es para la división entera por cero. Incertidumbre similar surge de , y .

Algunas de las funciones en podrían estar exentas, por ejemplo, abs() . Otros son específicamente problemáticos: malloc() y la familia son ejemplos principales.

Se podría hacer una evaluación similar para los otros encabezados en el Estándar C (2011) usados ​​en un entorno POSIX. (El estándar C es tan restrictivo que no hay interés en analizarlos en un entorno de 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 la asignación de memoria, etc.

  • Probablemente no es seguro
  • Posiblemente seguro
  • – No es seguro
  • – Seguro
  • Probablemente no es seguro
  • – Sin funciones
  • – Funciones sensibles a la regional (inseguras)
  • – Sin funciones
  • – Sin funciones
  • – Funciones sensibles a la regional (inseguras)

  • Posiblemente seguro

  • – No es seguro
  • – Permitido
  • – Sin funciones
  • – Sin funciones
  • Posiblemente seguro, probablemente no seguro
  • – Sin funciones
  • – Sin funciones
  • – Sin funciones
  • – No es seguro
  • – No todo es seguro (algunos están permitidos, otros no)
  • – Sin funciones
  • – No todo es seguro
  • Posiblemente seguro
  • Probablemente no sea seguro
  • regional (pero el time() está explícitamente permitido)
  • regional
  • regional
  • regional

Analizar los encabezados POSIX sería … más difícil ya que hay muchos de ellos, y algunas funciones pueden ser seguras, pero muchas no serán … pero también más simples porque POSIX dice qué funciones son seguras para la señal asíncrona (no muchas de ellas). Tenga en cuenta que un encabezado como tiene tres funciones seguras y muchas funciones inseguras.

NB: Casi todas las evaluaciones de las funciones C y los encabezados en un entorno POSIX son conjeturas semi-educadas. No tiene sentido una statement definitiva de un organismo de estándares.

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

  1. Siempre evítelo, dirá: simplemente no use printf() en controladores de señal.

  2. Al menos en sistemas conformes con POSIX, puede usar write(STDOUT_FILENO, ...) lugar de printf() . Sin embargo, el formateo puede no ser fácil: Imprima int desde el manejador de señal usando funciones de escritura o asincrona segura

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

El código fuente está en GitHub . Funciona al sobrecargar signal/sigaction , y luego secuestra temporalmente las entradas PLT de funciones inseguras; esto hace que las llamadas a funciones inseguras sean redirigidas a un contenedor.

Una técnica que es especialmente útil en progtwigs que tienen un bucle de selección es escribir un solo byte en una tubería al recibir una señal y luego manejar la señal en el bucle 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 le importa qué señal era, entonces el byte en la tubería puede ser el número de señal.

Puede usar printf en manejadores de señal si está usando la biblioteca pthread. unix / posix especifica que printf es atómico para los hilos cf Dave Butenhof responder aquí: https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw Tenga en cuenta que para obtener una imagen más clara de la salida de impresión, debe ejecutar su aplicación en una consola (en Linux use ctl + alt + f1 para iniciar la consola 1), en lugar de una pseudo-tty creada por la GUI.

Implemente su propio snprintf("%d señal asíncrona segura snprintf("%d y use write

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

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

  • sig_atomic_t globals
  • argumento de la señal int

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

El hecho de que strcpy también es seguro para las señales hace las cosas aún mejor.

El siguiente progtwig POSIX imprime para calcular el número de veces que recibió SIGINT hasta el momento, que puede activar con Ctrl + C , y la ID de señal.

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

C Principal:

 #define _XOPEN_SOURCE 700 #include  #include  #include  #include  #include  #include  #include  #include  /* 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; } 

Comstackr 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 aguas arriba .