Comunicacion y Sincronizacion
Comunicacion y Sincronizacion
Comunicacion y Sincronizacion
sincronización
José Ramón Herrero Zaragoza
Enric Morancho Llena
Dolors Royo Vallés
PID_00169387
GNUFDL • PID_00169387 Comunicación y sincronización
© 2010, FUOC. Se garantiza permiso para copiar, distribuir y modificar este documento según los términos de la GNU Free
Documentation License, Version 1.2 o cualquiera posterior publicada por la Free Software Foundation, sin secciones invariantes ni
textos de cubierta delantera o trasera. Se dispone de una copia de la licencia en el apartado "GNU Free Documentation License" de
este documento.
GNUFDL • PID_00169387 Comunicación y sincronización
Índice
Introducción............................................................................................... 5
Objetivos....................................................................................................... 6
1. Comunicación y sincronización..................................................... 7
Resumen....................................................................................................... 49
Actividades.................................................................................................. 51
Ejercicios de autoevaluación.................................................................. 51
Solucionario................................................................................................ 55
Glosario........................................................................................................ 59
Bibliografía................................................................................................. 60
Anexo............................................................................................................ 61
GNUFDL • PID_00169387 5 Comunicación y sincronización
Introducción
Estudiaremos dos escenarios: el caso de que los procesos que cooperan com-
partan recursos (una parte del espacio lógico o dispositivos) y el caso de que
no compartan (o no quieran compartir) el espacio lógico. En cada escenario,
veremos el repertorio típico de llamadas al sistema que el SO debe ofrecer, así
como las problemáticas que pueden aparecer. En especial, introduciremos la
problemática de los deadlocks (abrazos mortales o interbloqueos), que pueden
aparecer especialmente entre procesos cooperativos que comparten memoria
o dispositivos.
GNUFDL • PID_00169387 6 Comunicación y sincronización
Objetivos
4. Saber que todas las herramientas que sirven para asegurar el acceso exclu-
sivo a los recursos del sistema son difíciles de utilizar y que se debe tener
mucho cuidado para no producir situaciones inconsistentes o bloqueado-
ras cuando se trabaja con ellas.
1. Comunicación y sincronización
(1)
En sistemas concurrentes, los procesos cooperan en el cálculo y la realización Los dispositivos, el código y las
1 variables son algunos de los recur-
de tareas variadas y comparten recursos lógicos . En estos sistemas, los proce- sos que los procesos comparten.
sos han de sincronizar su acceso a los objetos compartidos y han de poder
intercambiar información entre ellos.
Ejemplos
Los siguientes son ejemplos claros de objetos del sistema a los que acceden de manera
concurrente los procesos:
• Las variables compartidas por todos los procesos dentro del sistema.
• Los dispositivos de naturaleza no compartida, como la impresora.
(2)
Veamos por qué es necesaria la sincronización de los procesos con un caso Suponemos que los procesos
2 pueden compartir memoria.
concreto. Un ejemplo claro de variable compartida es el caso de una variable
contador, a la que denominaremos trabajo_pendiente, que se modificada
por diferentes procesos (usuario y gestor); tenemos que:
(3)
a) Los diferentes procesos usuario que se están ejecutando de modo concu- Los trabajos que los procesos
usuario envían a la impresora son
rrente en el sistema la actualizan (la incrementan) cada vez que se envía un
ficheros.
trabajo3 a la impresora. Así, esta variable compartida indica cuántos trabajos
pendientes de impresión hay en el sistema.
...
trabajo_pendiente = trabajo_pendiente + 1
...
...
trabajo_pendiente = trabajo_pendiente - 1
...
Por ejemplo, en una máquina RISC (reduced instruction set computer) el código
generado una vez se ha compilado el código de nuestro ejemplo podría ser
el que indicamos a continuación: en el caso del código�del�proceso�usuario
(figura 3) y en el caso del código�del�proceso�gestor�de�la�impresora (figura 4).
porales de su contenido en registros del sistema4. Estas copias locales, como las
que se llevan a cabo en los registros R0 y R1, pueden ser inconsistentes con el
valor de la variable de la memoria en un determinado instante de la ejecución
del proceso. Esta inconsistencia es la que puede llevar a las dos situaciones
erróneas mencionadas:
GNUFDL • PID_00169387 11 Comunicación y sincronización
1)�No�impresión�de�un�trabajo
(5)
Si en este punto de la ejecución del código máquina pasa alguna cosa en el Un cambio de contexto porque
5 expira el quantum del proceso ges-
sistema que provoca que el proceso gestor deje de ejecutarse, el sistema queda tor, una interrupción de un dispo-
inconsistente. sitivo, etc.
2)�Intento�de�impresión�de�un�trabajo�no�existente
Figura 8. El proceso usuario es interrumpido mientras incrementa la variable trabajo_pendiente y el proceso gestor decrementa
la misma variable.
(6)
Cuando un proceso entra en una sección crítica, debe completar todas las ins- Es decir, el acceso se efectúa es-
trictamente de manera secuencial.
trucciones de dentro de la sección antes de que cualquier otro proceso pueda
acceder a la región crítica de la misma variable compartida. De esta manera, se
garantiza que las variables compartidas que se modifican dentro de la sección
tan sólo son modificadas por un único proceso a la vez. Esta manera de proce-
der se denomina acceso�a�la�sección�crítica�en�exclusión�mutua y garantiza
que hasta que un proceso no finaliza la modificación de la variable compartida
ningún otro proceso pueda empezar a modificarla6.
Antes de acceder a la sección crítica, todos los procesos formulan una petición
de acceso; entonces:
(7)
1) Si la sección crítica está ocupada por otro proceso, esperan a que se liberen Durmiéndose, poniéndose en al-
7 8 guna cola de espera.
bien bloqueándose , bien llevando a cabo una espera activa . En esta asigna-
tura consideraremos que los procesos en espera se bloquean y no realizan una (8)
Consultando continuamente el
espera activa porque, en general, ello supone una mejor utilización de los re- estado del recurso hasta que se
produzca un cambio de estado.
cursos del sistema operativo.
2) Cuando la sección crítica esté libre, entrará uno de los procesos en espera
y cerrará el acceso a cualquier otro proceso que esté interesado en entrar. Una
vez dentro, utilizará el recurso compartido y cuando lo haya utilizado, liberará
la exclusión mutua para que otros procesos puedan utilizar el recurso.
2.2. Semáforos
sem_wait (semaphore s) {
while (s <= 0) /* atómicamente */
espera();
s = s - 1;
GNUFDL • PID_00169387 16 Comunicación y sincronización
sem_signal (sempahore s) {
s = s + 1; /* atómicamente */
}
Observaciones:
• Existen dos tipos de semáforos en función del rango de valores que pueda
alcanzar el contador asociado al semáforo: binarios (el contador sólo pue-
de valer 0 o 1) y n-arios (el contador puede tomar cualquier valor mayor o
igual que cero). Los semáforos binarios son los que implementan de modo
más natural el acceso en exclusión mutua a una región crítica.
• Cabe destacar que las operaciones realizan sus acciones de manera ató-
mica. Supongamos que el contador asociado a un semáforo vale 0 y dos
procesos están esperando haciendo un sem_wait a que el contador tome
un valor mayor que cero. Si ahora algún proceso ejecuta un sem_signal
sobre este semáforo e incrementa el contador del semáforo, únicamente
uno de los procesos que ejecuta el sem_wait verá que el contador vale 1
y lo decrementará; el otro proceso deberá continuar esperando hasta que
se realice otro sem_signal.
Para asegurar el acceso a la sección crítica en exclusión mutua, hay que utilizar
los semáforos binarios o n-arios de la manera siguiente:
Como ejemplo, veamos cuál sería el código de los procesos usuario y del pro-
ceso gestor de la impresora en caso de que utilicen semáforos para actualizar
la variable compartida trabajo_pendiente.
semaphore exclusion;
...
sem_init(exclusion, 1);
...
/*Generar fichero*/
...
sem_wait(exclusion);
trabajo_pendiente = trabajo_pendiente + 1;
sem_signal(exclusion);
...
...
while (cierto){
GNUFDL • PID_00169387 18 Comunicación y sincronización
/* Espera activa */
while (trabajo_pendiente == 0);
sem_wait(exclusion);
trabajo_pendiente = trabajo_pendiente - 1;
sem_signal(exclusion);
/*Enviar trabajo a la impresora*/
...
}
Figura 16. Evolución del estado de los tres procesos usuario, del proceso gestor y del semáforo
GNUFDL • PID_00169387 19 Comunicación y sincronización
(9)
Supongamos que los tres procesos usuario generan los ficheros correspondien- Con esta utilización de los semá-
foros, el orden de acceso de los
tes y, tal como se ha indicado en el código del proceso usuario, a continua-
procesos a la sección crítica es to-
ción examinan el valor del semáforo exclusion con la intención de decre- talmente aleatorio.
semaphore trabajos_pendientes;
...
sem_init(trabajos_pendientes, 0);
...
while (cierto){
/*Generar fichero*/
...
sem_signal(trabajos_pendientes);
}
...
while (cierto){
sem_wait(trabajos_pendientes);
/*Enviar trabajo a la impresora*/
...
}
semaphore sync;
...
sem_init(sync, 0);
...
sem_wait(sync);
...
...
sem_signal(sync);
...
POSIX ofrece el tipo de datos sem_t para declarar variables de tipo semáforo.
La interfaz de llamadas al sistema es la siguiente (observad que el parámetro
de tipo semáforo se pasa por referencia):
(10)
Como ejemplo, la figura 23 muestra un programa10 que emula el funciona- Para generar el ejecutable co-
rrespondiente hay que utilizar la
miento de un motor de explosión de cuatro tiempos. Para hacerlo, crea cuatro biblioteca de pthreads (parámetro
flujos de ejecución que se sincronizan utilizando semáforos para garantizar de compilación -pthread).
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
sem_t sync_tiempo[4];
while (1) {
if (sem_wait(&sync_tiempo[t]) < 0) error();
int main()
{
int i;
pthread_t th[4];
/* Creación de threads */
for (i=0; i<4; i++)
if (pthread_create(&th[i], NULL, tiempo, (void*)i) < 0) error();
return(0);
}
Son muchos los sistemas operativos actuales que proporcionan algún mecanismo que
permite la comunicación de los procesos mediante el paso de mensajes. Por ejemplo,
UNIX ofrece una implementación específica de este mecanismo.
Proceso A
... send(B, mensaje)
Proceso B
... receive(A, mensaje)
donde A y B han de ser los identificadores de los procesos dentro del sistema y
la variable mensaje contiene la información que se están intercambiando. Este
tipo de comunicación se denomina comunicación� simétrica, ya que cada
emisor debe conocer todos los posibles receptores, y al revés.
Proceso A
... send(buzón1, mensaje)
Proceso B
... receive(buzón1, mensaje)
(11)
La telefonía clásica sería un
La comunicación�síncrona se basa en el intercambio síncrono de men- ejemplo convencional de este tipo
de comunicación.
sajes. Este intercambio implica que el emisor y el receptor deben aca-
bar la transferencia de información en el mismo momento, se deben
sincronizar11.
(12)
El correo postal sería un ejem-
La comunicación�asíncrona no bloquea el proceso y el sistema opera- plo convencional de este tipo de
comunicación.
tivo se encarga de guardar el mensaje temporalmente en la memoria
hasta que se lleva a cabo la operación receive complementaria. Con esta
manera de proceder, el emisor, una vez ha enviado el mensaje, puede
continuar su ejecución independientemente de lo que hagan los recep-
12
tores .
Por último, hemos de considerar la longitud de los mensajes, que puede ser
fija o variable. Debemos tener en cuenta, a la hora de decidir la longitud de
los mensajes, que hay que llegar a un compromiso entre la flexibilidad de los
mensajes de longitud variable y la sencillez y la facilidad de gestión de los
mensajes de longitud fija.
Nos interesa que el primer proceso que pida acceso a la sección crítica pueda
pasar. Para conseguirlo, el buzón se inicializa introduciendo un mensaje de
cualquier longitud. En el código que se presenta en la figura 26, el buzón de-
sarrolla un papel equivalente al de un semáforo binario.
También sería posible implementar semáforos n-arios con buzones, así se per-
mitiría que en el buzón pudiera haber hasta n mensajes.
Conceptualmente, los tres mecanismos son muy similares. Ofrecen una cola
de bytes que se gestionará siguiendo la política FIFO (first in, first out). Algunos
procesos (productores) añadirán información en un extremo de la cola y otros
procesos (consumidores) podrán extraerla del otro extremo. Por lo tanto, se-
rán mecanismos propicios para implementar modelos de comunicación pro-
ductor-consumidor.
Los tres mecanismos difieren en los requisitos que imponen a los procesos que
pretenden utilizarlos y en la serie de llamadas al sistema que deben invocar
para utilizarlos.
• Las�pipes permiten comunicar dos procesos que tengan algún tipo de pa-
rentesco (padre e hijo, dos hermanos, etc.).
• Las�named�pipes permiten comunicar dos procesos del sistema que tengan
acceso al directorio del sistema de ficheros donde se haya creado un fichero
de tipo named pipe.
• Los� sockets permiten comunicar procesos que se ejecutan en máquinas
diferentes.
La tabla 1 muestra las llamadas al sistema Unix que hay que invocar para crear,
abrir, leer y escribir sobre estos mecanismos. Una vez que el mecanismo está
disponible para el proceso (el proceso de usuario dispone de un file descriptor
para operar sobre el mecanismo de comunicación), los tres mecanismos se
acceden con las mismas llamadas al sistema read y write y desde el punto de
vista del usuario programador son idénticos.
Tabla 1. Llamadas al sistema UNIX que permiten acceder a las pipes, named pipes y sockets.
GNUFDL • PID_00169387 30 Comunicación y sincronización
Leer read
Escribir write
Cerrar close
Tabla 1. Llamadas al sistema UNIX que permiten acceder a las pipes, named pipes y sockets.
Aunque las lecturas y escrituras sobre estos mecanismos se realizan con las
mismas llamadas al sistema que permiten acceder a ficheros ordinarios (read
y write), hay tres casos particulares que se tratan de manera especial en el
caso de trabajar sobre colas de bytes:
• Lectura� sobre� cola� vacía. Cuando un proceso consumidor intenta leer Los file descriptors de
de una cola que en este momento se encuentra vacía, la cola puede estar escritura
vacía debido a que el proceso consumidor "es más rápido" que el proceso El hecho de que el SO utilice
productor o que el proceso consumidor ya ha consumido toda la informa- el número de file descriptors de
escritura abiertos provoca que
ción que el productor debía producir. Normalmente, al proceso consumi- dejar abierto innecesariamente
algún file descriptor de escritu-
dor le interesará tratar de modo diferente los dos casos; en el primer caso, ra sobre una cola de bytes pue-
lo más razonable sería esperar a que el productor produzca alguna cosa; en da provocar comportamientos
anómalos en nuestros progra-
el segundo caso, lo más razonable sería recibir la notificación de final de mas. Es aconsejable cerrar los
file descriptors tan pronto co-
fichero. Para que el SO pueda saber en qué caso nos encontramos, utilizará mo dejen de ser necesarios.
un dato que conoce con exactitud: el número de file descriptors de escritura
abiertos actualmente sobre esta cola. Si este número es mayor que 0, con-
siderará que estamos en el primer caso; si el número es 0, considerará que
estamos en el segundo caso. Por lo tanto, en el primer caso la llamada read
se bloqueará hasta que algún productor escriba alguna cosa en la cola; en
el segundo caso, la llamada read volverá inmediatamente indicando que
se han leído 0 caracteres.
(13)
• Escritura�sobre�cola�llena. Internamente, el SO asigna una medida a es- No consideramos el caso de
que una llamada write intente es-
tas colas. Si se produce información sobre una cola a un ritmo superior al
cribir un número de bytes superior
que la información es consumida, llegará un momento en el que la cola a la medida interna de la cola de
bytes.
se llenará. Si en este momento se intenta añadir más información, el com-
portamiento por defecto es que la llamada write se bloquee13 hasta que
haya bastante espacio disponible para escribir todos los datos.
Otras diferencias entre pipes y ficheros ordinarios son que no es posible utilizar
la llamada lseek para reposicionar el puntero de lectura escritura sobre una
pipe (la gestión de los datos es estrictamente FIFO) y que las pipes son volátiles.
Si detenemos el sistema operativo (shutdown), se pierde toda la información
que no haya sido leída de una pipe.
Ejemplo: pipes
default:
/* El proceso padre lee de la pipe y escribe en el canal 1 */
close(descFichero[1]); /* Permite detectar el final de transmisión cuando el
hijo acabe */
Figura 27. Fragmento de programa que utiliza una pipe para comunicar un proceso hijo con
un proceso padre.
GNUFDL • PID_00169387 32 Comunicación y sincronización
(14)
El proceso pide crear una pipe. El SO le devolverá dos file descriptors (uno de Process control block, estructura
de datos gestionada por el sistema
lectura y otro de escritura) para acceder a la pipe, como podéis ver en la figura
operativo en la que éste almacena
28, donde se muestra esquemáticamente el estado del proceso (espacio lógico, la información necesaria para ges-
tionar cada proceso.
PCB14, tabla de file descriptors) y del SO (tabla de punteros de lectura/escritura).
Figura 28. Estado del proceso justo después de haber creado la pipe
Para que esta pipe pueda comunicar dos procesos, hay que invocar la llamada
al sistema fork. De esta manera, el proceso hijo hereda del padre los file des-
criptors entre los que encontramos los que se han creado con la llamada. pipe:
descFichero[0] es el de lectura, y descFichero[1], el de escritura.
La figura 29 muestra el estado de los dos procesos mientras éstos están ejecu-
tando el código correspondiente a las sentencias while.
GNUFDL • PID_00169387 33 Comunicación y sincronización
Figura 29. Estado del proceso padre y del proceso hijo mientras los procesos se comunican.
#include <unistd.h>
#include <sys/wait.h>
int
main (int argc, char *argv[])
{
int p[2], st;
case 0:
GNUFDL • PID_00169387 34 Comunicación y sincronización
default:
switch (fork ())
{
case -1:
error ();
case 0:
if (dup2 (p[1], 1) < 0)
error ();
if (close (p[0]) < 0)
error ();
if (close (p[1]) < 0)
error ();
execlp ("ps", "ps", "aux", NULL);
error ();
}
}
if (close (p[0]) < 0)
error ();
if (close (p[1]) < 0)
error ();
if (wait (&st) < 0)
error ();
if (wait (&st) < 0)
error ();
return (0);
}
Figura 30. Programa que emula las llamadas al sistema que debe ejecutar el intérprete de
comandos cuando el usuario introduce el comando ps aux | grep getty. Ved también
Como hemos visto, un SO define una nueva máquina con una semántica más
elaborada que la de la máquina física sobre la que se ejecuta. A pesar de este
aumento del nivel semántico, el SO conserva muchas de las funciones y los
GNUFDL • PID_00169387 35 Comunicación y sincronización
(15)
2) Las excepciones, que son provocadas por el proceso de manera involuntaria Son instrucciones de lenguaje
máquina anómalas la división entre
durante la ejecución de una instrucción de lenguaje máquina o a causa de la
cero, la raíz cuadrada de un núme-
saturación de un elemento de hardware (overflow), de un acceso a la memoria ro negativo, etc.
3) Las llamadas� explícitas� al� sistema, que se pueden efectuar para enviar
señales a un proceso determinado. Para poder hacerlo, el proceso que envía
la señal ha de tener permiso. En general, sólo se permite enviar señales entre
procesos del mismo dominio de protección (el administrador puede enviar
señales a quien quiera). Se trata de señales como la orden para eliminar un
proceso o señales para sincronizar la ejecución de procesos que lo necesiten.
Por ejemplo, un proceso que tiene como función inicializar una estructura
de datos determinada podría utilizar una señal para informar de los procesos
usuarios de esta estructura que ya ha sido inicializada.
4) El reloj� del� sistema, que es utilizado por los procesos cuando necesitan
llevar a cabo ciertas acciones a intervalos concretos de tiempo. Por ejemplo, un
proceso que esté esperando una cierta entrada desde un módem puede pedir
ser avisado dentro de un cierto lapso de tiempo (timeout) con el fin de detectar
si la línea se ha cortado o no.
GNUFDL • PID_00169387 36 Comunicación y sincronización
El tratamiento que da un proceso a una señal que le llega puede ser de uno
de los tres tipos siguientes:
1)� Un� tratamiento� definido� por� el� mismo� usuario. El proceso indica me-
diante una llamada al SO qué procedimiento se debe ejecutar cuando llegue
una cierta señal.
Así pues, un proceso será destruido por el SO si recibe una señal no esperada a
causa de un error en su ejecución, a causa de un acontecimiento imprevisto en
uno de los dispositivos a los que accede o por la actuación deliberada de otro
proceso. En este caso, el SO es el encargado de enviar el estado de finalización
al proceso padre con el fin de indicarle el motivo de la destrucción.
Terminología
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.
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:
SIGINT Se ha pulsado la secuencia de teclas de control asociada a esta señal (típica- EXIT
mente Ctrl-C).
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
GNUFDL • PID_00169387 39 Comunicación y sincronización
(16)
Cada señal (SIGHUP, SIGKILL, etc.) tiene asociado un código numérico ente- Los habituados a trabajar con
16 algún intérprete de comandos
ro; por ejemplo, el SIGKILL tiene asociado el valor 9 . Para facilitar la legi- 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)
#include <signal.h>
Figura 32. Funciones que permiten manipular variables de tipo sigset_t y dos ejemplos
de utilización.
#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);
Generación de señales
(17)
POSIX proporciona diferentes llamadas al sistema para generar señales. Co- El nombre kill (matar) viene
17 dado porque las primeras imple-
mentaremos dos: kill y alarm (figura 34). 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);
(18)
La llamada alarm permite programar el temporizador asociado al proceso. POSIX ofrece otros temporiza-
18 dores con una resolución más fina,
Tiene como parámetro el número de segundos (de tiempo real) que deben 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.
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);
/* Opción 3: Correcta */
sigprocmask(SIG_BLOCK, &term, &old);
escritura_fichero();
sigprocmask(SIG_SETMASK, &old, NULL);
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.
int main()
{
struct sigaction act;
Figura 37. Espera del depósito de una señal utilizando una espera activa
Figura 38. Espera del depósito de una señal utilizando la llamada al sistema pause
(19)
Para solucionar este problema, la interfaz de señales POSIX proporciona la lla- Entre otras cosas, el SO garan-
tiza que no se depositará ninguna
mada al sistema sigsuspend (figura 39). Esta llamada realiza dos operaciones
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
• se deposite una señal sobre el pro-
hacer que el proceso se espere20 hasta el depósito de una señal no bloquea- ceso.
da.
sigset_t m, old;
sigset_t m, old;
Figura 41. Espera del depósito de una señal utilizando la llamada al sistema sigsuspend
...
sem_wait(disco)
sem_wait(impresora)
/*Utilizar el disco y la impresora*/
...
sem_signal(impresora)
sem_signal(disco)
...
...
sem_wait(impresora)
sem_wait(disco)
/*Utilizar el disco y la impresora*/
...
sem_signal(disco)
sem_signal(impresora)
...
Podemos observar que la ejecución secuencial de estos dos procesos es totalmente correc-
ta y no supone ningún error. En cambio, a la hora de ejecutarse de manera concurrente,
y según cómo se entrelacen las operaciones de los dos procesos, se puede generar una
situación de interbloqueo. Por ejemplo, si se produce la secuencia siguiente:
A partir de este momento, el proceso P1 espera que se libere el recurso disco, pero éste
está asignado al proceso P0, que no lo puede liberar porque está bloqueado esperando el
recurso impresora. Éste tampoco será liberado, ya que el proceso que lo tiene asignado,
el proceso P1, también está bloqueado. En definitiva, se ha creado un bucle de espera
del que no hay salida.
Resumen
Actividades
1. ¿Por qué interesa que las zonas de exclusión sean cuanto más pequeñas mejor? Proponed
un ejemplo en el que se vean claras las posibles ventajas.
2. ¿Es necesario acceder en exclusión mutua a una variable compartida si tan sólo se quiere
consultar su valor? ¿Por qué?
a) Suponed que un proceso quiere esperar un mensaje del buzón-A y un mensaje del buzón-B
(un mensaje de cada buzón). ¿Qué secuencia de sends y receives debería ejecutar?
b) ¿Qué secuencia de sends y receives se debería ejecutar si el mismo proceso quiere esperar
un mensaje de un buzón o un mensaje del otro, o de buzón-A o de buzón-B?
7. Cuando se hace el shut-down de una máquina con el sistema operativo Linux, el SO genera
la señal SIGTERM a todos los procesos que quedan en ejecución, espera unos segundos y, a
continuación, genera la señal SIGKILL sobre todos los procesos que quedan en ejecución.
¿Por qué creéis que procede de este modo y no genera directamente la señal SIGKILL?
8. Trabajando sobre un terminal en Linux, pulsar las teclas Ctrl-C suele provocar la muerte
del proceso en ejecución. ¿Podéis explicar los acontecimientos que se producen desde que el
usuario pulsa Ctrl-C hasta que el proceso muere?
9. Suponed que un proceso Unix muere debido al depósito de una señal. ¿Su proceso padre
puede saber qué señal ha provocado la muerte del hijo? Para responder podéis consultar la
página del manual de la llamada al sistema wait.
10. Buscad información sobre qué características del estándar de señales POSIX se pueden
cambiar utilizando el campo sa_flags de la estructura de datos struct sigaction de la
llamada al sistema sigaction.
11. Escribid un programa que calcule cuál es la medida interna de las pipes. Podéis utilizar
señales.
Ejercicios de autoevaluación
1. Os proponemos que creéis un generador de números aleatorios. Los números se irán ob-
teniendo de un vector de memoria intermedia (buffer) circular de medida maxbuff en la que
diferentes flujos irán insertando y extrayendo números siguiendo el esquema del productor/
consumidor. El sistema se compone de los tres tipos de flujos siguientes:
Implementad la solución de manera que si damos permiso a un proceso para acceder al vector
de memoria intermedia, el resto de procesos del mismo tipo que quieran acceder a él deberán
esperar que éste lo libere, independientemente de la prioridad de los procesos que pidan
permiso para acceder a éste.
Debéis resolver este problema utilizando tantos semáforos como necesitéis y también me-
moria compartida entre los procesos (variables compartidas). Disponéis de una función que
devuelve la prioridad del proceso que la ejecuta denominada int prioridad, que devuelve un
valor entre 0 y P - 1.
void camarero() {
int tmp, i;
while (true) {
sem_wait(sem2);
sem_wait(sem5);
tmp = n_horchatas;
n_horchatas = 0;
sem_signal(sem5);
for (i=0; i<tmp; i++) {
sem_wait(sem1);
servir_horchata();
}
sem_signal(sem3);
}
}
Figura 44
void cliente() {
ir_a_la_horchatería();
sem_wait(sem4);
sem_wait(sem5);
n_horchatas =
cuantas_horchatas();
sem_signal(sem5);
sem_signal(sem2);
sem_wait(sem3);
sem_signal(sem4);
tomar_horchatas();
}
Figura 45
void horchatero() {
int preparadas = 0;
GNUFDL • PID_00169387 53 Comunicación y sincronización
while (true) {
preparar_horchata();
sem_signal(sem1);
sem_wait(sem6);
preparadas++;
sem_signal(sem6);
}
}
Figura 46
Ayudas: antes de empezar, leed bien todas las preguntas, os puede ayudar sustituir
los nombres de los semáforos por otros más significativos. Las rutinas servir_horchata,
ir_a_la_horchatería, cuantas_horchatas, tomar_horchatas y preparar_horchata no modifican nin-
guna variable ni ningún semáforo compartido.
b) ¿Para qué se supone que sirven el semáforos sem5 y sem6? ¿Son realmente necesarios estos
semáforos? ¿Por qué?
4. En el programa de la figura 30, ¿qué efecto tendría eliminar las dos invocaciones a la
llamada al sistema close realizadas por el proceso padre justo antes de invocar las llamadas
al sistema wait?
6. Escribid un fragmento de código que espere la llegada de una señal de tipo SIGUSR1 o
SIGUSR2, pero que atienda cualquier otro tipo de señal que no esté bloqueada.
7. Escribid un fragmento de código que espere la llegada de una señal de tipo SIGUSR1 y
otro de tipo SIGUSR2 (en cualquier orden), pero que atienda cualquier otro tipo de señal
que no esté bloqueada.
GNUFDL • PID_00169387 55 Comunicación y sincronización
Solucionario
Ejercicios de autoevaluación
void Productor()
{for (;;)
{n = crearNombre ();
sem_wait(full); /*Esperar a que pueda poner un elemento*/
sem_wait(mutexProd); /*Exclusión mutua entre productores*/
/*(y aleatorizadores)*/
buffer[in] = n;
in = (in + 1) % maxbuff;
sem_signal(mutexProd); /*Fin de exclusión*/
sem_signal(empty); /*Indica nuevo elemento en el vector*/
/*de memoria intermedia*/
}
}
Cabe observar que el aleatorizador puede traer problemas. Por ejemplo, si sólo hay un aleato-
rizador, mientras está procesando los últimos cinco números que ha extraído, los producto-
res pueden llenar el vector de memoria intermedia –se quedarán todos bloqueados–, el alea-
torizador hará un sem_wait(full) –también se bloqueará– y nadie consumirá elementos.
Para arreglar esto, sólo hay que conseguir que el aleatorizador se guarde el sitio del último
elemento que extrae: se puede sustituir la línea de código.
sem_signal(full);/*FALLA*/
para
y eliminar el
Por otra parte, si hay más de un aleatorizador, éstos se pueden repartir los elementos
del vector de memoria intermedia sin que ninguno de ellos tome cinco –haga cinco
sem_wait(empty)–, el vector de memoria intermedia se puede llenar y se quedaría muy
bloqueado porque ningún aleatorizador sacaría elementos.
GNUFDL • PID_00169387 56 Comunicación y sincronización
Para corregir estos dos problemas, proponemos la solución siguiente para el aleatorizador:
/*Productores*/
elem = producir();
sem_wait(prod_mutex);
if (cap_prod_accediendo)
{cap_prod_accediendo = 0;
sem_signal(prod_mutex);
}
else
{prod_esperando[prioridad()]++;
sem_signal(prod_mutex);
sem_wait(prod_semaforos[prioridad()]);
}
sem_wait(full);
buffer[in] = elem; /*No es necesaria la exclusión porque sólo*/
in = (in + 1)% N; /*Puede acceder 1 productor*/
GNUFDL • PID_00169387 57 Comunicación y sincronización
sem_signal(empty);
sem_wait(prod_mutex);
precio (i = P - 1; (i >= 0) && (prods_esperando[i] == 0);i--);
if (i < 0)
_cap_prod_accediendo = 1;
else
{prod_esperando[i]-;
sem_signal(prod_semaforos[i]);
}
sem_signal(prod_mutex);
/*Consumidores*/
sem_wait(cons_mutex);
if (cap_cons_accediendo)
{cap_cons_accediendo = 0;
sem_signal(cons_mutex);
}
else
{cons_esperando[prioridad()]++;
sem_signal(cons_mutex);
sem_wait(cons_semaforos[prioridad()]);
}
sem_wait(empty);
elem = buffer[out];
out = (out + 1) % N;
sem_signal(full);
sem_wait(cons_mutex);
for (i = P - 1;(i >= 0) && (cons_esperando[i] == 0);i--);
if (i<0)
_cap_cons_accediendo = 1;
else
{cons_esperando[i]-;
sem_signal(cons_semaforos[i]);
}
sem_signal(cons_mutex);
consumir(elem);
El sem5 no es necesario. Gracias al sem2 y al sem4 no se puede dar el caso de que en el mismo
momento diferentes procesos accedan a n_horchatas.
El semáforo sem6 tampoco es necesario, ya que controla una exclusión mutua sobre una
variable (preparadas) que no es compartida.
El horchatero lo incrementa cada vez que tiene una preparada y el camarero la decrementa
cuando consume una.
GNUFDL • PID_00169387 58 Comunicación y sincronización
d) El sem4 sirve para tener exclusión mutua entre los clientes. Hasta que uno no está total-
mente servido, los otros pueden indicar al camarero que se esperan.
• A: ninguna
• B: usr2, pipe
• C: usr2, pipe, usr1, alrm
• D: usr2, pipe, usr1, alrm, int
• E: vacío
• F: alrm
• G: alrm
• H: todos
• I: alrm
6.
7.
Glosario
cambio de contexto m Manera de implementar la concurrencia de procesos en un sistema
multiprogramado que implica dejar de ejecutar el proceso que estaba ocupando al procesador
para pasar a ejecutar otro proceso.
deadlock m Bloqueo indefinido de dos o más procesos que esperan la liberación de algún
recurso compartido del sistema que ya ha sido asignado.
espera activa f Consulta continuada por parte de un proceso del estado de un recurso,
generalmente para determinar si está libre. Consume ciclos del procesador sin hacer ningún
trabajo útil.
paso de mensajes m Herramienta que ofrecen algunos sistemas que permite sincronizar
y comunicar procesos.
Bibliografía
Silberschatz, A.; Galvin. P. B.; Gagne G. (2008). Operating Systems Concepts (8.ª edición).
John Wiley & Sons.
Documentación disponible sobre llamadas en el sistema Unix en los recursos del aula.
GNUFDL • PID_00169387 61 Comunicación y sincronización
Anexo
1.�El�soporte�de�hardware�para�la�exclusión�mutua
1.1.�Modo�de�ejecución�privilegiado:�la�inhibición�de�las�interrupciones
1.2.�Modo�de�ejecución�no�privilegiado
1.2.1�Test�and�set
La instrucción test and set está pensada para dar un soporte de hardware al
problema del acceso a la zona crítica en exclusión mutua. Está diseñada ex-
presamente para resolver conflictos de acceso a zonas críticas y asegurar que
tan sólo un proceso pueda acceder a la vez a la sección crítica.
(21)
La idea básica es definir una variable de control (global para todos los proce- Recordad que la sección crítica
21 es un trozo de código asociado a
sos) asociada a la sección crítica que determine si el acceso es posible o no. la utilización de un recurso com-
La variable se inicializa en libre e indica que el recurso compartido está dis- partido.
ponible. Cada proceso que quiere acceder al recurso ejecuta la instrucción test
and set (TS) y le pasa por parámetro la variable de control asociada al recurso.
sem_wait: TS S
BNF sem_wait
return
1.2.2.�El�intercambio�(swap)
swap (A,B){
temp = A;
EN = B;
B = temp;
}
Todo proceso que quiere acceder a una zona crítica en exclusión mutua debe
ejecutar el código de la figura 51.
2.�Ejemplo:�procesos�productores�y�consumidores
2.1.�Un�productor�y�un�consumidor�con�un�vector�de�memoria�intermedia
ilimitada
while (cierto){
/* Producir */
...
/* Dejar en el buffer */
...
sem_signal(producido);
/* Otras operaciones */
...
}
while (cierto){
sem_wait(producido);
/* Leer del buffer */
...
/* Consumir */
...
/* Otras operaciones */
...
}
El proceso consumidor, por otra parte, con el fin de que el funcionamiento sea
correcto, se debe asegurar de que hay datos en el vector de memoria intermedia
antes de intentar acceder a ella. La manera que tiene de saberlo es ejecutar la
operación sem_wait.
(22)
Supongamos que el consumidor no tiene asignado el procesador y, por lo tan- El hecho de tener más de un
dato hace inviable el uso de un se-
to, no se ejecuta durante un cierto tiempo. Mientras el productor va generan-
máforo binario.
do datos y metiéndolos en el vector de memoria intermedia para que el con-
sumidor sepa cuántos datos debe procesar una vez vuelva a ejecutarse cada
sem_signal, debe quedar reflejado en el semáforo producido. Es, pues, ab-
solutamente necesario utilizar un semáforo n-ario22 para tener constancia del
número de señales y, por lo tanto, del número de datos pendientes de procesar
que hay en el vector de memoria intermedia.
2.2.�Diferentes�productores�y�diferentes�consumidores�con�un�vector�de
memoria�intermedia�ilimitada
GNUFDL • PID_00169387 67 Comunicación y sincronización
while (cierto){
/* Producir */
...
sem_wait(exclusion);
/* Dejar en el buffer */
...
sem_signal(exclusion);
sem_signal(producido);
/* Otras operaciones */
...
}
while (cierto){
sem_wait(producido);
sem_wait(exclusion);
/* Leer del buffer */
...
sem_signal(exclusion);
/* Consumir */
...
/* Otras operaciones */
...
}
2.3.�Diferentes�productores�y�diferentes�consumidores�con�un�vector�de
memoria�intermedia�limitado
while (cierto){
sem_wait(puedeproducir);
/* Producir */
pdata = producir();
sem_wait(p_exclusion);
/* Dejar en el buffer */
buffer[ent] = pdata;
ent = (ent + 1) mod capacidad;
sem_signal(p_exclusion);
sem_signal(puedeconsumir);
/*Otras operaciones*/
...
}
while(cierto){
sem_wait(puedeconsumir);
sem_wait(c_exclusion);
/*Leer del buffer */
cdata = buffer[sal];
sal = (sal + 1) mod capacidad;
sem_signal(c_exclusion);
sem_signal(puedeproducir);
/*Consumir*/
...
/*Otras operaciones*/
...
GNUFDL • PID_00169387 69 Comunicación y sincronización