Práctica 2: Concurrencia de procesos

Problema de la Barbería


Sistemas Operativos I
ETIX, curso 2009-2010

 
Enunciado | Programa y procesos | Semáforos y memoria compartida | normas y entrega

Enunciado

La práctica consiste en implementar el problema de la Barbería, con el objeto de ver cómo funciona un problema típico de concurrencia y uso de semáforos. Aprenderemos a crear procesos concurrentes en UNIX con el uso de la llamada  fork, y a sincronizarlos construyendo semáforos generales (siguiendo la implementación de Kearns) a partir de semáforos binarios que simularemos con ficheros.

Problema de la Barbería

En una barbería hay num_barberos barberos, cada uno con su silla reclinable. Además, tenemos una sala de espera con num_sillas sillas normales en las que se sientan los clientes para esperar su turno. El funcionamiento de la barbería se rige por las siguientes normas:

  1. Si no hay clientes que atender, el barbero se sienta en su silla reclinable y se duerme.
  2. Si hay clientes en espera y algún barbero dormido, el primer cliente que pueda despierta al barbero y este último le atiende.
  3. Si llega un cliente mientras los barberos están ocupados, se sienta en una de las sillas de espera.
  4. Si llega un cliente, y todos los barberos y todas las sillas de espera están ocupadas, se marcha sin ser atendido.
Nótese que los clientes se atienden sobre demanda: no se respeta necesariamente el orden de entrada.

El pseudocódigo (visto en clase) hace uso de dos semáforos generales llamados CLIENTES y BARBEROS ambos inicializados a cero. Además, se usa una variable compartida llamada clientes_espera que también inicializamos a cero. Para sincronizar el acceso a dicha variable compartida, se usa un semáforo binario llamado MUTEX, que inicializamos a uno.

El pseudocódigo de un barbero sería el siguiente:

void barbero()
{
  while (1) {                                /* bucle infinito */
      P(CLIENTES);                           /* se duerme si clientes es 0 */
      Pb(MUTEX);
      ... /* decrementar los clientes_espera */
      V(BARBEROS);
      Vb(MUTEX);

      mostrarMensaje("barbero","afeitando");
      sleep(rand() % 4 + 1);                           /* espera aleatoria entre 1 y 4 segundos */
      mostrarMensaje("barbero","termina de afeitar");
}

donde las funciones P y V corresponden a semáforos generales mientras que Pb y y Vb a semáforos binarios. El pseudocódigo de un cliente sería:

void cliente()
{
     mostrarMensaje("cliente","ha llegado");
     P(MUTEX);
     if (clientes_espera < num_sillas) {
            ... /* incrementar los clientes_espera */
            V(CLIENTES);                         /* despierta al barbero */
            Vb(MUTEX);
            P(BARBEROS);                         /* espera si no hay barbero libre */
            mostrarMensaje("cliente","siendo afeitado");
     }
     else {
           Vb(MUTEX);
           mostrarMensaje("cliente", "no atendido");
     }
}

Para mostrar mensajes, se usará exclusivamente la función;

void mostrarMensaje(char *quien, char *que)
{
  char mensaje[MAX_CADENA];
  time_t t=time(NULL);

  sprintf(mensaje, "%s pid %u: %s %s", quien, getpid(), que, ctime(&t) );
  write(STDOUT_FILENO,mensaje,strlen(mensaje));
}

de modo que, por ejemplo, un barbero afeitando haría la llamada mostrarMensaje("barbero","afeitando").


Programa y procesos

La práctica constará de un único programa llamado barberia que se encargará de crear todos los procesos barberos y clientes y controlar su finalización. Este programa recibirá tres parámetros como entrada:

$ barberia -b num_barberos -s num_sillas -c num_clientes

donde num_barberos indica cuántos barberos hay disponibles,  num_sillas indica el número de sillas de espera y num_clientes es el número de clientes que se van a generar.

Creación de procesos

Para crear todos los procesos, se utilizará la llamada fork(). Esta llamada crea una réplica o clon (proceso hijo) del proceso actual (proceso padre). Cuidado: la réplica tiene exactamente el mismo código y sus variables se inicializan con el mismo valor que tenían las del padre en el momento de ser invocada. Por tanto, justo después de llamar a fork(), el programa no podría distinguir si es el proceso padre o el hijo. Para evitar esto, fork() devuelve 0 en el proceso hijo, y un valor >0 en el proceso padre (en realidad, le devuelve al padre el pid, o identificador de proceso del hijo que acaba de crear). Si se produce un error, devuelve -1.

El proceso padre deberá crear inicialmente todos los barberos, guardando sus pid's en un array pid_barberos, para poder matarlos al terminar. Los procesos cliente se irán lanzando tras un retardo aleatorio de entre 1 a 7 segundos, para que su llegada no sea instantánea.

...

for (i=0;i<num_barberos; i++)
  switch ( pid_barberos[i]=fork() ) {
    case -1 : printf("error fork barbero número %d\n",i); exit(1);
    case  0 : barbero(); exit(0);
  }

for (i=0;i<num_clientes; i++)
  switch (fork()) {
    case -1 : printf("error fork cliente número %d\n",i); exit(1);
    case  0 : srand(getpid()); sleep(rand() % 7 + 1);    /* espera aleatoria de 1 a 7 segs */
         cliente(); exit(0);
  }
}

/* El padre espera por los hijos (clientes) */
for (i=0;i<num_clientes;i++) {
  sprintf(mensaje, "terminó el hijo %u\n",wait((int *)NULL));
  write(STDOUT_FILENO,mensaje,strlen(mensaje));
}

...

Como se puede ver, el proceso padre espera (wait) a que finalicen un número de num_clientes procesos hijos. Estos procesos que terminan serán sólo clientes, ya que los barberos ejecutan un bucle infinito. Como último paso, el proceso padre deberá hacer uso de la llamada al sistema kill para ir matando uno a uno los procesos barberos cuyos pid están almacenados en pid_barberos.


Semáforos y variables compartidas

La variable clientes_espera no se puede declarar como una simple variable general:

int clientes_espera;        /* Código incorrecto !!! */

dado que si hacemos eso, todos los procesos tendrían una variable propia diferente (a pesar de llamarse igual) y no compartida entre ellos. En lugar de ello, guardaremos el valor de esta variable en un fichero que habrá que crear inicialmente y consultar o actualizar cuando queramos usar o cambiar la variable. Para manejo de ficheros utilizaremos las llamadas open, read, write, lseek y close según sea necesario.

Para sincronizar el acceso a la variable compartida usaremos, como se ha dicho antes, semáforos generales que deberán construirse siguiendo la implementacion de Kearns (vista en clase), a partir de semáforos binarios.

En cuanto a los semáforos binarios a su vez, se construirán con ficheros, haciendo uso de una propiedad interesante de la llamada open en UNIX. Cuando se usa esta llamada para crear un fichero en modo exclusivo, se garantiza que dicha operación se realiza de manera atómica. Esto se puede aprovechar para las operaciones binarias Pb y Vb del siguiente modo:


void Pb(char *nombre) {
  int df;

  /* intento continuamente crear en modo exclusivo... */
  while ( (df=open(nombre, O_CREAT | O_EXCL, 0777)) == -1) ;

  /* cuando finalmente no falle la llamada, doy "paso" */
  close(df);
}


void Vb(char *nombre) {
  unlink(nombre);
}

Como se puede ver, para manejar un semáforo binario usando esta técnica, necesitaremos guardar únicamente el nombre del fichero auxiliar que le corresponde. Dicho de otro modo, podemos identificar el tipo semaforo_binario con un string o puntero a char que contiene el nombre del archivo correspondiente.

La implementación de los semáforos generales debe incluir:
  1. La definición del tipo semaforo_general, que será un struct con 3 semáforos binarios (esto es, tres nombres de ficheros): mutex, delay y valor.
  2. La operación InicializarSemaforo que dará al semáforo su valor inicial e inicializará los semáforos binarios auxiliares adecuadamente.
  3. La operación P.
  4. La operación V.
IMPORTANTE: Debe además tenerse en cuenta que

La lista completa de funciones a usar sería:

En todos los casos se deberá comprobar la página man correspondiente para conocer su funcionamiento y obtener además los include necesarios.


Normas y entrega

La práctica se realizará de forma individual o en grupos de dos personas, siendo preferible esto último. La presentación de la práctica tendrá lugar preferentemente en el laboratorio durante el horario de clase y, en el caso de los grupos, es obligatoria la presencia de los dos alumnos que lo componen.

La práctica es obligatoria y, por tanto, aprobarla es imprescindible para aprobar la asignatura. La puntuación de la práctica es de un máximo de 0.8 puntos, si se entrega dentro de plazo, y un máximo de 0,4 si se entrega fuera de plazo (inlcuyendo convocatoria de septiembre o diciembre). Una vez aprobada, su validez comprende un período de dos años.

La entrega se realizará utilizando el sistema de buzón de prácticas. La fecha límite de entrega de la práctica es el miércoles 27 de enero, quedando inhabilitado el buzón al día siguiente. Tras esta fecha, se abrirá un plazo de defensa en horario de tutorías para aquéllas prácticas entregadas en buzón dentro de plazo pero que excepcionalmente no hayan sido presentadas en el laboratorio.

Última actualización: 11/01/2010