¿Cómo evitar el uso de printf en un controlador de señales?
Dado 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 se utilizan printf
de esta manera.
Entonces mi pregunta es: ¿cuándo debemos evitar su uso printf
en un controlador de señales? ¿Existe un reemplazo recomendado?
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
signal
función¶5 Si la señal ocurre de otra manera que no sea el resultado de llamar a la función
abort
oraise
, 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 comovolatile sig_atomic_t
, o el controlador de señales llama a cualquier función en la biblioteca estándar que no sea laabort
función, la_Exit
función, laquick_exit
función o lasignal
funció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 lasignal
función da como resultado unaSIG_ERR
devolución, el valor deerrno
es 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 unakill()
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
errno
tenga una duración de almacenamiento estático que no sea asignando un valor a un objeto declarado comovolatile 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
errno
y las operaciones que asignan un valorerrno
será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 questrlen()
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 (perotime()
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.
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 configurarflag
y luego verificarloflag
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_fired
en 1 cuando SIGALRM detecta y en la función principal alarm_fired
se 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_t
que 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.
¿ Cómo evitar el uso
printf
en un manejador de señales?
Evítelo siempre, dirá: Simplemente no lo use
printf()
en manejadores de señales.Al menos en sistemas compatibles con POSIX, puede utilizar
write(STDOUT_FILENO, ...)
en lugar deprintf()
. 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.
Para fines de depuración, escribí una herramienta que verifica que, de hecho, solo estás llamando a funciones de la async-signal-safe
lista 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/sigaction
y luego secuestrando temporalmente las PLT
entradas de funciones inseguras; esto hace que las llamadas a funciones inseguras se redirijan a un contenedor.