¿Qué es una señal? Las señales son interrupciones de software.
Un programa robusto necesita manejar señales. Esto se debe a que las señales son una forma de enviar eventos asíncronos a la aplicación.
Un usuario que presiona ctrl+c, un proceso que envía una señal para matar a otro proceso, etc., son casos en los que un proceso necesita manejar la señal.
Señales de Linux
En Linux, cada señal tiene un nombre que comienza con los caracteres SIG. Por ejemplo:
- Una señal SIGINT que se genera cuando un usuario presiona ctrl+c. Esta es la forma de terminar programas desde la terminal.
- Se genera un SIGALRM cuando se apaga el temporizador establecido por la función de alarma.
- Se genera una señal SIGABRT cuando un proceso llama a la función de cancelación.
- etc
Cuando se produce la señal, el proceso tiene que decirle al kernel qué hacer con ella. Puede haber tres opciones a través de las cuales se puede disponer de una señal:
- La señal se puede ignorar. Al ignorar queremos decir que no se hará nada cuando se produzca la señal. La mayoría de las señales se pueden ignorar, pero las señales generadas por excepciones de hardware como dividir por cero, si se ignoran, pueden tener consecuencias extrañas. Además, no se pueden ignorar un par de señales como SIGKILL y SIGSTOP.
- La señal se puede captar. Cuando se elige esta opción, el proceso registra una función con kernel. El núcleo llama a esta función cuando se produce esa señal. Si la señal no es fatal para el proceso, en esa función el proceso puede manejar la señal correctamente o, de lo contrario, puede optar por terminar correctamente.
- Deje que se aplique la acción predeterminada. Cada señal tiene una acción predeterminada. Esto podría ser la finalización del proceso, ignorar, etc.
Como ya dijimos, no se pueden ignorar dos señales SIGKILL y SIGSTOP. Esto se debe a que estas dos señales proporcionan una forma para que el usuario raíz o el núcleo eliminen o detengan cualquier proceso en cualquier situación. La acción predeterminada de estas señales es finalizar el proceso. Ninguna de estas señales puede captarse ni ignorarse.
¿Qué sucede al inicio del programa?
Todo depende del proceso que llama a exec. Cuando se inicia el proceso, el estado de todas las señales es ignorado o predeterminado. Es la última opción que es más probable que suceda a menos que el proceso que llama a exec ignore las señales.
Es propiedad de las funciones ejecutivas cambiar la acción en cualquier señal para que sea la acción predeterminada. En términos más simples, si el padre tiene una función de captura de señal que se llama cuando ocurre la señal, entonces si ese padre ejecuta un nuevo proceso hijo, entonces esta función no tiene significado en el nuevo proceso y, por lo tanto, la disposición de la misma señal se establece en el valor predeterminado. en el nuevo proceso.
Además, dado que generalmente tenemos procesos que se ejecutan en segundo plano, el shell simplemente establece la disposición de la señal de salida como ignorada, ya que no queremos que un usuario finalice los procesos en segundo plano presionando una tecla ctrl + c porque eso anula el propósito de hacer un proceso ejecutar en segundo plano.
¿Por qué las funciones de captura de señales deberían ser reentrantes?
Como ya hemos discutido, una de las opciones para la disposición de la señal es captar la señal. En el código de proceso, esto se hace registrando una función en el núcleo a la que el núcleo llama cuando se produce la señal. Una cosa a tener en cuenta es que la función que registra el proceso debe ser reentrante.
Antes de explicar el motivo, primero comprendamos qué son las funciones de reentrada. Una función reentrante es una función cuya ejecución se puede detener por cualquier motivo (como una interrupción o una señal) y luego se puede volver a ingresar de manera segura antes de que sus invocaciones anteriores completen la ejecución.
Ahora, volviendo al problema, supongamos que se registra una función func() para una devolución de llamada cuando ocurre una señal. Ahora suponga que esta func() ya estaba en ejecución cuando se produjo la señal. Dado que esta función es una devolución de llamada para esta señal, el programador detendrá la ejecución actual en esta señal y esta función se volverá a llamar (debido a la señal).
El problema puede ser si func() funciona en algunos valores globales o estructuras de datos que quedan en un estado inconsistente cuando la ejecución de esta función se detuvo en el medio, entonces la segunda llamada a la misma función (debido a la señal) puede causar algunos resultados no deseados.
Entonces decimos que las funciones de captura de señales deben hacerse reentrantes.
Consulte nuestros artículos send-signal-to-process y Linux fuser command para ver ejemplos prácticos sobre cómo enviar señales a un proceso.
Hilos y Señales
Ya vimos en una de las secciones anteriores que el manejo de señales viene con su propia complejidad (como el uso de funciones reentrantes). Para agregar a la complejidad, generalmente tenemos aplicaciones de subprocesos múltiples donde el manejo de la señal se vuelve realmente complicado.
Cada subproceso tiene su propia máscara de señal privada (una máscara que define qué señales se pueden entregar), pero todos los subprocesos de la aplicación comparten la forma en que se realiza la disposición de la señal. Esto significa que una disposición para una señal particular establecida por un subproceso puede ser anulada fácilmente por otro subproceso. En este caso el mecanismo de disposición cambia para todos los hilos.
Por ejemplo, un subproceso A puede optar por ignorar una señal particular, pero un subproceso B en el mismo proceso puede optar por captar la misma señal registrando una función de devolución de llamada en el núcleo. En este caso, la solicitud realizada por el subproceso A es anulada por la solicitud del subproceso B.
Las señales se entregan solo a un único subproceso en cualquier proceso. Además de las excepciones de hardware o la caducidad del temporizador (que se entregan al subproceso que provocó el evento), todas las señales se pasan al proceso de forma arbitraria.
Para contrarrestar esta deficiencia, se pueden usar algunas API posix como pthread_sigmask(), etc.
En el próximo artículo (parte 2) de esta serie, discutiremos cómo capturar señales en un proceso y explicaremos el aspecto práctico del manejo de señales usando fragmentos de código.