Práctica de programación: Concurrencia

Esta práctica trata sobre mecanismos básicos de implementación de sincronización y comunicación entre procesos. La mayoría de los textos sobre el tema detallan algoritmos empleando recursos tales como variables y arrays sobre memoria, de este modo los ejemplos son claros y sencillos. Para llevar a cabo esta práctica implementaremos procesos que comparten una zona de memoria común. Como es lógico ésto conlleva otros problemas.

El segundo problema no es tal, pues ya manejamos punteros con soltura. En cuanto al primero, hay que recurrir al interface de aplicación de memoria compartida de un S.O. concreto. El que utiliza UNIX es muy fácil de manejar (véase un ejemplo).

Para ocultar algunos detalles de implementación se utilizarán ciertas funciones que facilitan la memoria compartida. El resultado viene a ser:


#include "rshmem.h"

int main() {
   /* crear zona de memoria compartida */
   if (!crearMemoria())
      fprintf(stderr, "error de crearMemoria\n");

   /* proceso padre */
   if (0!=fork()) {
      while (*memoria != 'x') ;
      printf("he comprobado 'x' en memoria compartida\npulse una tecla");
      getchar(); /* comprobar el semaforo con 'ipcs' */
      /* eliminar memoria compartida */
      if (!eliminarMemoria())
         fprintf(stderr, "error de eliminarMemoria\n");
      exit(0);
   /* proceso hijo */
   } else {
      *memoria = 'x';
      exit(0);
   }
}
(cargar fuente)

Para compilar el programa debemos escribir:

     cc -o ejemplo.c rshmem.c


Mediante estos programas de ejemplo se pueden ilustrar diversos problemas de acceso concurrente, e implantar alguna de las soluciones existentes, de las que se ilustra la primera:

  1. Problema de la sección crítica. (véase ejemplo).
  2. Algoritmo de Deker.
  3. Algoritmo de Peterson.
  4. Algoritmo de la Panadería.

Nota importante

A menudo, por cualquier razón los programas de prueba que incluyen procesos y recursos compartidos quedan bloqueados en manos de programadores inexpertos (y también en el resto de programadores).

A tal efecto hay que recordar que el comando "kill" viene muy bien al caso.

Sin embargo en el caso de utilizar memoria compartida, semaforos y colas de mensajes (ipc's todos ellos) la cosa es un poco diferente. Estos recursos, en manos del sistema, se registran en el nucleo cada vez que los creamos: Si el programa no se comporta bien y sale por peteneras, no se "des-registran", de modo que debemos hacerlo manualmente.

Para resolver este ligero problema incluyo un programita que permite deshacerse de la memoria compartida no deseada. Una vez compilado el programa hay que seguir los siguientes pasos:

  1. Consultar los ipc's del sistema: comando "ipcs".
  2. Averiguar cual es el nuestro y obtener su número identificador (handler), es la segunda columna.
  3. Ejecutar el programita y proporcionarle susodicho número. Tener en cuenta que el programa es muy simple y hay que darle la entrada correcta.
  4. Recuerde que el sistema de permisos funciona igual que el del sistema de ficheros, y solo usted puede borrar sus ipc's.

Referencias: W. Richard Stevens "Advanced Programming in the Unix Environment" Addison-Wesley 1992


Ejemplo de utilización de memoria compartida en UNIX

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define ARRAY_SIZE   4000
#define MALLOC_SIZE 10000
#define SHM_SIZE    10000
#define SHM_MODE    (SHM_R | SHM_W)     /* read/write */

char array[ARRAY_SIZE];  /* datos sin inicializar = bss */

int main() {
   int shmid;
   char *ptr, *shmptr;

   printf("array[] desde %x hasta %x\n", &array[0], &array[ARRAY_SIZE]);
   printf("stack sobre %x\n", &;shmid);

   if ((ptr=malloc(MALLOC_SIZE)) == NULL)
      fprintf(stderr, "error de malloc()\n");
   printf("malloc desde %x hasta %x\n", ptr, ptr+MALLOC_SIZE);

   if ((shmid=shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE))<0)
      fprintf(stderr, "error de shmget()\n");
   if ((shmptr=shmat(shmid, 0, 0)) == (void *) -1)
      fprintf(stderr, "error de shmat()\n");
   printf("shared memory desde %x hasta %x\n", shmptr, shmptr+SHM_SIZE);
   /* proceso padre */
   if (0!=fork()) {
      while (*shmptr != 'x') ;
      printf("he comprobado 'x' en memoria compartida\npulse una tecla");
      getchar(); /* comprobar el semaforo con 'ipcs' */
      /* eliminar memoria compartida */
      if (shmctl(shmid, IPC_RMID, 0) < 0)
         fprintf(stderr,"error de shmctl()\n");

      exit(0);
   /* proceso hijo */
   } else {
      *shmptr = 'x';
      exit(0);
   }
}
(cargar fuente)

y el resultado de su ejecución será algo asi como:


     array[] desde 40001220 hasta 400021c0
     stack sobre 7b03a5e8
     malloc desde 400041c8 hasta 400068d8
     memoria compartida desde c06d4000 hasta c06d6710
     he comprobado 'x' en memoria compartida

volver

rshmem.h

Fichero de cabecera con definiciones y declaraciones para usar memoria compartida.

#include <stdio.h>
#include <time.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define ARRAY_SIZE   4000
#define MALLOC_SIZE 10000
#define SHM_SIZE    10000
#define SHM_MODE    (SHM_R | SHM_W)     /* read/write */

#define TRUE      1
#define FALSE     0

#ifdef RUTINAS_SHMEM
   static int shmid; /* handler de memoria compartida */
   char *memoria;    /* puntero a zona de memoria compartida */
#else
   extern char *memoria;
#endif

/* Prototipos de funciones de memoria compartida */

void origenTiempo();
void tiempoPasa(); 
int crearMemoria() ;
int eliminarMemoria() ;

#define TP tiempoPasa();

(cargar fuente)
volver

rshmem.c

Fichero con funciones de creación de memoria compartida y varias de utilidad.

#define RUTINAS_SHMEM

#include "rshmem.h"

/* Crea memoria compartida.
*    - el manejador de memoria es interno
*    - manda mensajes de error por salida de error estándar.
*/
int crearMemoria() {
   char *funcName = "crearMemoria";
   if ((shmid=shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE))<0) {
      fprintf(stderr, "%s: error de shmget()\n", funcName);
   } else if ((memoria=shmat(shmid, 0, 0)) == (void *) -1) {
      fprintf(stderr, "%s: error de shmat()\n", funcName);
   } else {
      return TRUE;
   }
   return FALSE;
}

/* Destruye la memoria compartida creada por crearMemoria()
*/
int eliminarMemoria() {
   char *funcName = "eliminarMemoria";
   if (shmctl(shmid, IPC_RMID, 0) < 0) {
      fprintf(stderr,"%s: error de shmctl()\n", funcName);
      return FALSE ;
   } else
      return TRUE ;
}


/* Coloca una semilla en el temporizador del bucle de
*  tiempoPasa()
*/
void origenTiempo(){
   srand((unsigned int) time(NULL)) ;
}

/* Rutina que hace pasar un poco de tiempo con un bucle
*  sencillo
*/
void tiempoPasa() {
   unsigned int i;
   int a=3;

   /* Los parametros "50" y "2" dependen mucho de la velocidad
   *  de la computadora y de la configuracion del SO. Espero que
   *  funcionen bien
   */
   for (i=rand()/50; i>0; i--)
      a = a%3 + i;
}
(cargar fuente)
volver

Ejemplo de dos procesos con condición de carrera

#include "rshmem.h"

void incrementa(int *mem, int k) {
   int i;
   i=*mem; TP
   i=i+k;  TP
   *mem=i;
}

int main() {
   int  *recurso;
   char *marcaFin;

   /* crear zona de memoria compartida */
   if (!crearMemoria())
      fprintf(stderr, "error de crearMemoria\n");

   recurso  = (int *) memoria ;
   marcaFin =  memoria + sizeof(int) ;
   *recurso =   0 ;
   *marcaFin = 'p' ;
   if (0!=fork()) {    /* proceso padre */
      int i;
      for (i=0; i< 1000; i++)
         incrementa(recurso, -5);
      while (*marcaFin != 'x') ; /* espera al hijo */

      printf("El recurso valia 0 y ahora vale %d\n", *recurso);
      if (!eliminarMemoria())   /* eliminar memoria compartida */   
         fprintf(stderr, "error de eliminarMemoria\n");
      exit(0);
   } else {            /* proceso hijo */
      int i;
      for (i=0; i< 1000; i++)
         incrementa(recurso, 5);
      /* termina */
      *marcaFin = 'x';
      exit(0);
   }
}
(cargar fuente)

La función "incrementa()" implementa el acceso al recurso compartido. Se ha incluido ciertas llamadas al macro "TP" que implementan un retardo aleatorio con el fin de dar cierto tiempo de que ocurran interrupciones que puedan detener la ejecución del proceso en su "cuanto" de tiempo asignado por el SO. Esto da mayor realismo al procedimiento "incrementa()", que en el caso real puede ser más complejo y largo que en el caso actual.

Al compilar el programa y ejecutarlo varias veces nos encontramos con que el resultado de su ejecución varía, no siendo siempre 0, e ilustrando que las respectivas funciones "incrementa()", del proceso padre y del proceso hijo, entrelazan sus instrucciones, dando lugar a un resultado erróneo. volver


Ejemplo de destrucción de memoria compartida en UNIX

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define ARRAY_SIZE 4000
#define MALLOC_SIZE 10000
#define SHM_SIZE 10000
#define SHM_MODE (SHM_R | SHM_W) /* read/write */

int main() {
   int  shmid;
   char shmidStr[128];

   printf("Que zona de memoria desea destruir? ");

   shmid = atoi(gets(shmidStr));
   if (shmctl(shmid, IPC_RMID, 0))<0) 
      fprintf(stderr, "error de shmctl()\n"); 

   exit(0);
}
(cargar fuente)
volver
Apriete aqui para volver a la página raíz