sigaction - ¿Cómo evitar el uso de printf en un controlador de señal?
man kill (6)
¿Cómo evitar el uso de
printf
en un controlador de señal?
Siempre evítelo, dirá: simplemente no use
printf()
en controladores de señal.Al menos en sistemas conformes con POSIX, puede usar
write(STDOUT_FILENO, ...)
lugar deprintf()
. Sin embargo, el formateo puede no ser fácil: Imprima int desde el manejador de señal usando funciones de escritura o asincrona segura
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?
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
oraise
, 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 bloqueo que no sea la asignación un valor para un objeto declarado comovolatile sig_atomic_t
, o el manejador de señal llama a cualquier función en la biblioteca estándar distinta de la funciónabort
, la función_Exit
, la funciónquick_exit
o la función designal
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 designal
resulta en un retornoSIG_ERR
, el valor deerrno
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()
osigqueue()
para generar una señal que no está bloqueadaUna 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 comovolatile 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 aerrno
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 <string.h>
, 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 answer de Grijesh Chauhan .
Funciones estándar C y seguridad de la señal
chqrlie asks 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
<string.h>
o las funciones de clase de carácter de<ctype.h>
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 questrlen()
no sea seguro para llamar desde un manejador de señal.
Para muchas de las funciones en <string.h>
, 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 <ctype.h>
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 <math.h>
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 <complex.h>
, <fenv.h>
y <tgmath.h>
.
Algunas de las funciones en <stdlib.h>
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.
-
<assert.h>
- Probablemente no es seguro -
<complex.h>
- Posiblemente seguro -
<ctype.h>
- No es seguro -
<errno.h>
- Seguro -
<fenv.h>
- Probablemente no es seguro -
<float.h>
- Sin funciones -
<inttypes.h>
- Funciones sensibles a la<inttypes.h>
regional (inseguras) -
<iso646.h>
- Sin funciones -
<limits.h>
- Sin funciones -
<locale.h>
- Funciones sensibles a la<locale.h>
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 todo es seguro (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>
-<time.h>
regional (pero eltime()
está explícitamente permitido) -
<uchar.h>
-<uchar.h>
regional -
<wchar.h>
-<wchar.h>
regional -
<wctype.h>
-<wctype.h>
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 <pthread.h>
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 declaración definitiva de un organismo de estándares.
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.
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 programa 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 unflag
y luego verificar eseflag
desde el programa 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 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 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.
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.
Una técnica que es especialmente útil en programas 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.