Práctica 2: Concurrencia de procesos -productores/consumidores

Práctica Obligatoria
Sistemas Operativos I
ETIX, curso 2006-2007

 
Enunciado | funciones | normas y entrega

Enunciado

La práctica consiste en implementar el problema de los productores/consumidores utilizando las llamadas al sistema para manejo de memoria compartida (shmget, shmat, shmdt, shmctl) y para manejo de semáforos (semget, semctl, semop).

Programa principal

La práctica constará de un único programa llamado sem que se encargará de crear todos los procesos productores y consumidores. Este programa recibirá tres parámetros como entrada:

$ sem -p num_productores -c num_consumidores -n num_pasos

donde se indican cuántos productores y consumidores entrarán en juego, así como el número de operaciones que realizará cada uno ya sea de inserción, en el caso de los productores, o de extracción en el caso de los consumidores.

Creación de procesos

Para crear todos los procesos, se utilizará la llamada fork(). Esta llamada crea una réplica (proceso hijo) del proceso actual (proceso padre). 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. La parte principal del proceso padre lanza todos los productores y consumidores del siguiente modo:

...

for (i=0;i<num_productores; i++)
  switch (fork()) {
    -1 : printf("error fork productor número %d\n",i); exit(1);
     0 : productor(); exit(0);
  }

for (i=0;i<num_consumidores; i++)
  switch (fork()) {
    -1 : printf("error fork consumidor número %d\n",i); exit(1);
     0 : consumidor(); exit(0);
  }
}

/* El padre espera por los hijos */
for (i=0;i<num_productores+num_consumidores;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 todos los procesos hijos creados.

Productores y consumidores

El "trabajo" que realizan los productores y consumidores es insertar y extraer respectivamente una serie de números aleatorios en una cola circular. Cada productor escribe en la cola un número aleatorio entre i*100 e i*100+99, donde i es el número de productor. Por ejemplo, el productor 0 escribe un número aleatorio en el intervalo [0,99], el productor 1 en el intervalo [100,199], el 2 en el intervalo [200,299], etc. Los consumidores van tomando números de la cola e imprimiéndolos en pantalla. La única salida que ofrecerán estos procesos se programará usando exclusivamente la siguiente función:

void mostrarDato(char tipo, int i, int dato) {
  char mensaje[MAX_CADENA];
  time_t t;
  pid_t p;

  p=getpid();
  t=time(NULL);
  if (tipo=='P')
    sprintf(mensaje, "Productor  %d (%u): --->[%d]   %s",i,p,dato,ctime(&t));
  else
    sprintf(mensaje, "Consumidor %d (%u): [%d]--->   %s",i,p,dato,ctime(&t));
  write(STDOUT_FILENO,mensaje,strlen(mensaje));
}

donde tipo es un carácter igual a 'P' para productores o a 'C' para consumidores, i es el número de productor o consumidor, y dato es el número que se inserta o se extrae de la cola, dependiendo del caso. Tanto los productores como los consumidores esperarán (usando sleep) un tiempo aleatorio de hasta 3 segundos antes de realizar una nueva operación.

Cola circular

La cola en la que se insertan los datos será una estructura del siguiente tipo:

struct tCola {
  int numeros[TAM_COLA];
  int rpos,wpos,num;
};

donde TAM_COLA es constante (en principio 20), los valores rpos, wpos indican la posición de donde se extrae o en donde se escribe respectivamente, y el valor num es el número de elementos actualmente almacenados en la cola. Como la cola es circular, para incrementar rpos o wpos habrá que hacer posteriormente una operación módulo TAM_COLA (en lenguaje C, "x módulo y" se escribe x % y).

Memoria compartida

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

struct tCola cola;        /* Código incorrecto !!! */

dado que si hacemos eso, todos los procesos tendrían una cola propia diferente (a pesar de llamarse igual) y no compartida entre ellos. Para poder compartir la cola, usaremos las llamadas al sistema para manejo de memoria compartida shmget, shmat, shmdt, shmctl del siguiente modo:
  1. El proceso padre crea un área de memoria compartida con tamaño sizeof(struct tCola) usando la llamada shmget. Esta llamada no devuelve un puntero, sino un número, identificador de la zona de memoria compartida que se ha creado.
  2. El proceso padre asignará un puntero al identificador que devuelve shmget. Para ello, se usa la llamada shmat de modo que, por ejemplo, si shmget devolvió el valor id, y pcola es un puntero a una cola, ejecutaremos:

    pcola=(struct tCola *) shmat(id,0,0);

    A partir de ahí, podremos usar pcola de forma normal, pero se comporta ahora como una estructura compartida.
  3. Al finalizar cada proceso hijo, y también el padre, se desasigna el puntero pcola usando shmdt.
  4. Por último, el padre destruye el área de memoria compartida usando shmctl.

Semáforos

Como se ha visto en clase, será necesario manejar tres semáforos que llamaremos MUTEXCOLA (para el acceso a la cola) LLENO y VACIO. Para crear y manipular esos semáforos utilizaremos las llamadas del sistema semget, semop, semctl. El proceso padre creará los tres semáforos usando una única llamada a semget. Esta llamada devuelve un identificador de (grupo de) semáforos que llamaremos idsemaforos y declararemos como variable general. Cada semáforo particular se identifica con un número, en nuestro caso, del 0 al 2. Lo mejor es declararlos como constantes:

#define MUTEXCOLA 0
#define LLENO 1
#define VACIO 2

Se deberán programar las operaciones P(j) y V(j) sobre el semáforo j. Para ello, se usará la función semop (ver página man) pasándole como código de operación 1 (para sumar 1 al valor del semáforo haciendo una V) o código de operación -1 (para intentar restar uno al valor semáforo haciendo una P). La función será necesaria para inicializar el semáforo y para destruirlo al finalizar el proceso padre.


Lista de funciones a usar

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 entrega se realizará utilizando el sistema de buzón de prácticas. La fecha límite de entrega de la práctica es el viernes 19 de enero de 2007 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 en buzón que excepcionalmente no hayan sido presentadas en el laboratorio.
Última actualización: 12/12/2006