Los sistemas operativos compatibles con el estándar POSIX implementan un tipo de interrupción por software conocida como señales POSIX. Estas son enviadas a los procesos para informar de situaciones excepcionales durante la ejecución del programa, como por ejemplo:
SIGSEGV
Acceso a una dirección de memoria no válida.SIGFPE
Intento de ejecutar una operación aritmética inválida, como por ejemplo una división por cero.SIGILL
Intento de ejecutar una instrucción ilegal.SIGCHLD
Notificación de terminación de algún proceso hijo. Por defecto también se notifica que un proceso hijo ha sido detenido.SIGTERM
Notificación de que se ha solicitado la terminación del proceso.SIGINT
Notificación de que el proceso está controlado por una terminal y el usuario quiere interrumpirlo. Generalmente esta señal es motivada por la pulsación de la combinación de teclasCtrl-C
en la terminal desde la que se controla el proceso.SIGHUP
Notificación de que se ha cerrado la terminal a través de la que se controla el proceso, por lo que dicho proceso debe terminar. Al recibir esta señal muchos procesos no interactivos —como servicios o demonios— releen los archivos de configuración y reabren los de registro, sin tener que matar y volver a iniciar el proceso.
Estos son una pequeña muestra de una lista mucho más extensa.
Manejadores de señal
Para cada tipo de señal el proceso puede especificar una acción diferente:
SIG_DFL
Ejecutar la acción por defecto, lo que generalmente implica terminar el proceso inmediatamente.SIG_IGN
Ignorar la señal, lo que no es posible para todos los tipos de señales.Invocar un manejador de señal Invocar una función concreta del programa que actúa como manejador de la señal para realizar las acciones que el programador considere oportunas.
Esto último es interesante porque, por ejemplo, permite realizar las acciones necesarias para que el programa termine en condiciones seguras cuando reciba señales como SIGINT
o SIGTERM
. Por ejemplo: borrar archivos temporales, asegurar que los datos se escriben en disco y la estructura de su contenido es consistente, terminar procesos hijo a los que se les haya delegado parte del trabajo, cerrar canales de comunicación, detener hilos de ejecución, etc.
Para fijar el manejador de una señal concreta, simplemente hay usar la función de la librería estándar std::signal()—o alternativamente la llamada al sistema signal()— de la siguiente manera:
#include <csignal>
void signalHandler(int signum)
{
std::cout << "Señal (" << signum << ") recibida.\n";
// Terminar el programa
exit(signum);
}
int main(int argc, char* argv[])
{
// ...
// Configurar el manejador de la señal SIGINT
std::signal(SIGINT, signalHandler);
// Configurar el manejador de la señal SIGTERM
std::signal(SIGTERM, signalHandler);
// ...
}
Sin embargo, debemos de tener en cuenta que algunos aspectos de std::signal() y de signal() no están estandarizados. Es decir, son específicos de cada implementación, por lo que pueden comportarse de manera diferente en distintos sistemas operativos. Por eso el estándar POSIX introdujo la función alternativa sigaction(), que ofrece un control mucho más explícito sobre el comportamiento esperado —como veremos más adelante— para que en todos los sistemas operativos el manejo de señales funcione igual.
Seguridad respecto a las señales
Al trabajar con señales POSIX debemos tener presente que éstas pueden llegar en cualquier momento, interrumpiendo así la secuencia normal de ejecución de instrucciones del proceso. Es decir, los manejadores de señal son invocados de forma asíncrona respecto a la ejecución del proceso, lo que introduce problemas de concurrencia debido al posible acceso del manejador a datos que estaban siendo manipulados por el programa en el momento en que fue interrumpido. Por ello:
El estándar POSIX establece que desde un manejador de señal sólo se pueden invocar funciones seguras respecto a la asincronicidad de las señales. Estas funciones son aquellas que o son reentrantes o no interrumpibles respecto a las señales. Pero hay que tener mucho cuidado porque sólo unas pocas funciones de la librería del sistema cumplen con dicho requisito. De hecho, el estándar de C++ establece que el comportamiento está indefinido si dentro de un manejador de señal se llama a cualquier función de la librería estándar del lenguaje, excepto
std::abort
,std::_Exit
,std::quick_exit
ostd::signal
—en este último caso siempre que el primer argumento no sea el número de la señal que actualmente está siendo manejada—.En programas multihilo cualquier hilo en el que no se haya bloqueado una señal puede ser utilizado para atenderla. Esto introduce problemas adicionales de concurrencia que obligan al uso de cerrojos, semáforos y otros elementos de sincronización. Por eso es muy común bloquear las señales en todos los hilos excepto en uno, que de esta manera podrá ser el único interrumpido para manejarlas.
Incluso si se usan variables como banderas para notificar desde el manejador al programa principal que ha ocurrido una señal, con el objeto de que éste último ejecute las acciones necesarias, debemos especificar al compilador que no utilice con ellas variables optimizaciones que puedan dar problemas de concurrencia:
El tipo
volatile std::sig_atomic_t
para definir variables atómicas cumple con esos requisitos. La palabra reservada de Cvolatile
permite indicarle al compilador que no optimice el acceso a una variable porque su valor puede cambiar de improviso. Mientras quesig_atomic_t
es un tipo de entero que está garantizado que puede ser accedido como una entidad atómica —sin interrupciones— incluso en presencia de señales.En C++11 o posterior la plantilla std::atomic también permite definir variables atómicas.
Por lo tanto, la siguiente podría ser una forma correcta de terminar un proceso cuando llegan señales tales como SIGINT
o SIGTERM
:
#include <atomic>
std::atomic<bool> waitingForQuit(false);
void signalHandler(int signum)
{
// Indicar al programa principal que debe terminar
waitingForQuit = true;
}
int main(int argv, char* argv[])
{
// ...
// Configurar los manejadores de señal
std::signal(SIGINT, signalHandler);
std::signal(SIGTERM, signalHandler);
// ...
while (true) {
// ...
if (waitingForQuit) {
// Realizar las acciones necesarias para terminar el proceso
// en condiciones seguras.
// ...
std::exit(0);
}
}
}
Funciones reentrantes y no interrumpibles.
Como hemos comentado, un manejador de señal sólo se pueden invocar funciones seguras respecto a la asincronicidad de las señales. Y esto sólo ocurren con aquellas que o son reentrantes o no son interrumpibles respecto a las señales.
Una función es reentrante si puede ser interrumpida en medio de su ejecución y vuelta a llamar con total seguridad. En general, una función es reentrante si no modifica variables estáticas o globales, no modifica su propio código y no llama a otras funciones que no sean reentrantes.
Mientras que una función puede ser no interrumpible respecto a las señales, si al entrar en la función lo primero que hace el código es bloquea dichas señales, desbloqueándolas antes de salir.
Bloqueo de señales
A veces no interesa manejar todas las señales que puede recibir un proceso o puede ser interesante bloquearlas en instantes concretos de la ejecución del mismo. Por eso el sistema nos proporciona funciones para hacerlo.
A la colección de señales actualmente bloqueadas se las denomina máscara de señales y se hereda de padres a hijos durante la creación de los procesos. Posteriormente, durante la ejecución de un programa, esta máscara de señales se puede modificar utilizando las llamadas al sistema sigprocmask() o pthread_sigmask().
int sigprocmask (int how,
const sigset_t *restrict set,
sigset_t *restrict oldset)
int pthread_sigmask(int how,
const sigset_t *set,
sigset_t *oldset);
Es importante notar que ambas llamadas operan de la misma manera, sin embargo sigprocmask() sólo debe usarse en programas monohilo para modificar la máscara de señales del proceso. En los programas multihilo cada hilo tiene su propia máscara de señales, por lo que debe utilizarse pthread_sigmask() si deseamos modificarla. Según el estándar POSIX, el efecto de usar sigprocmask() en procesos multihilo no está especificado.
Ambas funciones están diseñadas tanto para examinar como para cambiar la máscara de señales:
oldset
Se utiliza para devolver la máscara de señales previa. Si se desea examinar la máscara de señales actual sin modificarla, sólo es necesario pasar un puntero a NULL enset
. De igual forma, si sólo se desea modificar la máscara de señales sin recuperar la máscara previa, basta con pasaroldset
a NULL.set
Se utiliza para indicar la nueva máscara de señales. Cómo se interprete este argumento para construir dicha nueva máscara depende del argumentohow
.how
Determina como cambiará la máscara de señales actual.
Los valores posibles para how
son:
SIG_BLOCK
Añade las señales indicadas enset
a la máscara actual para bloquearlas también.SIG_UNBLOCK
Elimina las señales indicadas enset
de la máscara actual para desbloquearlas.SIG_SETMASK
Usar el contenido deset
como máscara de señales actual, ignorando así el valor previo de dicha máscara.
Tanto los argumentos set
como oldset
son de tipo sigset_t
, que es con el que se representan los conjuntos de señales. Por portabilidad estos conjuntos no deben manipularse directamente sino a través de las siguientes funciones:
int sigemptyset(sigset_t *set)
Inicializaset
sin ninguna señal.int sigefillset(sigset_t *set)
Inicializaset
para que incluya todas las señales.int sigeaddset(sigset_t *set, int signum)
Añade la señalsignum
al conjunto de señalesset
.int sigedelset(sigset_t *set, int signum)
Elimina la señalsignum
del conjunto de señalesset
.int sigismember (const sigset_t *set, int signum)
Devuelve 1 si la señalsignum
está incluida en el conjuntoset
. Mientras que retorna 0 en caso contrario.
Manejo avanzado de señales
En el apartado sobre manejadores de señal vimos como podemos especificar un manejador para cada señal usando la función de la librería estándar std::signal() o la llamada al sistema signal()(). Sin embargo esta no es la forma recomendada de hacerlo. En su lugar, por motivos de portabilidad, se recomienda usar sigaction()().
int sigaction (int signum,
const struct sigaction *act,
struct sigaction *restrict oldact)
donde:
signum
Señal para la que se va a modificar la acción.act
Puntero a una estructurasigaction
que describe la nueva acción para la señalsignum
. Si este puntero es NULL, no se modificará la acción actual y su descripción podrá recuperarse a través deoldact
.oldact
Puntero a una estructurasigaction
que será rellenada con la antigua acción para la señalsignum
. Si este puntero es NULL, no se recuperará el valor previo de la acción para dicha señal.
Estructura sigaction
De la estructura sigaction
, que describe la acción para una señal, los campos más relevantes son:
sa_handler
Describe el manejador de la señal. Igual que ocurre con signal(), este campo puede valerSIG_DFL
,SIG_IGN
o un puntero a una función.sa_mask
Es un campo de tiposigset_t
que describe el conjunto de señales que serán bloqueadas mientras el manejador indicado ensa_handler
es ejecutado. Además de estas señales, también se bloqueará automáticamente la misma señal que provocó la ejecución del manejador.sa_flags
Especifica varios flags, combinados mediante operador OR, que puede afectar a como se maneja la señal.
Entre los valores posibles para sa_flags
los más comunes son:
SA_NOCLDSTOP
Este flag sólo tiene sentido si se usa con la señalSIGCHLD
y sirve para indicar que dicha señal sólo debe enviarse al padre de un proceso cuando uno de sus hijos termina, no cuando es detenido. Por defecto la señalSIGCHLD
se envía al padre en ambos casos.SA_RESTART
Este flag controla qué ocurre cuando la señal llega mientras el proceso o el hilo están dentro de ciertas llamadas al sistema —comoopen()
,read()
owrite()
—. Si no se especificaSA_RESTART
, dichas operaciones serán interrumpidas cuando termine el manejador de la señal, retornando con el código de errorEINTR
. Por el contrario, si se especificaSA_RESTART
, la llamada afectada continuará tras ejecutarse el manejador de la señal que la interrumpió.
Referencias
Unix signal — Wikipedia.
Signal Handling — The GNU C Library.