3.2.2. Linux: señales POSIX
Texto íntegro de la conversión. Las figuras y la disposición de las notas se pueden consultar en el PDF.
3.2.2. Linux: señales POSIX
El sistema operativo Unix ofrece a los procesos el mecanismo de las señales. Desgraciadamente, existen varias interfaces de señales (SystemV, BSD, POSIX, etc.) que presentan diferencias de funcionalidad muy significativas. Como la interfaz de señales más completa y que permite hacer programas más robustos es la interfaz POSIX (la utilizada por Linux), en este subapartado presentamos una breve descripción de ella. La interfaz POSIX también recibe el nombre de reliable signals, mientras que la SystemV también recibe el nombre de traditional signals.
Página 37
Terminología
A continuación, describimos algunos términos utilizados a lo largo de esta sección:
-
Generación de la señal: momento en el que se produce una señal.
-
Tratamiento de la señal: rutina de atención para una determinada señal (puede ser el tratamiento por defecto proporcionado por el SO o una rutina proporcionada por el usuario).
-
Depósito de la señal: momento en el que se empieza a ejecutar la rutina de tratamiento de una señal.
-
Señal pendiente: señal que ha sido generada pero que todavía no ha sido depositada.
-
Programación de una señal: hecho de asociar un tratamiento a una señal.
-
Captura de una señal: ejecución de una rutina proporcionada por el usuario para atender una determinada señal.
-
Señal bloqueada: si un proceso bloquea una señal, el SO no depositará temporalmente las señales de este tipo que se generen sobre el proceso. Cuando el proceso desbloquee esta señal, el SO depositará las señales pendientes. Sería equiparable a inhibir temporalmente las interrupciones hardware.
El término bloqueado y las señales pendientes
El término bloqueado tiene diferentes significados en función del contexto porque se puede aplicar a procesos y a señales. Recordemos que un proceso bloqueado es aquel que temporalmente no puede competir para utilizar el procesador porque está esperando algún acontecimiento.
El número de señales pendientes de una determinada señal está limitado. En algunas versiones POSIX, sólo puede haber una señal pendiente de cada tipo, mientras que otras versiones POSIX permiten acumular varias señales pendientes de un mismo tipo (queued signals). En este documento asumiremos que no será posible acumular señales pendientes.
-
Señal ignorada: si un proceso ignora una señal, el SO descartará las señales de este tipo que se generen sobre el proceso y no las depositará.
-
Conjunto de señales bloqueadas: atributo de todo proceso que indica qué señales tiene bloqueadas el proceso. Se almacena en el PCB del proceso y se manipula utilizando una llamada al sistema.
La figura 31 muestra un diagrama de tiempo en el que aparecen estos términos. Se representa la ejecución de un proceso que genera, bloquea, desbloquea e ignora señales.
Página 38
Figura 31. Terminología utilizada para las señales POSIX
Lista de señales
POSIX define una serie de señales con un significado concreto; otras implementaciones de señales presentan variaciones con respecto al tratamiento y al número de señales posibles. En la tabla 2 mostramos algunas de las señales definidas por POSIX con el tratamiento por defecto que asigna el SO. El significado de estos tratamientos es:
- EXIT: destrucción del proceso.
- EXIT+CORE: destrucción del proceso y generación de un fichero core que contiene el estado del proceso en el momento del depósito de la señal.
- IGNORE: ignorar la señal.
- STOP: detener la ejecución del proceso.
- CONT: reanudar la ejecución de un proceso detenido.
Nombre señal
SIGHUP terminal o un módem.
SIGINT mente Ctrl-C).
SIGABRT
SIGFPE
SIGKILL
SIGSEGV
SIGPIPE
SIGALRM
SIGTERM
SIGUSR1
SIGUSR2
SIGCHLD
SIGSTOP mente Ctrl-Z). La señal no se puede ignorar, bloquear o programar.
SIGCONT
Tabla 2. Lista de algunas señales POSIX: nombre de la señal, significado asociado y acción por defecto llevada a cabo por el SO
Notas y pies de figura de la fuente
Ved también En el módulo "La gestión de la memoria" hablamos de estos ficheros core cuando un proceso intenta acceder a una dirección de memoria inválida. Significado de la señal Tratamiento por defecto Se ha producido un corte de la línea de comunicación, generalmente con un EXIT Se ha pulsado la secuencia de teclas de control asociada a esta señal (típica- EXIT Aborta la ejecución del proceso. EXIT + CORE Se ha producido una excepción de coma flotante. EXIT + CORE Destruir el proceso. La señal no se puede ignorar, bloquear o programar. EXIT Se ha producido un acceso indebido a la memoria. EXIT + CORE Se ha escrito en una pipe sin ningún lector. EXIT Se ha producido una expiración del temporizador. EXIT Se solicita que el proceso acabe ("Prepare to die"). EXIT Disponible para usos propios de las aplicaciones. EXIT Disponible para usos propios de las aplicaciones. EXIT Ha muerto algún proceso hijo. IGNORE Se ha pulsado la secuencia de teclas de control asociada a esta señal (típica- STOP Reanuda la ejecución de un proceso que haya sido detenido con SIGSTOP. CONT
Página 39
Cada señal (SIGHUP, SIGKILL, etc.) tiene asociado un código numérico entero; por ejemplo, el SIGKILL tiene asociado el valor 916. Para facilitar la legibilidad del código, en los fragmentos de código de ejemplo utilizaremos los nombres simbólicos de las señales y no su código numérico. Conjuntos de señales (signal sets)
Diferentes llamadas al sistema relacionadas con la interfaz de señales POSIX están parametrizadas con una variable que representa un conjunto de señales. Por ejemplo, nos puede interesar bloquear las señales SIGUSR1, SIGUSR2 y SIGTERM. Para hacerlo, necesitamos definir una variable de tipo "conjunto de señales" que contenga estos tres tipos de señales. POSIX nos ofrece un tipo de datos (sigset_t) y una serie de funciones para manipular estas variables. La figura 32 enumera estas funciones y describe su funcionalidad; también muestra un par de ejemplos: crear un conjunto con las señales SIGUSR1 y SIGUSR2 y crear un conjunto con todas las señales salvo la SIGTERM.
#include <signal.h>
int sigemptyset(sigset_t set); / Inicializa "set" en el conjunto vacío */ int sigfillset(sigset_t set); / Inicializa "set" con todos los signals */
int sigaddset(sigset_t set, int signum); / Añade "signum" a "set" */ int sigdelset(sigset_t set, int signum); / Elimina "signum" de "set" */ int sigismember(const sigset_t set, int signum); / Indica si "signum" pertenece a "set" */
/* Ejemplos de uso / sigset_t u12 / Contendrá los signals SIGUSR1 y SIGUSR2 / noterm; / Contendrá todos los signals salvo el SIGTERM */
sigemptyset(&u12); sigaddset(&u12, SIGUSR1); sigaddset(&u12, SIGUSR2); sigfillset(¬erm); sigdelset(¬erm, SIGTERM);
Figura 32. Funciones que permiten manipular variables de tipo sigset_t y dos ejemplos de utilización.
Programación de una señal
La llamada al sistema que permite definir la rutina de atención a una señal es la llamada sigaction (figura 33). Su primer parámetro es el identificador de señal del que queremos modificar la rutina de tratamiento. El segundo parámetro es un puntero a una estructura (struct sigaction) que indica cuál será el nuevo tratamiento:
- En el campo sa_handler, indicaremos qué rutina queremos que atienda esta señal (en caso de que queramos ignorar la señal, hay que indicar el valor SIG_IGN, y en caso de querer recuperar el tratamiento por defecto, hay que indicar el valor SIG_DFL). Cuando esta rutina se ejecute, recibirá como parámetro el código numérico correspondiente a la señal que ha
Notas y pies de figura de la fuente
(16)Los habituados a trabajar con algún intérprete de comandos Unix probablemente habrán utilizado el comando kill -9 pid para matar un proceso. Este comando genera la señal número 9 (SIGKILL) sobre el proceso con identificador pid.
Página 40
provocado su ejecución (es posible asociar la misma rutina a señales diferentes).
-
En el campo sa_mask, de tipo "conjunto de señales", indicaremos qué señales queremos que estén bloqueadas mientras se ejecute la rutina de atención.
-
En el campo sa_flags, podremos modificar el comportamiento por defecto de las señales POSIX. El valor 0 hace que el tratamiento de esta señal siga el estándar POSIX.
#include <signal.h>
struct sigaction { void (*sa_handler)(int); sigset_t sa_mask;
int sa_flags; };
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
Figura 33. Interfaz de la llamada al sistema sigaction
La llamada devuelve el valor 0 si se ha podido reprogramar la señal y -1 en caso de error. Un error típico es intentar reprogramar una señal que el SO no permite reprogramar (por ejemplo, el SIGKILL). Además, en el tercer parámetro nos devuelve el puntero a una estructura struct sigaction mediante la que podemos recuperar cuál era hasta el momento la programación de esta señal.
En el estándar POSIX, la programación de la señal es válida hasta el momento en el que se vuelva a programar esta misma señal (en el estándar SystemV, sólo era válida hasta el primer depósito de una señal de este tipo).
En el caso de invocar la llamada al sistema fork, el proceso hijo hereda la programación de las señales del proceso padre. En el caso de invocar alguna llamada al sistema exec, todas las señales recuperan la programación por defecto salvo aquellos que hayan sido programadas como SIG_IGN.
Generación de señales
POSIX proporciona diferentes llamadas al sistema para generar señales. Comentaremos dos: kill17 y alarm (figura 34). int kill(pid_t pid, int sig); unsigned int alarm(unsigned int seconds);
Figura 34. Llamadas al sistema que permiten generar señales.
Notas y pies de figura de la fuente
(17)El nombre kill (matar) viene dado porque las primeras implementaciones de señales se utilizaban únicamente para provocar la muerte de procesos.
Página 41
La llamada kill genera una señal "inmediatamente" sobre un determinado proceso. Tiene como parámetro el identificador de proceso sobre el que queremos generar la señal y el tipo de señal que se va a generar. La llamada devolverá error si intentamos generar una señal sobre un proceso que pertenece a otro usuario o sobre un proceso inexistente.
La llamada alarm permite programar el temporizador asociado al proceso. Tiene como parámetro el número de segundos18 (de tiempo real) que deben transcurrir antes de que expire el temporizador. Cuando el temporizador expire, el SO generará una señal SIGALRM sobre el proceso. Para generar un nuevo SIGALRM, habrá que programar nuevamente el temporizador. En el caso de invocar la llamada alarm con el parámetro 0, el SO ignorará la última programación del temporizador.
Conjunto de señales bloqueadas
Todo proceso tiene un atributo en su PCB que indica qué señales tiene bloqueadas en este momento. La gestión de este atributo se debe realizar utilizando la llamada al sistema sigprocmask. El primer parámetro de la llamada (how) indica el tipo de modificación que queremos hacer (SIG_BLOCK, SIG_UNBLOCK o SIG_SETMASK); la llamada permite modificar el valor de este atributo añadiendo (SIG_BLOCK), eliminando (SIG_UNBLOCK) o asignando directamente (SIG_SETMASK) al conjunto de señales indicado en el segundo parámetro (set). En el tercer parámetro, la llamada nos puede devolver el valor del atributo antes de realizar este cambio.
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/* el atributo se modifica en función de valor de "how" (SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK):
SIG_BLOCK: atributo = atributo UNIÓN set; SIG_UNBLOCK: atributo = atributo INTERSECCIÓN COMPLEMENTARIO(set)
SIG_SETMASK: atributo = set; */
Figura 35. Llamada al sistema sigprocmask
El valor de este atributo se hereda al invocar la llamada fork y se mantiene al invocar alguna de las llamadas exec. Si el valor de este atributo se modifica dentro de la rutina de atención a alguna señal (o utilizando el campo sa_mask de la estructura struct sigaction de la llamada al sistema sigaction), al acabar la ejecución de la rutina de atención, el SO restaurará el valor previo de este atributo. Es decir, la ejecución de una rutina de tratamiento de una señal no puede modificar permanentemente el valor de este atributo.
Notas y pies de figura de la fuente
(18)POSIX ofrece otros temporizadores con una resolución más fina, pero no son objeto de este documento.
Página 42
La figura 36 muestra un fragmento de un código de ejemplo que utiliza esta llamada. Se pretende bloquear temporalmente la señal SIGTERM mientras el proceso está escribiendo datos en un fichero (se entiende que hay que hacerlo sin modificar al resto de señales bloqueadas por el proceso). Se quiere evitar que se pueda depositar esta señal mientras el proceso escribe datos en el fichero porque esto provocaría la muerte del proceso pudiendo dejar el fichero en un estado incoherente. El programa bloquea temporalmente la señal, con lo que, si la señal se genera, se depositará únicamente cuando todos los datos hayan sido escritos en el fichero. Se presentan tres opciones para hacerlo: la primera y la segunda no son del todo correctas porque no tienen en consideración qué señales pueden estar bloqueadas actualmente; la tercera opción tiene en cuenta todas las posibilidades.
sigset_t term, old;
sigemptyset(&term); sigaddset(&term, SIGTERM);
/* Opción 1: Errónea si ya tenemos algún signal bloqueado */ sigprocmask(SIG_SETMASK, &term, NULL);
escritura_fichero(); sigprocmask(SIG_UNBLOCK, &term, NULL);
/* Opción 2: Errónea si ya tenemos el SIGTERM bloqueado */ sigprocmask(SIG_BLOCK, &term, NULL); escritura_fichero();
sigprocmask(SIG_UNBLOCK, &term, NULL);
/* Opción 3: Correcta */
sigprocmask(SIG_BLOCK, &term, &old); escritura_fichero();
sigprocmask(SIG_SETMASK, &old, NULL);
Figura 36. Ejemplos de utilización de la llamada sigprocmask
Otro posible uso del bloqueo de señales es garantizar el acceso en exclusión mutua a una estructura de datos a la que se acceda tanto desde el programa principal como desde una rutina de atención a una señal. Hay que bloquear esta señal mientras el programa principal esté modificando la estructura de datos.
Página 43
Espera del depósito de una señal
Una de las operaciones más habituales que se realizan con señales es la sincronización, en la que un proceso debe esperar hasta que se deposite un determinado tipo de señal. Asumimos que el proceso está esperando una señal de tipo SIGUSR1 y que la rutina de tratamiento a esta señal pone en TRUE la variable global usr1.
Una primera aproximación consistiría en realizar la espera efectuando una espera activa (figura 37), es decir, que el proceso esté continuamente consultando el valor de esta variable hasta que cambie de valor. Ello penaliza al resto de procesos en ejecución en el sistema y, en general, no sería una solución aceptable.
int usr1 = FALSE;
void ras_usr1(int signum) { usr1 = TRUE;
}
int main()
{ struct sigaction act;
/* Programación de la rutina de atención en el SIGUSR1 */ act.sa_handler = ras_usr1;
sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGUSR1, &act, NULL);
while (usr1 == FALSE); /* Espera activa */ ...
}
Figura 37. Espera del depósito de una señal utilizando una espera activa
Las versiones tradicionales de señales ofrecían la llamada al sistema pause pero, en general, la utilización de esta llamada provoca que los programas no sean reliables. La llamada pause hace esperar al proceso hasta que se deposite una señal sobre éste. Si utilizamos esta llamada (figura 38), el código resultante puede comportarse de manera errónea en caso de que el SIGUSR1 se deposite una vez que se ha verificado que usr1 tiene el valor FALSE pero antes de invocar la llamada al sistema pause; se ejecutará la rutina de atención a la señal y el proceso esperará el depósito de una señal que ya ha sido depositada.
Página 44
if (usr1 == FALSE) pause();
Figura 38. Espera del depósito de una señal utilizando la llamada al sistema pause Para solucionar este problema, la interfaz de señales POSIX proporciona la llamada al sistema sigsuspend (figura 39). Esta llamada realiza dos operaciones de manera atómica19:
- hacer que mask sea el nuevo valor del atributo del conjunto de señales bloqueadas por el proceso y
- hacer que el proceso se espere20 hasta el depósito de una señal no bloqueada.
Cuando se deposite la señal, se ejecutará el tratamiento asociado y, al acabar, se restaurará el valor previo del atributo y el proceso continuará su ejecución.
int sigsuspend(const sigset_t *mask);
Figura 39. Llamada al sistema sigsuspend
Mediante la utilización de esta llamada, podremos dar una nueva solución al problema anterior. La figura 40 muestra el código resultante asumiendo que queremos permitir tratar cualquier otra señal mientras esperamos el depósito del SIGUSR1.
sigset_t m, old;
sigemptyset(&m); sigaddset(&m, SIGUSR1);
sigprocmask(SIG_BLOCK, &m, &old); sigemptyset(&m);
while (usr1 == FALSE) sigsuspend(&m); /* Esperando cualquier señal */
sigprocmask(SIG_SETMASK, &old, NULL);
Figura 40. Llamada al sistema sigsuspend
Si mientras ejecutamos el SIGUSR1 queremos permitir únicamente el tratamiento de las señales que no estuviesen bloqueadas, habría que introducir algunas modificaciones en el código (figura 41).
sigset_t m, old;
sigprocmask(SIG_BLOCK, NULL, &m); /* Obtenemos signals bloqueadas */ sigaddset(&m, SIGUSR1);
Notas y pies de figura de la fuente
(19)Entre otras cosas, el SO garantiza que no se depositará ninguna señal sobre el proceso mientras se realizan estas dos operaciones. (20)En las llamadas pause y sigsuspend, el SO no implementa esta espera utilizando una espera activa. El SO provoca que el proceso pase al estado blocked hasta que se deposite una señal sobre el proceso.
Página 45
sigprocmask(SIG_BLOCK, &m, &old); sigdelset(&m, SIGUSR1);
while (usr1 == FALSE) sigsuspend(&m);/* Esperando SIGUSR1 o cualquier señal no bloqueada */
sigprocmask(SIG_SETMASK, &old, NULL);
Figura 41. Espera del depósito de una señal utilizando la llamada al sistema sigsuspend
En los ejercicios de autoevaluación se proponen diferentes actividades relacionadas con el conjunto de señales bloqueadas que tiene el proceso y con la utilización de la llamada al sistema sigsuspend.
Depósito de señales sobre procesos bloqueados
Mientras un proceso está bloqueado (por ejemplo, leyendo del teclado o de una pipe vacía, esperando la muerte de un proceso hijo, haciendo un sem_wait sobre un semáforo), es posible que el SO deposite una señal sobre este proceso. Si la señal no está bloqueada, se ejecutará el tratamiento asociado. Ahora bien, si este tratamiento no provoca la muerte del proceso, ¿qué sucederá con la llamada al sistema invocada por el proceso?
-
En algunos casos, el SO abortará la llamada al sistema (la llamada devolverá un -1 como resultado y en la variable errno –código de error– encontraremos el valor EINTR) y el proceso continuará su ejecución.
-
En otros casos, el SO reiniciará la llamada al sistema con lo que el proceso se volverá a bloquear.
En función del tipo de llamada al sistema y de cómo haya sido definido el tratamiento de la señal (campo sa_flags de la estructura struct sigaction de la llamada al sistema sigaction) estaremos en un caso o en otro. Para encontrar más información al respecto, podéis consultar el manual del sistema operativo ejecutando el comando man 7 signal sobre algún sistema Linux.
En todo caso, aconsejamos llevar a cabo un control de errores exhaustivo en las llamadas al sistema con el fin de detectar llamadas al sistema que el SO no haya podido servir.
Ver fragmento extraído sin normalizar
3.2.2. Linux: señales POSIX
El sistema operativo Unix ofrece a los procesos el mecanismo de las señales.
Desgraciadamente, existen varias interfaces de señales (SystemV, BSD, POSIX,
etc.) que presentan diferencias de funcionalidad muy significativas. Como la
interfaz de señales más completa y que permite hacer programas más robustos
es la interfaz POSIX (la utilizada por Linux), en este subapartado presentamos
una breve descripción de ella. La interfaz POSIX también recibe el nombre de
reliable signals, mientras que la SystemV también recibe el nombre de traditio-
nal signals.
## Página 37
<!-- source-page: 37 -->
GNUFDL • PID_00214803 37 Comunicación y sincronización
Terminología
A continuación, describimos algunos términos utilizados a lo largo de esta
sección:
• Generacióndelaseñal: momento en el que se produce una señal.
• Tratamientodelaseñal: rutina de atención para una determinada señal
(puede ser el tratamiento por defecto proporcionado por el SO o una rutina
proporcionada por el usuario).
• Depósitodelaseñal: momento en el que se empieza a ejecutar la rutina
de tratamiento de una señal.
• Señalpendiente: señal que ha sido generada pero que todavía no ha sido
depositada.
• Programacióndeunaseñal: hecho de asociar un tratamiento a una señal.
• Capturadeunaseñal: ejecución de una rutina proporcionada por el usua-
rio para atender una determinada señal.
• Señalbloqueada: si un proceso bloquea una señal, el SO no deposita-
rá temporalmente las señales de este tipo que se generen sobre el proce-
so. Cuando el proceso desbloquee esta señal, el SO depositará las señales
pendientes. Sería equiparable a inhibir temporalmente las interrupciones
hardware.
El término bloqueado y las señales pendientes
El término bloqueado tiene diferentes significados en función del contexto porque se pue-
de aplicar a procesos y a señales. Recordemos que un proceso bloqueado es aquel que
temporalmente no puede competir para utilizar el procesador porque está esperando al-
gún acontecimiento.
El número de señales pendientes de una determinada señal está limitado. En algunas
versiones POSIX, sólo puede haber una señal pendiente de cada tipo, mientras que otras
versiones POSIX permiten acumular varias señales pendientes de un mismo tipo (queued
signals). En este documento asumiremos que no será posible acumular señales pendien-
tes.
• Señalignorada: si un proceso ignora una señal, el SO descartará las señales
de este tipo que se generen sobre el proceso y no las depositará.
• Conjuntodeseñalesbloqueadas: atributo de todo proceso que indica qué
señales tiene bloqueadas el proceso. Se almacena en el PCB del proceso y
se manipula utilizando una llamada al sistema.
La figura 31 muestra un diagrama de tiempo en el que aparecen estos términos.
Se representa la ejecución de un proceso que genera, bloquea, desbloquea e
ignora señales.
## Página 38
<!-- source-page: 38 -->
GNUFDL • PID_00214803 38 Comunicación y sincronización
Figura 31. Terminología utilizada para las señales POSIX
Lista de señales
POSIX define una serie de señales con un significado concreto; otras imple-
mentaciones de señales presentan variaciones con respecto al tratamiento y
al número de señales posibles. En la tabla 2 mostramos algunas de las señales
definidas por POSIX con el tratamiento por defecto que asigna el SO. El signi-
ficado de estos tratamientos es:
• EXIT: destrucción del proceso. Ved también
• EXIT+CORE: destrucción del proceso y generación de un fichero core que
contiene el estado del proceso en el momento del depósito de la señal. En el módulo "La gestión de la
memoria" hablamos de estos
• IGNORE: ignorar la señal. ficheros core cuando un proce-
so intenta acceder a una direc-
• STOP: detener la ejecución del proceso. ción de memoria inválida.
• CONT: reanudar la ejecución de un proceso detenido.
Nombre señal Significado de la señal Tratamiento por defecto
SIGHUP Se ha producido un corte de la línea de comunicación, generalmente con un EXIT
terminal o un módem.
SIGINT Se ha pulsado la secuencia de teclas de control asociada a esta señal (típica- EXIT
mente Ctrl-C).
SIGABRT Aborta la ejecución del proceso. EXIT + CORE
SIGFPE Se ha producido una excepción de coma flotante. EXIT + CORE
SIGKILL Destruir el proceso. La señal no se puede ignorar, bloquear o programar. EXIT
SIGSEGV Se ha producido un acceso indebido a la memoria. EXIT + CORE
SIGPIPE Se ha escrito en una pipe sin ningún lector. EXIT
SIGALRM Se ha producido una expiración del temporizador. EXIT
SIGTERM Se solicita que el proceso acabe ("Prepare to die"). EXIT
SIGUSR1 Disponible para usos propios de las aplicaciones. EXIT
SIGUSR2 Disponible para usos propios de las aplicaciones. EXIT
SIGCHLD Ha muerto algún proceso hijo. IGNORE
SIGSTOP Se ha pulsado la secuencia de teclas de control asociada a esta señal (típica- STOP
mente Ctrl-Z). La señal no se puede ignorar, bloquear o programar.
SIGCONT Reanuda la ejecución de un proceso que haya sido detenido con SIGSTOP. CONT
Tabla 2. Lista de algunas señales POSIX: nombre de la señal, significado asociado y acción por defecto llevada a cabo por el SO
## Página 39
<!-- source-page: 39 -->
GNUFDL • PID_00214803 39 Comunicación y sincronización
Cada señal (SIGHUP, SIGKILL, etc.) tiene asociado un código numérico ente- (16)Los habituados a trabajar con
ro; por ejemplo, el SIGKILL tiene asociado el valor 916. Para facilitar la legi- algún intérprete de comandos
Unix probablemente habrán utili-
bilidad del código, en los fragmentos de código de ejemplo utilizaremos los zado el comando kill -9 pid
para matar un proceso. Este co-
nombres simbólicos de las señales y no su código numérico. mando genera la señal número 9
(SIGKILL) sobre el proceso con
identificador pid.
Conjuntos de señales (signal sets)
Diferentes llamadas al sistema relacionadas con la interfaz de señales POSIX
están parametrizadas con una variable que representa un conjunto de señales.
Por ejemplo, nos puede interesar bloquear las señales SIGUSR1, SIGUSR2 y
SIGTERM. Para hacerlo, necesitamos definir una variable de tipo "conjunto de
señales" que contenga estos tres tipos de señales. POSIX nos ofrece un tipo
de datos (sigset_t) y una serie de funciones para manipular estas variables.
La figura 32 enumera estas funciones y describe su funcionalidad; también
muestra un par de ejemplos: crear un conjunto con las señales SIGUSR1 y
SIGUSR2 y crear un conjunto con todas las señales salvo la SIGTERM.
#include <signal.h>
int sigemptyset(sigset_t *set); /* Inicializa "set" en el conjunto vacío */
int sigfillset(sigset_t *set); /* Inicializa "set" con todos los signals */
int sigaddset(sigset_t *set, int signum); /* Añade "signum" a "set" */
int sigdelset(sigset_t *set, int signum); /* Elimina "signum" de "set" */
int sigismember(const sigset_t *set, int signum); /* Indica si "signum" pertenece a "set" */
/* Ejemplos de uso */
sigset_t u12 /* Contendrá los signals SIGUSR1 y SIGUSR2 */
noterm; /* Contendrá todos los signals salvo el SIGTERM */
sigemptyset(&u12); sigaddset(&u12, SIGUSR1); sigaddset(&u12, SIGUSR2);
sigfillset(¬erm); sigdelset(¬erm, SIGTERM);
Figura 32. Funciones que permiten manipular variables de tipo sigset_t y dos ejemplos
de utilización.
Programación de una señal
La llamada al sistema que permite definir la rutina de atención a una señal es
la llamada sigaction (figura 33). Su primer parámetro es el identificador de
señal del que queremos modificar la rutina de tratamiento. El segundo pará-
metro es un puntero a una estructura (struct sigaction) que indica cuál
será el nuevo tratamiento:
• En el campo sa_handler, indicaremos qué rutina queremos que atienda
esta señal (en caso de que queramos ignorar la señal, hay que indicar el
valor SIG_IGN, y en caso de querer recuperar el tratamiento por defecto,
hay que indicar el valor SIG_DFL). Cuando esta rutina se ejecute, recibi-
rá como parámetro el código numérico correspondiente a la señal que ha
## Página 40
<!-- source-page: 40 -->
GNUFDL • PID_00214803 40 Comunicación y sincronización
provocado su ejecución (es posible asociar la misma rutina a señales dife-
rentes).
• En el campo sa_mask, de tipo "conjunto de señales", indicaremos qué
señales queremos que estén bloqueadas mientras se ejecute la rutina de
atención.
• En el campo sa_flags, podremos modificar el comportamiento por de-
fecto de las señales POSIX. El valor 0 hace que el tratamiento de esta señal
siga el estándar POSIX.
#include <signal.h>
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
};
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
Figura 33. Interfaz de la llamada al sistema sigaction
La llamada devuelve el valor 0 si se ha podido reprogramar la señal y -1 en caso
de error. Un error típico es intentar reprogramar una señal que el SO no per-
mite reprogramar (por ejemplo, el SIGKILL). Además, en el tercer parámetro
nos devuelve el puntero a una estructura struct sigaction mediante la que
podemos recuperar cuál era hasta el momento la programación de esta señal.
En el estándar POSIX, la programación de la señal es válida hasta el momento
en el que se vuelva a programar esta misma señal (en el estándar SystemV, sólo
era válida hasta el primer depósito de una señal de este tipo).
En el caso de invocar la llamada al sistema fork, el proceso hijo hereda la
programación de las señales del proceso padre. En el caso de invocar alguna
llamada al sistema exec, todas las señales recuperan la programación por de-
fecto salvo aquellos que hayan sido programadas como SIG_IGN.
Generación de señales
POSIX proporciona diferentes llamadas al sistema para generar señales. Co- (17)El nombre kill (matar) viene
mentaremos dos: kill17 y alarm (figura 34). dado porque las primeras imple-
mentaciones de señales se utiliza-
ban únicamente para provocar la
muerte de procesos.
int kill(pid_t pid, int sig);
unsigned int alarm(unsigned int seconds);
Figura 34. Llamadas al sistema que permiten generar señales.
## Página 41
<!-- source-page: 41 -->
GNUFDL • PID_00214803 41 Comunicación y sincronización
La llamada kill genera una señal "inmediatamente" sobre un determinado
proceso. Tiene como parámetro el identificador de proceso sobre el que que-
remos generar la señal y el tipo de señal que se va a generar. La llamada devol-
verá error si intentamos generar una señal sobre un proceso que pertenece a
otro usuario o sobre un proceso inexistente.
La llamada alarm permite programar el temporizador asociado al proceso. (18)POSIX ofrece otros temporiza-
Tiene como parámetro el número de segundos18 (de tiempo real) que deben dores con una resolución más fina,
pero no son objeto de este docu-
transcurrir antes de que expire el temporizador. Cuando el temporizador expi- mento.
re, el SO generará una señal SIGALRM sobre el proceso. Para generar un nuevo
SIGALRM, habrá que programar nuevamente el temporizador. En el caso de
invocar la llamada alarm con el parámetro 0, el SO ignorará la última progra-
mación del temporizador.
Conjunto de señales bloqueadas
Todo proceso tiene un atributo en su PCB que indica qué señales tiene blo-
queadas en este momento. La gestión de este atributo se debe realizar uti-
lizando la llamada al sistema sigprocmask. El primer parámetro de la lla-
mada (how) indica el tipo de modificación que queremos hacer (SIG_BLOCK,
SIG_UNBLOCK o SIG_SETMASK); la llamada permite modificar el valor de es-
te atributo añadiendo (SIG_BLOCK), eliminando (SIG_UNBLOCK) o asignando
directamente (SIG_SETMASK) al conjunto de señales indicado en el segundo
parámetro (set). En el tercer parámetro, la llamada nos puede devolver el va-
lor del atributo antes de realizar este cambio.
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/* el atributo se modifica en función de valor de "how" (SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK):
SIG_BLOCK: atributo = atributo UNIÓN set;
SIG_UNBLOCK: atributo = atributo INTERSECCIÓN COMPLEMENTARIO(set)
SIG_SETMASK: atributo = set;
*/
Figura 35. Llamada al sistema sigprocmask
El valor de este atributo se hereda al invocar la llamada fork y se mantiene
al invocar alguna de las llamadas exec. Si el valor de este atributo se modifica
dentro de la rutina de atención a alguna señal (o utilizando el campo sa_mask
de la estructura struct sigaction de la llamada al sistema sigaction), al
acabar la ejecución de la rutina de atención, el SO restaurará el valor previo de
este atributo. Es decir, la ejecución de una rutina de tratamiento de una señal
no puede modificar permanentemente el valor de este atributo.
## Página 42
<!-- source-page: 42 -->
GNUFDL • PID_00214803 42 Comunicación y sincronización
La figura 36 muestra un fragmento de un código de ejemplo que utiliza esta
llamada. Se pretende bloquear temporalmente la señal SIGTERM mientras el
proceso está escribiendo datos en un fichero (se entiende que hay que hacerlo
sin modificar al resto de señales bloqueadas por el proceso). Se quiere evitar
que se pueda depositar esta señal mientras el proceso escribe datos en el fichero
porque esto provocaría la muerte del proceso pudiendo dejar el fichero en un
estado incoherente. El programa bloquea temporalmente la señal, con lo que,
si la señal se genera, se depositará únicamente cuando todos los datos hayan
sido escritos en el fichero. Se presentan tres opciones para hacerlo: la primera
y la segunda no son del todo correctas porque no tienen en consideración
qué señales pueden estar bloqueadas actualmente; la tercera opción tiene en
cuenta todas las posibilidades.
sigset_t term, old;
sigemptyset(&term); sigaddset(&term, SIGTERM);
/* Opción 1: Errónea si ya tenemos algún signal bloqueado */
sigprocmask(SIG_SETMASK, &term, NULL);
escritura_fichero();
sigprocmask(SIG_UNBLOCK, &term, NULL);
/* Opción 2: Errónea si ya tenemos el SIGTERM bloqueado */
sigprocmask(SIG_BLOCK, &term, NULL);
escritura_fichero();
sigprocmask(SIG_UNBLOCK, &term, NULL);
/* Opción 3: Correcta */
sigprocmask(SIG_BLOCK, &term, &old);
escritura_fichero();
sigprocmask(SIG_SETMASK, &old, NULL);
Figura 36. Ejemplos de utilización de la llamada sigprocmask
Otro posible uso del bloqueo de señales es garantizar el acceso en exclusión
mutua a una estructura de datos a la que se acceda tanto desde el programa
principal como desde una rutina de atención a una señal. Hay que bloquear
esta señal mientras el programa principal esté modificando la estructura de
datos.
## Página 43
<!-- source-page: 43 -->
GNUFDL • PID_00214803 43 Comunicación y sincronización
Espera del depósito de una señal
Una de las operaciones más habituales que se realizan con señales es la sin-
cronización, en la que un proceso debe esperar hasta que se deposite un de-
terminado tipo de señal. Asumimos que el proceso está esperando una señal
de tipo SIGUSR1 y que la rutina de tratamiento a esta señal pone en TRUE la
variable global usr1.
Una primera aproximación consistiría en realizar la espera efectuando una es-
pera activa (figura 37), es decir, que el proceso esté continuamente consultan-
do el valor de esta variable hasta que cambie de valor. Ello penaliza al resto
de procesos en ejecución en el sistema y, en general, no sería una solución
aceptable.
int usr1 = FALSE;
void ras_usr1(int signum)
{
usr1 = TRUE;
}
int main()
{
struct sigaction act;
/* Programación de la rutina de atención en el SIGUSR1 */
act.sa_handler = ras_usr1;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);
while (usr1 == FALSE); /* Espera activa */
...
}
Figura 37. Espera del depósito de una señal utilizando una espera activa
Las versiones tradicionales de señales ofrecían la llamada al sistema pause
pero, en general, la utilización de esta llamada provoca que los programas no
sean reliables. La llamada pause hace esperar al proceso hasta que se deposite
una señal sobre éste. Si utilizamos esta llamada (figura 38), el código resultante
puede comportarse de manera errónea en caso de que el SIGUSR1 se deposite
una vez que se ha verificado que usr1 tiene el valor FALSE pero antes de invocar
la llamada al sistema pause; se ejecutará la rutina de atención a la señal y el
proceso esperará el depósito de una señal que ya ha sido depositada.
## Página 44
<!-- source-page: 44 -->
GNUFDL • PID_00214803 44 Comunicación y sincronización
if (usr1 == FALSE) pause();
Figura 38. Espera del depósito de una señal utilizando la llamada al sistema pause
Para solucionar este problema, la interfaz de señales POSIX proporciona la lla- (19)Entre otras cosas, el SO garan-
mada al sistema sigsuspend (figura 39). Esta llamada realiza dos operaciones tiza que no se depositará ninguna
señal sobre el proceso mientras se
de manera atómica19: realizan estas dos operaciones.
(20)En las llamadas pause y sig-
• hacer que mask sea el nuevo valor del atributo del conjunto de señales suspend, el SO no implementa
bloqueadas por el proceso y esta espera utilizando una espera
activa. El SO provoca que el proce-
so pase al estado blocked hasta que
• hacer que el proceso se espere20 hasta el depósito de una señal no bloquea- se deposite una señal sobre el pro-
ceso.
da.
Cuando se deposite la señal, se ejecutará el tratamiento asociado y, al acabar,
se restaurará el valor previo del atributo y el proceso continuará su ejecución.
int sigsuspend(const sigset_t *mask);
Figura 39. Llamada al sistema sigsuspend
Mediante la utilización de esta llamada, podremos dar una nueva solución al
problema anterior. La figura 40 muestra el código resultante asumiendo que
queremos permitir tratar cualquier otra señal mientras esperamos el depósito
del SIGUSR1.
sigset_t m, old;
sigemptyset(&m); sigaddset(&m, SIGUSR1);
sigprocmask(SIG_BLOCK, &m, &old);
sigemptyset(&m);
while (usr1 == FALSE)
sigsuspend(&m); /* Esperando cualquier señal */
sigprocmask(SIG_SETMASK, &old, NULL);
Figura 40. Llamada al sistema sigsuspend
Si mientras ejecutamos el SIGUSR1 queremos permitir únicamente el trata-
miento de las señales que no estuviesen bloqueadas, habría que introducir al-
gunas modificaciones en el código (figura 41).
sigset_t m, old;
sigprocmask(SIG_BLOCK, NULL, &m); /* Obtenemos signals bloqueadas */
sigaddset(&m, SIGUSR1);
## Página 45
<!-- source-page: 45 -->
GNUFDL • PID_00214803 45 Comunicación y sincronización
sigprocmask(SIG_BLOCK, &m, &old);
sigdelset(&m, SIGUSR1);
while (usr1 == FALSE)
sigsuspend(&m);/* Esperando SIGUSR1 o cualquier señal no bloqueada */
sigprocmask(SIG_SETMASK, &old, NULL);
Figura 41. Espera del depósito de una señal utilizando la llamada al sistema sigsuspend
En los ejercicios de autoevaluación se proponen diferentes actividades relacio-
nadas con el conjunto de señales bloqueadas que tiene el proceso y con la
utilización de la llamada al sistema sigsuspend.
Depósito de señales sobre procesos bloqueados
Mientras un proceso está bloqueado (por ejemplo, leyendo del teclado o
de una pipe vacía, esperando la muerte de un proceso hijo, haciendo un
sem_wait sobre un semáforo), es posible que el SO deposite una señal sobre
este proceso. Si la señal no está bloqueada, se ejecutará el tratamiento asocia-
do. Ahora bien, si este tratamiento no provoca la muerte del proceso, ¿qué
sucederá con la llamada al sistema invocada por el proceso?
• En algunos casos, el SO abortará la llamada al sistema (la llamada devolve-
rá un -1 como resultado y en la variable errno –código de error– encon-
traremos el valor EINTR) y el proceso continuará su ejecución.
• En otros casos, el SO reiniciará la llamada al sistema con lo que el proceso
se volverá a bloquear.
En función del tipo de llamada al sistema y de cómo haya sido definido el
tratamiento de la señal (campo sa_flags de la estructura struct sigaction
de la llamada al sistema sigaction) estaremos en un caso o en otro. Para
encontrar más información al respecto, podéis consultar el manual del sistema
operativo ejecutando el comando man 7 signal sobre algún sistema Linux.
En todo caso, aconsejamos llevar a cabo un control de errores exhaustivo en
las llamadas al sistema con el fin de detectar llamadas al sistema que el SO no
haya podido servir.