señales - ¿Cómo se ejecutan los manejadores de señal asíncronos en Linux?
señales kill linux (2)
Me gustaría saber exactamente cómo funciona la ejecución de controladores de señales asíncronas en Linux. En primer lugar, no estoy seguro de qué hilo ejecuta el controlador de señal. En segundo lugar, me gustaría saber los pasos que se siguen para que el subproceso ejecute el manejador de señal.
Sobre el primer asunto, he leído dos explicaciones diferentes, aparentemente contradictorias:
El kernel de Linux, por Andries Brouwer, §5.2 "Señales receptoras" afirma :
Cuando llega una señal, el proceso se interrumpe, los registros actuales se guardan y se invoca el controlador de señal. Cuando el controlador de señal vuelve, la actividad interrumpida continúa.
La pregunta de StackOverflow "Cómo lidiar con señales asíncronas en un programa multiproceso" me lleva a pensar que el comportamiento de Linux es como el de SCO Unix :
Cuando se envía una señal a un proceso, si se está capturando, será manejada por uno, y solo uno, de los hilos que cumplan alguna de las siguientes condiciones:
Un hilo bloqueado en una llamada al sistema sigwait(2) cuyo argumento incluye el tipo de señal capturada.
Un hilo cuya máscara de señal no incluye el tipo de señal capturada.
Consideraciones adicionales:
- Un hilo bloqueado en sigwait(2) tiene preferencia sobre un hilo que no bloquea el tipo de señal.
- Si más de un hilo cumple con estos requisitos (tal vez dos hilos están llamando a sigwait(2) ), se elegirá uno de ellos. Esta elección no es predecible por los programas de aplicación.
- Si ningún hilo es elegible, la señal permanecerá `` pendiente '''' en el nivel del proceso hasta que algún hilo sea elegible.
Además, "El modelo de manejo de señales de Linux" de Moshe Bar afirma que "las señales asíncronas se entregan al primer hilo encontrado que no bloquea la señal", lo cual interpreto que significa que la señal se envía a un hilo que tiene su sigmask sin incluir la señal .
¿Cuál es correcto?
En el segundo asunto, ¿qué sucede con la pila y registrar los contenidos para la secuencia seleccionada? Supongamos que thread-to-run-the-signal-handler T está en medio de la ejecución de una función do_stuff()
. ¿La pila del hilo T se usa directamente para ejecutar el manejador de señal (es decir, la dirección del trampolín de señal se empuja hacia la pila de T y el flujo de control va al manejador de señal)? Alternativamente, ¿se usa una pila separada? ¿Como funciona?
Estas dos explicaciones realmente no son contradictorias si se toma en cuenta el hecho de que los hackers de Linux tienden a confundirse acerca de la diferencia entre un hilo y un proceso, principalmente debido al error histórico de intentar pretender que los hilos podrían implementarse como procesos que comparten memoria. :-)
Dicho esto, la explicación n. ° 2 es mucho más detallada, completa y correcta.
En cuanto a la pila y los contenidos de registro, cada subproceso puede registrar su propia pila alternativa de manejo de señal, y el proceso puede elegir por señal qué señales se entregarán en pilas alternativas de manejo de señal. El contexto interrumpido (registros, máscara de señal, etc.) se guardará en una estructura ucontext_t
en la pila (posiblemente alternativa) para el hilo, junto con la dirección de retorno del trampolín. Los manejadores de señal instalados con el indicador SA_SIGINFO
pueden examinar esta estructura ucontext_t
si así lo ucontext_t
, pero la única cosa portátil que pueden hacer con ella es examinar (y posiblemente modificar) la máscara de señal guardada. (No estoy seguro de si la modificación está sancionada por el estándar, pero es muy útil porque permite al manejador de señal reemplazar atómicamente la máscara de señal del código interrumpido al regresar, por ejemplo, dejar la señal bloqueada para que no vuelva a suceder .)
La fuente n. ° 1 (Andries Brouwer) es correcta para un proceso de subproceso único. La fuente n.º 2 (SCO Unix) es incorrecta para Linux, porque Linux no prefiere los hilos en sigwait (2). Moshe Bar tiene razón sobre el primer hilo disponible.
¿Qué hilo recibe la señal? Las páginas del manual de Linux son una buena referencia. Un proceso utiliza el clone(2) con CLONE_THREAD para crear varios hilos. Estos hilos pertenecen a un "grupo de hilos" y comparten una sola ID de proceso. El manual para el clon (2) dice:
Las señales se pueden enviar a un grupo de hilos como un todo (es decir, un TGID) usando kill(2) , o a un hilo específico (es decir, TID) usando tgkill(2) .
Las disposiciones y acciones de la señal son para todo el proceso: si se envía una señal no controlada a un hilo, entonces afectará (terminará, detendrá, continuará, se ignorará) a todos los miembros del grupo de hilos.
Cada subproceso tiene su propia máscara de señal, establecida por sigprocmask(2) , pero las señales pueden estar pendientes: para todo el proceso (es decir, entregable a cualquier miembro del grupo de subprocesos), cuando se envía con kill (2); o para un hilo individual, cuando se envía con tgkill (2). Una llamada a sigpending(2) devuelve un conjunto de señales que es la unión de las señales pendientes para todo el proceso y las señales que están pendientes para el hilo de llamada.
Si kill (2) se usa para enviar una señal a un grupo de hilos, y el grupo de hilos ha instalado un controlador para la señal, entonces el manejador se invocará en exactamente un miembro arbitrariamente seleccionado del grupo de hilos que no haya bloqueado el señal. Si hay múltiples hilos en un grupo esperando aceptar la misma señal usando sigwaitinfo(2) , el kernel seleccionará arbitrariamente uno de estos hilos para recibir una señal enviada usando kill (2).
Linux no es SCO Unix, porque Linux podría dar la señal a cualquier hilo, incluso si algunos hilos esperan una señal (con sigwaitinfo, sigtimedwait o sigwait) y algunos hilos no lo están. El manual para sigwaitinfo(2) advierte,
En el uso normal, el programa que llama bloquea las señales en el conjunto a través de una llamada previa a sigprocmask (2) (para que la disposición predeterminada para estas señales no ocurra si están pendientes entre llamadas sucesivas a sigwaitinfo () o sigtimedwait ()) y no establece controladores para estas señales. En un programa multiproceso, la señal debe bloquearse en todos los hilos, para evitar que la señal sea tratada de acuerdo con su disposición predeterminada en un hilo que no sea el que llama a sigwaitinfo () o sigtimedwait ()).
El código para seleccionar un hilo para la señal vive en linux/kernel/signal.c (el enlace apunta al espejo de GitHub). Ver las funciones wants_signal () y completa_signal (). El código selecciona el primer hilo disponible para la señal. Un hilo disponible es uno que no bloquea la señal y no tiene otras señales en su cola. Primero, el código comprueba el hilo principal, luego verifica los otros hilos en un orden desconocido para mí. Si no hay ningún hilo disponible, entonces la señal se bloquea hasta que un hilo desbloquea la señal o vacía su cola.
¿Qué sucede cuando un hilo recibe la señal? Si hay un manejador de señal, el núcleo hace que el hilo llame al controlador. La mayoría de los manejadores se ejecutan en la pila del hilo. Un controlador puede ejecutarse en una pila alternativa si el proceso usa sigaltstack(2) para proporcionar la pila, y sigaction(2) con SA_ONSTACK para establecer el controlador. El núcleo empuja algunas cosas en la pila elegida y establece algunos de los registros de la secuencia.
Para ejecutar el controlador, el hilo debe estar ejecutándose en el espacio de usuario. Si el hilo se está ejecutando en el kernel (quizás para una llamada al sistema o un error de página), no ejecuta el controlador hasta que se dirige al espacio de usuario. El núcleo puede interrumpir algunas llamadas al sistema, por lo que el hilo ejecuta el controlador ahora, sin esperar a que finalice la llamada del sistema.
El manejador de señal es una función C, por lo que el kernel obedece a la convención de la arquitectura para llamar a las funciones C. Cada arquitectura, como arm, i386, powerpc o sparc, tiene su propia convención. Para powerpc, para llamar al controlador (signum), el núcleo establece el registro r3 en signum. El núcleo también establece la dirección de retorno del manejador al trampolín de señal. La dirección de devolución va en la pila o en un registro por convención.
El núcleo pone un trampolín de señal en cada proceso. Este trampolín llama a sigreturn(2) para restaurar el hilo. En el kernel, sigreturn (2) lee información (como registros guardados) de la pila. El kernel había insertado esta información en la pila antes de llamar al controlador. Si hubo una llamada al sistema interrumpida, el kernel podría reiniciar la llamada (solo si el controlador usó SA_RESTART), o fallar la llamada con EINTR, o devolver una breve lectura o escritura.