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 estática o de almacenamiento de subprocesos que no sea 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_t
, o el controlador de señales llama a cualquier función de la biblioteca estándar que no sea abort
función, el _Exit
función, el quick_exit
función, o el 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 llamada al signal
la función da como resultado un SIG_ERR
devuelve, el valor de errno
es indeterminado.
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.
Signal Concepts 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ñales que no sea el resultado de:
-
El proceso llamando a
abort()
,raise()
,kill()
,pthread_kill()
, osigqueue()
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 duración de almacenamiento estático que no sea mediante la asignación de un valor a un objeto declarado como volatile sig_atomic_t
, o si el controlador 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 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 señales asíncronas.
Cuando se envía una señal a un subproceso, si la acción de esa señal especifica la finalización, la detención o la continuación, todo el proceso se finalizará, 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.
El POSIX 2016 update amplía la lista de funciones seguras para incluir, en particular, una gran cantidad 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, 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ñales.
Para muchas de las funciones en <string.h>
, es difícil ver por qué no se declararon seguros para señales asíncronas, y estoy de acuerdo con el 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 la señal asíncrona. Porque strtok()
retiene el estado entre llamadas, y el controlador de señal no podría decir fácilmente si alguna parte del código que usa strtok()
estaría desordenado. El strcoll()
y strxfrm()
Las funciones funcionan con datos sensibles a la configuración regional, y la carga de 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 tener los mismos problemas que strcoll()
y strxfrm()
.
Me resulta 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 integer 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 buenos 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 de 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 (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>
— 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>
— Dependiente 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 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.
Nota: 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 evitar usar printf
en un controlador de señal?
-
Siempre evítelo, dirá:simplemente no use
printf()
en manejadores de señales. -
Al menos en sistemas compatibles con POSIX, puede usar
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 asíncronas
Puede usar alguna variable de indicador, establecer ese indicador dentro del controlador de señal y, en función de ese indicador, llamar a 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 dentro de un controlador de señal. Una técnica útil es usar un controlador de señal para establecer un flag
y luego verifique que flag
desde el programa principal e imprima un mensaje si es necesario.
Observe en el siguiente ejemplo, el controlador de señal ding() establece una marca alarm_fired
a 1 como SIGALRM capturado y en la función principal alarm_fired
El valor se examina 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:Introducción a la programación de Linux, 4.ª 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
, este tipo siempre se accede de forma atómica, evita la incertidumbre sobre la interrupción del acceso a una variable. (lea:Acceso a datos atómicos y manejo de señales para una expiación detallada).
Lea Definición de controladores de señales:para aprender a escribir una función de controlador de señales que se puede establecer con el signal()
o sigaction()
funciones
Lista de funciones autorizadas en la página del manual, llamar a esta función dentro del controlador de señales es seguro.