Sucesos asíncronos.
Dejamos la sesión anterior con un problema que resolver. Hablamos
de lo nefasta que era la espera ocupada y de lo poco que
contribuía al buen comportamiento de los sistemas compartidos.
Por eso, debemos siempre usar las llamadas al sistema en su
versión bloqueante. En ellas, el proceso pasa al estado de
bloqueado y no consume CPU mientras espera a que le
den el recurso que ha solicitado.
Sin embargo, el proceso no sabe cuándo va a disponer del recurso.
¿Quiere esto decir que nuestro programa no podrá hacer hada
mientras esté esperando por el recurso?
Una solución sería
que el proceso tuviea un hijo con fork
y el hijo
hiciera algo de trabajo mientras él espera por el recurso.
¿Pero cuánto trabajo le manda si no sabe cuánto
tiempo esperará? Además,
la creación de nuevos hijos penaliza el rendimiento debido a los
cambios de proceso.
La solución que da UNIX a este tipo de problemas pasan por la
multiplexión de E/S síncrona, que veremos
en Sistemas Operativos II y
por las señales. Imaginad la situación:
en vez de realizar
una llamada bloqueante, el programa sigue adelante y, cuando el
recurso está disponible, es avisado por el Sistema Operativo
mediante una señal e, inmediatamente obligado por el propio
Sistema Operativo a ejecutar una rutina para atender a esa señal.
¿Qué son las señales?
Las señales son sucesos asíncronos
(que no se sabe cuándo se
van a producir) y que afectan al comportamiento de un proceso.
Las causas de la producción de una señal son varias:
- Pueden ser enviadas por el Sistema Operativo debido a
que ocurra un susceso.
- Pueden ser debidas a un error o una protección de
hardware.
- Las puede mandar un proceso.
- etc.
Las consecuencias de la recepción de una señal pueden
ser también variadas:
- Pueden no tener ninguna consecuencia.
- Pueden parar la ejecución de un proceso.
- Pueden hacer reanudar la ejecución de un proceso parado.
- Pueden matar al proceso (acabar con su ejecución).
- Pueden hacer que el proceso acabe y genere un fichero
de
core
.
- Pueden hacer que se ejecute una función del programa
previamente definida por el programador.
- etc.
Existe siempre una consecuencia (comportamiento) por defecto de
una señal. La lista de señales y su comportamiento
por defecto en el servidor la tenéis en la página de manual
signal(3HEAD)
. Para acceder a esta página de
manual,
teclead man -s 3HEAD signal
. También tenéis
un listado en el resumen.
Algunas señales famosas.
Algunas de las señales más comunes en un sistema UNIX son:
SIGINT
: se la manda el terminal al proceso
que estemos ejecutando en primer plano si pulsamos la
tecla de interrupción (normalmente CTRL+C). Por defecto,
mata el proceso.
SIGTERM
y SIGKILL
: se mandan
a un proceso cuando queremos que acabe. Ambas por defecto
matan al proceso, pero la segunda no se puede hacer que
varíe el resultado (siempre mata al proceso) mientras
que la primera sí es posible cambiar su comportamiento
por defecto.
SIGSTOP
y SIGCONT
: sirven para
que un proceso pare su ejecución y continúe.
No puede
ser cambiado su comportamiento. Existe una versión
que es la que manda el terminal si se pulsa la tecla de
parada (normalmente CTRL+Z): SIGTSTP
.
SIGALRM
: es usada para que el proceso
reciba la señal una vez transcurrido un tiempo prefijado.
SIGHUP
: su nombre viene de que los procesos
la reciben cuando un usuario se desconecta (cuelga,
hangs up) del ordenador. Algunos demonios del
ordenador la interpretan como manera de reiniciarse
y volver a leer el fichero de configuración.
SIGFPE
, SIGILL
,
SIGSEGV
, SIGBUS
,
SIGSYS
, etc.: son el reflejo de un suceso
de hardware (operación matemática ilegal,
instrucción
ilegal, acceso a memoria ilegal, llamada al sistema
ilegal, etc.).
SIGUSR1
y SIGUSR2
: a disposición
del usuario.
SIGCHLD
: la envía un hijo a su padre cuando
muere (solución al problema de la sesión pasada).
SIGTTIN
y SIGTTOU
: si tenemos
un proceso en segundo plano y este intenta leer o
escribir en el terminal, recibe automáticamente una de
estas señales. La primera para el proceso por defecto.
La segunda no hace nada por defecto. Es por eso que
normalmente un proceso en segundo plano puede escribir
en la pantalla pero si necesita entrada por el teclado
se para automáticamente.
Envío de señales.
Desde la shell podemos enviar una señal a un proceso mediante
la orden kill
. Por ejemplo, para mandar la señal
SIGTERM al proceso cuyo PID es 7803. haríamos:
kill -SIGTERM 7803
o
kill -15 7803
La orden kill
de HPUX admite el nombre de la señal
como
parámetro, mientras que en Solaris hay que poner el número
de la señal obligatoriamente. En este caso, a SIGTERM le
corresponde el número 15. Podéis ver la correspondencia
entre señales y números en la página de manual de
Solaris
man -s 3head signal
.
Linux admite ambas formas de expresar la señal que queremos
mandar.
Para que la señal tenga efecto, el proceso tiene que ser nuestro
o ser nosotros el superusuario (root). Imaginaos si
cualquiera
pudiera mandar señales a otro. Simplemente con mandar SIGSTOP
a la shell de otro usuario, paralizaríamos su terminal o
mandando SIGKILL haríamos que se desconectara...
Existe todo un arsenal de órdenes de
la shell familiares de
kill
que todo buen administrador
de una máquina UNIX no debería dejar de conocer:
pkill
: permite mandar una señal a un conjunto
de procesos, seleccionables según muy diversos criterios
pgrep
: igual que la anterior, pero no envía
ninguna señal, sino que devuelve los PIDs de los
procesos que cumplen la condición para así poder
usarlos en órdenes posteriores
killall
: sirve para enviar señales a
procesos que estén ejecutando el programa que
especifiquemos
fuser
: nos da los PIDs de los procesos que
tengan abierto un determinado fichero. Podéis
así saber qué proceso os impide desmontar
un lápiz USB, por ejemplo
kill
: la misma orden kill
actúa
de modo especial si le pasáis como PID un 0
(envía las señales a los familiares de la shell),
un -1 (la envía a todos los procesos del usuario) u
otro número negativo (lo veremos en Sistemas Operativos II)
Para ver un ejemplo de envío de señales, ejecutad la
siguiente orden desde la shell:
sleep 60 &
Con ayuda de ps
, anotad el PID del proceso en
ejecución. Paradlo
a continuación enviándole la señal SIGSTOP.
Comprobad que se para.
Haced que continúe mandándole la señal SIGCONT.
Existe una llamada al sistema (kill
) para enviar
señales desde nuestro programa en C. Miradla en el
resumen. También existe
otra que manda
la señal al propio proceso (raise
).
Existe otra llamada al sistema muy interesante que hace que un
proceso se quede bloqueado hasta que se reciba cualquier señal.
Se denomina pause
y, con ella, se evita la espera
ocupada por la recepción de una señal. Sin embargo,
hay que ser
cautelosos en el uso de pause()
. Siempre existe la
posibilidad de que la señal por la que estamos esperando se
reciba antes de caer en el pause()
. En tal caso,
dicha señal sería atendida en la posible manejadora y,
al volver,
caemos en el pause
y, como la señal ya se ha
recibido, no salimos de él jamás. Sería necesario,
pues,
bloquear la señal antes y desbloquearla justo la
instrucción
previa al pause()
. Pero ni siquiera así
evitaríamos
el error. Se nos puede colar la señal justo entre las dos
instrucciones. La manera correcta de hacerlo es usando la
llamada al sistema sigsuspend
que desbloquea
la señal y se queda esperando de un modo atómico.
De dicha llamada hablaremos un poco más adelante en la
sesión.
pause
queda reservado casi exclusivamente para
bucles infinitos de pause
s.
-
Normalmente, cuando una llamada al sistema es bloqueante y
el proceso está parado en la llamada al sistema esperando a que
se disponga del recurso solicitado, si se recibe una señal,
la llamada al sistema vuelve inmediatamente, devolviendo condición
de error y asignando a la variable
errno
el valor
EINTR
. Tanto errno
como EINTR
están definidas en
el fichero de cabeceras errno.h
.
Esto es así para permitir a nuestro programa cambiar su
comportamiento en caso de que la recepción de la señal
indicara algo.
Vamos a verlo con un ejemplo. En el siguiente código, un
padre tiene un hijo. El padre se queda bloqueado con una
llamada al sistema wait
esperando a que el hijo
muera. El padre también ha registrado la señal
SIGUSR1
para que, si se recibe, salte a una manejadora donde se
imprime un mensaje.
El hijo está en un bucle infinito de pause
s
sin consumir CPU. En principio, el padre espera en vano,
pues el hijo no va a morir nunca:
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
void nonada(int s)
{
printf("PADRE: Estoy en la manejadora.\n");
}
int main(void)
{
char lInea[40];
int valor_devuelto;
struct sigaction ss;
switch (fork())
{
case -1: perror("fork"); return 1;
case 0: /* HIJO */
for (;;) pause();
default: /* PADRE */
ss.sa_handler=nonada;
sigemptyset(&ss.sa_mask);
ss.sa_flags=0;
if (-1==sigaction(SIGUSR1,&ss,NULL))
{perror("PADRE: sigaction");
return 1;}
printf("PADRE: SIGUSR1(%d) registrada.\n",SIGUSR1);
if (wait(&valor_devuelto)==-1)
{perror("PADRE: wait");
return 1;}
}
return 0;
}
Compilamos y ejecutamos el programa en segundo plano, para
no perder el control del terminal. El padre nos indica que ha
registrado la señal. Vemos que hay corriendo dos
procesos, el padre y el hijo y que uno es hijo del otro:
<gyermo@ENCINA>/home/gyermo/PRIVADO/BLOQSIG$ gcc bloqsig.c -o bloqsig
<gyermo@ENCINA>/home/gyermo/PRIVADO/BLOQSIG$ bloqsig &
[1] 8829
PADRE: SIGUSR1(16) registrada.
<gyermo@ENCINA>/home/gyermo/PRIVADO/BLOQSIG$ ps -f
UID PID PPID C STIME TTY TIME CMD
gyermo 8830 8829 0 16:25:00 pts/17 0:00 bloqsig
gyermo 8829 8685 0 16:25:00 pts/17 0:00 bloqsig
gyermo 8685 8683 0 16:12:55 pts/17 0:00 -ksh
Mandamos la señal SIGUSR1
al proceso padre (
PID=8829 en nuestro ejemplo):
<gyermo@ENCINA>/home/gyermo/PRIVADO/BLOQSIG$ kill -16 8829
PADRE: Estoy en la manejadora.
PADRE: wait: Interrupted system call
[1] + Terminado(1) bloqsig &
Como se indicaba más arriba, al recibir la señal,
el padre salta a la manejadora nonada
.
Cuando vuelve, en lugar de seguir bloqueado y continuar
esperando en el wait
, por el mero hecho de
haber recibido la señal, wait
falla, devuelve
-1 y nos da el error "Llamada al sistema interrumpida".
Pero no se trata de un error estrictamente hablando, como
estáis viendo. De querer evitarlo, se puede configurar
el sigaction
en el campo flags para que
las llamadas al sistema interrumpidas no den error y
continúen o, se puede discriminar esta situación
mediante el propio programa:
for (;;)
{int retorno;
retorno=wait(&valor_devuelto);
if (retorno==-1 && errno!=EINTR)
{perror("PADRE: wait");
return 1;}
if (retorno!=-1) break; /* Un hijo requiere atención */
/* Se ha producido una interrupción, a volver a esperar */ }
No os olvidéis de matar al hijo de la prueba anterior que,
como el padre murió, fue adoptado por init
:
<gyermo@ENCINA>/home/gyermo/PRIVADO/BLOQSIG$ ps -f
UID PID PPID C STIME TTY TIME CMD
gyermo 8830 1 0 16:25:00 pts/17 0:00 bloqsig
gyermo 8685 8683 0 16:12:55 pts/17 0:00 -ksh
<gyermo@ENCINA>/home/gyermo/PRIVADO/BLOQSIG$ kill 8830
Máscara de bloqueo de señales.
Un proceso puede decidir bloquear la recepción de una o varias
señales. Las señales bloqueadas constituyen la
máscara de
señales bloqueadas del proceso y se dice de ellas que están
enmascaradas.
Si se recibe una señal y la señal está enmascarada,
la señal
se ignora, aunque permanece "pendiente de atención". Si en
un momento futuro, se desbloquea (desenmascara) esa señal,
se atenderá inmediatamente (como si se acabara de recibir).
Si, mientras permanece bloqueada, una señal se recibe varias
veces, a todos los efectos es como si se hubiera recibido sólo
una vez (las señales "no se apilan").
Para modificar la máscara de señales bloqueadas de un
proceso,
se puede usar la llamada al sistema:
#include <signal.h>
int sigprocmask(int quE_cosa, const sigset_t *nuevo, sigset_t *viejo);
Los parámetros nuevo
y viejo
funcionan
como el campo sa_mask
de sigaction
.
En viejo
, se devuelve el valor de la máscara
antes de la llamada a la función, por si se quiere guardar
para luego restaurarla. El parámetro quE_cosa
debe
valer SIG_SETMASK
si lo que queremos es establecer
de un golpe las señales que queremos que estén
bloqueadas y las
que no. Si sólo queremos bloquear unas señales,
dejando las demás
como estén, usaremos SIG_BLOCK
. Si lo que queremos
es desbloquear un conjunto de señales dejando el resto inalteradas
usaremos SIG_UNBLOCK
.
El conjunto de posibilidades que se nos ofrecen se completa con
dos funciones:
#include <signal.h>
int sigpending(sigset_t *set);
int sigsuspend(const sigset_t *mask);
La primera simplemente devuelve el valor de la máscara de
señales bloqueadas de un proceso. La segunda es más
útil.
Reemplaza temporalmente la máscara actual por aquella que le
pasamos como parámetro y bloquea al proceso (lo deja
sin consumir CPU) hasta que se produzca una de las
señales que hemos permitido temporalmente. Por tanto, debéis
incluir en el conjunto de señales mask
todas
aquellas señales que queráis prohibir mientras dure
la ejecución de sigsuspend
. Una vez recibida la
señal, la llamada al sistema vuelve, se deja la máscara de
señales que había antes automáticamente y
el programa continúa
su ejecución normalmente. Es habitual que la señal que deba
desbloquear sigsuspend
esté bloqueada antes de
llegar a ella, para tener bien localizado el lugar donde se
recibe la señal.
Señal de alarma.
Hay una llamada al sistema, alarm
que hace que
el proceso que la ejecuta reciba la señal SIGALRM
pasados un numero de segundos que se le especifica como
parámetro. Con esta llamada es con la que se construye la
función de biblioteca sleep
.
Un error bastante frecuente consiste en pensar que la llamada
al sistema alarm
hace que un proceso se pare
durante el tiempo que se especifica. No, lo que hace es avisar
al sistema operativo para que tome nota de mandar la señal
SIGALRM
pasados los segundos que se especifican.
-
En condiciones normales, cuando queremos que se ejecute una
función en C dentro de un programa nuestro, lo que hacemos
es poner el nombre de la función con sus parámetros dentro
del código fuente del programa. Cuando el flujo de ejecución
del programa pase por ese punto, se realizará una llamada a
la función.
Las funciones de rellamada o callback funcionan de manera
diferente. En este caso, la función con los parámetros
que le
correspondan es llamada en cualquier momento de la ejecución del
programa, interrumpiendo el flujo normal de instrucciones del
programa. Es decir, el programa salta momentáneamente a la
función de rellamada para luego seguir con la ejecución
normal del programa.
Normalmente, hay que registrar las funciones de rellamada
del programa. Esto sirve para indicar al Sistema Operativo
qué función de las codificadas en nuestro programa queremos
que se comporte como de rellamada y en qué condiciones queremos
que sea llamada. Para poder especificar qué función queremos
que sea llamada se usan los punteros a funciones de
C. Un puntero a función es un puntero que apunta a la
dirección
de memoria donde comienza el código de una función. Para
especificar en qué condiciones queremos que sea llamada la
función, se usan funciones de registro específicas.
Por ejemplo, la función de biblioteca atexit
permite registrar una función de rellamada para que sea invocada
cuando finalice el programa. Es muy útil si queremos realizar
alguna operación siempre que el programa finalice. Tened en
cuenta, no obstante, que si el proceso es matado con SIGKILL,
la función no se ejecutará. Fijémonos en el
prototipo de la
función atexit
:
#include <stdlib.h>
int atexit(void (*func)(void));
El puntero a función es ese tipo tan extraño que aparece
como
parámetro de la función (void (*func)(void)
).
El
tipo puntero a función es void (*)(void)
. El
tipo es algo complicado porque no sólo se ha de especificar
que se trata de un puntero a función (con el asterisco entre
paréntesis) sino también qué prototipo tiene que
tener la función
para que sea aceptable registrarla con esa función. En este
caso, la función ha de tener un prototipo: void
f(void)
. Así, si queremos que la función
salida
se ejecute cuando el programa acabe,
debemos poner:
#include <stdlib.h>
[...]
void salida(void)
{/* Lo que queramos que haga la función. */
}
[...]
void main(void)
{
if (atexit(salida)==-1)
{perror("atexit"); exit(1);}
[...]
}
Observad cómo el nombre de la función
sin parámetros es
considerado por el compilador como un puntero a dicha función.
Es algo parecido al caso de los arrays, en los que el nombre
del array sin nada detrás es considerado por el compilador
como un puntero al primer elemento del array.
Tened en cuenta también que hasta que no se llame a
la función
de registro, no se entera el Sistema Operativo que
queremos que la función de rellamada sea invocada cuando
ocurra la condición. La función de registro sirve para que
el Sistema Operativo tome nota para luego llamarla. Por eso
es conveniente llamar a la funciónd de registro cuanto antes.
Cambiando el comportamiento por defecto de las señales.
Lo que hace el mecanismo de señales de UNIX realmente potente
es que es posible variar el comportamiento por defecto que
produce en un proceso la recepción de determinadas señales.
Se puede avisar al Sistema Operativo que, cuando el proceso
reciba una determinada señal, se invoque una función que le
especifiquemos. Esto, como podéis imaginar, no es más que
especificar una función de rellamada mediante una función de
registro. El prototipo de la función de registro es:
int sigaction(int sig, const struct sigaction *acciOn_nueva,
struct sigaction *acciOn_vieja);
Nada ver el prototipo el avispado lector podrá deducir muchas
cosas:
- Probablemente la llamada al sistema devuelva un código
de error -1 si falla.
- Hay que decirle en el primer parámetro qué
señal es
la que provocará que se llame a la función de
rellamada.
- El segundo parámetro lleva delante
const
,
luego no va a ser modificado por la función (es un
parámetro de entrada pasado por referencia (ver
la sesión 7).
- El tercer parámetro es una estructura modificada por
la función. Ahí nos devolverá algo.
Es un parámetro
de salida (volved a ver la sesión 7).
- El nombre de las variables del prototipo
acciOn_nueva
y acciOn_vieja
parecen indicarnos que en el primero le especificaremos
qué función queremos que se ejecute a partir de
ahora
al recibir la señal y en el segundo nos devolverá la
función que se estaba ejecutando hasta ahora.
- No aparece el puntero a la función, por lo que suponemos
que será un campo de la estructura
sigaction
.
Este alarde de clarividencia se ve confirmado cuando vemos
la definición del tipo struct sigaction
:
struct sigaction
{void (*)(int) sa_handler;
sigset_t sa_mask;
int sa_flags;};
¡Helo ahí! El primer campo de la estructura es un puntero a
función que nos indicará la función de rellamada
que se invocará
cuando se reciba la señal. La función tiene que recibir un
parámetro de tipo entero y no devolver nada. Lo que ya no podemos
adivinar es qué valor contiene ese parámetro cuando el
Sistema
Operativo llama a esa función. Pues bien, es el número de la
señal que ha provocado la llamada. Gracias a este parámetro
podemos usar una única función de rellamada para atender
múltiples
señales. También, en lugar de una función, podemos
especificar
como primer parámetro las macros SIG_DFL
, para que
se ejecute la acción por defecto de la señal o
SIG_IGN
para que el proceso ignore la recepción de
la señal.
El segundo parámetro especifica las señales que queremos que
se bloqueen mientras se está ejecutando la función de
rellamada.
Por defecto, sólo se impide la recepción de la misma
señal que
provocó la llamada a la manejadora. Se han de usar unas funciones
de biblioteca para especificar este segundo parámetro, que es
de tipo especial. Mirad el
resumen para ver cuáles son estas
funciones.
El tercer parámetro es un campo
de bits que especifica algunas opciones:
SA_NOCLDSTOP
: no queremos que se genere
SIGCHLD
cuando un hijo es parado.
SA_RESETHAND
: cuando se vuelva de la función
de rellamada, restaurar el comportamiento de la señal
al comportamiento por defecto.
SA_RESTART
: hacer que las llamadas al
sistema no devuelvan -1 y den el error EINTR
cuando se produzca la señal. Ver
este apartado.
SA_NODEFER
: hacer que la señal no se
bloquee dentro de la función de rellamada, como ocurre
por defecto.
Señal SIGCHLD.
Esta señal la recibe un padre cuando uno de sus hijos muere
o necesita atención, es decir, cuando puede hacer una llamada
al sistema wait
sabiendo que no se va a bloquear.
La señal carece de poca utilidad en su comportamiento por
defecto, pues no hace nada. Podemos registrar una función de
rellamada para que antienda a los hijos que vayan muriendo o
podemos simplemente indicar que se ignore (con
SIG_IGN
en sigaction
) y de este modo
ninguno de los hijos del proceso se quedará zombie.
Apilamiento de señales.
Las señales no se apilan para los procesos. Quiere esto decir
que si se recibe una señal y, sin haberse atendido, se recibe
otra del mismo tipo, cuentan como una única señal.
Aviso de compatibilidad con algunos Linux.
Puede ocurrir que las funciones de señales, como
sigaction
,
den errores de compilación en algún Linux, por ejemplo, en
los del aula de prácticas. Si os dan y para que no os den,
incluid al inicio de vuestro código:
#define __USE_POSIX
Notad que delante de "USE" hay dos caracteres de subrayado
seguidos.
Finalmente, usad en Linux siempre el compilador de GNU, gcc.
Práctica.
Codificad una función que se llame nuevosleep
a la que se le pase el número de segundos que tiene el programa
que esperar e imprima una cuenta atrás de esos segundos por la
salida estándar. Así:
nuevosleep(3);
Y sale:
3, [[un segundo...]] 2, [[un segundo...]], 1, [[un segundo...]] 0.
No se puede usar la función de biblioteca sleep
.
Podéis suponer que existe en el programa una función
ya definida
llamada void nonada(int)
, que no hace nada.
Soluciones y comentarios.
Órdenes de la shell relacionadas.
kill
- manda una señal a un proceso
Funciones de biblioteca relacionadas.
atexit
- registra una función de rellamada para que sea llamada
cuando el programa acabe
LPEs.
- ¿Por qué
pause
o
sigsuspend
me dan error siempre cuando, aparentemente, no ha
pasado nada erróneo?
Solución: tanto pause
como
sigsuspend
son llamadas al sistema
bloqueantes, por lo tanto es de aplicación lo dicho
en este apartado de esta
misma sesión. En el apartado, sustituid wait
por pause
o sigsuspend
. También
se habla de ello en la solución de la práctica de
ejemplo.
- Véase también los
LPEs del ejercicio de esta
misma sesión