<< >> Título

Guía de implementación


En este trabajo, las cuestiones principales en materia de concurrencia pueden ser: ¿cómo sabe el servidor que hay una orden pendiente? ¿cómo sabe el cliente que su orden se ha realizado? y ¿de qué manera el cliente y el servidor se envían información?

Las anteriores preguntas constituyen el problema fundamental de sincronización y comunicación que hay que resolver. Los dos apartados que vienen a continuación tratan de este asunto, según decidan emplear memoria compartida o colas de mensajes. Lean el apartado que se corresponda con su elección.

Los últimos apartados de esta sección tratan de asuntos generales de implementación, válidos para cualquiera de las herramientas IPC que empleen.

Comunicación con memoria compartida y semáforos

Problemas de sincronización

La primera cuestión es cómo indica el cliente al servidor que hay una orden pendiente. Se puede disponer de un semáforo, cuya clave conozcan tanto los clientes como el servidor. El semáforo estará inicializado a cero. Cada vez que un cliente solicita un servicio, realiza una operación V sobre el semáforo. Por su parte, en cada iteración el servidor haría una operación P sobre el mismo semáforo.

Un proceso cliente ha de bloquearse hasta que el servidor le envíe una respuesta. Esto se puede conseguir con semáforos, igual en el caso anterior, pero en sentido inverso: el cliente, tras haber enviado su mensaje, se bloqueará con una operación P sobre un semáforo de petición servida. Cuando el servidor complete su petición, ejecutará una V sobre ese mismo semáforo.

Para realizar las transferencias de información, habrán de utilizar una o varias zonas de memoria compartida. Tendrán que tener cuidado si varios procesos, por ejemplo varios clientes, desean escribir simultáneamente en la zona. Así que tendrán que emplear un semáforo para garantizar la exclusión mutua.

Propuesta de algoritmo de comunicación

Estos son fragmentos algorítmicos que desarrollan las soluciones que acabamos de plantear, y que estarán en el interior de las funciones de envío y recepción (véase interfaz de comunicaciones). En estos algoritmos se supone que la misma zona de memoria se emplea para la emisión de peticiones y de respuestas.

El semáforo cerrojo_memoria inicialmente vale uno, mientras que los de petición_pendiente y petición_servida están inicializados a cero.

Proceso cliente (al realizar una petición):


P (cerrojo_memoria)	/* Bloqueo de la memoria compartida */

  ... escribe petición en la memoria ...

V (petición_pendiente)	/* Aviso al servidor */


P (petición_servida)	/* Espera por que se complete */

  ... recoge la respuesta de la memoria ...

V (cerrojo_memoria) /* Desbloqueo de la memoria compartida */

Proceso servidor (al esperar por una petición):


P (petición_pendiente)	/* Espera por una petición */

... recoge la petición de la memoria ...

	... atiende la petición ...

... escribe la respuesta en la memoria ...

/* Para dar por servida una petición */
V (petición_servida)

Comunicación mediante colas de mensajes

Si utilizan colas de mensajes, no son necesarias herramientas adicionales para sincronizar procesos. En primer lugar, el proceso que quiera recibir información ejecutará la operación msgrcv con el parámetro msgflg a cero. Esto significa que el proceso quedará bloqueado mientras no llegue un mensaje a la cola.

En este trabajo hay dos flujos de comunicación: uno va de los clientes al servidor y otro en sentido contrario. Pueden utilizar una cola para cada flujo, o bien hacer todas las transferencias con una sola cola de mensajes. En este último caso han de asignar tipos diferentes para los mensajes de peticiones y de respuestas (el tipo son los primeros bytes del mensaje). La llamada msgrcv permite especificar que se atiendan sólo los mensajes de un tipo determinado. El problema en este caso es cómo conseguir que cada cliente utilice un identificador diferente (una buena idea es utilizar el pid del proceso, que es único).

A continuación se muestran unas líneas de código con las operaciones de comunicación entre los clientes y el servidor, haciendo uso de una única cola de mensajes. Hay bastantes detalles que no aparecen en estos algoritmos, así que no las tomen al pie de la letra.

Proceso cliente:


/* Construye la petición y la envía al servidor */
struct {
  long mtype;
  long tipo;
  ... otros campos ...
} mi_mensaje;

mi_mensaje.mtype = CLAVE_SERVIDOR;
mi_mensaje.tipo = MI_CLAVE_UNICA;
... prepara el resto del mensaje ...

msgsnd (cola,&mi_mensaje,tamaño1,0);

/* Recibe la respuesta */
/* Sólo reconoce el mensaje cuyo tipo sea MI_CLAVE_UNICA */

msgrcv (cola,&mi_respuesta,tamaño2,MI_CLAVE_UNICA,0);

Proceso servidor:


struct {
	long mtype;
	long tipo;
	... otros campos ...
} mi_petición;


/* Espera por una petición y la recibe */
msgrcv (cola,&mi_petición,tamaño1,CLAVE_SERVIDOR,0);

... atiende la petición y genera la respuesta...


/* Envía la respuesta a la cola de mensajes, con el tipo del cliente para que sea sólo él quien la reciba */
struct {
	long mtype;
	... otros campos ...
} mi_respuesta;

mi_respuesta.mtype = mi_petición.tipo;
msgsnd (cola,&mi_respuesta,tamaño2,0);

Cómo dejar al servidor ejecutándose en segundo plano

Según las especificaciones, el programa servidor ha de mantenerse ejecutándose en segundo plano, esperando continuamente la llegada de peticiones de procesos clientes. Para lograr este comportamiento habrá que realizar algunas operaciones nada triviales que es preciso citar aquí.

En primer lugar, para dejar al servidor ejecutándose mientras proseguimos con el shell, se ha de recurrir a la llamada fork para crear un proceso hijo (el propio servidor) que se dejará concurrente sin esperar por que termine.

Aun así, el proceso no les funcionará a no ser que inhiban la señal SIGTTOU, puesto que de ordinario se deja colgados a los procesos de segundo plano (background) que intentan escribir en la terminal. Puede que les suene a arameo, pero hay que hacerlo.

En fin, la receta completa en C para lanzar el servidor es más o menos la que sigue:

#include <signal.h>

...
...

if ( fork()==0 )
{
  signal (SIGTTOU,SIG_IGN); /* Ignora el cuelgue por escribir en TTY */
  
  ... código del servidor (el bucle principal y lo demás)
}

Redirección de la salida estándar

La salida estándar se corresponde con el descriptor de fichero número 1. Si se cierra el descriptor 1 e inmediatamente después se abre un fichero, se le asigna el descriptor 1. El resultado es que la salida estándar se redirige a este fichero:

close(1); /* se cierra la salida estándar */

open("mi_fichero.txt"); /* se le asigna el descriptor 1 */

Deberán escribir dos líneas parecidas a estas justo antes de invocar a una función exec() en el código del servidor.

Detección de la muerte del servidor

¿Cómo sabe un cliente que el servidor ha finalizado? La forma más simple de detectarlo consiste en que una vez que un cliente se desbloquea, comprueba el valor de retorno de las funciones que ha invocado para operar con los recursos IPC utilizados; si se detecta retorno por error y la variable global del sistema errno contiene un código que indica que los recursos IPC utilizados ya no existen, entonces es que el servidor ha finalizado y los ha borrado.


<< >> Título