Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Lap
Lap
Ttulo:
Autores:
Creative Commons License: Usted es libre de copiar, distribuir y comunicar pblicamente la obra, bajo
las condiciones siguientes: 1. Reconocimiento. Debe reconocer los crditos de la obra de la manera
especicada por el autor o el licenciador. 2. No comercial. No puede utilizar esta obra para nes
comerciales. 3. Sin obras derivadas. No se puede alterar, transformar o generar una obra derivada a
partir de esta obra. Ms informacin en: http://creativecommons.org/licenses/by-nc-nd/3.0/
Prefacio
Lectores destinatarios
Este libro est especialmente pensado para estudiantes de segundo
o tercer curso de titulaciones de Ingeniera en Informtica, como por
ejemplo Grado en Ingeniera Informtica. Se asume que el lector tiene
conocimientos de Programacin (particularmente en algn lenguaje de
programacin de sistemas como C) y Sistemas Operativos. El material
que compone el presente libro ha sido desarrollado por profesores vinculados durante aos a aspectos relacionados con la Programacin,
los Sistemas Operativos y los Sistemas Distribuidos.
Agradecimientos
Los autores del libro quieren dar las gracias a los profesores Carlos
Villarrubia Jimnez, Eduardo Domnguez Parra y Miguel ngel Redondo Duque por su excepcional soporte en cualquier cuestin relacionada con el mundo de los Sistemas Operativos, particularmente en la
generacin de una imagen de sistema operativo especialmente modificada para la parte prctica de la asignatura Programacin Concurrente
y Tiempo Real. Este agradecimiento se hace extensivo al profesor Flix Jess Villanueva Molina por su soporte en la parte de Sistemas de
Tiempo Real.
Gracias a Alba Baeza Pins por la revisin gramatical y ortogrfica
del presente documento.
Finalmente, nuestro agradecimiento a la Escuela de Informtica de
Ciudad Real y al Departamento de Tecnologas y Sistemas de Informacin de la Universidad de Castilla-La Mancha.
Resumen
Este libro recoge los aspectos fundamentales, desde una perspectiva esencialmente prctica y en el mbito de los sistemas operativos,
de Programacin Concurrente y de Tiempo Real, asignatura obligatoria en el segundo curso del Grado en Ingeniera en Informtica en la
Escuela Superior de Informtica (Universidad de Castilla-La Mancha).
El principal objetivo que se pretende alcanzar es ofrecer al lector una
visin general de las herramientas existentes para una adecuada programacin de sistemas concurrentes y de los principales aspectos de
la planificacin de sistemas en tiempo real.
La evolucin de los sistemas operativos modernos y el hardware de
procesamiento, esencialmente multi-ncleo, hace especialmente relevante la adquisicin de competencias relativas a la programacin concurrente y a la sincronizacin y comunicacin entre procesos, para
incrementar la productividad de las aplicaciones desarrolladas. En este contexto, el presente libro discute herramientas clsicas, como los
semforos y las colas de mensajes, y alternativas ms flexibles, como
los monitores o los objetos protegidos. Desde el punto de vista de la
implementacin se hace uso de la familia de estndares POSIX y de
los lenguajes de programacin C, C++ y Ada.
Por otra parte, en este libro tambin se discuten los fundamentos
de la planificacin de sistemas en tiempo real con el objetivo de poner
de manifiesto su importancia en el mbito de los sistemas crticos.
Conceptos como el tiempo de respuesta o el deadline de una tarea son
esenciales para la programacin de sistemas en tiempo real.
Abstract
This book addresses, from a practical point of view and within the
context of Operating Systems, the basics of Concurrent and Real-Time
Programming, a mandatory course taught in the second course of the
studies in Computer Science at the Faculty of Computing (University of
Castilla-La Mancha). The main goal of this book is to provide a general
overview of the existing tools that can be used to tackle concurrent
programming and to deal with real-time systems scheduling.
Both the evolution of operating systems and processing hardware,
especially multi-core processors, make the acquisition of concurrent
programming-related competences essential in order to increase the
performance of the developed applications. Within this context, this
book describes in detail traditional tools, such as semaphores or message queues, and more flexible solutions, such as monitors or protected objects. From the implementation point of view, the POSIX family
of standards and the programming languages C, C++ and Ada are employed.
On the other hand, this book also discusses the basics of real-time
systems scheduling so that the reader can appreciate the importance
of this topic and how it affects to the design of critical systems. Concepts such as response time and deadline are essential to understand
real-time systems programming.
VII
ndice general
1.
Conceptos Bsicos . . . . . . . . . . . . . . . . . . .
1.1.
El concepto de proceso . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . 40
. . . . . . . . . . . . 43
2.1.
Conceptos bsicos . . . . . . . . . . . . . . . . . . 44
2.2.
Implementacin . . . . . . . . . . . . . . . . . . . 47
2.2.1. Semforos
. . . . . . . . . . . . . . . . . . . 47
. . . . . . . . . . . . . . . 55
Paso de Mensajes . . . . . . . . . . . . . . . . . . . . 75
3.1.
Conceptos bsicos . . . . . . . . . . . . . . . . . . 76
Implementacin . . . . . . . . . . . . . . . . . . . 82
Motivacin . . . . . . . . . . . . . . . . . . . . . 100
4.2.
Introduccin . . . . . . . . . . . . . . . . . . . . 124
5.2.
. . . . . . . . . . . . . . 124
. . . . . . . . . . 130
5.4.
. . . . . . . . . 139
A.
A.1.
Enunciado . . . . . . . . . . . . . . . . . . . . . 159
A.2.
B.
B.1.
Enunciado . . . . . . . . . . . . . . . . . . . . . 169
B.2.
C.
C.1.
C.2.
C.3.
. . . . . . . . . 184
Captulo
Conceptos Bsicos
Comunicando procesos
Los procesos necesitan algn
tipo de mecanismo explcito
tanto para compartir informacin como para sincronizarse. El hecho de que se
ejecuten en una misma mquina fsica no implica que
los recursos se compartan de
manera implcita sin problemas.
[2]
pila
instr_1
instr_2
instr_3
memoria
instr_i
datos
instr_n
texto
programa
Figura 1.1: Esquema grfico de un programa y un proceso.
1.1.
El concepto de proceso
1.1.1.
A medida que un proceso se ejecuta, ste va cambiando de un estado a otro. Cada estado se define en base a la actividad desarrollada
por un proceso en un instante de tiempo determinado. Un proceso
puede estar en uno de los siguientes estados (ver figura 1.2):
Nuevo, donde el proceso est siendo creado.
En ejecucin, donde el proceso est ejecutando operaciones o
instrucciones.
En espera, donde el proceso est a la espera de que se produzca
un determinado evento, tpicamente la finalizacin de una operacin de E/S.
Preparado, donde el proceso est a la espera de que le asignen
alguna unidad de procesamiento.
proceso
[3]
admitido
nuevo
interrupcin
terminado
salida
preparado
en ejecucin
espera E/S
terminacin E/S
en espera
#include <sys/types.h>
#include <unistd.h>
pid_t getpid (void); // ID proceso.
pid_t getppid (void); // ID proceso padre.
uid_t getuid (void); // ID usuario.
uid_t geteuid (void); // ID usuario efectivo.
Las primitivas getpid() y getppid() se utilizan para obtener los identificadores de un proceso, ya sea el suyo propio o el de su padre.
Recuerde que UNIX asocia cada proceso con un usuario en particular, comnmente denominado propietario del proceso. Al igual que
ocurre con los procesos, cada usuario tiene asociado un ID nico dentro del sistema, conocido como ID del usuario. Para obtener este ID
se hace uso de la primitiva getuid(). Debido a que el ID de un usuario
puede cambiar con la ejecucin de un proceso, es posible utilizar la
primitiva geteuid() para obtener el ID del usuario efectivo.
1.1.2.
[4]
fork()
proceso_A
(cont.)
proceso_B
(hijo)
hereda_de
Independencia
Los procesos son independientes entre s, por lo que no
tienen mecanismos implcitos para compartir informacin y sincronizarse. Incluso
con la llamada fork(), los procesos padre e hijo son totalmente independientes.
[5]
#include
#include
#include
#include
<sys/types.h>
<unistd.h>
<stdlib.h>
<stdio.h>
1 El
[6]
Anteriormente se coment que era necesario utilizar el valor devuelto por fork() para poder asignar un cdigo distinto al proceso padre y
al hijo, respectivamente, despus de realizar dicha llamada. El uso de
fork() por s solo es muy poco flexible y generara una gran cantidad de
cdigo duplicado y de estructuras if-then-else para distinguir entre el
proceso padre y el hijo.
Idealmente, sera necesario algn tipo de mecanismo que posibilitara asignar un nuevo mdulo ejecutable a un proceso despus de
su creacin. Este mecanismo es precisamente el esquema en el que
se basa la familia exec de llamadas al sistema. Para ello, la forma de
combinar fork() y alguna primitiva de la familia exec se basa en permitir que el proceso hijo ejecute exec para el nuevo cdigo, mientras
que el padre contina con su flujo de ejecucin normal. La figura 1.5
muestra de manera grfica dicho esquema.
Listado 1.4: Creacin de una cadena de procesos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main (void) {
int i = 1, n = 4;
pid_t childpid;
for (i = 1; i < n; i++) {
childpid = fork();
if (childpid > 0) // Cdigo del padre.
break;
}
printf("Proceso %ld con padre %ld\n", (long)getpid(),
(long)getppid()); // PID == 1?
pause();
return 0;
}
[7]
proceso_A
fork()
proceso_A
(cont.)
proceso_B
(hijo)
execl()
SIGCHLD
wait()
Limpieza
tabla proc
exit()
zombie
#include <unistd.h>
int execl (const char *path, const char *arg, ...);
int execlp (const char *file, const char *arg, ...);
int execle (const char *path, const char *arg, ...,
char *const envp[]);
int execv (const char *path, char *const argv[]);
int execvp (const char *file, char *const argv[]);
int execve (const char *path, char *const argv[],
char *const envp[]);
[8]
padre
h1
h2
...
hn
Compilacin
Listado 1.6: Uso de fork+exec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include
#include
#include
#include
<sys/types.h>
<unistd.h>
<stdlib.h>
<stdio.h>
2 Por
[9]
#include
#include
#include
#include
<sys/types.h>
<unistd.h>
<stdlib.h>
<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int *status);
pid_t waitpid (pid_t pid, int *status, int options);
// BSD style
pid_t wait3 (int *status, int options,
struct rusage *rusage);
pid_t wait4 (pid_t pid, int *status, int options,
struct rusage *rusage);
[10]
#include
#include
#include
#include
#include
#include
<sys/types.h>
<unistd.h>
<stdlib.h>
<stdio.h>
<signal.h>
<wait.h>
#define NUM_HIJOS 5
void finalizarprocesos ();
void controlador (int senhal);
pid_t pids[NUM_HIJOS];
int main (int argc, char *argv[]) {
int i;
char *num_hijos_str;
num_hijos_str = (char*)malloc(sizeof(int));
sprintf(num_hijos_str, " %d", NUM_HIJOS);
// Manejo de Ctrol+C.
if (signal(SIGINT, controlador) == SIG_ERR) {
fprintf(stderr, "Abrupt termination.\n");
exit(EXIT_FAILURE);
}
for (i = 0; i < NUM_HIJOS; i++)
switch(pids[i] = fork()) {
case 0: // Cdigo del hijo.
execl("./exec/hijo", "hijo", num_hijos_str, NULL);
break; // Para evitar entrar en el for.
}
free(num_hijos_str);
// Espera terminacin de hijos...
for (i = 0; i < NUM_HIJOS; i++)
waitpid(pids[i], 0, 0);
return EXIT_SUCCESS;
}
void finalizarprocesos () {
int i;
printf ("\n-------------- Finalizacion de procesos ------------\n");
for (i = 0; i < NUM_HIJOS; i++)
if (pids[i]) {
printf ("Finalizando proceso [ %d]... ", pids[i]);
kill(pids[i], SIGINT); printf("<Ok>\n");
}
}
46
47
48
49
50
51
52
53 void controlador (int senhal) {
54
printf("\nCtrl+c captured.\n"); printf("Terminating...\n\n");
55
// Liberar recursos...
56
finalizarprocesos();
57
exit(EXIT_SUCCESS);
58 }
[11]
El anterior listado muestra la inclusin del cdigo necesario para esperar de manera adecuada la finalizacin de los procesos hijo y
gestionar la terminacin abrupta del proceso padre mediante la combinacin de teclas Ctrl+C.
En primer lugar, la espera
a la terminacin de los hijos se con
trola mediante las lneas 37-38 utilizando la primitiva waitpid. Esta
primitiva se comporta de manera anloga a wait, aunque bloqueando al proceso que la ejecuta para esperar a otro proceso con un pid
concreto. Note como previamente
se ha utilizado un array auxiliar de
nominado pids (lnea 13 ) para almacenar el pid de
todos y cada uno
de los procesos hijos creados mediante fork (lnea 28 ). As, slo habr
que esperarlos despus de haberlos lanzado.
Por otra parte, note cmo se captura la seal SIGINT, es decir,
la
terminacin mediante Ctrl+C, mediante la funcin signal (lnea 22 ), la
cual permite asociar la captura de una seal con una funcin de retrollamada. sta funcin definir el cdigo que se tendr que ejecutar
cuando se capture dicha seal. Bsicamente, en esta ltima funcin,
denominada controlador, se incluir el cdigo necesario para liberar
los recursos previamente reservados, como por ejemplo la memoria
dinmica, y para destruir los procesos hijo que no hayan finalizado.
Si signal() devuelve un cdigo de error SIG_ERR, el programador es
responsable de controlar dicha situacin excepcional.
Listado 1.10: Primitiva POSIX signal
1 #include <signal.h>
2
3 typedef void (*sighandler_t)(int);
4
5 sighandler_t signal (int signum, sighandler_t handler);
1.1.3.
Sincronizacin
Conseguir que dos cosas
ocurran al mismo tiempo se
denomina comnmente sincronizacin. En Informtica,
este concepto se asocia a
las relaciones existentes entre eventos, como la serializacin (A debe ocurrir antes
que B) o la exclusin mutua
(A y B no pueden ocurrir al
mismo tiempo).
Procesos e hilos
[12]
cdigo
datos
archivos
registros
pila
cdigo
datos
archivos
registros
registros
registros
pila
hebra
Un contador de programa.
Un conjunto de registros.
Una pila.
Sin embargo, y a diferencia de un proceso, las hebras que pertenecen a un mismo proceso comparten la seccin de cdigo, la seccin
de datos y otros recursos proporcionados por el sistema operativo,
como los manejadores de los archivos abiertos o las seales. Precisamente, esta diferencia con respecto a un proceso es lo que supone su
principal ventaja.
Desde otro punto de vista, la idea de hilo surge de la posibilidad de
compartir recursos y permitir el acceso concurrente a esos recursos.
La unidad mnima de ejecucin pasa a ser el hilo, asignable a un procesador. Los hilos tienen el mismo cdigo de programa, pero cada uno
recorre su propio camino con su PC, es decir, tiene su propia situacin
aunque dentro de un contexto de comparticin de recursos.
Informalmente, un hilo se puede definir como un proceso ligero
que tiene la misma funcionalidad que un proceso pesado, es decir, los
mismos estados. Por ejemplo, si un hilo abre un fichero, ste estar
disponible para el resto de hilos de una tarea.
En un procesador de textos, por ejemplo, es bastante comn encontrar hilos para la gestin del teclado o la ejecucin del corrector
ortogrficos, respetivamente. Otro ejemplo podra ser un servidor web
que manejara hilos independientes para atender las distintas peticiones entrantes.
Las ventajas de la programacin multihilo se pueden resumir en
las tres siguientes:
pila
pila
[13]
Capacidad de respuesta, ya que el uso de mltiples hilos proporciona un enfoque muy flexible. As, es posible que un hilo se
encuentra atendiendo una peticin de E/S mientras otro contina con la ejecucin de otra funcionalidad distinta. Adems, es
posible plantear un esquema basado en el paralelismo no bloqueante en llamadas al sistema, es decir, un esquema basado en
el bloqueo de un hilo a nivel individual.
Comparticin de recursos, posibilitando que varios hilos manejen el mismo espacio de direcciones.
Eficacia, ya que tanto la creacin, el cambio de contexto, la destruccin y la liberacin de hilos es un orden de magnitud ms
rpida que en el caso de los procesos pesados. Recuerde que las
operaciones ms costosas implican el manejo de operaciones de
E/S. Por otra parte, el uso de este tipo de programacin en arquitecturas de varios procesadores (o ncleos) incrementa enormemente el rendimiento de la aplicacin.
Caso de estudio. POSIX Pthreads
Pthreads es un estndar POSIX que define un interfaz para la creacin y sincronizacin de hilos. Recuerde que se trata de una especificacin y no de una implementacin, siendo sta ltima responsabilidad
del sistema operativo.
POSIX
Portable Operating System Interface es una familia de estndares definidos por el comit IEEE con el objetivo de
mantener la portabilidad entre distintos sistemas operativos. La X de POSIX es una
referencia a sistemas Unix.
[14]
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int suma;
void *mi_hilo (void *valor);
int main (int argc, char *argv[]) {
pthread_t tid;
pthread_attr_t attr;
if (argc != 2) {
fprintf(stderr, "Uso: ./pthread <entero>\n");
return -1;
}
pthread_attr_init(&attr); // Att predeterminados.
// Crear el nuevo hilo.
pthread_create(&tid, &attr, mi_hilo, argv[1]);
pthread_join(tid, NULL); // Esperar finalizacin.
printf("Suma total: %d.\n", suma);
return 0;
}
void *mi_hilo (void *valor) {
int i, ls;
ls = atoi(valor);
i = 0, suma = 0;
while (i <= ls)
suma += (i++);
pthread_exit(0);
}
1.2.
[15]
1.2.1.
[16]
while (1) {
// Produce en nextP.
while (cont == N);
// No hacer nada.
buffer[in] = nextP;
in = (in + 1) % N;
cont++;
}
while (1) {
while (cont == 0);
// No hacer nada.
nextC = buffer[out];
out = (out + 1) % N;
cont--;
// Consume nextC.
}
cont++
r1 = cont
r1 = r1 + 1
cont = r1
cont-r2 = cont
r2 = r2 - 1
cont = r2
1.2.2.
La seccin crtica
El segmento de cdigo en el que un proceso puede modificar variables compartidas con otros procesos se denomina seccin crtica
(ver figura 1.9). Para evitar inconsistencias, una de las ideas que se
plantean es que cuando un proceso est ejecutando su seccin crtica
ningn otro procesos puede ejecutar su seccin crtica asociada.
El problema de la seccin crtica consiste en disear algn tipo de
solucin para garantizar que los procesos involucrados puedan operar
sin generar ningn tipo de inconsistencia. Una posible estructura para
abordar esta problemtica se plantea en la figura 1.9, en la que el
cdigo se divide en las siguientes secciones:
Seccin de entrada, en la que se solicita el acceso a la seccin
crtica.
Seccin crtica, en la que se realiza la modificacin efectiva de
los datos compartidos.
Seccin de salida, en la que tpicamente se har explcita la salida de la seccin crtica.
Seccin restante, que comprende el resto del cdigo fuente.
Normalmente, la seccin de entrada servir para manipular algn
tipo de mecanismo de sincronizacin que garantice el acceso exclusivo
a la seccin crtica de un proceso. Mientras tanto, la seccin de salida servir para notificar, mediante el mecanismo de sincronizacin
p:
p:
c:
c:
p:
c:
r1 =
r1 =
r2 =
r2 =
cont
cont
cont
r1 + 1
cont
r2 - 1
= r1
= r2
[17]
do {
SECCIN_ENTRADA
SECCIN_CRTICA
SECCIN_SALIDA
SECCIN_RESTANTE
} while (1);
Figura 1.9: Estructura general de un proceso.
[18]
[19]
[20]
p1 en SC
(premisa)
p1 en SC --> flag[1]=true y (flag[2]=false o turn!=2) (premisa)
flag[1]=true y (flag[2]=false o turn!=2)
(MP 1,2)
flag[2]=false o turn!=2 (A o B)
(EC 3)
Demostrando A
5. flag[2]=false
(premisa)
6. flag[2]=false --> p2 en SC (premisa)
7. p2 en SR (p2 no SC)
(MP 5,6)
Demostrando B
8. turn=!2
(premisa)
9. flag[1]=true
(EC 3)
10. flag[1]=true y turn=1
(IC 8,9)
11. (flag[1]=true y turn=1) --> p2 en SE (premisa)
12. p2 en SE (p2 no SC)
(MP 10,11)
[21]
Soluciones hardware
En general, cualquier solucin al problema de la seccin crtica se
puede abstraer en el uso de un mecanismo simple basado en cerrojos.
De este modo, las condiciones de carrera no se produciran, siempre
y cuando antes de acceder a la seccin crtica, un proceso adquiriese el uso exclusivo del cerrojo, bloqueando al resto de procesos que
intenten abrirlo. Al terminar de ejecutar la seccin crtica, dicho cerrojo quedara liberado en la seccin de salida. Este planteamiento se
muestra de manera grfica en la figura 1.10.
Evidentemente, tanto las operaciones de adquisicin y liberacin
del cerrojo han de ser atmicas, es decir, su ejecucin ha de producirse en una unidad de trabajo indivisible.
El problema de la seccin crtica se podra solucionar fcilmente
deshabilitando las interrupciones en entornos con un nico procesador. De este modo, sera posible asegurar un esquema basado en la
ejecucin en orden de la secuencia actual de instrucciones, sin permitir la posibilidad de desalojo, tal y como se plantea en los kernels
no apropiativos. Sin embargo, esta solucin no es factible en entornos
multiprocesador, los cuales representan el caso habitual en la actualidad.
Como ejemplo representativo de soluciones hardware, una posibilidad consiste en hacer uso de una instruccin swap, ejecutada de manera atmica mediante el hardware correspondiente, como parte de la
seccin de entrada para garantizar la condicin de exclusin mutua.
[22]
do {
adquirir_cerrojo
SECCIN_CRTICA
liberar_cerrojo
SECCIN_RESTANTE
} while (1);
Figura 1.10: Acceso a la seccin crtica mediante un cerrojo.
Consideraciones
Las soluciones estudiadas hasta ahora no son, por lo general, legibles y presentan dificultadas a la hora de extenderlas a problemas
ms generales en los que se manejen n procesos.
Por otra parte, los procesos que intentan acceder a la seccin crtica
se encuentra en un estado de espera activa, normalmente modelado
mediante bucles que comprueban el valor de una variable de manera
ininterrumpida. Este planteamiento es muy ineficiente y no se puede
acoplar en sistemas multiprogramados.
La consecuencia directa de estas limitaciones es la necesidad de
plantear mecanismos que sean ms sencillos y que sean independientes del nmero de procesos que intervienen en un determinado problema.
1.2.3.
[23]
Semforos
Un semforo es un mecanismo de sincronizacin que encapsula
una variable entera que slo es accesible mediante dos operaciones
atmicas estndar: wait y signal. La inicializacin de este tipo abstracto de datos tambin se considera relevante para definir el comportamiento inicial de una solucin. Recuerde que la modificacin del
valor entero encapsulado dentro de un semforo ha de modificarse
mediante instrucciones atmicas, es decir, tanto wait como signal han
de ser instrucciones atmicas. Tpicamente, estas operaciones se utilizarn para gestionar el acceso a la seccin crtica por un nmero
indeterminado de procesos.
La definicin de wait es la siguiente:
Listado 1.20: Semforos; definicin de wait
1 wait (sem) {
2
while (sem <= 0); // No hacer nada.
3
sem--;
4 }
A continuacin se enumeran algunas caractersticas de los semforos y las implicaciones que tiene su uso en el contexto de la sincronizacin entre procesos.
Slo es posible acceder a la variable del semforo mediante wait
o signal. No se debe asignar o comparar los valores de la variable
encapsulada en el mismo.
Los semforos han de inicializarse con un valor no negativo.
La operacin wait decrementa el valor del semforo. Si ste se
hace negativo, entonces el proceso que ejecuta wait se bloquea.
La operacin signal incrementa el valor del semforo. Si el valor
no era positivo, entonces se desbloquea a un proceso bloqueado
previamente por wait.
Estas caractersticas tienen algunas implicaciones importantes. Por
ejemplo, no es posible determinar a priori si un proceso que ejecuta
wait se quedar bloqueado o no tras su ejecucin. As mismo, no es
posible conocer si despus de signal se despertar algn proceso o no.
Por otra parte, wait y signal son mutuamente excluyentes, es decir,
si se ejecutan de manera concurrente, entonces se ejecutarn secuencialmente y en un orden no conocido a priori.
[24]
do {
wait (sem)
SECCIN_CRTICA
signal (sem)
SECCIN_RESTANTE
} while (1);
Figura 1.11: Acceso a la seccin crtica mediante un semforo.
[25]
while (1) {
// Produce en nextP.
wait (empty);
wait (mutex);
// Guarda nextP en buffer.
signal (mutex);
signal (full);
}
while (1) {
wait (full);
wait (mutex);
// Rellenar nextC.
signal (mutex);
signal (empty);
// Consume nextC.
}
Figura 1.12: Procesos productor y consumidor sincronizados mediante un semforo.
Paso de mensajes
El mecanismo de paso de mensajes es otro esquema que permite
la sincronizacin entre procesos. La principal diferencia con respecto
a los semforos es que permiten que los procesos se comuniquen sin
tener que recurrir al uso de varibles de memoria compartida.
En los sistemas de memoria compartida hay poca cooperacin del
sistema operativo, estn orientados al proceso y pensados para entornos centralizados, y es tarea del desarrollador establecer los mecanismos de comunicacin y sincronizacin. Sin embargo, los sistemas
basados en el paso de mensajes son vlidos para entornos distribuidos, es decir, para sistemas con espacios lgicos distintos, y facilitar
los mecanismos de comunicacin y sincronizacin es labor del sistema
operativo, sin necesidad de usar memoria compartida.
El paso de mensajes se basa en el uso de los dos primitivas siguientes:
send, que permite el envo de informacin a un proceso.
receive, que permite la recepcin de informacin por parte de un
proceso.
[26]
Message
Message
Mensaje
Proceso1
Proceso2
Message
Message
Mensaje
// Proceso 1
while (1) {
// Trabajo previo p1...
send (*, P2)
// Trabajo restante p1...
}
// Proceso 2
while (1) {
// Trabajo previo p2...
receive (&msg, P1)
// Trabajo restante p2...
}
[27]
Datos
compartidos
Condiciones
x
y
Operaciones
Cdigo
inicializacin
Monitores
Aunque los semforos tienen ciertas ventajas que garantizan su
xito en los problemas de sincronizacin entre procesos, tambin sufren ciertas debilidades. Por ejemplo, es posible que se produzcan errores de temporizacin cuando se usan. Sin embargo, la debilidad ms
importante reside en su propio uso, es decir, es fcil que un programador cometa algn error al, por ejemplo, intercambiar errneamente
un wait y un signal.
Con el objetivo de mejorar los mecanismos de sincronizacin y evitar este tipo de problemtica, en los ltimos aos se han propuesto
soluciones de ms alto nivel, como es el caso de los monitores.
Bsicamente, un tipo monitor permite que el programador defina
una serie de operaciones pblicas sobre un tipo abstracto de datos que
gocen de la caracterstica de la exclusin mutua. La idea principal est
ligada al concepto de encapsulacin, de manera que un procedimiento
definido dentro de un monitor slo puede acceder a las variables que
se declaran como privadas o locales dentro del monitor.
Esta estructura garantiza que slo un proceso est activo cada vez
dentro del monitor. La consecuencia directa de este esquema es que el
programador no tiene que implementar de manera explcita esta restriccin de sincronizacin. Esta idea se traslada tambin al concepto
de variables de condicin.
En esencia, un monitor es un mecanismo de sincronizacin de ms
alto nivel que, al igual que un cerrojo, protege la seccin crtica y garantiza que solamente pueda existir un hilo activo dentro de la misma.
Sin embargo, un monitor permite suspender un hilo dentro de la seccin crtica posibilitando que otro hilo pueda acceder a la misma. Este
segundo hilo puede abandonar el monitor, liberndolo, o suspenderse
dentro del monitor. De cualquier modo, el hilo original se despierta y
continua su ejecucin dentro del monitor. Este esquema es escalable
[28]
1.2.4.
[29]
R1
R2
P1
Esperando R2...
P2
Esperando R1...
Tpicamente, un proceso ha de solicitar un recurso antes de utilizarlo y ha de liberarlo despus de haberlo utilizado. En modo de
operacin normal, un proceso puede utilizar un recurso siguiendo la
siguiente secuencia:
[30]
P1
R1
P2
Figura 1.16: Condicin de espera circular con dos procesos y dos recursos.
1. Solicitud: si el proceso no puede obtenerla inmediatamente, tendr que esperar hasta que pueda adquirir el recurso.
2. Uso: el proceso ya puede operar sobre el recurso.
3. Liberacin: el proceso libera el recurso.
Los interbloqueos pueden surgir si se dan, de manera simultnea,
las cuatro condiciones de Coffman [12]:
1. Exclusin mutua: al menos un recurso ha de estar en modo no
compartido, es decir, slo un proceso puede usarlo cada vez. En
otras palabras, si el recurso est siendo utilizado, entonces el
proceso que desee adquirirlo tendr que esperar.
2. Retencin y espera: un proceso ha de estar reteniendo al menos
un recurso y esperando para adquirir otros recursos adicionales,
actualmente retenidos por otros procesos.
3. No apropiacin: los recursos no pueden desalojarse, es decir,
un recurso slo puede ser liberado, de manera voluntaria, por el
proceso que lo retiene despus de finalizar la tarea que estuviera
realizando.
4. Espera circular: ha de existir un conjunto de procesos en competicin P = {p0 , p1 , ..., pn1 } de manera que p0 espera a un recurso
retenido por p1 , p1 a un recurso retenido por p2 , ..., y pn1 a un
recurso retenido por p0 .
Todas estas condiciones han de darse por separado para que se
produzca un interbloqueo, aunque resulta importante resaltar que
existen dependencias entre ellas. Por ejemplo, la condicin de espera circular implica que se cumpla la condicin de retencin y espera.
Grafos de asignacin de recursos
Los interbloqueos se pueden definir de una forma ms precisa, facilitando adems su anlisis, mediante los grafos de asignacin de recursos. El conjunto de nodos del grafo se suele dividir en dos subconjuntos, uno para los procesos activos del sistema P = {p0 , p1 , ..., pn1 } y
otro formado por los tipos de recursos del sistema R = {r1 , r2 , ..., rn1 }.
R2
[31]
R1
P1
R3
P2
R2
P3
R4
Por otra parte, el conjunto de aristas del grafo dirigido permite establecer las relaciones existentes entre los procesos y los recursos. As,
una arista dirigida del proceso pi al recurso rj significa que el proceso
pi ha solicitado una instancia del recurso rj . Recuerde que en el modelo de sistema planteado cada recurso tiene asociado un nmero de
instancias. Esta relacin es una arista de solicitud y se representa
como pi rj .
Por el contrario, una arista dirigida del recurso rj al proceso pi
significa que se ha asignado una instancia del recurso rj al proceso pi .
Esta arista de asignacin se representa como rj pi . La figura 1.17
muestra un ejemplo de grafo de asignacin de recursos.
Los grafos de asignacin de recursos que no contienen ciclos no
sufren interbloqueos. Sin embargo, la presencia de un ciclo puede generar la existencia de un interbloqueo, aunque no necesariamente. Por
ejemplo, en la parte izquierda de la figura 1.18 se muestra un ejemplo
de grafo que contiene un ciclo que genera una situacin de interbloqueo, ya que no es posible romper el ciclo. Note cmo se cumplen las
cuatro condiciones de Coffman.
La parte derecha de la figura 1.18 muestra la existencia de un ciclo
en un grafo de asignacin de recursos que no conduce a un interbloqueo, ya que si los procesos p2 o p4 finalizan su ejecucin, entonces el
ciclo se rompe.
[32]
R1
R3
P2
R1
P1
P2
P3
P1
P3
P4
R2
R4
Figura 1.18: Grafos de asignacin de recursos con deadlock (izquierda) y sin deadlock
(derecha).
R2
[33]
Impedir o evitar
Detectar y recuperar
Ignorar
1
Prevencin
Evasin
Evitar las 4
condiciones
Coffman
Poltica
asignacin
recursos
Info sobre
el estado de
recursos
2
Algoritmo
deteccin
3
Algoritmo
recuperacin
Figura 1.19: Diagrama general con distintos mtodos para tratar los interbloqueos.
[34]
Prevencin de interbloqueos
Como se ha comentado anteriormente, las tcnicas de prevencin
de interbloqueos se basan en garantizar que algunas de las cuatro
condiciones de Coffman no se cumplan.
La condicin de exclusin mutua est asociada a la propia naturaleza del recurso, es decir, depende de si el recurso se puede compartir
o no. Los recursos que pueden compartirse no requieren un acceso
mutuamente excluyente, como por ejemplo un archivo de slo lectura. Sin embargo, en general no es posible garantizar que se evitar un
interbloqueo incumpliendo la condicin de exclusin mutua, ya que
existen recursos, como por ejemplo una impresora, que por naturaleza no se pueden compartir..
La condicin de retener y esperar se puede negar si se garantiza
de algn modo que, cuando un proceso solicite un recurso, dicho proceso no est reteniendo ningn otro recurso. Una posible solucin a
esta problemtica consiste en solicitar todos los recursos necesarios
antes de ejecutar una tarea. De este modo, la poltica de asignacin de
recursos decidir si asigna o no todo el bloque de recursos necesarios
para un proceso. Otra posible opcin sera plantear un protocolo que
permitiera a un proceso solicitar recursos si y slo si no tiene ninguno
retenido.
Qu desventajas presentan los esquemas planteados para evitar la condicin de retener y esperar?
[35]
Algoritmo
del banquero
Peticiones de recursos
Algoritmo de
seguridad
Estado
seguro?
No
Deshacer
Asignar recursos
Evasin de interbloqueos
La prevencin de interbloqueos se basa en restringir el modo en
el que se solicitan los recursos, con el objetivo de garantizar que al
menos una de las cuatro condiciones se incumpla. Sin embargo, este
planteamiento tiene dos consecuencias indeseables: la baja tasa de
uso de los recursos y la reduccin del rendimiento global del sistema.
Una posible alternativa consiste en hacer uso de informacin relativa a cmo se van a solicitar los recursos. De este modo, si se dispone
de ms informacin, es posible plantear una poltica ms completa que
tenga como meta la evasin de potenciales interbloqueos en el futuro.
Un ejemplo representativo de este tipo de informacin podra ser la
secuencia completa de solicitudes y liberaciones de recursos por parte
de cada uno de los procesos involucrados.
Las alternativas existentes de evasin de interbloqueos varan en
la cantidad y tipo de informacin necesaria. Una solucin simple se
basa en conocer nicamente el nmero mximo de recursos que un
[36]
[37]
p0
p1
p2
p3
p4
Allocation
0012
1000
1354
0632
0014
Max
001
175
235
065
065
2
0
6
2
6
[38]
need3 = [0, 0, 2, 0]
need4 = [0, 6, 4, 2]
A continuacin, se analiza el estado de los distintos procesos.
p0 : need0 work = [0, 0, 0, 0] [1, 5, 2, 0]; f inish[0] = f .
work = [1, 5, 2, 0] + [0, 0, 1, 2] = [1, 5, 3, 2]
f inish = [v, f, f, f, f ]
p1 : need1 > work = [0, 7, 5, 0] > [1, 5, 3, 2]
p2 : need2 work = [1, 0, 0, 2] [1, 5, 3, 2]; f inish[2] = f .
work = [1, 5, 3, 2] + [1, 3, 5, 4] = [2, 8, 8, 6]
f inish = [v, f, v, f, f ]
p1 : need1 work = [0, 7, 5, 0] [2, 8, 8, 6]; f inish[1] = f .
work = [2, 8, 8, 6] + [1, 0, 0, 0] = [3, 8, 8, 6]
f inish = [v, v, v, f, f ]
p3 : need3 work = [0, 0, 2, 0] [3, 8, 8, 6]; f inish[3] = f .
work = [3, 8, 8, 6] + [0, 6, 3, 2] = [3, 14, 11, 8]
f inish = [v, v, v, v, f ]
p4 : need4 work = [0, 6, 4, 2] [3, 14, 11, 8]; f inish[4] = f .
work = [3, 14, 11, 8] + [0, 0, 1, 4] = [3, 14, 12, 12]
f inish = [v, v, v, v, v]
Por lo que se puede afirmar que el sistema se encuentra en un
estado seguro.
Si el proceso p1 solicita recursos por valor de [0, 4, 2, 0], puede atenderse con garantas de inmediato esta peticin?
En primer lugar habra que aplicar el algoritmo del banquero para
determinar si dicha peticin se puede conceder de manera segura.
1. request1 need1 [0, 4, 2, 0] [0, 7, 5, 0]
2. request1 available1 [0, 4, 2, 0] [1, 5, 2, 0]
3. Asignacin de recursos.
a) available = available request1 = [1, 5, 2, 0] [0, 4, 2, 0] = [1, 1, 0, 0]
b) allocation1 = allocation1 + request1 = [1, 0, 0, 0] + [0, 4, 2, 0] =
[1, 4, 2, 0]
c) need1 = need1 request1 = [0, 7, 5, 0] [0, 4, 2, 0] = [0, 3, 3, 0]
4. Comprobacin de estado seguro.
[39]
[40]
1.3.
El rango de aplicacin de los dispositivos electrnicos, incluyendo los ordenadores, ha crecido exponencialmente en los ltimos aos.
Uno de estos campos est relacionado con las aplicaciones de tiempo
real, donde es necesario llevar a cabo una determinada funcionalidad
atendiendo a una serie de restricciones temporales que son esenciales
en relacin a los requisitos de dichas aplicaciones. Por ejemplo, considere el sistema de control de un coche. Este sistema ha de ser capaz
de responder a determinadas acciones en tiempo real.
Es importante resaltar que las aplicaciones de tiempo real tienen
caractersticas que los hacen particulares con respecto a otras aplicaciones ms tradicionales de procesamiento de informacin. A lo largo
del tiempo han surgido herramientas y lenguajes de programacin especialmente diseados para facilitar su desarrollo.
STR
1.3.1.
[41]
En el diseo y desarrollo de sistemas de tiempo real son esenciales aspectos relacionados con la programacin concurrente, la planificacin en tiempo real y la fiabilidad y la tolerancia a fallos. Estos
aspectos sern cubiertos en captulos sucesivos.
Caractersticas de un sistema de tiempo real
El siguiente listado resume las principales caractersticas que un
sistema de tiempo real puede tener [3]. Idealmente, un lenguaje de
programacin o sistema operativo que se utilice para el desarrollo de
un sistema de tiempo real debera proporcionar dichas caractersticas.
Complejidad, en trminos de tamao o nmero de lneas de cdigo fuente y en trminos de variedad de respuesta a eventos del
mundo real. Esta caracterstica est relacionada con la mantenibilidad del cdigo.
Tratamiento de nmeros reales, debido a la precisin necesaria
en mbitos de aplicacin tradicionales de los sistemas de tiempo
real, como por ejemplo los sistemas de control.
Fiabilidad y seguridad, en trminos de robustez del software
desarrollado y en la utilizacin de mecanismos o esquemas que la
garanticen. El concepto de certificacin cobra especial relevancia
en el mbito de la fiabilidad como solucin estndar que permite
garantizar la robustez de un determinado sistema.
[42]
1.3.2.
LynuxWorks
Un ejemplo representativo de
sistema operativo de tiempo
real es LynuxWorks, el cual
da soporte a una virtualizacin completa en dispositivos
empotrados.
Captulo
Semforos y Memoria
Compartida
n este captulo se introduce el concepto de semforo como mecanismo bsico de sincronizacin entre procesos. En la vida
real, un semforo es un sistema basado en seales luminosas
que rige el comportamiento de los vehculos que comparten un tramo
de una calzada. sta es la esencia de los semforos software, conformando una estructura de datos que se puede utilizar en una gran
variedad de problemas de sincronizacin.
As mismo, y debido a la necesidad de compartir datos entre procesos, en este captulo se discute el uso de los segmentos de memoria
compartida como mecanismo bsico de comunicacin entre procesos.
Como caso particular, en este captulo se discuten los semforos y
la memoria compartida en POSIX y se estudian las primitivas necesarias para su uso y manipulacin. Posteriormente, la implementacin
planteada a partir de este estndar se utilizar para resolver algunos
problemas clsicos de sincronizacin entre procesos, como por ejemplos los filsofos comensales o los lectores-escritores.
[44]
compartidas por diversos problemas de sincronizacin especficos. Estos patrones pueden resultar muy tiles a la hora de afrontar nuevos
problemas de sincronizacin.
2.1.
Conceptos bsicos
Un semforo es una herramienta de sincronizacin genrica e independiente del dominio del problema, ideada por Dijsktra en 1965.
Bsicamente, un semforo es similar a una variable entera pero con
las tres diferencias siguientes:
1. Al crear un semforo, ste se puede inicializar con cualquier valor
entero. Sin embargo, una vez creado, slo es posible incrementar
y decrementar en uno su valor. De hecho, no se debe leer el valor
actual de un semforo.
2. Cuando un proceso decrementa el semforo, si el resultado es
negativo entonces dicho proceso se bloquea a la espera de que
otro proceso incremente el semforo.
3. Cuando un proceso incrementa el semforo, si hay otros procesos
bloqueados entonces uno de ellos se desbloquear.
Las operaciones de sincronizacin de los semforos son, por lo tanto, dos: wait y signal. Estas operaciones tambin se suelen denominar
P (del holands Proberen) y V (del holands Verhogen).
Todas las modificaciones del valor entero asociado a un semforo se
han de ejecutar de manera atmica, es decir, de forma indivisible. En
otras palabras, cuando un proceso modifica el valor de un semforo,
ningn otro proceso puede modificarlo de manera simultnea.
Los semforos slo se deben manipular mediante las operaciones atmicas wait y signal.
[45]
p1: proceso
p1: proceso
p1: proceso
5
creacin
[inicializacin]
wait
[p1 bloqueado]
sem (1)
sem (0)
sem (-1)
wait
[decremento]
signal
[incremento]
[p1 desbloqueado]
sem (0)
p2: proceso
p2: proceso
do {
wait (sem)
SECCIN_CRTICA
signal (sem)
SECCIN_RESTANTE
} while (1);
Los semforos se suelen clasificar en semforos contadores y semforos binarios. Un semforo contador es aqul que permite que
su valor pueda variar sin restricciones, mientras que en un semforo
binario dicho valor slo puede ser 0 1. Tradicionalmente, los semforos binarios se suelen denominar mutex.
En el contexto del problema de la seccin crtica discutido en la
seccin 1.2.2, los semforos binarios se pueden utilizar como mecanismo para garantizar el acceso exclusivo a la misma, evitando as las
ya discutidas condiciones de carrera. En este contexto, los procesos
involucrados compartiran un semforo inicializado a 1, mientras que
su funcionamiento estara basado en el esquema de la figura 2.1.
Por otra parte, los semforos contadores se pueden utilizar para
gestionar el acceso concurrente a las distintas instancias que conforman un recurso. En este caso, el semforo se inicializara al nmero de instancias disponibles originalmente. As, cuando un proceso
pi deseara adquirir una instancia del recurso, ejecutara wait sobre el
semforo decrementando el valor del mismo.
[46]
P1
P2
P3
P1
P2
P3
r1
s1
t1
r1
s1
t1
r2
S(a)
t2
W(a)
s2
S(c)
r3
W(c)
t3
S(b)
s3
S(d)
r4
W(b)
t4
r5
W(d)
t5
a
r2
s2
r3
s3
r4
r5
s4
s5
t2
t3
t4
t5
s4
s5
Por el contrario, cuando un proceso pi liberara dicha instancia, ejecutara signal sobre el recurso incrementando el valor del mismo. Si
el semforo llegase a 0, entonces todas las instancias del recurso estaran ocupadas. Ante esta situacin, si otro proceso pj ejecuta wait,
entonces se bloqueara hasta que el valor del semforo sea mayor que
0, es decir, hasta que otro proceso ejecutara signal.
Los semforos tambin se usan para sincronizar eventos, delimitando hasta qu punto puede llegar un proceso ejecutando su cdigo
antes de que otro proceso alcance otro punto de su cdigo. La figura
2.3 muestra la estructura de tres procesos (P1, P2 y P3) en base a sus
instrucciones y los puntos de sincronizacin entre dichos procesos.
Un punto de sincronizacin, representado mediante lneas azules
en la figura, especifica si una instruccin de un proceso se ha de ejecutar, obligatoriamente, antes que otra instruccin especfica de otro
proceso. Para cada punto de sincronizacin se ha definido un semforo
binario distinto, inicializado a 0. Por ejemplo, el punto de sincronizacin asociado al semforo a implica que la operacin s1, del proceso
P2, se ha de ejecutar antes que la operacin r3 para que el proceso P1
pueda continuar su ejecucin.
Adems de estos aspectos bsicos que conforman la definicin de
un semforo, es importante considerar las siguientes consecuencias
derivadas de dicha definicin:
En general, no es posible conocer si un proceso se bloquear
despus de decrementar el semforo.
Sincronizacin
La sincronizacin tambin se
refiere a situaciones en las
que se desea evitar que dos
eventos ocurran al mismo
tiempo o a forzar que uno
ocurra antes o despus que
otro.
2.2. Implementacin
[47]
2.2.
2.2.1.
Implementacin
Semforos
[48]
#include <semaphore.h>
/* Devuelve un puntero al semforo o SEM FAILED */
sem_t *sem_open (
const char *name, /* Nombre del semforo */
int oflag,
/* Flags */
mode_t mode,
/* Permisos */
unsigned int value /* Valor inicial */
);
sem_t *sem_open (
const char *name,
int oflag,
);
#include <semaphore.h>
/* Devuelven 0 si todo correcto o -1 en caso de error */
int sem_close (sem_t *sem);
int sem_unlink (const char *name);
Finalmente, las primitivas para decrementar e incrementar un semforo se exponen a continuacin. Note cmo la primitiva de incremento utiliza el nombre post en lugar del tradicional signal.
Listado 2.5: Primitivas sem_wait y sem_post en POSIX.
1
2
3
4
5
#include <semaphore.h>
/* Devuelven 0 si todo correcto o -1 en caso de error */
int sem_wait (sem_t *sem);
int sem_post (sem_t *sem);
En el presente captulo se utilizarn los semforos POSIX para codificar soluciones a diversos problemas de sincronizacin que se irn
Control de errores
Recuerde que primitivas como sem_close y sem_unlink
tambin devuelven cdigos
de error en caso de fallo.
2.2. Implementacin
[49]
Patrn fachada
La interfaz de gestin de semforos propuesta en esta
seccin es una implementacin del patrn fachada facade.
#include <semaphore.h>
typedef int bool;
#define false 0
#define true 1
/* Crea un semforo POSIX */
sem_t *crear_sem (const char *name, unsigned int valor);
/* Obtener un semforo POSIX (ya existente) */
sem_t *get_sem (const char *name);
/* Cierra un semforo POSIX */
void destruir_sem (const char *name);
/* Incrementa el semforo */
void signal_sem (sem_t *sem);
/* Decrementa el semforo */
void wait_sem (sem_t *sem);
2.2.2.
Memoria compartida
Los problemas de sincronizacin entre procesos suelen integrar algn tipo de recurso compartido cuyo acceso, precisamente, hace necesario el uso de algn mecanismo de sincronizacin como los semforos. El recurso compartido puede ser un fragmento de memoria,
por lo que resulta relevante estudiar los mecanismos proporcionados
por el sistema operativo para dar soporte a la memoria compartida.
En esta seccin se discute el soporte POSIX para memoria compartida, haciendo especial hincapi en las primitivas proporcionadas y en
un ejemplo particular.
La gestin de memoria compartida en POSIX implica la apertura
o creacin de un segmento de memoria compartida asociada a un
nombre particular, mediante la primitiva shm_open(). En POSIX, los
segmentos de memoria compartida actan como archivos en memoria.
Una vez abiertos, es necesario establecer el tamao de los mismos mediante ftruncate() para, posteriormente, mapear el segmento al espacio
de direcciones de memoria del usuario mediante la primitiva mmap.
[50]
A continuacin se expondrn las primitivas POSIX necesarias para la gestin de segmentos de memoria compartida. Ms adelante, se
plantear un ejemplo concreto para compartir una variable entera.
Listado 2.7: Primitivas shm_open y shm_unlink en POSIX.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <mman.h>
/* Devuelve el descriptor de archivo o -1 si error */
int shm_open(
const char *name, /* Nombre del segmento */
int oflag,
/* Flags */
mode_t mode
/* Permisos */
);
/* Devuelve 0 si todo correcto o -1 en caso de error */
int shm_unlink(
const char *name /* Nombre del segmento */
);
#include <unistd.h>
#include <sys/types.h>
int ftruncate(
int fd,
/* Descriptor de archivo */
off_t length /* Nueva longitud */
);
2.2. Implementacin
[51]
#include <sys/mman.h>
void *mmap(
void *addr,
size_t length,
int prot,
int flags,
int fd,
off_t offset
);
/*
/*
/*
/*
/*
/*
Direccin de memoria */
Longitud del segmento */
Proteccin */
Flags */
Descriptor de archivo */
Desplazamiento */
int munmap(
void *addr,
/* Direccin de memoria */
size_t length /* Longitud del segmento */
);
[52]
shm_fd, 0);
if (p == MAP_FAILED) {
fprintf(stderr, "Error al mapear la variable: %s\n",
strerror(errno));
exit(EXIT_FAILURE);
}
*p = valor;
munmap(p, sizeof(int));
return shm_fd;
2.3.
[53]
En esta seccin se plantean algunos problemas clsicos de sincronizacin y se discuten posibles soluciones basadas en el uso de semforos. En la mayor parte de ellos tambin se hace uso de segmentos
de memoria compartida para compartir datos.
2.3.1.
El buffer limitado
Pr oductor
Consumidor
Para controlar el acceso exclusivo a la seccin crtica se puede utilizar un semforo binario, de manera que la primitiva wait posibilite
dicho acceso o bloquee a un proceso que desee acceder a la seccin crtica. Para controlar las dos situaciones anteriormente mencionadas, se
pueden usar dos semforos independientes que se actualizarn cuando se produzca o se consuma un elemento del buffer, respectivamente.
La solucin planteada se basa en los siguientes elementos:
mutex, semforo binario que se utiliza para proporcionar exclusin mutua para el acceso al buffer de productos. Este semforo
se inicializa a 1.
empty, semforo contador que se utiliza para controlar el nmero de huecos vacos del buffer. Este semforo se inicializa a n,
siendo n el tamao del buffer.
[54]
while (1) {
// Produce en nextP.
wait (empty);
wait (mutex);
// Guarda nextP en buffer.
signal (mutex);
signal (full);
}
while (1) {
wait (full);
wait (mutex);
// Rellenar nextC.
signal (mutex);
signal (empty);
// Consume nextC.
}
[55]
Proceso A
Proceso B
wait(mutex);
// Seccin crtica
cont = cont + 1;
signal(mutex);
wait(mutex);
// Seccin crtica
cont = cont - 1;
signal(mutex);
Figura 2.6: Aplicacin del patrn mutex para acceder a la variable compartida cont.
Proceso A
Proceso B
sentencia a1
signal(sem);
wait(sem);
sentencia b1;
2.3.2.
Lectores y escritores
Suponga una estructura o una base de datos compartida por varios procesos concurrentes. Algunos de estos procesos simplemente
tendrn que leer informacin, mientras que otros tendrn que actualizarla, es decir, realizar operaciones de lectura y escritura. Los primeros
procesos se denominan lectores mientras que el resto sern escritores.
[56]
[57]
Proceso lector
Proceso escritor
wait(mutex);
num_lectores++;
if num_lectores == 1 then
wait(acceso_esc);
signal(mutex);
/* SC: Lectura */
/* */
wait(mutex);
num_lectores--;
if num_lectores == 0 then
signal(acceso_esc);
signal(mutex);
wait(acceso_esc);
/* SC: Escritura */
/* ... */
signal(acceso_esc);
La solucin planteada garantiza que no existe posibilidad de interbloqueo pero se da una situacin igualmente peligrosa, ya que un
escritor se puede quedar esperando indefinidamente en un estado de
inanicin (starvation) si no dejan de pasar lectores.
Priorizando escritores
En esta seccin se plantea una modificacin sobre la solucin anterior con el objetivo de que, cuando un escritor llegue, los lectores
puedan finalizar pero no sea posible que otro lector tenga prioridad
sobre el escritor.
Para modelar esta variacin sobre el problema original, ser necesario contemplar de algn modo esta nueva prioridad de un escritor
sobre los lectores que estn esperando para acceder a la seccin crtica. En otras palabras, ser necesario integrar algn tipo de mecanismo
que priorice el turno de un escritor frente a un lector cuando ambos
se encuentren en la seccin de entrada.
Una posible solucin consiste en aadir un semforo turno que gobierne el acceso de los lectores de manera que los escritores puedan
adquirirlo de manera previa. En la imagen 2.9 se muestra el pseudocdigo de los procesos lector y escritor, modificados a partir de la versin
original.
[58]
Proceso lector
Proceso escritor
wait(turno);
signal(turno);
wait(mutex);
num_lectores++;
if num_lectores == 1 then
wait(acceso_esc);
signal(mutex);
// SC: Lectura
// ...
wait(mutex);
num_lectores--;
if num_lectores == 0 then
signal(acceso_esc);
signal(mutex);
wait(turno);
wait(acceso_esc);
// SC: Escritura
// ...
signal(turno);
signal(acceso_esc);
Si un escritor se queda bloqueado al ejecutar wait sobre dicho semforo, entonces forzar a futuros lectores a esperar. Cuando el ltimo lector abandona la seccin crtica, se garantiza que el siguiente
proceso que entre ser un escritor.
Como se puede apreciar en el proceso escritor, si un escritor llega
mientras hay lectores en la seccin crtica, el primero se bloquear en
la segunda sentencia. Esto implica que el semforo turno est cerrado, generando una barrera que encola al resto de lectores mientras el
escritor sigue esperando.
Respecto al lector, cuando el ltimo abandona y efecta signal sobre acceso_esc, entonces el escritor que permaneca esperando se desbloquea. A continuacin, el escritor entrar en su seccin crtica debido a que ninguno de los otros lectores podr avanzar debido a que
turno los bloquear.
Cuando el escritor termine realizando signal sobre turno, entonces
otro lector u otro escritor podr seguir avanzando. As, esta solucin
garantiza que al menos un escritor contine su ejecucin, pero es posible que un lector entre mientras haya otros escritores esperando.
En funcin de la aplicacin, puede resultar interesante dar prioridad a los escritores, por ejemplo si es crtico tener los datos continuamente actualizados. Sin embargo, en general, ser el planificador,
y no el programador el responsable de decidir qu proceso o hilo en
concreto se desbloquear.
[59]
Patrones de sincronizacin
El problema de los lectores y escritores se caracteriza porque la presencia de un proceso en la seccin crtica no excluye necesariamente
a otros procesos. Por ejemplo, la existencia de un proceso lector en la
seccin crtica posibilita que otro proceso lector acceda a la misma. No
obstante, note cmo el acceso a la variable compartida num_lectores
se realiza de manera exclusiva mediante un mutex.
Sin embargo, la presencia de una categora, por ejemplo la categora
escritor, en la seccin crtica s que excluye el acceso de procesos de
otro tipo de categora, como ocurre con los lectores. Este patrn de
exclusin se suele denominar exclusin mutua categrica [5].
Por otra parte, en este problema tambin se da una situacin que
se puede reproducir con facilidad en otros problemas de sincronizacin. Bsicamente, esta situacin consiste en que el primer proceso
en acceder a una seccin de cdigo adquiera el semforo, mientras
que el ltimo lo libera. Note cmo esta situacin ocurre en el proceso
lector.
El nombre del patrn asociado a este tipo de situaciones es lightswitch o interruptor [5], debido a su similitud con un interruptor de
luz (la primera persona lo activa o enciende mientras que la ltima lo
desactiva o apaga).
Listado 2.13: Implementacin del patrn interruptor.
1 import threading
2 from threading import Semaphore
3
4 class Interruptor:
5
6
def __init__ (self):
7
self._contador = 0
8
self._mutex = Semaphore(1)
9
10
def activar (self, semaforo):
11
self._mutex.acquire()
12
self._contador += 1
13
if self._contador == 1:
14
semaforo.acquire()
15
self._mutex.release()
16
17
def desactivar (self, semaforo):
18
self._mutex.acquire()
19
self._contador -= 1
20
if self._contador == 0:
21
semaforo.release()
22
self._mutex.release()
[60]
2.3.3.
Los filsofos se encuentran comiendo o pensando. Todos ellos comparten una mesa redonda con cinco sillas, una para cada filsofo. En
el centro de la mesa hay una fuente de arroz y en la mesa slo hay cinco palillos, de manera que cada filsofo tiene un palillo a su izquierda
y otro a su derecha.
Cuando un filsofo piensa, entonces se abstrae del mundo y no se
relaciona con otros filsofos. Cuando tiene hambre, entonces intenta
acceder a los palillos que tiene a su izquierda y a su derecha (necesita
ambos). Naturalmente, un filsofo no puede quitarle un palillo a otro
filsofo y slo puede comer cuando ha cogido los dos palillos. Cuando
un filsofo termina de comer, deja los palillos y se pone a pensar.
Este problema es uno de los problemas clsicos de sincronizacin entre procesos, donde es necesario gestionar el acceso concurrente a los recursos compartidos, es decir, a los propios palillos. En
este contexto, un palillo se puede entender como un recurso indivisible, es decir, en todo momento se encontrar en un estado de uso o en
un estado de no uso.
La solucin planteada a continuacin se basa en representar cada
palillo con un semforo inicializado a 1. De este modo, un filsofo
que intente hacerse con un palillo tendr que ejecutar wait sobre el
mismo, mientras que ejecutar signal para liberarlo (ver figura 2.11).
Recuerde que un filsofo slo puede acceder a los palillos adyacentes
al mismo.
Aunque la solucin planteada garantiza que dos filsofos que estn
sentados uno al lado del otro nunca coman juntos, es posible que el
sistema alcance una situacin de interbloqueo. Por ejemplo, suponga que todos los filsofos cogen el palillo situado a su izquierda. En
ese momento, todos los semforos asociados estarn a 0, por lo que
ningn filsofo podr coger el palillo situado a su derecha.
Evitando el interbloqueo
Una de las modificaciones ms directas para evitar un interbloqueo
en la versin clsica del problema de los filsofos comensales es limitar el nmero de filsofos a cuatro. De este modo, si slo hay cuatro
filsofos, entonces en el peor caso todos cogern un palillo pero siempre quedar un palillo libre. En otras palabras, si dos filsofos vecinos
[61]
Proceso filsofo
while (1) {
// Pensar...
wait(palillos[i]);
wait(palillos[(i+1) % 5]);
// Comer...
signal(palillos[i]);
signal(palillos[(i+1) % 5]);
}
Figura 2.11: Pseudocdigo de la solucin del problema de los filsofos comensales con
riesgo de interbloqueo.
while (1) {
/* Pensar... */
wait(sirviente);
wait(palillos[i]);
wait(palillos[(i+1)]);
/* Comer... */
signal(palillos[i]);
signal(palillos[(i+1)]);
signal(sirviente);
}
Figura 2.13: Pseudocdigo de la solucin del problema de los filsofos comensales sin
riesgo de interbloqueo.
[62]
Proceso filsofo i
#define N 5
#define IZQ (i-1) % 5
#define DER (i+1) % 5
int estado[N];
sem_t mutex;
sem_t sem[N];
void filosofo (int i) {
while (1) {
pensar();
coger_palillos(i);
comer();
dejar_palillos(i);
}
}
Figura 2.14: Pseudocdigo de la solucin del problema de los filsofos comensales sin
interbloqueo.
Compartiendo el arroz
Una posible variacin del problema original de los filsofos comensales consiste en suponer que los filsofos comern directamente de
[63]
Proceso filsofo
while (1) {
// Pensar...
wait(palillos);
// Comer...
signal(palillos);
}
Figura 2.15: Variacin con una bandeja al centro del problema de los filsofos comensales. A la izquierda se muestra una representacin grfica, mientras que a la derecha se
muestra una posible solucin en pseudocdigo haciendo uso de un semforo contador.
Patrones de sincronizacin
La primera solucin sin interbloqueo del problema de los filsofos
comensales y la solucin a la variacin con los palillos en el centro
comparten una caracterstica. En ambas soluciones se ha utilizado
un semforo contador que posibilita el acceso concurrente de varios
procesos a la seccin crtica.
En el primer caso, el semforo sirviente se inicializaba a 4 para permitir el acceso concurrente de los filsofos a la seccin crtica, aunque
luego compitieran por la obtencin de los palillos a nivel individual.
En el segundo caso, el semforo palillos se inicializaba a 2 para
permitir el acceso concurrente de dos procesos filsofo, debido a que
los filsofos disponan de 2 pares de palillos.
Este tipo de soluciones se pueden encuadrar dentro de la aplicacin
del patrn multiplex [5], mediante el uso de un semforo contador cu-
[64]
wait(multiplex);
// Seccin crtica
signal(multiplex);
2.3.4.
[65]
Norte
Sur
Figura 2.17: Esquema grfico del problema del puente de un solo carril.
[66]
wait(mutexN);
cochesNorte++;
if cochesNorte == 1 then
wait(puente);
signal(mutexN);
/* SC: Cruzar el puente */
/* ... */
wait(mutexN);
cochesNorte--;
if cochesNorte == 0 then
signal(puente);
signal(mutexN);
2.3.5.
El barbero dormiln
El problema original de la barbera fue propuesto por Dijkstra, aunque comnmente se suele conocer como el problema del barbero dormiln. El enunciado de este problema clsico de sincronizacin se expone a continuacin.
La barbera tiene una sala de espera con n sillas y la habitacin del
barbero tiene una nica silla en la que un cliente se sienta para que
el barbero trabaje. Si no hay clientes, entonces el barbero se duerme.
Si un cliente entra en la barbera y todas las sillas estn ocupadas, es
decir, tanto la del barbero como las de la sala de espera, entonces el
cliente se marcha. Si el barbero est ocupado pero hay sillas disponibles, entonces el cliente se sienta en una de ellas. Si el barbero est
durmiendo, entonces el cliente lo despierta.
Para abordar este problema clsico se describirn algunos pasos
que pueden resultar tiles a la hora de enfrentarse a un problema de
sincronizacin entre procesos. En cada uno de estos pasos se discutir
la problemtica particular del problema de la barbera.
1. Identificacin de procesos, con el objetivo de distinguir las distintas entidades que forman parte del problema. En el caso del
barbero dormiln, estos procesos son el barbero y los clientes.
Tpicamente, ser necesario otro proceso manager encargado del
lanzamiento y gestin de los procesos especficos de dominio.
2. Agrupacin en clases o trozos de cdigo, con el objetivo de estructurar a nivel de cdigo la solucin planteada. En el caso del
barbero, el cdigo se puede estructurar de acuerdo a la identificacin de procesos.
3. Identificacin de recursos compartidos, es decir, identificacin
de aquellos elementos compartidos por los procesos. Ser nece-
[67]
Identificacin
de procesos
Identificacin
de clases
3
Identificacin de
recursos compartidos
4
Identificacin de
eventos de sincronizacin
5
Implementacin
Figura 2.18: Secuencia de pasos para disear una solucin ante un problema de sincronizacin.
[68]
acceso a esta variable ha de ser exclusivo, por lo que una vez se utiliza
el patrn mutex.
Otra posible opcin hubiera sido un semforo contador inicializado
al nmero inicial de sillas libres en la sala de espera de la barbera. Sin embargo, si utilizamos un semforo no es posible modelar la
restriccin que hace que un cliente abandone la barbera si no hay
sillas libres. Con un semforo, el cliente se quedara bloqueado hasta
que hubiese un nuevo hueco. Por otra parte, para modelar el recurso compartido representado por el silln del barbero se ha optado por
un semforo binario, denominado silln, con el objetivo de garantizar
que slo pueda haber un cliente sentado en l.
Respecto a los eventos de sincronizacin, en este problema existe
una interaccin directa entre el cliente que pasa a la sala del barbero,
es decir, el proceso de despertar al barbero, y la notificacin del barbero al cliente cuando ya ha terminado su trabajo. El primer evento
se ha modelado mediante un semforo binario denominado barbero,
ya que est asociado al proceso de despertar al barbero por parte de
un cliente. El segundo evento se ha modelado, igualmente, con otro
semforo binario denominado fin, ya que est asociado al proceso de
notificar el final del trabajo a un cliente por parte del barbero. Ambos
semforos se inicializan a 0.
En la figura 2.19 se muestra el pseudocdigo de una posible solucin al problema del barbero dormiln. Como se puede apreciar, la
parte relativa al proceso barbero es trivial, ya que ste permanece de
manera pasiva a la espera de un nuevo cliente, es decir, permanece
bloqueado mediante wait sobre el semforo barbero. Cuando terminado de cortar, el barbero notificar dicha finalizacin al cliente mediante
signal sobre el semforo fin. Evidentemente, el cliente estar bloqueado por wait.
El pseudocdigo del proceso cliente es algo ms complejo, ya que
hay que controlar si un cliente puede o no pasar a la sala de espera.
Para ello, en primer lugar se ha de comprobar si todas las sillas estn
ocupadas, es decir, si el valor de la variable compartida num_clientes
a llegado al mximo, es decir, a las N sillas que conforman la sala de
espera. En tal caso, el cliente habr de abandonar la barbera. Note
cmo el acceso a num_clientes se gestiona mediante mutex.
Si hay alguna silla disponible, entonces el cliente esperar su turno.
En otras palabras, esperar el acceso al silln del barbero. Esta situacin se modela ejecutando wait sobre el semforo silln. Si un cliente
se sienta en el silln, entonces deja una silla disponible.
A continuacin, el cliente sentado en el silln despertar al barbero y esperar a que ste termine su trabajo. Finalmente, el cliente
abandonar la barbera liberando el silln para el siguiente cliente (si
lo hubiera).
[69]
Proceso cliente
Proceso barbero
1. si
1.1
2. si
2.1
2.2
2.3
2.4
2.5
2.6
2.7
1.
2.
3.
4.
5.
no sillas libres,
marcharse
hay sillas libres,
sentarse
esperar silln
levantarse silla
ocupar silln
despertar barbero
esperar fin
levantarse silln
wait(mutex);
if num_clientes == N then
signal(mutex);
return;
num_clientes++;
signal(mutex);
dormir
despertar
cortar
notificar fin
volver a 1
while (1) {
wait(barbero);
cortar();
signal(fin);
}
wait(silln);
wait(mutex);
num_clientes--;
signal(mutex);
signal(barbero);
wait(fin);
signal(silln);
Patrones de sincronizacin
La solucin planteada para el problema del barbero dormiln muestra el uso del patrn sealizacin para sincronizar tanto al cliente con
el barbero como al contrario. Esta generalizacin se puede asociar al
patrn rendezvous [5], debido a que el problema de sincronizacin que
se plantea se denomina con ese trmino. Bsicamente, la idea consiste en sincronizar dos procesos en un determinado punto de ejecucin,
de manera que ninguno puede avanzar hasta que los dos han llegado.
En la figura 2.20 se muestra de manera grfica esta problemtica
en la que se desea garantizar que la sentencia a1 se ejecute antes
que b2 y que b1 ocurra antes que a2. Note cmo, sin embargo, no se
establecen restricciones entre las sentencias a1 y b1, por ejemplo.
Por otra parte, en la solucin tambin se refleja una situacin bastante comn en problemas de sincronizacin: la llegada del valor de
una variable compartida a un determinado lmite o umbral. Este tipo
de situaciones se asocia al patrn scoreboard o marcador [5].
[70]
Proceso A
Proceso B
sentencia a1
sentencia a2
sentencia b1;
sentencia b2;
Figura 2.20: Patrn rendezvous para la sincronizacin de dos procesos en un determinado punto.
2.3.6.
[71]
[72]
2.3.7.
[73]
Si todos los renos estn disponibles (lnea 8 ), entonces
Santa pre
parar el trineo y notificar a todos los renos (lneas 9-12 ). Note cmo
tendr que hacer tantas notificaciones (signal) como renos haya disponibles.
Si hay suficientes duendes para que sean ayudados (lneas
15-20 ), entonces Santa los ayudar, notificando esa ayuda de manera
explcita (signal) mediante el semforo DuendesSem.
[74]
vacaciones();
wait(mutex);
renos += 1;
/* Todos los renos listos? */
if (renos == TOTAL_RENOS)
signal(santa_sem);
signal(mutex);
/* Esperar la notificacin de Santa */
wait(renos_sem);
engancharTrineo();
wait(duendes_mutex);
wait(mutex);
duendes += 1;
/* Est completo el grupo de duendes? */
if (duendes == NUM_DUENDES_GRUPO)
signal(santa_sem);
else
signal(duendes_mutex);
signal(mutex);
wait(duendes_sem);
obteniendoAyuda();
wait(mutex);
duendes -= 1;
/* Nuevo grupo de duendes? */
if (duendes == 0)
signal(duendes_mutex);
signal(mutex);
Captulo
Paso de Mensajes
n este captulo se discute el concepto de paso de mensajes como mecanismo bsico de sincronizacin entre procesos. Al contrario de lo que ocurre con los semforos, el paso de mensajes
ya maneja, de manera implcita, el intercambio de informacin entre
procesos, sin tener que recurrir a otros elementos como por ejemplo el
uso de segmentos de memoria compartida.
El paso de mensajes est orientado a los sistemas distribuidos y
la sincronizacin se alcanza mediante el uso de dos primitivas bsicas: i) el envo y ii) la recepcin de mensajes. As, es posible establecer
esquemas basados en la recepcin bloqueante de mensajes, de manera que un proceso se quede bloqueado hasta que reciba un mensaje
determinado.
Adems de discutir los conceptos fundamentales de este mecanismo de sincronizacin, en este captulo se estudian las primitivas POSIX utilizadas para establecer un sistema de paso de mensajes mediante las denominadas colas de mensajes POSIX (POSIX Message
Queues).
Finalmente, este captulo plantea algunos problemas clsicos de
sincronizacin en los que se utiliza el paso de mensajes para obtener
una solucin que gestione la ejecucin concurrente de los mismos y el
problema de la seccin crtica, estudiado en el captulo 1.
75
[76]
3.1.
Conceptos bsicos
Dentro del mbito de la programacin concurrente, el paso de mensajes representa un mecanismo bsico de comunicacin entre procesos basado, principalmente, en el envo y recepcin de mensajes.
En realidad, el paso de mensajes representa una abstraccin del
concepto de comunicacin que manejan las personas, representado
por mecanismos concretos como por ejemplo el correo electrnico. La
comunicacin entre procesos (InterProcess Communication, IPC) se
basa en esta filosofa. El proceso emisor genera un mensaje como un
bloque de informacin que responde a un determinado formato. El
sistema operativo copia el mensaje del espacio de direcciones del emisor en el espacio de direcciones del proceso receptor, tal y como se
muestra en la figura 3.1.
En el contexto del paso de mensajes, es importante recordar que
un emisor no puede copiar informacin, directamente, en el espacio de
direcciones de otro proceso receptor. En realidad, esta posibilidad est
reservada para el software en modo supervisor del sistema operativo.
En su lugar, el proceso emisor pide que el sistema operativo entregue un mensaje en el espacio de direcciones del proceso receptor utilizando, en algunas situaciones, el identificador nico o PID del proceso
receptor. As, el sistema operativo hace uso de ciertas operaciones de
copia para alcanzar este objetivo: i) recupera el mensaje del espacio de
direcciones del emisor, ii) lo coloca en un buffer del sistema operativo
y iii) copia el mensaje de dicho buffer en el espacio de direcciones del
receptor.
A diferencia de lo que ocurre con los sistemas basados en el uso de
semforos y segmentos de memoria compartida, en el caso de los sistemas en paso de mensajes el sistema operativo facilita los mecanismos
de comunicacin y sincronizacin sin necesidad de utilizar objetos de
memoria compartida. En otras palabras, el paso de mensajes permite
que los procesos se enven informacin y, al mismo tiempo, se sincronicen.
Desde un punto de vista general, los sistemas de paso de mensajes
se basan en el uso de dos primitivas de comunicacin:
Envo (send), que permite el envo de informacin por parte de
un proceso.
Recepcin (receive), que permite la recepcin de informacin por
parte de un proceso.
Tpicamente, y como se discutir en la seccin 3.1.3, la sincronizacin se suele plantear en trminos de llamadas bloqueantes, cuya
principal implicacin es el bloqueo de un proceso hasta que se satisfaga una cierta restriccin (por ejemplo, la recepcin de un mensaje).
[77]
Info a
compartir
Mensaje
Mecanismo de comunicacin
entre procesos (IPC) del
sistema operativo
Espacio de direcciones
de p0
Copia
de info
Espacio de direcciones
de p1
3.1.1.
Teniendo en cuenta la figura 3.1, el lector podra pensar que el envo de un mensaje podra modificar el espacio de direcciones de un
proceso receptor de manera espontnea sin que el propio receptor estuviera al corriente. Esta situacin se puede evitar si no se realiza una
copia en el espacio de direcciones del proceso receptor hasta que ste
no la solicite de manera explcita (mediante una primitiva de recepcin).
Ante esta problemtica, el sistema operativo puede almacenar mensajes de entrada en una cola o buzn de mensajes que acta como
buffer previo a la copia en el espacio de direcciones del receptor. La
figura 3.2 muestra de manera grfica la comunicacin existente entre
dos procesos mediante el uso de un buzn o cola de mensajes.
3.1.2.
[78]
Proceso1
send ()
Message
Message
receive ()
Mensaje
Cola de mensajes
Proceso2
[79]
Sincronizacin
Receive
Send
Bloqueante
No bloq.
Bloqueante
No bloq.
Comprob. llegada
Direccionamiento
Directo
Send
Receive
Indirecto
Esttico
Dinmico
Propiedad
Disciplina de cola
Formato
Contenido
Longitud
FIFO
Prioridad
Fija
Variable
Figura 3.3: Aspectos bsicos de diseo en sistemas de paso de mensajes para la comunicacin y sincronizacin entre procesos.
un proceso que espera un mensaje suele necesitar la informacin contenida en el mismo para continuar con su ejecucin. Un problema que
surge con este esquema est derivado de la prdida de un mensaje,
ya que el proceso receptor se quedara bloqueado. Dicho problema se
puede abordar mediante una naturaleza no bloqueante.
Direccionamiento
Recuperacin por tipo
Tambin es posible plantear
un esquema basado en la recuperacin de mensajes por
tipo, es decir, una recuperacin selectiva atendiendo al
contenido del propio mensaje.
[80]
El otro esquema general es el direccionamiento indirecto, es decir, mediante este esquema los mensajes no se envan directamente
por un emisor a un receptor, sino que son enviados a una estructura
de datos compartida, denominada buzn o cola de mensajes, que almacenan los mensajes de manera temporal. De este modo, el modelo
de comunicacin se basa en que un proceso enva un mensaje a la
cola y otro proceso toma dicho mensaje de la cola.
La principal ventaja del direccionamiento indirecto respecto al directo es la flexibilidad, ya que existe un desacoplamiento entre los
emisores y receptores de mensajes, posibilitando as diversos esquemas de comunicacin y sincronizacin:
uno a uno, para llevar a cabo una comunicacin privada entre
procesos.
muchos a uno, para modelar situaciones cliente/servidor. En este caso, el buzn se suele conocer como puerto.
uno a muchos, para modelar sistemas de broadcast o difusin de
mensajes.
muchos a muchos, para que mltiples clientes puedan acceder a
un servicio concurrente proporcionado por mltiples servidores.
Cabecera
PID destino
PID origen
Longitud del mensaje
Info. de control
Cuerpo
Finalmente, es importante considerar que la asociacin de un proceso con respecto a un buzn puede ser esttica o dinmica. As mismo, el concepto de propiedad puede ser relevante para establecer relaciones entre el ciclo de vida de un proceso y el ciclo de vida de un
buzn asociado al mismo.
Tipo de mensaje
Contenido
Formato de mensaje
El formato de un mensaje est directamente asociado a la funcionalidad proporcionada por la biblioteca de gestin de mensajes y al
mbito de aplicacin, entendiendo mbito como su uso en una nica
mquina o en un sistema distribuido. En algunos sistemas de paso de
mensajes, los mensajes tienen una longitud fija con el objetivo de no
sobrecargar el procesamiento y almacenamiento de informacin. En
otros, los mensajes pueden tener una longitud variable con el objetivo de incrementar la flexibilidad.
En la figura 3.4 se muestra el posible formato de un mensaje tpico, distinguiendo entre la cabecera y el cuerpo del mensaje. Como
se puede apreciar, en dicho mensaje van incluidos los identificadores
nicos de los procesos emisor y receptor, conformando as un direccionamiento directo explcito.
Disciplina de cola
La disciplina de cola se refiere a la implementacin subyacente que
rige el comportamiento interno de la cola. Tpicamente, las colas o buzones de mensajes se implementan haciendo uso de una cola FIFO
[81]
(First-In First-Out), es decir, los mensajes primeros en salir son aquellos que llegaron en primer lugar.
Sin embargo, es posible utilizar una disciplina basada en el uso
de un esquema basado en prioridades, asociando un valor nmerico de importancia a los propios mensajes para establecer polticas de
ordenacin en base a los mismos.
do {
SECCIN_ENTRADA
SECCIN_CRTICA
SECCIN_SALIDA
SECCIN_RESTANTE
} while (1);
3.1.3.
do {
receive (buzn, &msg)
SECCIN_CRTICA
send (buzn, msg)
SECCIN_RESTANTE
while(1);
[82]
3.2.
Implementacin
En esta seccin se discuten las primitivas POSIX ms importantes relativas al paso de mensajes. Para ello, POSIX proporciona las
denominadas colas de mensajes (POSIX Message Queues).
En el siguiente listado de cdigo se muestra la primitiva mq_open(),
utilizada para abrir una cola de mensajes existente o crear una nueva.
Como se puede apreciar, dicha primitiva tiene dos versiones. La ms
completa incluye los permisos y una estructura para definir los atributos, adems de establecer el nombre de la cola de mensajes y los
flags.
Si la primitiva mq_open() abre o crea una cola de mensajes de manera satisfactoria, entonces devuelve el descriptor de cola asociada a
la misma. Si no es as, devuelve 1 y establece errno con el error generado.
Compilacin
Utilice los flags adecuados
en cada caso con el objetivo
de detectar la mayor cantidad de errores en tiempo de
compilacin (e.g. escribir sobre una cola de mensajes de
slo lectura).
3.2. Implementacin
[83]
Al igual que ocurre con los semforos nombrados, las colas de mensajes en POSIX se basan en la definicin de un nombre especfico para
llevar a cabo la operacin de apertura o creacin de una cola.
Listado 3.1: Primitiva mq_open en POSIX.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
[84]
Por otra parte, en la lnea 10 se lleva a cabo la operacin de creacin de la cola de mensajes, identificada por el nombre /prueba y sobre
la que el proceso que la crea podr llevar a cabo operaciones de lectura
(recepcin) y escritura (envo). Note cmo la estructura relativa a los
atributos previamente
comentados se pasa por referencia. Finalmen
te, en las lneas 14-17 se lleva a cabo el control de errores bsicos a la
hora de abrir la cola de mensajes.
Las primitivas relativas al cierre y a la eliminacin de una cola
de mensajes son muy simples y se muestran en el siguiente listado de
cdigo. La primera de ellas necesita el descriptor de la cola, mientras
que la segunda tiene como parmetro nico el nombre asociado a la
misma.
Listado 3.3: Ejemplo de creacin de una cola de mensajes en POSIX.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Mximo no de mensajes */
/* Tama
no mximo de mensaje */
Desde el punto de vista de la programacin concurrente, las primitivas ms relevantes son las de envo y recepcin de mensajes, ya
que permiten no slo sincronizar los procesos sino tambin compartir informacin entre los mismos. En el siguiente listado de cdigo se
muestra la signatura de las primitivas POSIX send y receive.
Listado 3.4: Primitivas de cierre y eliminacin de una cola de mensajes en POSIX.
1
2
3
4
5
6
7
8
9
#include <mqueue.h>
int mq_close (
mqd_t mqdes
);
int mq_unlink (
const char *name
);
/* Nombre de la cola */
Por una parte, la primitiva send permite el envo del mensaje definido por msg_ptr, y cuyo tamao se especifica en msg_len, a la cola
asociada al descriptor especificado por mqdes. Es posible asociar una
prioridad a los mensajes que se envan a una cola mediante el parmetro msg_prio.
3.2. Implementacin
[85]
#include <mqueue.h>
int mq_send (
mqd_t mqdes,
const char *msg_ptr,
size_t msg_len,
unsigned msg_prio
);
ssize_t mq_receive (
mqd_t mqdes,
char *msg_ptr,
size_t msg_len,
unsigned *msg_prio
);
/*
/*
/*
/*
Descriptor de la cola */
Mensaje */
Tama
no del mensaje */
Prioridad */
/*
/*
/*
/*
Descriptor de la cola */
Buffer para el mensaje */
Tama
no del buffer */
Prioridad o NULL */
El siguiente listado de cdigo muestra un ejemplo muy representativo del uso del paso de mensajes en POSIX, ya que refleja la recepcin bloqueante, una de las operaciones ms representativas de este
mecanismo.
En las lneas 6-7 se refleja el uso de la primitiva receive, utilizando
buffer como variable receptora del contenido del mensaje a recibir y
prioridad para obtener el valor de la misma. Recuerde que la semntica
de las operaciones de una cola, como por ejemplo que la recepcin
sea no bloqueante, se definen a la hora de su creacin, tal y como
se mostr en uno de los listados anteriores, mediante la estructura
mq_attr. Sin embargo, es posible modificar y obtener los valores de
configuracin asociados a una cola mediante las primitivas mq_setattr
y mq_getattr, respectivamente.
Listado 3.6: Ejemplo de recepcin bloqueante en una cola de mensajes en POSIX.
1
2
3
4
5
6
7
8
9
10
11
12
mqd_t qHandler;
int rc;
unsigned int prioridad;
char buffer[2048];
rc = mq_receive(qHandler, buffer,
sizeof(buffer), &prioridad);
if (rc == -1)
fprintf(stderr, " %s\n", strerror(errno));
else
printf ("Recibiendo mensaje: %s\n", buffer);
[86]
El buffer de recepcin de mensajes ha de tener un tamao mayor o igual a mq_msgsize, mientras que el buffer de envo de un
mensaje ha de tener un tamao menor o igual a mq_msgsize.
mqd_t qHandler;
int rc;
unsigned int prioridad;
char buffer[512];
sprintf (buffer, "[ Saludos de %d ]", getpid());
mq_send(qHandler, buffer, sizeof(buffer), 1);
Llegados a este punto, el lector se podra preguntar cmo es posible incluir tipos o estructuras de datos especficas de un determinado dominio con el objetivo de intercambiar informacin entre varios
procesos mediante el uso del paso de mensajes. Esta duda se podra
plantear debido a que tanto la primitiva de envo como la primitiva
En POSIX...
En el estndar POSIX, las
primitivas mq_timedsend y
mq_timedreceive
permiten
asociar timeouts para especificar el tiempo mximo
de bloqueo al utilizar las
primitivas send y receive,
respectivamente.
3.2. Implementacin
[87]
Muchos APIs utilizan char * como buffer genrico. Simplemente, considere un puntero a buffer, junto con su tamao, a la
hora de manejar estructuras de datos como contenido de mensaje en las primitivas send y receive.
typedef struct {
int valor;
} TData;
TData buffer;
/* Atributos del buzn */
mqAttr.mq_maxmsg = 10;
mqAttr.mq_msgsize = sizeof(TData);
/* Recepcin */
mq_receive(qHandler, (char *)&buffer,
sizeof(buffer), &prioridad);
/* Envo */
mq_send(qHandler, (const char *)&buffer,
sizeof(buffer), 1);
[88]
3.2.1.
El uso de distintas colas de mensajes, asociado al manejo de distintos tipos de tareas o de distintos tipos de mensajes, por parte de
un proceso que maneje una semntica de recepcin bloqueante puede
dar lugar al denominado problema del bloqueo.
Suponga que un proceso ha sido diseado para atender peticiones
de varios buzones, nombrados como A y B. En este contexto, el proceso
se podra bloquear a la espera de un mensaje en el buzn A. Mientras
el proceso se encuentra bloqueado, otro mensaje podra llegar al buzn
B. Sin embargo, y debido a que el proceso se encuentra bloqueado en
el buzn A, el mensaje almacenado en B no podra ser atendido por
el proceso, al menos hasta que se desbloqueara debido a la nueva
existencia de un mensaje en A.
Esta problemtica es especialmente relevante en las colas de mensajes POSIX, donde no es posible llevar a cabo una recuperacin selectiva por tipo de mensaje2 .
Existen diversas soluciones al problema del bloqueo. Algunas de
ellas son especficas del interfaz de paso de mensajes utilizado, mientras que otras, como la que se discutir a continuacin, son generales
y se podran utilizar con cualquier sistema de paso de mensajes.
Dentro del contexto de las situaciones especficas de un sistema de
paso de mensajes, es posible utilizar un esquema basado en recuperacin selectiva, es decir, un esquema que permite que los procesos
obtengan mensajes de un buzn en base a un determinado criterio.
Por ejemplo, este criterio podra definirse en base al tipo de mensaje,
codificado mediante un valor numrico. Otro criterio ms exclusivo podra ser el propio identificador del proceso, planteando as un esquema
estrechamente relacionado con una comunicacin directa.
2 En
3.2. Implementacin
[89]
[90]
ei
rec
)
ve (
receive ()
Buzn Beeper
rec
ei
ve (
)
Proceso2
Proceso1
Proceso3
receive ()
Buzn Beeper
Proceso1
rece
ive (
)
Proceso2
Proceso3
receive ()
Buzn A
Buzn B
Figura 3.7: Mecanismo de notificacin basado en un buzn o cola tipo beeper para
gestionar el problema del bloqueo.
3.3.
En esta seccin se plantean varios problemas clsicos de sincronizacin, algunos de los cuales ya se han planteado en la seccin 2.3,
y se discuten cmo podran solucionarse utilizando el mecanismo del
paso de mensajes.
3.3.1.
[91]
El buffer limitado
El problema del buffer limitado o problema del productor/consumidor, introducido en la seccin 1.2.1, se puede resolver de una forma
muy sencilla utilizando un nico buzn o cola de mensajes. Recuerde
que en la seccin 2.3.1 se plante una solucin a este problema utilizando dos semforos contadores para controlar, respectivamente, el
nmero de huecos llenos y el nmero de huecos vacos en el buffer.
Sin embargo, la solucin mediante paso de mensajes es ms simple, ya que slo es necesario utilizar un nico buzn para controlar
la problemtica subyacente. Bsicamente, tanto los productores como
los consumidores comparten un buzn cuya capacidad est determinada por el nmero de huecos del buffer.
while (1) {
// Producir...
producir();
// Nuevo item...
send (buzn, msg);
}
while (1) {
recibir (buzn, &msg)
// Consumir...
consumir();
}
Figura 3.8: Solucin al problema del buffer limitado mediante una cola de mensajes.
[92]
3.3.2.
Por otra parte, la solucin planteada tambin hace uso de un buzn, denominado mesa, que tiene una capacidad de cuatro mensajes.
Inicialmente, dicho buzn se rellena con cuatro mensajes que representan el nmero mximo de filsofos que pueden coger palillos cuando se encuentren en el estado hambriento. El objetivo es evitar una
situacin de interbloqueo que se podra producir si, por ejemplo, todos los filsofos cogen a la vez el palillo que tienen a su izquierda.
Al manejar una semntica bloqueante a la hora de recibir mensajes
(intentar adquirir un palillo), la solucin planteada garantiza el acceso
exclusivo a cada uno de los palillos por parte de un nico filsofo. La
accin de dejar un palillo sobre la mesa est representada por la primitiva send, posibilitando as que otro filsofo, probablemente bloqueado
por receive, pueda adquirir el palillo para comer.
Tabaco
Papel
Fsforos
[93]
Tabaco
Papel
Fsforos
Fumador1
Fumador2
Fumador3
Tabaco
Agente
Papel
3.3.3.
El problema de los fumadores de cigarrillos es otro problema clsico de sincronizacin. Bsicamente, es un problema de coordinacin
entre un agente y tres fumadores en el que intervienen tres ingredientes: i) papel, ii) tabaco y iii) fsforos. Por una parte, el agente dispone
de una cantidad ilimitada respecto a los tres ingredientes. Por otra
parte, cada fumador dispone de un nico elemento, tambin en cantidad ilimitada, es decir, un fumador dispone de papel, otro de tabaco y
otro de fsforos.
Cada cierto tiempo, el agente coloca sobre una mesa, de manera
aleatoria, dos de los tres ingredientes necesarios para liarse un cigarrillo (ver figura 3.11). El fumador que tiene el ingrediente restante
coge los otros dos de la mesa, se lia el cigarrillo y se lo notifica al
agente. Este ciclo se repite indefinidamente.
La problemtica principal reside en coordinar adecuadamente al
agente y a los fumadores, considerando las siguientes acciones:
1. El agente pone dos elementos sobre la mesa.
2. El agente notifica al fumador que tiene el tercer elemento que ya
puede liarse un cigarrillo (se simula el acceso del fumador a la
mesa).
[94]
Proceso agente
Proceso fumador
while (1) {
/* Genera ingredientes */
restante = poner_ingredientes();
/* Notifica al fumador */
send(buzones[restante], msg);
/* Espera notificacin fumador */
receive(buzon_agente, &msg);
}
while (1) {
/* Espera ingredientes */
receive(mi_buzon, &msg);
// Liando...
liar_cigarrillo();
/* Notifica al agente */
send(buzon_agente, msg);
}
Agente
Buzn
F_Tab
F_Fsf
Recuperacin no selectiva utilizando mltiples buzones (ver figura 3.14). En este caso, es necesario emplear tres buzones para
que el agente pueda comunicarse con los tres tipos de agentes
y, adicionalmente, otro buzn para que los fumadores puedan
indicarle al agente que ya terminaron de liarse un cigarrillo.
Este ltimo caso de recuperacin no selectiva est estrechamente ligado al planteamiento de las colas de mensajes POSIX, donde no
existe un soporte explcito para llevar a cabo una recuperacin selectiva. En la figura 3.12 se muestra el pseudocdigo de una posible
solucin atendiendo a este segundo esquema.
F_Pap
B_T
B_P
B_F
B_A
[95]
Entero
...
25
26
27
28
...
51
52
53
54
55
56
57
Traduccin
...
...
Cdigo
ASCII
97
98
...
121
122
65
66
...
89
90
46
44
33
63
95
Figura 3.15: Esquema con las correspondencias de traduccin. Por ejemplo, el carcter codificado con el valor 1 se corresponde con el cdigo ASCII 97, que representa el
carcter a.
3.3.4.
[96]
Cadena de
entrada
34
15
12
55
Cadena
descodificada
72
111
108
97
33
Representacin
textual
'H'
'o'
'l'
'a'
'!'
[97]
Puntuacin
1
Cliente
Buzn
Traduccin
Buzn
Cliente
Buzn
Puntuacin
Buzn
Notificacin
2
Traduccin
Figura 3.17: Esquema grfico con la solucin planteada, utilizando un sistema de paso
de mensajes, para sincronizar y comunicar a los procesos involucrados en la simulacin
planteada. Las lneas punteadas indican pasos opcionales (traduccin de smbolos de
puntuacin).
[98]
Proceso cliente
Proceso traduccin
obtener_datos(&datos);
crear_buzones();
lanzar_procesos();
while (1) {
// Espera tarea.
receive(buzn_traduccin, &orden);
// Traduccin...
if (es_smbolo(orden.datos)) {
send(buzn_puntuacin, orden);
receive(buzn_notificacin, &msg);
}
else
traducir(&orden, &msg);
// Notifica traduccin.
send(buzon_cliente, msg);
}
calcular_num_subv(datos, &num_sub);
for i in range (0, num_subv) {
// Generar orden.
generar_orden(&orden, datos);
// Solicita traduccin.
send(buzn_traduccin, orden);
}
for i in range (0, num_subv) {
// Espera traduccin.
receive(buzon_cliente, &msg);
actualizar(datos, msg);
}
mostrar_resultado (datos);
Proceso puntuacin
while (1) {
receive(buzn_puntuacin, &orden);
traducir(&orden, &msg);
send(buzon_notificacin, msg);
}
Captulo
Otros Mecanismos de
Sincronizacin
n este captulo se discuten otros mecanismos de sincronizacin, principalmente de un mayor nivel de abstraccin, que
se pueden utilizar en el mbito de la programacin concurrente. Algunos de estos mecanismos mantienen la misma esencia que los
ya estudiados anteriormente, como por ejemplo los semforos, pero
aaden cierta funcionalidad que permite que el desarrollador se abstraiga de determinados aspectos problemticos.
En concreto, en este captulo se estudian soluciones vinculadas al
uso de lenguajes de programacin que proporcionan un soporte nativo
de concurrencia, como Ada, y el concepto de monitor como ejemplo
representativo de esquema de sincronizacin de ms alto nivel.
Por una parte, el lector podr asimilar cmo Ada95 proporciona
herramientas y elementos especficos que consideran la ejecucin concurrente y la sincronizacin como aspectos fundamentales, como es el
caso de las tareas o los objetos protegidos. Los ejemplos planteados,
como es el caso del problema de los filsofos comensales, permitirn
apreciar las diferencias con respecto al uso de semforos POSIX.
Por otra parte, el concepto de monitor permitir comprender la
importancia de soluciones de ms alto nivel que solventen algunas
de las limitaciones que tienen mecanismos como el uso de semforos
y el paso de mensajes. En este contexto, se estudiar el problema
del buffer limitado y cmo el uso de monitores permite plantear una
solucin ms estructurada y natural.
99
[100]
4.1.
Motivacin
[101]
Aunque los semforos y las colas de mensajes pueden presentar ciertas limitaciones que justifiquen el uso de mecanismos
de un mayor nivel de abstraccin, los primeros pueden ser ms
adecuados que estos ltimos dependiendo del problema a solucionar. Considere las ventajas y desventajas de cada herramienta antes de utilizarla.
4.2.
Concurrencia en Ada 95
Ada es un lenguaje de programacin orientado a objetos, fuertemente tipado, multi-propsito y con un gran soporte para la programacin concurrente. Ada fue diseado e implementado por el Departamento de Defensa de los Estados Unidos. Su nombre se debe a Ada
Lovelace, considerada como la primera programadora en la historia de
la Informtica.
Uno de los principales pilares de Ada reside en una filosofa basada
en la reduccin de errores por parte de los programadores tanto como
sea posible. Dicha filosofa se traslada directamente a las construcciones del propio lenguaje. Esta filosofa tiene como objetivo principal la
creacin de programas altamente legibles que conduzcan a una maximizacin de su mantenibilidad.
Ada se utiliza particularmente en aplicaciones vinculadas con la
seguridad y la fiabilidad, como por ejemplos las que se enumeran a
continuacin:
1 Ada
[102]
[103]
4.2.1.
En Ada 95, una tarea representa la unidad bsica de programacin concurrente [3]. Desde un punto de vista conceptual, una tarea
es similar a un proceso. El siguiente listado de cdigo muestra la definicin de una tarea sencilla.
Listado 4.2: Definicin de tarea simple en Ada 95.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- Especificacin de la tarea T.
task type T;
-- Cuerpo de la tarea T.
task body T is
-- Declaraciones locales al cuerpo.
-- Orientado a objetos (Duration).
periodo:Duration := Duration(10);
-- Identificador de tarea.
id_tarea:Task_Id := Null_Task_Id;
begin
-- Current Task es similar a getpid().
id_tarea := Current_Task;
-- Bucle infinito.
loop
-- Retraso de periodo segundos.
delay periodo;
-- Impresin por salida estndar.
Put("Identificador de tarea: ");
Put_Line(Image(ID_T));
end loop;
end T;
[104]
[105]
[106]
task Consumidor;
task body Consumidor is
Dato : Integer;
begin
loop
-- Lectura en Dato.
Buffer.Leer(Dato);
Put_Line("[Consumidor] Dato obtenido...");
Put_Line(Datoimg);
delay 1.0;
end loop;
end Consumer;
[107]
[108]
4.2.2.
La declaracin de V en la lnea 13 declara una instancia de dicho
tipo protegido y le asigna el valor inicial al dato que ste encapsula.
Este valor slo puede ser accedido mediante Leer, Escribir e Incrementar. En este contexto, es importante destacar las diferencias existentes
al usar procedimientos o funciones protegidas:
[109]
Un procedimiento protegido, como Escribir o Incrementar, proporciona un acceso mutuamente excluyente tanto de lectura como de escritura a la estructura de datos encapsulada por el tipo
protegido. En otras palabras, las llamadas a dichos procedimientos se ejecutarn en exclusin mutua.
Una funcin protegida, como Leer, proporciona acceso concurrente de slo lectura a la estructura de datos encapsulada. En
otras palabras, es posible que mltiples tareas ejecuten Leer de
manera concurrente. No obstante, las llamadas a funciones protegidas son mutuamente excluyentes con las llamadas a procedimientos protegidos.
Finalmente, una entrada protegida es similar a un procedimiento
protegido. La diferencia reside en que la entrada est asociada a una
barrera integrada en el propio objeto protegido. Como se coment anteriormente, si la evaluacin de dicha barrera obtiene un valor lgico
falso, entonces la tarea que invoca dicha entrada se quedar suspendida hasta que la barrera se evale a verdadero y, adicionalmente, no
existan otras tareas activas dentro del objeto protegido.
A continuacin se discutir un problema de sincronizacin clsico, ya discutido en la seccin 2.3.3, implementado haciendo uso de
objetos protegidos.
Modelando el problema de los filsofos comensales
En esta seccin se discute una posible implementacin del problema de los filsofos comensales utilizando Ada 95 y objetos protegidos2 .
En la seccin 2.3.3, los palillos se definan como recursos indivisibles y se modelaban mediante semforos binarios. De este modo, un
2 Cdigo fuente adaptado a partir de la implementacin de
http://es.wikibooks.org/wiki/Programaci %C3 %B3n_en_Ada/Tareas/Ejemplos
[110]
[111]
El aspecto ms destacable de esta implementacin es el uso de sendas barreras en los entries Coger y Soltar para controlar si un cubierto
est libre u ocupado antes de cogerlo o soltarlo, respectivamente.
La vida de un filsofo
Recuerde que, en el problema clsico de los filsofos comensales, stos viven un ciclo continuo compuesta de
las actividades de comer y
pensar.
[112]
7
8 task body TFilosofo is
9
10
procedure Comer is
11
begin
12
-- Intenta obtener el cubierto izquierdo y el derecho.
13
Coger(Cubierto_Izquierda.all);
14
Coger(Cubierto_Derecha.all);
15
Put(Id & "c "); delay 1.0;
16
Soltar(Cubierto_Derecha.all);
17
Soltar(Cubierto_Izquierda.all);
18
end Comer;
19
20
Procedure Pensar is
21
begin
22
Put(Id & "p ");
23
delay 3.0;
24
end Pensar;
25
26 begin
27
loop
28
Comer; Pensar;
29
end loop;
30 end TFilosofo;
[113]
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Si desea llevar a cabo una simulacin para comprobar que, efectivamente, el acceso concurrente a los palillos es correcto, recuerde que
ha de ejecutar los siguientes comandos:
$ gnatmake problema_filosofos.adb
$ ./problema_filosofos
Si la compilacin y la ejecucin fueron correctas, entonces el resultado obtenido de la simulacin debera ser similar al siguiente.
Introduzca el numero de cubiertos/filosofos: 5
Ac
Ec
Ep
Dp
Cp
Bc
Dc
Ac
Cc
Cp
Dc
Ac
Ap
Ep
Dp
Cp
Ap
Bc
Bp
Cc
Bc
Bp
Cc
Ap
Ec
Ep
Dp
Ap
Ec
Dc
Ac
Bc
Cp
Dc
Ac
Cp
Bp
Dp
Cp
Ec
Bc
Bp
Cc
Bc
Ep
Ac
Ap
Bp
Ep
Dp
Ap
Ec
Dc
Cc
Bc
Ep
Dc
Ac
Cp
Bp
Dp
Ap
Ec
Dc
Bp
Cc
Bc
Ep
Cc
Bc
Bp
Dp
Dp
Ap
Ec
Dc
Ac
Cp
Dc
Cc
Ac
Ec
Bp
Dp
Cp
Ec
Ep
Ac
Cc
Cp
Ep
Ac
Ap
Bp
Dp
Cp
Ap
Bc
Dc
Cc
Ec
Ep
Cc
Ap
Ec
Dc
Dp
Cp
Bc
Dc
Ac
Bc
Cp
Ep
Ac
Ap
Ep
Dp
Cp
Ec
Bc
Bp
Cc
Ec
Bp
Ac
Ap
Ep
Ep
Dp
Ap
Bc
Dc
Cc
Bc
Bp
Dc
Ac
Cp
Ep
Dp
Ap
Ec
Dc
Bp
Cc
Bc
Bp
Cc
Cp
Bp
Dp
Ap
Ec
Dc
Ac
Bc
Ep
Ac
Cp
Bp
Dp
Ap
Ec
Dc
Cc
Ec
Ep
Cc
Ec
Ep
Dp
Ap
Bc
Dc
Ac
Cp
Bp
Cc
4.3.
El concepto de monitor
[114]
En este contexto, el monitor permite modelar el buffer de una manera ms natural mediante un mdulo independiente de manera que
las llamadas concurrentes para insertar o extraer elementos del buffer se ejecutan en exclusin mutua. En otras palabras, el monitor
es, por definicin, el responsable de ejecutarlas secuencialmente de
manera correcta. La figura 4.1 muestra la sintaxis de un monitor. En
la siguiente seccin se discutir cmo se puede utilizar un monitor
haciendo uso de las facilidades que proporciona el estndar POSIX.
Sin embargo, y aunque el monitor proporciona exclusin mutua
en sus operaciones, existe la necesidad de la sincronizacin dentro
del mismo. Una posible opcin para abordar esta problemtica podra
consistir en hacer uso de semforos. No obstante, las implementaciones suelen incluir primitivas que sean ms sencillas an que los propios semforos y que se suelen denominar variables de condicin.
Las variables de condicin, sin embargo, se manejan con primitivas muy parecidas a las de los semforos, por lo que normalmente
se denominan wait y signal. As, cuando un proceso o hilo invoca
wait sobre una variable de condicin, entonces se quedar bloqueado
hasta que otro proceso o hilo invoque signal. Note que wait siempre
bloquear al proceso o hilo que lo invoque, lo cual supone una diferencia importante con respecto a su uso en semforos.
[115]
monitor nombre_monitor {
/* Declaracin de variables compartidas */
procedure p1 (...) { ... }
procedure p2 (...) { ... }
...
procedure pn (...) { ... }
inicializacin (...) {
...
}
}
Figura 4.1: Sintaxis general de un monitor.
La construccin del monitor garantiza que, en cualquier momento de tiempo, slo haya un proceso activo en el monitor.
Sin embargo, puede haber ms procesos o hilos suspendidos
en el monitor.
4.3.1.
Monitores en POSIX
Para modelar el comportamiento de un monitor utilizando las primitivas que ofrece el estndar POSIX y, en concreto, la biblioteca Pthreads, es necesario utilizar algn mecanismo para garantizar la ex-
[116]
clusin mutua en las operaciones del monitor y proporcionar la sincronizacin necesario. En este contexto, POSIX Pthreads ofrece tanto
mutexes como variables de condicin para llevar a cabo dicho modelado.
Con el objetivo de ejemplificar el uso de monitores mediante estos
dos elementos definidos en POSIX, en esta seccin se retoma el ejemplo del buffer limitado previamente discutido en la seccin 2.3.1, en la
que se hizo uso de semforos para controlar el acceso al buffer. Para
ello, se asumirn las siguientes suposiciones:
1. El buffer estar implementado en base a la definicin de monitor.
2. Los productores y consumidores de informacin, es decir, las
entidades que interactan con el buffer, estarn implementados
mediante hilos a travs de la biblioteca Pthread.
Con el objetivo de encapsular el estado del buffer, siguiendo la filosofa planteada por el concepto de monitor, se ha definido la clase Buffer mediante el lenguaje de programacin C++, tal y como se muestra
en el siguiente listado de cdigo.
Listado 4.12: Clase Buffer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef __BUFFER_H__
#define __BUFFER_H__
#include <pthread.h>
#define MAX_TAMANYO 5
class Buffer {
public:
Buffer ();
~Buffer ();
void anyadir (int dato);
int extraer ();
private:
int _n_datos;
int _primero, _ultimo;
int _datos[MAX_TAMANYO];
pthread_mutex_t _mutex;
pthread_cond_t _buffer_no_lleno;
pthread_cond_t _buffer_no_vacio;
};
#endif
La funcionalidad
relevante de la clase Buffer est especificada en
las lneas 12-13 , en las que se declaran las funciones anyadir y extraer.
Respecto al estado del propio buffer, cabe destacar dos grupos de
elementos:
Las variables de clase relacionadas con el nmero de elementos
en el buffer y los apuntadores al primer y ltimo elemento (lneas
[117]
16-18 ), respectivamente. La figura 4.2 muestra de manera grfica
dichos apuntadores.
Las variables de clase que realmente modelan el comportamiento
del buffer, es decir, el mutex de tipo pthread_mutex_t para garantizar el acceso excluyente sobre el buffer y las variables de
condicin para llevar a cabo la sincronizacin.
Estas variables de condicin permiten modelar, a su vez, dos situaciones esenciales a la hora de interactuar con el buffer:
1. La imposibilidad de aadir elementos cuando el buffer est lleno.
Esta situacin se contempla mediante la variable _buffer_no_lleno
del tipo pthread_cond.
2. La imposibilidad de extraer elementos cuando el buffer est vaco.
Esta situacin se contempla mediante la variable _buffer_no_vaco
del tipo pthread_cond.
El constructor y el destructor de la clase Buffer permiten inicializar el estado de la misma, incluyendo los elementos propios de la
biblioteca Pthread, y liberar recursos, respectivamente, tal y como se
muestra en el siguiente listado de cdigo.
La funcin anyadir permite insertar un nuevo elemento en el buffer de tamao limitado. Con el objetivo de garantizar que dicha funcin se ejecute en exclusin
en la
mutua, el mutex se adquiere justo
primera instruccin (lnea 6 ) y se libera en la ltima (lnea 20 ), como
se aprecia en el siguiente listado.
Listado 4.13: Clase Buffer. Constructor y destructor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "buffer.h"
#include <assert.h>
Buffer::Buffer ():
_n_datos(0), _primero(0), _ultimo(0)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_buffer_no_lleno, NULL);
pthread_cond_init(&_buffer_no_vacio, NULL);
}
Buffer::~Buffer ()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_buffer_no_lleno);
pthread_cond_destroy(&_buffer_no_vacio);
}
A continuacin, y antes de insertar cualquier elemento en el buffer, es necesario comprobar que ste no est lleno. Para ello, se usa la
condicin del bucle while (lnea 8 ). Si es as, es decir, si el buffer ha
alcanzado su lmite, entonces se ejecuta un wait sobre la variable de
condicin _buffer_no_lleno, provocando las dos siguientes consecuencias:
[118]
primero
ltimo
Figura 4.2: Esquema grfico del buffer limitado modelado mediante un monitor.
void
Buffer::anyadir
(int dato)
{
/* Adquisicin el mutex para exclusin mutua */
pthread_mutex_lock(&_mutex);
/* Para controlar que no haya desbordamiento */
while (_n_datos == MAX_TAMANYO)
/* Sincronizacin */
pthread_cond_wait(&_buffer_no_lleno, &_mutex);
/* Actualizacin del estado del buffer */
_datos[_ultimo % MAX_TAMANYO] = dato;
++_ultimo;
++_n_datos;
/* Sincronizacin */
pthread_cond_signal(&_buffer_no_vacio);
pthread_mutex_unlock(&_mutex);
}
La actualizacin del estado del buffer mediante la funcin anyadir consiste en almacenar el dato en el buffer (lnea 13 ), actualizar el
apuntador para el siguiente elemento a insertar
(lnea 14 ) e incrementar el nmero de elementos del buffer (lnea 15 ).
[119]
int
Buffer::extraer ()
{
int dato;
/* Adquisicin el mutex para exclusin mutua */
pthread_mutex_lock(&_mutex);
/* No se puede extraer si no hay nada */
while (_n_datos == 0)
/* Sincronizacin */
pthread_cond_wait(&_buffer_no_vacio, &_mutex);
/* Actualizacin del estado del buffer */
dato = _datos[_primero % MAX_TAMANYO];
++_primero;
--_n_datos;
/* Sincronizacin */
pthread_cond_signal(&_buffer_no_lleno);
pthread_mutex_unlock(&_mutex);
return dato;
}
[120]
interacten con el monitor (buffer). El siguiente listado muestra la declaracin de las funciones asociadas a dichos hilos y la creacin de los
mismos.
Note cmo la primitiva pthread_create se utiliza para asociar cdigo a los hilos productores
y consumidor, mediante
las funciones pro
ductor_func (lnea 21 ) y consumidor_func (lnea 23 ), respectivamente.
El propio puntero al buffer se pasa como parmetro en ambos casos
para que los hilos puedan interactuar con l. Cabe destacar igualmente el uso de la primitiva pthread_join (lnea 27 ) con el objetivo de
esperar a los hilos creados anteriormente.
Listado 4.16: Creacin de hilos mediante Pthread.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include
#include
#include
#include
#include
<iostream>
<stdlib.h>
<sys/types.h>
<unistd.h>
"buffer.h"
#define NUM_HILOS 8
static void *productor_func (void *arg);
static void *consumidor_func (void *arg);
int main (int argc, char *argv[]) {
Buffer *buffer = new Buffer;
pthread_t tids[NUM_HILOS];
int i;
srand((int)getpid());
/* Creacin de hilos */
for (i = 0; i < NUM_HILOS; i++)
if ((i % 2) == 0)
pthread_create(&tids[i], NULL, productor_func, buffer);
else
pthread_create(&tids[i], NULL, consumidor_func, buffer);
/* Esperando a los hilos */
for (i = 0; i < NUM_HILOS; i++)
pthread_join(tids[i], NULL);
delete buffer;
return EXIT_SUCCESS;
}
[121]
4.4.
Consideraciones finales
Como ya se introdujo al principio del presente captulo, concretamente en la seccin 4.1, algunas de las limitaciones asociadas a los
mecanismos de sincronizacin de ms bajo nivel han propiciado la
aparicin de otros esquemas de ms alto nivel que facilitan la implementacin de soluciones concurrentes.
En estos esquemas se encuadran lenguajes de programacin como
Ada, pero tambin existen bibliotecas de ms alto nivel que incluyen
tipos abstractos de datos orientados a la programacin concurrente.
Con el objetivo de discutir, desde el punto de vista prctico, otra solucin distinta a la biblioteca Pthreads, el anexo C describe la biblioteca
de hilos de ZeroC ICE, un middleware de comunicaciones orientado
a objetos para el desarrollo de aplicaciones cliente/servidor, y los mecanismos de sincronizacin que sta proporciona. En este contexto,
el lector puede apreciar, desde un punto de vista prctico, cmo las
soluciones estudiadas a nivel de proceso en otros captulos se pueden
reutilizar a la hora de interactuar con hilos haciendo uso del lenguaje
de programacin C++.
Finalmente, algunos de los problemas clsicos de sincronizacin ya
estudiados, como por ejemplo los filsofos comensales o el problema
del productor/consumidor, se resuelven haciendo uso de la biblioteca
de hilos de ICE. Como se podr apreciar en dicho anexo, las soluciones
de ms alto nivel suelen mejorar la legibilidad y la mantenibilidad del
cdigo fuente.
Captulo
Planificacin en Sistemas
de Tiempo Real
123
[124]
5.1.
Introduccin
5.2.
[125]
Representacin de requisitos temporales. Por ejemplo, considere la necesidad de establecer un tiempo mximo de respuesta
para un sistema empotrado.
Satisfaccin de requisitos temporales, vinculada tradicionalmente a la planificacin de procesos. Tpicamente, la satisfaccin
de dicho requisitos considerar el comportamiento del sistema en
el peor caso posible con el objetivo de calcular una cota superior.
5.2.1.
Mecanismos de representacin
Relojes
Dentro de un contexto temporal, una de los aspectos esenciales
est asociado al concepto de reloj. Desde el punto de vista de la programacin, un programa tendr la necesidad de obtener informacin
bsica vinculada con el tiempo, como por ejemplo la hora o el tiempo
que ha transcurrido desde la ocurrencia de un evento. Dicha funcionalidad se puede conseguir mediante dos caminos distintos:
Accediendo al marco temporal del entorno.
Utilizando un reloj hardware externo que sirva como referencia.
La primera opcin no es muy comn debido a que el propio entorno
necesitara, por ejemplo, llevar a cabo una sincronizacin con el reloj a
travs de interrupciones. La segunda opcin es ms plausible desde la
perspectiva del programador, ya que la informacin temporal se puede
obtener mediante las primitivas proporcionadas por un lenguaje de
programacin o incluso a travs del controlador de un reloj interno o
externo.
Esta segunda opcin puede parecer muy sencilla y prctica, pero puede no ser suficiente en determinados contextos. Por ejemplo,
[126]
la programacin de sistemas distribuidos puede plantear ciertas complicaciones a la hora de manejar una nica referencia temporal por
varios servicios. Por ejemplo, considere que los servicios que conforman el sistema distribuido se ejecutan en tres mquinas diferentes. En
esta situacin, la referencia temporal de dichos servicios ser diferente (aunque probablemente mnima). Para abordar esta problemtica,
se suele definir un servidor de tiempo que proporcione una referencia
temporal comn al resto de servicios. En esencia, el esquema se basa
en la misma solucin que garantiza un reloj interno o externo, es decir,
se basa en garantizar que la informacin temporal est sincronizada.
Retomando el reloj como referencia temporal comn, es importante resaltar diversos aspectos asociados al mismo, como la resolucin o
precisin del mismo (por ejemplo 1 segundo o 1 microsegundo), el rango de valores que es capaz de representar y la exactitud o estabilidad
que tiene.
Desde el punto de vista de la programacin, cada lenguaje suele
proporcionar su propia abstraccin de reloj. Por ejemplo, Ada proporciona dos paquetes vinculados a dicha abstraccin:
Calendar, paquete de biblioteca obligatorio que proporciona estructuras de datos y funciones bsicas relativas al tratamiento
del tiempo.
Real_Time, paquete de biblioteca opcional que proporciona un
nivel de granularidad mayor, entre otras aspectos, que Calendar.
El paquete Calendar en Ada implemente el tipo abstracto de datos
Time, el cual representa un reloj para leer el tiempo. As mismo, dicho
tipo mantiene una serie de funciones para efectuar conversiones entre
elementos de tipo Time y otras unidades de tiempo asociadas a tipos
del lenguaje, como el tipo entero usado para representar aos, meses
o das, o el tipo Duration usado para representar segundos. Por otra
parte, Time tambin proporciona operadores aritmticos que sirven
para combinar parmetros de tipo Duration y Time y para comparar
valores de tipo Time.
El tipo abstracto de datos Time es un ejemplo representativo de
herramienta, proporcionada en este caso por el lenguaje de programacin Ada, para facilitar el tratamiento de informacin temporal.
A continuacin se muestra un listado de cdigo que permite medir
el tiempo empleado en llevar a cabo una serie de cmputos. Como se
puede
apreciar, Clock permite obtener referencias temporales (lneas
5 y 8 ) y es posible utilizar el operador de resta para obtener un valor
de tipo Duration al restar dos elementos del tipo Time (lnea 10 ).
[127]
[128]
Este primer planteamiento sera incorrecto, debido a que los 7 segundos de espera se realizan despus de la finalizacin de Accion_1,
sin considerar el instante de comienzo de la ejecucin de sta.
El siguiente listado contempla esta problemtica.
Como se puede
apreciar, la variable Transcurrido de la lnea 3 almacena el tiempo
empleado en ejecutar Accion_1. De este modo,
es posible realizar un
retraso de 7,0 T ranscurrido segundos (lnea 5 ) antes de ejecutar Accion_2.
Desafortunadamente, este planteamiento puede no ser correcto ya
que la instruccin de la lnea 5 no es atmica, es decir, no se ejecuta
en un instante de tiempo indivisible. Por ejemplo, considere la situacin en la que Accion_1 consume 2 segundos. En este caso en concreto, 7,0 T ranscurrido debera equivaler a 5,0. Sin embargo, si justo
despus de realizar este clculo se produce un cambio de contexto, el
retraso se incrementara ms all de los 5,0 segundos calculados.
[129]
Inicio := Clock;
Accion_1; -- Ejecucin de Accin 1.
Transcurrido := Clock - Inicio;
delay 7.0 - (Transcurrido);
Accion_2; -- Ejecucin de Accin 2.
Algunos lenguajes de programacin introducen sentencias especficas que permiten gestionar este tipo de retardos absolutos. En el caso
particular de Ada, esta sentencia est representada por la construccin delay until <Time>:
Note cmo delay until establece un lmite inferior, el propio retraso
absoluto, antes de que Accion_2 se ejecute. No obstante, esta solucin
no contempla la posibilidad de que Accion_1 tarde ms de 7 segundos
en ejecutarse.
Listado 5.6: Uso de delay until en Ada.
1
2
3
4
5
6
Inicio := Clock;
Accion_1; -- Ejecucin de Accin 1.
delay until (Inicio + 7.0);
Accion_2; -- Ejecucin de Accin 2.
Llamada1
update()
4.4
Llamada2
update()
3.6
Llamada3
update()
Figura 5.3: Ejemplo de control de la deriva acumulada. Las llamadas a update se realizan cada 4 segundos de media.
[130]
Finalmente, resulta interesante destacar algunos factores relevantes que afectan a los retardos y que hay que considerar:
Lectura
1
Lectura
Lectura
1
Lectura
...
5.2.2.
En el mbito de la programacin de sistemas de tiempo real, el control de requisitos temporales es esencial para garantizar un adecuado
funcionamiento del sistema. Una de las restricciones ms simples que
puede tener un sistema empotrado es identificar y actuar en consecuencia cuando un determinado evento no tiene lugar.
Por ejemplo, considere que un sensor de temperatura ha de realizar
una comprobacin de la misma cada segundo. Sin embargo, y debido
a cualquier tipo de problema, esta comprobacin puede fallar y no
realizarse en un determinado intervalo de tiempo. En este contexto, el
sistema podra tener definido un tiempo de espera lmite o timeout
de manera que, si ste expira, el sistema pueda emitir una situacin
de fallo.
Otro posible ejemplo consistira en garantizar que un determinado
fragmento de cdigo no tarde en ejecutarse ms de un tiempo predefinido. De no ser as, el programa debera ser capaz de lanzar un
mecanismo de recuperacin de errores o una excepcin.
Timeout
El propio reloj interno puede verse afectado si se implementa mediante un mecanismo basado en interrupciones que puede ser
inhibido durante algn instante de tiempo.
1
Lectura
Notificacin
de fallo
Figura 5.4: Uso de un timeout para identificar un fallo de funcionamiento en un
sistema empotrado.
[131]
Hasta ahora, y dentro del contexto de la programacin concurrente, no se ha controlado el tiempo empleado por un proceso o hilo en
acceder, de manera exclusiva, a la seccin crtica. En otras palabras,
aunque un proceso obtenga el acceso exclusivo a una determinada
seccin de cdigo, es deseable controlar el tiempo mximo que puede
invertir en dicha seccin. En esencia, esta gestin est asociada a un
control de los requisitos temporales.
POSIX considera en algunas de sus primitivas un tiempo lmite para acceder a la seccin crtica. Por ejemplo, la primitiva de POSIX pthread_cond_timedwait devuelve un error si el tiempo absoluto indicado
en uno de sus argumentos se alcanza antes de que un hilo haya realizado un signal o broadcast sobre la variable de condicin asociada
(vea seccin 4.3.1).
Listado 5.8: Primitiva pthread_cond_timedwait.
1 #include <pthread.h>
2
3 int pthread_cond_timedwait(pthread_cond_t *restrict cond,
4
pthread_mutex_t *restrict mutex,
5
const struct timespec *restrict abstime);
6
7 int pthread_cond_wait(pthread_cond_t *restrict cond,
8
pthread_mutex_t *restrict mutex);
Desafortunadamente, este tipo de bloqueo no es fcilmente limitable ya que mantiene una fuerte dependencia con la aplicacin en
cuestin. Por ejemplo, en el problema del buffer limitado se puede dar
la situacin de que los productores se bloqueen a la espera de que
los consumidores obtengan algn elemento del buffer. Sin embargo,
puede ser que stos no estn preparadas hasta ms adelante para
obtener informacin. En este contexto, el productor debera limitar el
tiempo de espera asociado, por ejemplo, al decremento del valor de un
semforo.
El siguiente listado de cdigo muestra cmo hacer uso de la primitiva sem_timedwait de POSIX para tratar esta problemtica.
[132]
Este tipo de esquemas permite captura cdigo descontrolado, acotando superiormente el tiempo de ejecucin que puede consumir.
5.3.
Esquemas de planificacin
Como ya se introdujo anteriormente en la seccin 5.1, en un sistema concurrente no existe, generalmente, una necesidad real de especificar el orden exacto en el que se ejecutan los distintos procesos que
participan en el mismo.
[133]
Hasta ahora, las restricciones de orden vinculadas a las necesidades de sincronizacin de los problemas discutidos se han modelado a
travs del uso de semforos, monitores o incluso colas de mensajes.
Por ejemplo, si recuerda el problema de la barbera discutido en la
seccin 2.3.5, el cliente tena que despertar al barbero antes de que
ste efectuara la accin de cortar. Esta interaccin se llevaba a cabo
mediante un signal sobre el semforo correspondiente.
No obstante, el comportamiento global de la simulacin de la barbera no era determinista. Por ejemplo, ante distintas ejecuciones con
el mismo nmero inicial de clientes, el resultado sera distinto debido
a que el orden de ejecucin de las distintas acciones que conforman
cada proceso variara de una simulacin con respecto a otra.
Desde una perspectiva general, un sistema concurrente formado
por n procesos tendra n! formas distintas de ejecutarse en un procesador, suponiendo un esquema no apropiativo. En esencia, y aunque el
resultado final sea el mismo, el comportamiento temporal de los distintos procesos vara considerablemente [3]. As, si uno de los procesos
tiene impuesto un requisito temporal, entonces solamente lo cumplir
en aquellos casos en los que se ejecute al principio.
Un sistema de tiempo real necesita controlar este no determinismo
a travs de algn mecanismo que garantice que los distintos procesos ejecutan sus tareas en el plazo y con las restricciones previamente
definidas. Este mecanismo se conoce tradicionalmente como planificacin o scheduling.
La planificacin esttica es la que se basa en realizar una prediccin antes de la ejecucin del sistema, mientras que la dinmica se basa en tomar decisiones en tiempo de ejecucin.
[134]
5.3.1.
En esta seccin se describen los fundamentos de un modelo simple de tareas [3] que sirva como base para discutir los aspectos ms
importante de un sistema de planificacin. Sobre este modelo se irn
aadiendo distintas caractersticas que tendrn como resultado variaciones en el esquema de planificacin inicial.
Las principales caractersticas de este modelo inicial son las siguientes:
El nmero de procesos involucrados en la planificacin es fijo.
Todos los procesos son peridicos y su periodo (T) se conoce a
priori.
[135]
La independencia de los procesos en el modelo simple posibilita que todos se ejecuten en paralelo, llevando al sistema a su
carga mximo y generando un instante crtico.
5.3.2.
El ejecutivo cclico
El primer esquema de planificacin a discutir se denomina ejecutivo cclico debido al mecanismo utilizado para ejecutar las distintas
tareas involucradas en la planificacin. En esencia, el objetivo a alcanzar consiste en definir un modelo de planificacin para un conjunto de
tareas que se pueda repetir a lo largo del tiempo, de manera que todas
ellas se ejecuten con una tasa apropiada.
Cuando se hace uso de este esquema no existe nocin alguna de
concurrencia, ya que las tareas son procedimientos que se planifican
para que cumplan sus respectivos deadlines. Tpicamente, el ejecutivo
cclico se modela como una tabla de llamadas a procedimientos.
Esta tabla representa el concepto de hiperperiodo o ciclo principal, dividido a su vez en una serie de ciclos secundarios de duracin
fija. El clculo del ciclo principal (Tm ) se realiza obteniendo el mnimo
comn mltiplo de los periodos de todos los procesos, es decir,
Tm = m.c.m(Ti , i {1, 2, . . . , n})
(5.1)
[136]
Tm = 200 ms
Tc = 50 ms
a
50
d e
100
a
150
Figura 5.5: Esquema grfico de planificacin segn el enfoque de ejecutivo cclico para
el conjunto de procesos de la tabla 5.1.
Proceso
a
b
c
d
e
Periodo (T)
50
50
100
100
200
Tabla 5.1: Datos de un conjunto de procesos para llevar a cabo la planificacin en base
a un ejecutivo cclico.
d
200
[137]
do {
interrupcin; llamada_a; llamada_b; llamada_c;
interrupcin; llamada_a; llamada_b; llamada_d; llamada_e;
interrupcin; llamada_a; llamada_b; llamada_c;
interrupcin; llamada_a; llamada_b; llamada_d;
while (1);
Figura 5.6: El cdigo asociado al ejecutivo cclico consiste simplemente en un bucle con
llamadas a procedimientos en un determinado orden.
[138]
5.3.3.
La planificacin mediante ejecutivo cclico no contempla la ejecucin de procesos o hilos durante la ejecucin, es decir, no hay una
nocin de concurrencia. Un planteamiento totalmente diferente consiste en soportar la ejecucin de procesos, al igual que ocurre en los
sistemas operativos modernos. En este contexto, la respuesta a responder es Cul es el proceso a ejecutar en cada momento?.
En base a este planteamiento, un proceso puede estar en ejecucin o esperando la ocurrencia de algn tipo de evento en un estado
de suspensin. En el caso de procesos peridicos, este evento estar temporizado, mientras que en el casode procesos no peridicos o
espordicos, dicho evento no estar temporizado.
Aunque existe una gran variedad de alternativas de planificacin,
en el presente captulo se contemplarn las dos siguientes:
1. Fixed Priority Scheduling (FPS), planificacin por prioridad esttica.
2. Earliest Deadline First (EDF), planificacin por tiempo lmite
ms cercano.
El esquema FPS es uno de los ms utilizados y tiene su base en
calcular, antes de la ejecucin, la prioridad Pi de un proceso. Dicha
prioridad es fija y esttica. As, una vez calculada la prioridad de todos
los procesos, en funcin de un determinado criterio, es posible establecer una ejecucin por orden de prioridad. En sistemas de tiempo
real, este criterio se define en base a los requisitos de temporizacin.
Por otra parte, el esquema EDF se basa en que el prximo proceso
a ejecutar es aqul que tenga un menor deadline Di absoluto. Normalmente, el deadline relativo se conoce a priori (por ejemplo, 50 ms
despus del comienzo de la ejecucin del proceso), pero el absoluto se
calcula en tiempo de ejecucin, concediendo una naturaleza dinmica
a este tipo de esquema de planificacin.
Antes de pasar a discutir los aspecto relevantes de un planificador,
es importante considerar la nocin de apropiacin en el contexto de
una planificacin basada en prioridades. Recuerde que, cuando se utiliza un esquema apropiativo, es posible que un proceso en ejecucin
con una baja prioridad sea desalojado por otro proceso de ms alta
prioridad, debido a que este ltimo ha de ejecutarse con cierta urgencia. Tpicamente, los esquemas apropiativos suelen ser preferibles
debido a que posibilitan la ejecucin inmediata de los procesos de alta
prioridad.
5.4.
[139]
Un sistema de tiempo real es planificable si los tiempos de respuesta de todos los procesos o tareas que lo componen son
menores que sus respectivos deadlines.
Requisitos
temporales
Modelo de anlisis de
tiempo de respuesta
Sistema
planificable?
5.4.1.
[140]
(5.2)
Este esquema de asignacin es ptimo para un conjunto de procesos planificado mediante un esquema de prioridades esttico y apropiativo.
Note que en el modelo simple planteado anteriormente, el deadline
de un proceso es igual a su periodo, es decir, D = T . Por lo tanto, este
esquema de asignacin de prioridades coincide con el esquema Deadline Monotonic Scheduling (DMS). La principal consecuencia es que
los procesos o tareas con un menor deadline tendrn una mayor prioridad. Este esquema es ptimo para sistemas de tiempo real crticos,
asumiendo el modelo simple con tareas espordicas y bloqueos.
5.4.2.
Factor de utilizacin
El concepto de factor de utilizacin est vinculado a un test de planificabilidad que, aunque no es exacto, es muy simple y prctico. La
base de este test se fundamenta en que, considerando la utilizacin
de un conjunto de procesos, empleando el esquema FPS, se puede garantizar un test de planificabilidad si se cumple la siguiente ecuacin
para todos los procesos [7]:
N
X
1
Ci
( ) N (2 N 1)
T
i
i=1
(5.3)
El sumatorio de la parte izquierda de la ecuacin representa la utilizacin total del conjunto de procesos. Este valor se compara con el
lmite de utilizacin que viene determinado por la parte derecha de la
ecuacin. La siguiente tabla muestra el lmite de utilizacin para algunos valores de N (nmero de procesos).
N (no de procesos)
1
2
3
4
5
10
Lmite de utilizacin
1
0.828
0.78
0.757
0.743
0.718
Tabla 5.2: Lmite de utilizacin para distintos valores del nmero de procesos (N) involucrados en la planificacin.
[141]
T
50
40
20
C
10
10
5
P
1
2
3
U (Factor de utilizacin)
0.2
0.25
0.25
Tabla 5.3: Lmite de utilizacin para distintos valores del nmero de procesos (N) involucrados en la planificacin.
[142]
10
20
30
40
50
Tiempo
(a)
Proceso
10
20
30
40
50
Tiempo
Instante de
activacin
Desalojado
En ejecucin
(b)
Figura 5.8: Distintos esquemas grficos utilizados para mostrar patrones de ejecucin
para los procesos de la tabla 5.3. (a) Diagrama de Gantt. (b) Lneas de tiempo.
Instante de
finalizacin
[143]
Clculo Ri
(5.4)
Di <= Ri?
Figura 5.9: Esquema grfico de un esquema de anlisis del tiempo de respuesta para
un conjunto de procesos o tareas.
(5.5)
A continuacin se deducir el tiempo de interferencia de un proceso pi . Para ello, suponga que todos los procesos se activan en un
[144]
Ri
e
Tj
(5.6)
Tj
#Activaciones pj
Figura 5.10: Esquema grfica del nmero de activaciones de un proceso pj con periodo
Tj que interrumpe a un proceso pi con un tiempo de respuesta total Ri .
A partir de este razonamiento, el clculo del tiempo total de interrupcin de pj sobre pi es sencillo, ya que slo hay que considerar
cunto tiempo interrumpe pj a pi en cada activacin. Dicho tiempo
viene determinado por Cj , es decir, el tiempo de ejecucin de pj en el
peor caso posible. Por lo tanto, la expresin de Ii es la siguiente:
Ii = d
Ri
e Cj
Tj
(5.7)
(d
jhp(i)
Ri
e Cj )
Tj
(5.8)
[145]
(d
jhp(i)
Ri
e Cj )
Tj
(5.9)
wi0 = Ci +
(Cj )
jhp(i)
wi1 = Ci +
(d
wi0
e Cj )
Tj
(d
wi1
e Cj )
Tj
jhp(i)
wi2 = Ci +
X
jhp(i)
O en general:
win = Ci +
X
jhp(i)
(d
win1
e Cj )
Tj
(5.10)
Debido a que el conjunto {wi0 , wi1 , wi2 , . . . , win } es montono no decreciente, en el mbito de la planificacin en tiempo real se podr parar
de iterar cuando se cumpla una de las dos siguientes condiciones:
Si win1 = win , entonces ya se habr encontrado la solucin a la
ecuacin, es decir, el valor de Ri .
Si win > Di , entonces no es necesario seguir iterando ya que el
sistema no ser planificable respecto al proceso o tarea pi , ya que
su tiempo de respuesta es mayor que su deadline.
Una vez alcanzado el punto en el que se ha discutido un modelo
anlitico para el clculo del tiempo de respuesta de un proceso, es importante recordar que un sistema de tiempo real es planificable si
y slo si i {1, 2, . . . , n}, Ri Di , es decir, si los tiempos de respuesta
de todos los procesos son menores o iguales a sus respectivos deadlines. Esta condicin es una condicin suficiente y necesaria para que
el sistema sea planificable. Adems, este modelo de anlisis de tiempo
real se puede completar, como se discutir ms adelante, siendo vlido
para computar el tiempo de respuesta.
A continuacin se muestra un ejemplo de clculo del tiempo de
respuesta para el conjunto de procesos cuyas caractersticas se resumen en la tabla 5.4.
[146]
T
80
40
20
C
40
10
5
P
1
2
3
Tabla 5.4: Periodo (T), tiempo de ejecucin en el peor de los casos (C) y prioridad (P) de
un conjunto de tres procesos para el clculo del tiempo de respuesta (R).
(d
jhp(b)
wb0
15
e Cj ) = 10 + d e 5 = 15
Tj
40
wa1 = Ca +
(d
jhp(a)
55
wa0
55
e Cj ) = 40 + d e 10 + d e 5 = 40 + 20 + 15 = 75
Tj
40
20
wa2 = Ca +
(d
wa1
75
75
e Cj ) = 40 + d e 10 + d e 5 = 40 + 20 + 20 = 80
Tj
40
20
(d
wa2
80
80
e Cj ) = 40 + d e 10 + d e 5 = 40 + 20 + 20 = 80
Tj
40
20
jhp(a)
Finalmente
wa3 = Ca +
X
jhp(a)
[147]
c
20
40
60
80
5.4.3.
En esta seccin se extiende el modelo simple planteado en la seccin 5.3.1, incluyendo aspectos relevantes como por ejemplo tareas
espordicas y la posibilidad de que un proceso bloquee a otro como
consecuencia, por ejemplo, de la adquisicin de un recurso compartido.
En primer lugar, se consideran procesos con un deadline inferior
a su periodo (D < T ) con el objetivo de manejar un modelo menos
pesimista que el anterior.
En segundo lugar, se considera la posibilidad de interaccin entre
procesos, hecho que condiciona el clculo del tiempo de respuesta.
Esta interaccin se plantea en trminos de un sistema concurrente
de manera que un proceso de baja prioridad puede bloquear a otra
tarea de alta prioridad debido a que utilice un recurso que esta ltima
necesite durante un periodo de tiempo. Este tipo de situaciones se
enmarcan en el concepto de inversin de prioridad y, aunque no se
pueden evitar por completo, s que es posible mitigar su efecto. Por
ejemplo, se puede plantear un esquema que limite el bloqueo y que
ste sea ponderable.
Si se considera este nuevo factor para realizar el clculo del tiempo
de respuesta de un proceso, entonces la ecuacin 5.5 se reformula de
la siguiente forma:
Ri = Ci + Ii + Bi
(5.11)
[148]
P
1
2
3
4
Instante de activacin
0
2
2
4
Secuencia de ejecucin
ERRRRE
EE
ESSE
EERSE
Inversin de prioridad
del proceso d
Tiempo de bloqueo de d
a1
aR
c1
cS d1
d2
cS
c2 b1 b2
aR
10
aR
aR dR dS d3 a2
12
14
16
Figura 5.12: Diagrama de Gantt para el ejemplo de inversin de prioridad para el conjunto de procesos de la tabla 5.5.
[149]
ejecucin de una seccin crtica por parte de otro proceso. Por ejemplo, si un proceso p1 est suspendido esperando a que otro proceso p2
termine de acceder a un objeto protegido, entonces la prioridad de p2
obtiene el mismo valor (hereda) que la prioridad de p1 , en caso de que
fuera menor.
Este planteamiento posibilita que el proceso que herede una prioridad alta tenga preferencia sobre otros procesos que, en caso de no
utilizar este esquema, alargaran el tiempo de respuesta de la tarea
cuya prioridad se hereda. En el ejemplo de la figura 5.12, si el proceso
a hubiese heredado la prioridad del proceso d al acceder al recurso R,
entonces a tendra prioridad sobre b y c, respectivamente.
Cuando se utiliza un protocolo de herencia de prioridad, la prioridad de un proceso ser el mximo de su propia prioridad y las
prioridades de otros procesos que dependan del mismo.
Para llevar a cabo un clculo del tiempo de bloqueo de un proceso utilizando este esquema simple de herencia de prioridad se puede
utilizar la siguiente frmula [3]:
Bi =
R
X
(utilizacion(r, i) Cr )
(5.12)
r1
donde R representa el nmero de recursos o secciones crticas, mientras que utilizacion(r, i) = 1 si el recurso r se utiliza al menos por un
proceso cuya prioridad es menor que la de pi y, al menos, por un proceso con prioridad mayor o igual a pi . Si no es as, utilizacion(r, i) = 0.
Por otra parte, Cr es el tiempo de ejecucin del recurso o seccin crtica
r en el peor caso posible.
La figura 5.13 muestra las lneas temporales y los tiempos de respuesta para el conjunto de procesos de la tabla 5.5 utilizando un esquema simple en el que se refleja el problema de la inversin de prioridad y el protocolo de herencia de prioridad, respectivamente.
En el caso del esquema simple, se puede apreciar de nuevo cmo
el tiempo de respuesta del proceso d, el cual tiene la mayor prioridad,
es de 17 unidades de tiempo, siendo el tiempo de respuesta ms alto
de todos los procesos. El extremo opuesto est representado, curiosamente, por el proceso de menor prioridad, es decir, el proceso a, cuyo
tiempo de respuesta es el menor de todos (9 unidades de tiempo). Una
vez ms, se puede apreciar cmo el bloqueo del recurso S por parte del
proceso a penaliza enormemente al proceso d.
[150]
Proceso
d
E
R
S
Rb=8
R
12
d
E
Rd=9
E
Rc=12
E
20
16
b
E
Ra=17
(a)
Proceso
Rd=12
Rc=6
E
E
b
a
R
4
Rb=14
R
8
E
12
16
Ra=17
20
(b)
Instante de
activacin
En ejecucin
Desalojado
Figura 5.13: Lneas de tiempo para el conjunto de procesos de la tabla 5.5 utilizando
a) un esquema de prioridades fijas con apropiacin y b) el protocolo de herencia de
prioridad. A la derecha de cada proceso se muestra el tiempo de respuesta asociado.
Instante de
finalizacin
[151]
[152]
En el caso particular del protocolo de techo de prioridad inmediato es importante considerar las siguientes propiedades [3]:
Cada proceso pi tiene asignada una prioridad Pi por defecto (por
ejemplo, utilizando un esquema DMS).
Cada recurso ri tiene asociado un valor cota esttico o techo de
prioridad T Pi , que representa la prioridad mxima de los procesos que hacen uso del mismo.
Cada proceso pi puede tener una prioridad dinmica, que es el
valor mximo entre su propia prioridad esttica y los techos de
prioridad de los recursos que tenga bloqueados.
En esencia, el protocolo de techo de prioridad inmediato consiste
en que un proceso slo puede hacer uso de un recurso si su prioridad
dinmica es mayor que el techo de todos los recursos que estn siendo
usados por otros procesos. Por ejemplo, si un proceso p1 utiliza un
recurso r1 con techo de prioridad Q y otro proceso p2 con prioridad P2 ,
P2 Q, quiere usar otro recurso r2 , entonces el proceso p2 tambin se
bloquea.
La duracin mxima de un bloqueo viene determinada por la siguiente ecuacin:
Bi = max{Cj,k }, j lp(i), k hc(i)
(5.13)
[153]
Proceso
Rd=6
E
Rc=12
E
b
a
E
0
Rb=14
R
4
Instante de
activacin
E
8
12
En ejecucin
Ra=17
16
Desalojado
20
Instante de
finalizacin
Figura 5.14: Lnea de tiempo para el conjunto de procesos de la tabla 5.5 utilizando el
protocolo de techo de prioridad inmediato.
[154]
T
80
150
100
500
C
10
20
10
12
D
80
150
15
30
r1
3
2
1
2
r2
2
1
-
r3
1
4
r4
5
-
Tabla 5.6: Periodo (T), tiempo de ejecucin en el peor de los casos (C), deadline, y tiempo
de uso de los recursos r1 , r2 , r3 y r4 para un conjunto de cuatro procesos.
[155]
wa0
w0
e Cc + d a e Cd =
Tc
Td
37
37
10 + 5 + d
e 10 + d e 12 = 37
100
50
wa1 = Ca + Ba + Ia = Ca + Ba + d
T
100
200
100
50
C
20
20
5
12
D
100
200
15
20
r1
3
2
1
2
r2
2
-
r3
4
r4
5
-
Tabla 5.7: Periodo (T), tiempo de ejecucin en el peor de los casos (C), deadline, y tiempo
de uso de los recursos r1 , r2 , r3 y r4 para un conjunto de cuatro procesos.
[156]
T
100
200
100
50
C
20
20
5
12
D
100
200
15
20
r1
3
2
1
2
r2
2
-
r3
4
r4
5
-
Tabla 5.8: Obteniendo bd (tiempo de bloqueo de la tarea d). Las tareas a y d son las que
tienen menor prioridad que d (nicas que pueden bloquearla).
Tarea
a
b
c
d
T
100
200
100
50
C
20
20
5
12
D
100
200
15
20
r1
3
2
1
2
r2
2
-
r3
4
r4
5
-
Tabla 5.9: Obteniendo bd (tiempo de bloqueo de la tarea d). Los recursos r1 y r3 tiene
un techo de prioridad mayor o igual que la prioridad de d (las tareas de menos prioridad
slo pueden bloquear a d si acceden a estos recursos).
[157]
wd1 = Cd + Bd + d
20
wd0
e 5 = 12 + 3 + 5 = 20
e Cc = 12 + 3 + d
Tc
100
wa1 = Ca + Ba + d
12 + 3 + d
wa0
w0
e Cd + d a e Cc =
Td
Tc
31
31
e 12 + d
e 5 = 20 + 2 + 12 + 5 = 31
50
100
[158]
Finalmente,
Rb = Cb + Bb + Ib
wb0 = Cb + Bb + Cd + Cc + Ca = 20 + 0 + 20 + 12 + 5 = 57
wb0
w0
w0
e Cd + d b e Cc + d b e Ca =
Td
Tc
Ta
57
57
57
e5+d
e 20 =
20 + 0 + d e 12 + d
50
100
100
20 + 0 + 24 + 5 + 20 = 69
wb1 = Cb + Bb + d
wb2 = Cb + Bb + d
5.4.4.
Consideraciones finales
El modelo simple de tareas discutido en este captulo se puede extender an ms considerando otros aspectos con el objetivo de llevar
a cabo un anlisis ms exhaustivo del tiempo de respuesta. Por ejemplo, sera posible considerar aspectos que tienen una cierta influencia
en la planificacin de un sistema de tiempo real, como por ejemplo el
jitter o variabilidad temporal en el envo de seales digitales, los cambios de contexto, el tiempo empleado por el planificador para decidir
cul ser el siguiente proceso o tarea a ejecutar, las interrupciones,
etc.
Por otra parte, en el mbito de los sistemas de tiempo real no
crticos se pueden utilizar otras alternativas de planificacin menos
pesimistas y ms flexibles, basadas en una variacin ms dinmica
de las prioridades de los procesos y considerando especialmente la
productividad del sistema.
En [3] se contemplan algunas de estas consideraciones y se realiza un anlisis ms exhaustivo de ciertos aspectos discutidos en el
presente captulo.
Apndice
A.1.
Enunciado
Norte
Sur
159
[160]
A.2.
Cdigo fuente
DIROBJ
DIREXE
DIRHEA
DIRSRC
:=
:=
:=
:=
obj/
exec/
include/
src/
#ifndef __SEMAFOROI_H__
#define __SEMAFOROI_H__
#include <semaphore.h>
// Crea un semforo POSIX.
sem_t *crear_sem (const char *name, unsigned int valor);
// Obtiene un semforo POSIX (ya existente).
sem_t *get_sem (const char *name);
// Cierra un semforo POSIX.
void destruir_sem (const char *name);
// Incrementa el semforo.
void signal_sem (sem_t *sem);
// Decrementa el semforo.
void wait_sem (sem_t *sem);
#endif
[161]
#include
#include
#include
#include
#include
#include
<stdio.h>
<errno.h>
<stdlib.h>
<unistd.h>
<fcntl.h>
<semaforoI.h>
[162]
#ifndef __VARIABLEI_H__
#define __VARIABLEI_H__
// Crea un objeto de memoria compartida.
// Devuelve el descriptor de archivo.
int crear_var (const char *name, int valor);
// Obtiene el descriptor asociado a la variable.
int obtener_var (const char *name);
// Destruye el objeto de memoria compartida.
void destruir_var (const char *name);
// Modifica el valor del objeto de memoria compartida.
void modificar_var (int shm_fd, int valor);
// Devuelve el valor del objeto de memoria compartida.
void consultar_var (int shm_fd, int *valor);
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
<stdio.h>
<errno.h>
<string.h>
<stdlib.h>
<unistd.h>
<fcntl.h>
<sys/mman.h>
<sys/stat.h>
<sys/types.h>
#include <memoriaI.h>
int crear_var (const char *name, int valor) {
int shm_fd;
int *p;
// Abre el objeto de memoria compartida.
shm_fd = shm_open(name, O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
fprintf(stderr, "Error al crear la variable: %s\n",
strerror(errno));
exit(1);
}
// Establecer el tamao.
if (ftruncate(shm_fd, sizeof(int)) == -1) {
fprintf(stderr, "Error al truncar la variable: %s\n",
strerror(errno));
exit(1);
}
// Mapeo del objeto de memoria compartida.
p = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED,
shm_fd, 0);
if (p == MAP_FAILED) {
fprintf(stderr, "Error al mapear la variable: %s\n", strerror(
errno));
exit(1);
}
36
37
38
39
*p = valor;
40
munmap(p, sizeof(int));
41
42
return shm_fd;
43 }
[163]
44
45 int obtener_var (const char *name) {
46
int shm_fd;
47
48
// Abre el objeto de memoria compartida.
49
shm_fd = shm_open(name, O_RDWR, 0666);
50
if (shm_fd == -1) {
51
fprintf(stderr, "Error al obtener la variable: %s\n", strerror(
errno));
52
exit(1);
53
}
54
55
return shm_fd;
56 }
[164]
/* includes previos... */
#include <memoriaI.h>
void destruir_var (const char *name) {
int shm_fd = obtener_var(name);
if (close(shm_fd) == -1) {
fprintf(stderr, "Error al destruir la variable: %s\n",
strerror(errno));
exit(1);
}
if (shm_unlink(name) == -1) {
fprintf(stderr, "Error al destruir la variable: %s\n",
strerror(errno));
exit(1);
}
}
void modificar_var (int shm_fd, int valor) {
int *p;
// Mapeo del objeto de memoria compartida.
p = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
if (p == MAP_FAILED) {
fprintf(stderr, "Error al mapear la variable: %s\n",
strerror(errno));
exit(1);
}
*p = valor;
munmap(p, sizeof(int));
}
void consultar_var (int shm_fd, int *valor) {
int *p, valor;
// Mapeo del objeto de memoria compartida.
p = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
if (p == MAP_FAILED) {
fprintf(stderr, "Error al mapear la variable: %s\n",
strerror(errno));
exit(1);
}
*valor = *p;
munmap(p, sizeof(int));
}
[165]
#include
#include
#include
#include
#include
#include
#include
#include
#define
#define
#define
#define
#define
#define
#define
COCHES 30
MAX_T_LANZAR 3
PUENTE "puente"
MUTEXN "mutex_n"
MUTEXS "mutex_s"
COCHESNORTE "coches_norte"
COCHESSUR "coches_sur"
pid_t pids[COCHES];
void liberarecursos (); void finalizarprocesos ();
void controlador (int senhal);
int main (int argc, char *argv[]) {
pid_t pid_hijo; int i;
srand((int)getpid());
// Creacin de semforos y segmentos de memoria compartida.
crear_sem(PUENTE, 1); crear_sem(MUTEXN, 1); crear_sem(MUTEXS, 1);
crear_var(COCHESNORTE, 0); crear_var(COCHESSUR, 0);
// Manejo de Ctrol+C.
if (signal(SIGINT, controlador) == SIG_ERR) {
fprintf(stderr, "Abrupt termination.\n"); exit(EXIT_FAILURE);
}
for (i = 0; i < COCHES; i++) {
// Se lanzan los coches...
switch (pid_hijo = fork()) {
case 0:
if ((i % 2) == 0) // Coche Norte.
execl("./exec/coche", "coche", "N", PUENTE, MUTEXN, COCHESNORTE
, NULL);
else
// Coche Sur.
execl("./exec/coche", "coche", "S", PUENTE, MUTEXS, COCHESSUR,
NULL);
break;
}// Fin switch.
sleep(rand() % MAX_T_LANZAR);
}// Fin for.
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<stdio.h>
<string.h>
<stdlib.h>
<unistd.h>
<signal.h>
<sys/wait.h>
<memoriaI.h>
<semaforoI.h>
[166]
63 }
64
65 void controlador (int senhal) {
66
finalizarprocesos(); liberarecursos();
67
printf ("\nFin del programa (Ctrol + C)\n"); exit(EXIT_SUCCESS);
68 }
[167]
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <semaforoI.h>
#include <memoriaI.h>
#define MAX_TIME_CRUZAR 5
void coche (); void cruzar ();
int main (int argc, char *argv[]) {
coche(argv[1], argv[2], argv[3], argv[4]);
return 0;
}
/* salida permite identificar el origen del coche (N o S) */
void coche (char *salida, char *id_puente,
char *id_mutex, char *id_num_coches) {
int num_coches_handle, valor;
sem_t *puente, *mutex;
srand((int)getpid());
puente = get_sem(id_puente);
mutex = get_sem(id_mutex);
num_coches_handle = obtener_var(id_num_coches);
/* Acceso a la variable compartida num coches */
wait_sem(mutex);
consultar_var(num_coches_handle, &valor);
modificar_var(num_coches_handle, ++valor);
/* Primer coche que intenta cruzar desde su extremo */
if (valor == 1)
wait_sem(puente); /* Espera el acceso al puente */
signal_sem(mutex);
cruzar(salida); /* Tardar un tiempo aleatorio */
wait_sem(mutex);
consultar_var(num_coches_handle, &valor);
modificar_var(num_coches_handle, --valor);
/* Ultimo coche en cruzar desde su extremo */
if (valor == 0)
signal_sem(puente); /* Libera el puente */
signal_sem(mutex);
}
void cruzar (char *salida) {
if (strcmp(salida, "N") == 0)
printf(" %d cruzando de N a S...\n", getpid());
else
printf(" %d cruzando de S a N...\n", getpid());
sleep(rand() % MAX_TIME_CRUZAR + 1);
}
Apndice
Filsofos comensales
B.1.
Enunciado
Los filsofos se encuentran comiendo o pensando. Todos comparten una mesa redonda con cinco sillas, una para cada filsofo. En el
centro de la mesa hay una fuente de arroz y en la mesa slo hay cinco
palillos, de manera que cada filsofo tiene un palillo a su izquierda y
otro a su derecha.
Cuando un filsofo piensa, entonces se abstrae del mundo y no se
relaciona con ningn otro filsofo. Cuando tiene hambre, entonces intenta acceder a los palillos que tiene a su izquierda y a su derecha
(necesita ambos). Naturalmente, un filsofo no puede quitarle un palillo a otro filsofo y slo puede comer cuando ha cogido los dos palillos.
Cuando un filsofo termina de comer, deja los palillos y se pone a
pensar.
Figura B.1: Esquema grfico del problema original de
los filsofos comensales (dining philosophers).
169
[170]
B.2.
Cdigo fuente
DIROBJ := obj/
DIREXE := exec/
DIRSRC := src/
CFLAGS := -c -Wall
LDLIBS := -lrt
CC := gcc
all : dirs manager filosofo
dirs:
mkdir -p $(DIROBJ) $(DIREXE)
manager: $(DIROBJ)manager.o
$(CC) -o $(DIREXE)$@ $^ $(LDLIBS)
filosofo: $(DIROBJ)filosofo.o
$(CC) -o $(DIREXE)$@ $^ $(LDLIBS)
$(DIROBJ) %.o: $(DIRSRC) %.c
$(CC) $(CFLAGS) $^ -o $@
clean :
rm -rf *~ core $(DIROBJ) $(DIREXE) $(DIRSRC)*~
[171]
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
<unistd.h>
<signal.h>
<string.h>
<stdlib.h>
<stdio.h>
<signal.h>
<wait.h>
<sys/types.h>
<sys/stat.h>
<mqueue.h>
[172]
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
if ((pids[i] = fork()) == 0) {
sprintf(filosofo, " %d", i);
sprintf (buzon_palillo_izq, " %s %d", BUZON_PALILLO, i);
sprintf (buzon_palillo_der, " %s %d",
BUZON_PALILLO, (i + 1) % FILOSOFOS);
execl("./exec/filosofo", "filosofo", filosofo, BUZON_MESA,
buzon_palillo_izq, buzon_palillo_der, NULL);
}
for (i = 0; i < FILOSOFOS; i++) waitpid(pids[i], 0, 0);
finalizarprocesos(); liberarecursos();
printf ("\n Fin del programa\n");
return 0;
}
void controlador (int senhal) {
finalizarprocesos(); liberarecursos();
printf ("\n Fin del programa (Control + C)\n");
exit(EXIT_SUCCESS);
}
void liberarecursos () {
int i; char caux[30];
printf ("\n Liberando buzones... \n");
mq_close(qHandlerMesa); mq_unlink(BUZON_MESA);
for (i = 0; i < FILOSOFOS; i++) {
sprintf (caux, " %s %d", BUZON_PALILLO, i);
mq_close(qHandlerPalillos[i]); mq_unlink(caux);
}
}
void finalizarprocesos () {
int i;
printf ("-------------- Terminando ------------- \n");
for (i = 0; i < FILOSOFOS; i++) {
if (pids[i]) {
printf ("Finalizando proceso [ %d]... ", pids[i]);
kill(pids[i], SIGINT); printf ("<Ok>\n");
}
}
}
[173]
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <mqueue.h>
#define MAX_TIME_PENSAR 7
#define MAX_TIME_COMER 5
void filosofo (char *filosofo, char *buzon_mesa,
char *buzon_palillo_izq, char *buzon_palillo_der);
void controlador (int senhal);
int main (int argc, char *argv[]) {
filosofo(argv[1], argv[2], argv[3], argv[4]);
return 0;
}
void filosofo (char *filosofo, char *buzon_mesa,
char *buzon_palillo_izq, char *buzon_palillo_der) {
mqd_t qHandlerMesa, qHandlerIzq, qHandlerDer;
int n_filosofo;
char buffer[64];
// Retrollamada de finalizacin.
if (signal(SIGINT, controlador) == SIG_ERR) {
fprintf(stderr, "Abrupt termination.\n"); exit(EXIT_FAILURE);
}
n_filosofo = atoi(filosofo);
// Recupera buzones.
qHandlerMesa = mq_open(buzon_mesa, O_RDWR);
qHandlerIzq = mq_open(buzon_palillo_izq, O_RDWR);
qHandlerDer = mq_open(buzon_palillo_der, O_RDWR);
srand((int)getpid());
while (1) {
printf("[Filosofo %d] pensando...\n", n_filosofo);
sleep(rand() % MAX_TIME_PENSAR); // Pensar.
mq_receive(qHandlerMesa, buffer, sizeof(buffer), NULL);
// Hambriento (coger palillos)...
mq_receive(qHandlerIzq, buffer, sizeof(buffer), NULL);
mq_receive(qHandlerDer, buffer, sizeof(buffer), NULL);
// Comiendo...
printf("[Filosofo %d] comiendo...\n", n_filosofo);
sleep(rand() % MAX_TIME_COMER); // Comer.
// Dejar palillos...
mq_send(qHandlerIzq, buffer, sizeof(buffer), 0);
mq_send(qHandlerDer, buffer, sizeof(buffer), 0);
mq_send(qHandlerMesa, buffer, sizeof(buffer), 0);
}
}
void controlador (int senhal) {
printf("[Filosofo %d] Finalizado (SIGINT)\n", getpid());
exit(EXIT_SUCCESS);
}
Apndice
La biblioteca de hilos de
ICE
C.1.
Fundamentos bsicos
ICE (Internet Communication Engine) [6] es un middleware de comunicaciones orientado a objetos, es decir, ICE proporciona herramientas, APIs, y soporte de bibliotecas para construir aplicaciones distribuidas cliente-servidor orientadas a objetos.
Una aplicacin ICE se puede usar en entornos heterogneos. Los
clientes y los servidores pueden escribirse en diferentes lenguajes de
programacin, pueden ejecutarse en distintos sistemas operativos y en
distintas arquitecturas, y pueden comunicarse empleando diferentes
tecnologas de red. La tabla C.1 resume las principales caractersticas
de ICE.
Figura C.1: ZeroC ICE es un
middleware de comunicaciones que se basa en la filosofa de CORBA pero con una
mayor simplicidad y practicidad.
[176]
Nombre
Definido por
Documentacin
Lenguajes
Plataformas
Destacado
Descargas
C.2.
Manejo de hilos
ICE proporciona distintas utilidades para la gestin de la concurrencia y el manejo de hilos. Respecto a este ltimo aspecto, ICE proporciona una abstraccin muy sencilla para el manejo de hilos con el
objetivo de explotar el paralelismo mediante la creacin de hilos dedicados. Por ejemplo, sera posible crear un hilo especfico que atenda
las peticiones de una interfaz grfica o crear una serie de hilos encargados de tratar con aquellas operaciones que requieren una gran
cantidad de tiempo, ejecutndolas en segundo plano.
En este contexto, ICE proporciona una abstraccin de hilo muy
sencilla que posibilita el desarrollo de aplicaciones multihilo altamente portables e independientes de la plataforma de hilos nativa. Esta
abstraccin est representada por la clase IceUtil::Thread.
[177]
Para mostrar cmo llevar a cabo la implementacin de un hilo especfico a partir de la biblioteca de ICE se usar como ejemplo uno de
los problemas clsicos de sincronizacin: el problema de los filsofos
comensales, discutido en la seccin 2.3.3.
Bsicamente, los filsofos se encuentran comiendo o pensando. Todos comparten una mesa redonda con cinco sillas, una para cada filsofo. Cada filsofo tiene un plato individual con arroz y en la mesa
slo hay cinco palillos, de manera que cada filsofo tiene un palillo a
su izquierda y otro a su derecha.
[178]
#ifndef __FILOSOFO__
#define __FILOSOFO__
#include <iostream>
#include <IceUtil/Thread.h>
#include <Palillo.h>
#define MAX_COMER 3
#define MAX_PENSAR 7
using namespace std;
class FilosofoThread : public IceUtil::Thread {
public:
FilosofoThread (const int& id, Palillo* izq, Palillo *der);
virtual void run ();
private:
void coger_palillos ();
void dejar_palillos ();
void comer () const;
void pensar () const;
int _id;
Palillo *_pIzq, *_pDer;
};
#endif
[179]
La implementacin de la funcin run() es trivial a partir de la descripcin del enunciado del problema.
Listado C.3: La funcin FilosofoThread::run()
1
2
3
4
5
6
7
8
9
10
void
FilosofoThread::run ()
{
while (true) {
coger_palillos();
comer();
dejar_palillos();
pensar();
}
}
El problema de los filsofos comensales es uno de los problemas clsicos de sincronizacin y existen muchas modificaciones del mismo que permiten asimilar mejor los conceptos tericos de la programacin concurrente.
[180]
#include
#include
#include
#include
<IceUtil/Thread.h>
<vector>
<Palillo.h>
<Filosofo.h>
#define NUM 5
int main (int argc, char *argv[]) {
std::vector<Palillo*> palillos;
std::vector<IceUtil::ThreadControl> threads;
int i;
// Se instancian los palillos.
for (i = 0; i < NUM; i++)
palillos.push_back(new Palillo);
// Se instancian los filsofos.
for (i = 0; i < NUM; i++) {
// Cada filsofo conoce los palillos
// que tiene a su izda y derecha.
IceUtil::ThreadPtr t =
new FilosofoThread(i, palillos[i], palillos[(i + 1) % NUM]);
// start sobre hilo devuelve un objeto ThreadControl.
threads.push_back(t->start());
}
// Unin de los hilos creados.
std::vector<IceUtil::ThreadControl>::iterator it;
for (it = threads.begin(); it != threads.end(); ++it) it->join();
return 0;
}
C.3.
[181]
#ifndef __PALILLO__
#define __PALILLO__
#include <IceUtil/Mutex.h>
class Palillo : public IceUtil::Mutex {
};
#endif
Para que los filsofos puedan utilizar los palillos, habr que utilizar
la funcionalidad previamente discutida, es decir, las funciones lock() y
[182]
void
FilosofoThread::coger_palillos ()
{
_pIzq->lock();
_pDer->lock();
}
void
FilosofoThread::dejar_palillos ()
{
_pIzq->unlock();
_pDer->unlock();
}
C.3.1.
Evitando interbloqueos
[183]
#include <IceUtil/Mutex.h>
class Test {
public:
void mi_funcion () {
_mutex.lock();
for (int i = 0; i < 5; i++)
if (i == 3) return; // Generar un problema...
_mutex.unlock();
}
private:
IceUtil::Mutex _mutex;
};
int main (int argc, char *argv[]) {
Test t;
t.mi_funcion();
return 0;
}
[184]
#include <IceUtil/Mutex.h>
class Test {
public:
void mi_funcion () {
IceUtil::Mutex::Lock lock(_mutex);
for (int i = 0; i < 5; i++)
if (i == 3) return; // Ningn problema...
} // El destructor de lock libera el cerrojo.
private:
IceUtil::Mutex _mutex;
};
C.3.2.
Sin embargo, existe una diferencia fundamental entre ambas. Internamente, el cerrojo recursivo est implementado con un contador
inicializado a cero. Cada llamada a lock() incrementa el contador, mientras que cada llamada a unlock() lo decrementa. El cerrojo estar disponible para otro hilo cuando el contador alcance el valor de cero.
[185]
C.4.
Introduciendo monitores
Las soluciones de alto nivel permiten que el desarrollador tenga ms flexibilidad a la hora de solucionar un problema. Este
planteamiento se aplica perfectamente al uso de monitores.
[186]
No olvide comprobar la condicin asociada al uso de wait siempre que se retorne de una llamada a la misma.
C.4.1.
[187]
: Consumidor
: Queue
: Productor
get ()
wait ()
notify
put ()
return item
[188]
#include <IceUtil/Monitor.h>
#include <IceUtil/Mutex.h>
#include <deque>
using namespace std;
template<class T>
class Queue : public IceUtil::Monitor<IceUtil::Mutex> {
public:
void put (const T& item) { // Aade un nuevo item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
_queue.push_back(item);
notify();
}
T get () { // Consume un item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
while (_queue.size() == 0)
wait();
T item = _queue.front();
_queue.pop_front();
return item;
}
private:
deque<T> _queue; // Cola de doble entrada.
};
No olvide... usar Lock y TryLock para evitar posibles interbloqueos causados por la generacin de alguna excepcin o una
terminacin de la funcin no prevista inicialmente.
[189]
[190]
#include <IceUtil/Monitor.h>
#include <IceUtil/Mutex.h>
#include <deque>
template<class T>
class Queue : public IceUtil::Monitor<IceUtil::Mutex> {
public:
Queue () : _consumidoresEsperando(0) {}
void put (const T& item) { // Aade un nuevo item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
_queue.push_back(item);
if (_consumidoresEsperando)
notify();
}
T get () { // Consume un item.
IceUtil::Monitor<IceUtil::Mutex>::Lock lock(*this);
while (_queue.size() == 0) {
try {
_consumidoresEsperando++;
wait();
_consumidoresEsperando--;
}
catch (...) {
_consumidoresEsperando--;
throw;
}
}
T item = _queue.front();
_queue.pop_front();
return item;
}
private:
std::deque<T> _queue; // Cola de doble entrada.
int _consumidoresEsperando;
};
Bibliografa
[192]
BIBLIOGRAFA