next up previous contents
Next: Estructura del Computador Up: Procesos Previous: Monitores

Sistemas de mensajes

Como mencionamos antes, los mensajes se inventaron para hacer que procesos que no comparten memoria puedan comunicarse, pero también pueden usarse para sincronizar procesos que comparten la memoria.

En todo sistema de mensajes existe una operación para enviar un mensaje y otra para recibir un mensaje. En algunos sistemas también existe una operación para responder un mensaje. Sin embargo la semántica de estos sistemas varía ampliamente, y por lo tanto la programación también varía. A continuación clasificaremos los sistemas de mensajes según sus aspectos semánticos.

Comunicación directa

En este tipo de sistemas los mensajes se envían directamente al proceso que debe recibirlo. Para ello, al enviar el mensaje se especifica la identificación del proceso receptor ( process identication o pid).

send(dest_pid, msg)

Ejemplos de este tipo de mensajes son los que implementan nSystem y el lenguaje ADA.

Los sistemas de comunicación directa se presentan en varios sabores:

  1. Recepción de un solo proceso: El sistema permite seleccionar el proceso emisor del mensaje.

    msg= Receive(sender_pid)

    Si es otro el proceso emisor, el mensaje queda encolado hasta que se especifique la identificación de ese proceso.

  2. Recepción de cualquier proceso: El sistema obliga a recibir los mensajes provenientes de cualquier proceso.

    msg= Receive(&sender_pid)

    Esto significa que el receptor debe estar siempre preparado para recibir mensajes de cualquier proceso. Este es el caso de nSystem y ADA.

  3. Recepción de algunos procesos: Se puede especificar un conjunto de los identificadores de procesos de donde se acepta la recepción de mensajes de inmediato. Los mensajes de otros procesos quedan encolados.

  4. Broadcast o envío a todos los procesos: Se envía un mensaje que es recibido por todos los procesos.

    Broadcast(msg)

    El mensaje se recibe en cada proceso mediante Receive.

  5. Multicast o envío a varios procesos: Se envía un mismo mensaje a varios procesos. Esto es equivalente a usar varias veces Send con el mismo mensaje pero con distintos destinatarios.

Comunicación indirecta

Los mensajes se envía a través de canales de comunicación. Al enviar un mensaje se especifica la identificación del canal.

send(chan_id, msg)

Por ejemplo los streams o pipes de Unix corresponden a este tipo de mensajes.

En algunos sistemas de comunicación indirecta también se puede especificar un conjunto de canales de donde recibir mensajes. Por ejemplo en Unix se usa select para seleccionar un conjunto de streams por lo cuales se desea recibir datos. Esto es muy útil cuando se desea leer comandos ingresados a través de varios terminales. En Unix System V se puede hacer lo mismo con poll.

Los canales de comunicación pueden ser del siguiente tipo:

  1. Punto a punto unidireccional: Sólo un proceso puede enviar mensajes por un canal y sólo un proceso puede recibir mensajes de ese canal. En el lenguaje OCCAM los canales son de este tipo.

  2. Punto a punto bidireccional: Un par de procesos usa el canal para comunicarse en ambos sentidos. Los streams de Unix pertenecen a esta clase de canales.

  3. Puerta o port: Cualquier proceso puede enviar mensajes por ese canal. El emisor especifica otro canal por donde recibir la respuesta.

Los canales pueden estar orientados hacia el envío de mensajes como es el caso del lenguaje OCCAM. En otros sistemas los canales están orientados hacia el envío de secuencias de caracteres como es el caso de los streams de Unix.

Buffering

Los sistemas de mensajes se agrupan según la cantidad de mensajes que se pueden enviar sin que el emisor se quede bloqueado.

  1. 0-buffering: Al enviar un mensaje, el emisor se queda bloqueado hasta que:

    Al segundo tipo de mensajes corresponde el nSystem y los sistemas de RPC o Remote Procedure Call.

    La comunicación del tipo 0-buffering también se denomina síncrona.

  2. buffer acotado: Los mensajes se copian en un buffer de taman o finito de modo que el emisor puede continuar y enviar nuevos mensajes. Si el buffer está lleno, el emisor se bloquea hasta que algún proceso receptor desocupe parte del buffer.

    Este tipo de comunicación se denomina asíncrona. Al enviar un mensaje, el solo hecho de que Send retorne no implica que el mensaje fue recibido. Si el emisor necesita conocer cuando un mensaje fue recibido, debe esperar un mensaje de vuelta (un reconocimiento) del receptor.

    Emisor:

      Send(dest_pid, msg);
      ack= Receive(dest_pid);
    

    Receptor:

      msg= Receive(&dest_pid);
      Send(dest_pid, ack);
    

  3. buffer ilimitado: Como el caso anterior, sólo que el buffer es de taman o infinito. Esta clasificación sólo sirve para fines teóricos, pues en la práctica no existe.

Ejemplo: El problema de los escritores y lectores resuelto con mensajes

A continuación veremos una solución equitativa para el problema de los lectores y escritores. Usaremos mensajes y tareas de nSystem para lograr que los lectores y escritores ingresen en orden FIFO. Los lectores podrán entrar concurrentemente y por lo tanto podrán salir en cualquier orden. Pero los escritores no podrán entrar mientras hayan otros lectores u otro escritor usando la estructura compartida.

La solución se basa en que los lectores y escritores tienen que enviar un mensaje para poder entrar a consultar o actualizar la estructura de datos compartida. Este mensaje lo llamaremos petición de entrada. Como el envío de mensajes en nSystem es síncrono, el lector o escritor se quedará bloqueado hasta que la petición de entrada sea respondida. Entonces la clave de esta solución está en que la petición sea respondida en el momento adecuado.

Las peticiones de entrada serán enviadas a una tarea de servicio denominada queue_task. Esta tarea sirve para encolar las peticiones de entrada.

Al salir de la zona de consulta o actualización, los lectores y escritores envían una notificación de salida --un mensaje-- a una segunda tarea de servicio que llamaremos ctrl_task. Esta tarea se encarga de responder las peticiones de entrada y las notificaciones de salida y por lo tanto es la que controla el acceso a la estructura de datos.

Veamos primero como se codifican las peticiones y notificaciones:

typedef enum
{
  ENTERREAD, ENTERWRITE,
  EXITREAD, EXITWRITE
} REQUEST;

Inicialmente:

queue_task= nEmitTask(QueueProc);
ctrl_task= nEmitTask(CtrlProc);

Los procedimientos para la petición de ingreso y notificación de salida:

void EnterRead()
  { SendRequest(ENTERREAD, queue_tast); }
void EnterWrite()
  { SendRequest(ENTERWRITE, queue_task); }
void ExitRead()
  { SendRequest(EXITREAD, ctrl_task); }
void ExitWrite()
  { SendRequest(EXITWRITE, ctrl_task); }

void SendRequest(REQ Req, nTask task)
  { nSend(task, &Req); }

La tarea de encolamiento recibe secuencialmente las peticiones de entrada y las reenvía a la tarea de control. Al reenviar cada petición, la tarea de encolamiento se queda bloqueada hasta que la petición sea respondida por la tarea de control. Si en el intertanto otros lectores o escritores envían peticiones de ingreso, éstos quedarán naturalemente bloqueados en espera de que la tarea de encolamiento pueda recibir sus peticiones.

int QueueProc()
{
  for(;;)
  {
    nTask task;
    /* Estado A: */
    REQUEST* req=(REQUEST*)
                 nReceive(&task, -1);
    /* Estado B: */
    nSend(ctrl_task, req);
    nReply(req->task, 0);
} }

La tarea de control recibe las peticiones de entrada provenientes de la tarea de encolamiento y las notificaciones de salida directamente de los lectores y escritores. Esta tarea siempre esta lista para recibir las notificaciones y las responde de inmediato para no bloquear a los escritores o lectores en esta operación.

La programación de la tarea de control se facilita enormemente gracias a la presencia de la tarea de encolamiento. En efecto, mientras la tarea de control no responda una petición de entrada, no recibirá nuevas peticiones de entrada, porque la tarea de encolamiento se encuentra bloqueada. Sólo recibirá notificaciones de salida.

Las peticiones de ingreso son siempre respondidas en orden FIFO. Se pueden responder varias peticiones de lectores sin recibir aún sus notificaciones de salidas. Al recibir una petición de un escritor, ésta se responde sólo cuando hayan salido todos los lectores que alcanzaron a ingresar. Mientras el escritor no envíe su notificación de salida no se responde cualquier otra petición de ingreso, sea ésta de un escritor o de un lector.

int CtrlProc()
{
  int readers= 0;
  nTask task;
  /* Estado A: */
  REQUEST* req=(REQUEST*)
               nReceive(&task, -1);
  for(;;)
  {
    /* Autorizamos el ingreso de todos
     * los lectores que vengan,
     * mientras no llegue algun
     * escritor */
    while (*req!=ENTERWRITE)
    {
      if (*req==ENTERREAD)
        readers++;
      else /* *req==EXITREAD (1) */
        readers--;
      nReply(task, 0);
      /* Estado B: */
      REQUEST* req=(REQUEST*)
                   nReceive(&task, -1);
    }
    /* task es un escritor. No podemos
     * responder su peticion mientras
     * hayan lectores pendientes */
    while (readers>0)
    {
      nTask rdr_task;
      /* Estado C: */
      nReceive(&rdr_task, -1);
      /* es un EXITREAD (2) */
      readers--;
      nReply(rdr_task, 0);
    }
    /* Ahora si dejamos entrar al
     * escritor */
    nReply(task, 0);
    /* Ahora no podemos aceptar mas
     * ingresos mientras no salga */
    /* Estado D: */
    req= (REQUEST*)nReceive(&task, -1);
    /* Puede ser la salida o una nueva
     * peticion de entrada */
    if (*req==EXITWRITE)
    { /* Ok, era la salida */
      nReply(task, 0);
      /* Esperamos la siguiente peticion,
       * Estado A: */
      req= (REQUEST*)nReceive(&task, -1);
    }
    else
    { /* No era la salida, pero la
       * proxima sí tiene que ser */
      nTask wrt_task;
      /* Estado E: */
      nReceive(&wrt_task, -1);
      /* tiene que ser la notificacion
       * de salida del escritor (3) */
      nReply(wrt_task, 0);
} } }

Observe que en (1) la petición tiene que ser un EXITREAD ya que se descartaron las dos posibles peticiones de ingreso y no puede ser un EXITWRITE porque en este estado no hay escritor actualizando. En (2) no pueden llegar peticiones de entrada porque la tarea de encolamiento se encuentra bloqueada a la espera de la respuesta a la petición del escritor, y el escritor no ha ingresado todavía por lo que no puede enviar un EXITWRITE. En (3) el mensaje tiene que ser el EXITWRITE del único escritor autorizado para ingresar. La tarea de encolamiento está bloqueada y no puede enviar más peticiones y como no hay lectores consultando, tampoco puede ser un EXITREAD.

Diagrama de procesos

Para comprender mejor el disen o de una aplicación multitarea es útil hacer un diagrama de las tareas (o procesos) involucrados. Cada nodo en el diagrama corresponde a una tarea y la presencia de una flecha entre dos nodos indica que una tarea envía mensajes a otra tarea del tipo especificado en el rótulo.

La figura gif es el diagrama de procesos para la solución de los lectores y escritores con mensajes.

  
Figure: Diagrama de procesos para los lectores y escritores.

Diagrama de estados de un proceso

El diagrama de estados de un proceso es un grafo en donde cada nodo indica un estado de espera por el que puede pasar ese proceso. Un estado de espera es una situación en donde el proceso queda bloqueado a la espera de algún evento que producirá otro proceso o el usuario. En el nodo se indica el o los eventos que espera el proceso en ese estado. La figura gif es el diagrama de estados para la tarea de encolamiento.

  
Figure: Diagrama estados de la tarea de encolamiento.

Las flechas entre los nodos indican la ocurrencia de un evento que hace que el proceso pueda continuar hasta caer en un nuevo estado de espera. Cada flecha se rotula con el evento que causa la transición entre dos estados. En algunos casos el estado al cual se transita depende de una condición que es útil especificar en el rótulo de la flecha, como se aprecia en el diagrama de estados de la tarea de control (ver figura gif). También es útil indicar en los rótulos las acciones más importantes que se realizan al transitar de un estado a otro. Estas acciones vienen a continuación del caracter ``/''.

  
Figure: Diagrama estados de la tarea de encolamiento.



next up previous contents
Next: Estructura del Computador Up: Procesos Previous: Monitores



José M. Piquer
Fri Apr 9 15:57:37 CLT 1999