Práctica de comunicación entre procesos


Universidad de Las Palmas de Gran Canaria


Sistemas Operativos

Práctica de comunicación entre procesos
curso 1996/97

Escuela Universitaria de Informática

L a presente práctica les servirá para trabajar con procesos concurrentes que se comunicarán y sincronizarán entre sí mediante herramientas del sistema UNIX. Se trata de construir un intérprete de órdenes al estilo de los shells de UNIX, con un proceso central (un servidor) que ejecutará órdenes solicitadas por otros procesos, que actuarán como clientes suyos.

Este documento introduce brevemente el modelo de comunicación cliente/servidor. A continuación, en las secciones de la a la , se describe en líneas generales la aplicación que hay que desarrollar. La sección es una guía de implementación que trata, entre otras cosas, del mecanismo para comunicar a los procesos concurrentes y del protocolo de sincronización.

Al finalizar este trabajo, habrán adquirido experiencia en programación de aplicaciones concurrentes en entorno UNIX, utilizando estas herramientas:

* Funciones para ejecutar y sincronizar procesos concurrentes (fork, wait y exit)

* Funciones de ejecución de programas (familia de funciones exec...)

* Servicios IPC (semáforos, memoria compartida y colas de mensajes)

Índice

El modelo cliente/servidor

Un proceso puede proporcionar unos servicios a los restantes procesos del sistema. Estos servicios serán operaciones de diverso tipo, por ejmplo imprimir un documento, leer o escribir una información, etc. En el modelo cliente/servidor, cuando un proceso desea un servicio que proporciona cierto proceso, le envía un mensaje solicitando ese servicio: una petición. El proceso que cumple el servicio se llama servidor y el solicitante se llama cliente.

Los procesos clientes y servidores han de seguir un protocolo de comunicaciones que defina: a)cómo se codifican las peticiones; y b)cómo se sincronizan entre sí los procesos.

Los clientes y servidores han de estar de acuerdo en cómo se escriben los mensajes: en qué orden van los posibles parámetros de la petición, cuántos bytes ocupan, etc.

La forma de sincronización nos dice si el cliente puede seguir adelante justo después de enviar la petición (no bloqueante), o por el contrario tiene que esperar a que el servidor le envíe una respuesta (bloqueante). Si la comunicación es no bloqueante, habrá que definir un mecanismo para que el cliente pueda saber si la respuesta del cliente está disponible. En esta práctica se adoptará una comunicación bloqueante: el cliente siempre esperará hasta recibir una respuesta del cliente.

El diálogo cliente/servidor es casi siempre bidireccional. Por un lado, el cliente envía información al servidor (el tipo de servicio solicitado más los parámetros); por otro, el servidor devuelve información al cliente (los resultados del servicio, códigos de error en caso de producirse, etc.)

Descripción general

Esta práctica consiste en el diseño e implementación de un intérprete de órdenes al estilo de los shells de UNIX, utilizando una arquitectura cliente/servidor. El intérprete de órdenes será un proceso servidor que se ejecutará en segundo plano. Tal servidor atenderá las peticiones de otros procesos clientes, según un protocolo de comunicaciones conocido por ambas clases de procesos.

Por tanto, en esta práctica hay que escribir el código de un proceso servidor y de un proceso cliente.

Especificaciones del servidor y del cliente

Programa servidor (intérprete de órdenes)

El programa servidor tendrá la siguiente sintaxis:

	servidor

Al invocar al servidor, quedará automáticamente en segundo plano (background), en espera de atender las futuras peticiones de los procesos clientes hasta que reciba una orden especial de finalización.

El servidor ejecutará las órdenes secuencialmente, una detrás de otra. Esto es, el servidor no puede lanzar varias órdenes concurrentes, y siempre tendrá que esperar a que una orden finalice para tramitar la siguiente.

Los clientes que envíen peticiones mientras el servidor está ejecutando una orden tendrán que esperar. Cuando una orden termine de ejecutarse, el servidor deberá notificar el resultado de la ejecución al proceso cliente que la demandó (el cual estará esperando por la notificación).

Mientras no haya órdenes que atender, el servidor permanecerá bloqueado.

Programa cliente

El programa cliente admitirá dos opciones de trabajo:

cliente arg1 arg2 ... argN

cliente finaliza

El cliente admitirá uno o más argumentos, que constituyen la orden que se transmite al servidor. El cliente quedará bloqueado hasta que la orden haya sido ejecutada por completo, momento en el que el servidor le enviará una notificación.

El primer argumento (arg1) es el nombre del programa que se quiere ejecutar. Es obligatorio al menos un argumento. En el caso especial de que el primer argumento sea la cadena finaliza, se enviará al servidor una petición de que finalice.

Las órdenes enviadas no contendrán asteriscos, redirecciones ni tuberías (complicarían en exceso la implementación de la práctica).

La salida de datos de la orden ejecutada se mostrará en la misma terminal del cliente.

En caso de que el servidor finalice o aborte durante la ejecución de la orden, el cliente deberá desbloquearse y terminar imprimiendo un mensaje alusivo.

Ejemplos

Toda orden que se le envía al servidor para que la ejecute deberá ser pasada como parámetro de ejecución del programa cliente, es decir, si por ejemplo queremos que se ejecute un cliente que demande la ejecución del orden del sistema date (mostrar fecha y hora actual), entonces se deberá teclear desde el símbolo del sistema lo siguiente:

$ cliente date

Otro ejemplo: para solicitarle al servidor que muestre el contenido de un supuesto directorio cuya ruta es /mi_dir, entonces habría que teclear desde el símbolo del sistema la línea:

$ cliente ls /mi_dir

Las órdenes con caracteres comodín, como ls *.txt, no les van a funcionar correctamente, así que no las empleen.

Comunicación entre procesos

Los clientes y el servidor han de transferirse información (las peticiones y las respuestas). Además, los procesos del sistema tendrán que sincronizarse en ciertos momentos (ej. cuando un cliente espera a que se complete una petición).

El sistema operativo UNIX ofrece múltiples mecanismos para realizar comunicaciones entre procesos: utilizar un fichero normal, un fichero FIFO o pipe, zonas de memoria compartida o colas de mensajes. Los ficheros FIFO y las colas de mensajes facilitan además la sincronización.

El UNIX tiene semáforos como herramienta especialmente destinada a la sincronización entre procesos.

Para esta práctica, ustedes tendrán que optar entre uno de estos dos conjuntos de herramientas:

* Memoria compartida y semáforos

* Colas de mensajes

Interfaz de comunicaciones

Las comunicaciones se establecerán a través de algún mecanismo de comunicación entre procesos, que ustedes habrán de elegir. No obstante, la interfaz de comunicaciones será impuesta, tal y como se define en este documento.

Básicamente, se tienen sendas estructuras para que el cliente o el servidor envíen mensajes, así como dos parejas de funciones de envío y recepción de peticiones y respuestas.

Las estructuras de datos empleadas serán las siguientes:

/* Constantes */

#define DIM_ORDEN	200 	/* Long. máxima de la orden */
#define DIM_CANAL	128	/* Long. máx. nombre de tty */

#define FINALIZA		7	/* Código de finalización */


/* Códigos de error */

typedef enum
{
	ERR_OK = 0,
	ERR_HIJO = 1000,
	ERR_CARGA = 2000,
	ERR_EJEC = 3000

} Codigo_error;


/* Estructuras para peticiones y respuestas */

struct Peticion
{
	char	orden[DIM_ORDEN];	/* orden para ejecutar */
	char	canal[DIM_CANAL];	/* fichero donde se visualiza */
};

struct Respuesta
{
	Codigo_error resultado;
};

El cliente enviará al servidor una petición con la estructura struct Peticion. El campo orden contendrá la orden completa. En caso de una orden de finalizar, el campo orden[0] tendrá el valor 7 (constante FINALIZA). El campo canal es una cadena de caracteres donde se guarda la ruta de la terminal del cliente.

Las funciones para efectuar las comunicaciones han de tener estos prototipos:

/* Funciones de comunicación para el cliente */
int envia_peticion ( const struct Peticion* );
int espera_respuesta ( struct Respuesta* );

/* Funciones de comunicación para el servidor */
int recibe_peticion ( struct Peticion* );
int envia_respuesta ( const struct Respuesta* );

Estas cuatro funciones devuelven un número entero, cuyo significado se describe a continuación:

Valor

Significado
0
La operación se completó satisfactoriamente
1
El servidor ha finalizado antes de tramitar la orden (en las funciones propias del cliente)
-1
Hubo algún error

Las estructuras y prototipos de funciones que conforman la interfaz de comunicaciones están ya definidas en el archivo /prac/cliser/comunicacion.h

Descripción detallada

Funcionamiento del servidor

El proceso intérprete de órdenes deberá esperar a que algún proceso cliente le solicite sus servicios. Mientras esto no ocurra permanecerá bloqueado. Cuando se le demande un servicio, saldrá del estado de bloqueo y actuará. Básicamente el esquema de funcionamiento de este proceso es el siguiente:

Generar el proceso demonio.
Establecer mecanismos de comunicación.

for (;;)
{
	recibe_petición (&mi_peticion);

	if ( orden de finalizar ) break;

	Procesar línea de órdenes.
	Ejecutar orden.
	Esperar por la finalización de la orden.
	envia_respuesta (codigo_de_respuesta);
}

Eliminar los mecanismos de comunicación.
Terminar

Observe cómo se trata de un proceso que entra en un bucle infinito, del cual se sale cuando algún proceso cliente le ordene al servidor su finalización. Esta es una estructura típica para un proceso servidor, y aparece con frecuencia en los lenguajes de paso de mensajes o los sistemas dirigidos por eventos (por ejemplo, al programar en un entorno de ventanas). Consiste en un bucle sin fin en el que cada iteración consiste en recibir un mensaje y atenderlo, así de simple.

Seguidamente se detallan los aspectos más importantes de cada punto de la ejecución.

Generación de un proceso demonio

Consiste en la creación de un nuevo proceso que se deberá ejecutar en segundo plano (proceso demonio). Una vez creado el nuevo proceso, el proceso deberá finalizar. De esta forma, el servidor no acapara la terminal donde se ha lanzado.

Establecimiento de los mecanismos de comunicación

Consiste en la creación de los recursos que permitirán la comunicación entre el servidor y los clientes. Para esta práctica se podrán utilizar dos clases de herramientas de intercomunicación: colas de menajes, o bien usar conjuntamente semáforos y memoria compartida.

Espera por una nueva petición

El servidor habrá de bloquearse mientras no tenga ninguna solicitud de servicio. Así pues, se tendrá que diseñar un mecanismo de sincronización y comunicación que garantice que mientras no haya solicitud, el servidor permanezca en estado de bloqueo (sin consumo de CPU). Además, este mecanismo deberá garantizar la integridad de las peticiones; esto es, una petición que se está recogiendo no podrá ser destruida por la llegada de una nueva.

Comprobación del servicio solicitado

En este paso se comprueba si el servicio requerido se corresponde con una orden de finalización del servidor; o, por contra, se solicita la ejecución de un programa. Como se ha dicho, esto viene expresado por el valor del campo orden[0] de la petición. Si vale FINALIZA, el servidor sale del bucle, elimina los recursos IPC que haya creado y termina su ejecución.

Procesamiento de la orden

En este paso se produce el análisis de la cadena de caracteres que contiene la orden a ejecutar. Este tratamiento tiene la finalidad de obtener: a) el nombre del fichero ejecutable y b) cada uno de los argumentos del programa. Las funciones de rastreo de cadenas, como strchr, pueden ser útiles para trocear la línea de órdenes.

Cada uno de los datos extraídos se empleará para invocar posteriormente al programa solicitado, haciendo uso de alguna función exec... del UNIX.

Ejecución de la orden

El programa servidor creará un nuevo proceso hijo mediante la función fork. El proceso padre deberá esperar a que el hijo recién lanzado finalice su ejecución; la espera se hará mediante la función wait.

Por su parte, el proceso hijo deberá cargar y ejecutar el programa solicitado con los argumentos especificados en la línea de órdenes mediante el uso de alguna de las funciones exec.

La salida estándar de la orden habrá de redirigirse al archivo especificado en el campo canal de la petición recibida. Más adelante se explica cómo se realiza esto.

Algunas de las situaciones de error que pueden surgir, y que deberán ser notificadas al cliente en la respuesta, son:

1. No se pudo crear el proceso hijo con el fork (devolver ERR_HIJO)

2. No se pudo lanzar el programa con exec (devolver ERR_CARGA)

3. La orden dio un resultado erróneo (devolver ERR_EJEC)

Si no ocurre nada anómalo, se le entrega al cliente el valor ERR_OK

Espera por la finalización del servicio solicitado

Tal y como se ha indicado, esto lo realizará el servidor mediante el uso de la llamada al sistema wait. Lo único a reseñar es que no se deberá llegar a este punto si se produce un error en la invocación a la función fork.

Si el error se produce en la carga o en la ejecución del programa solicitado, entonces, se podrá detectar consultando el argumento utilizado en la llamada a la función wait.

Notificación del resultado al cliente

El servidor deberá comunicar al cliente que le solicitó el servicio si la orden demandada se ha realizado o no. Para ello se utilizará la función envia_respuesta(), que ustedes habrán de implementar.

Estructura de un proceso cliente

Los pasos básicos de ejecución de un cliente son:

Construir petición en mi_peticion

envia_peticion(&mi_peticion);

recibe_respuesta(&resultado);

Comprobar resultado de la ejecución.

Finalizar.

Seguidamente se describe lo que hace cada uno de estos pasos:

Construcción de la petición para el servidor

Las primeras instrucciones del servidor consisten en elaborar la petición que se enviará al servidor. Se tendrá que leer los parámetros argc y argv[] de la función main en el programa cliente, y convertirlos en un struct peticion.

Por otra parte, el cliente deberá rellenar el campo canal de la estructura peticion, con la ruta de la terminal o el archivo donde se desea visualizar el resultado. Para conocer la terminal actual, se invoca a la función función().

Solicitud de servicio y espera por la notificación de servicio realizado

Una vez que el cliente ha construido la petición, deberá solicitar el servicio al proceso servidor. Deberá hacer uso del mecanismo de sincronización y comunicación diseñado por ustedes, basado en algún IPC de UNIX. La implementación deberá garantizar que el cliente se mantenga bloqueado hasta que el servidor le notifique que se ha completado su petición, o bien hasta que el servidor finalice.

Observe que el servidor puede encontrarse ocupado con peticiones de otros clientes, y que no se deben interferir unas peticiones con otras.

Comprobación del resultado del servicio realizado

Una vez que se le ha notificado que el servicio ha sido realizado, el cliente deberá consultar el campo resultado de la estructura struct Respuesta, para determinar si la solicitud de tramitó correctamente o no.

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.

Plan de trabajo

Probablemente esta es la primera vez que tengan que acometer la construcción de una aplicación concurrente. El desarrollo de esta clase de sistemas exige ser más cuidadoso y metódico. En este apartado le proponemos unas recomendaciones que pueden ayudarles a completar el trabajo con un mínimo de complicaciones.

Comprenda claramente en qué consiste la práctica

Esto es, requerimientos y funcionamiento general. Si tiene dudas pregunte al profesor que tiene asignado para la tutorización y corrección de su práctica.

Entienda el problema de concurrencia planteado

Antes de pasar al diseño de los programas del servidor y cliente céntrese en los requerimientos de sincronización y comunicación que se plantean y estudie, mediante trazas, las distintas situaciones de comunicación que se pueden dar en un sistema con el proceso servidor y varios clientes. Una vez estudiado el problema planteado, diseñe una solución mediante las técnicas explicadas en clase. Algunos casos de estudio vistos en el aula son muy similares al problema de este trabajo.

Domine los IPC de UNIX

Una vez diseñada la solución al problema de comunicación, pase a la implementación en el entorno UNIX. Para ello es prerrequisito conocer el funcionamiento general de los mecanismos IPC de UNIX. Una vez dominado este aspecto, pase a la implementación, en este estado del diseño, y como consecuencia de su conocimiento del funcionamiento de las herramientas IPC, es posible que se plantee un rediseño de los mecanismos de comunicación debido a que se dará cuenta que los mecanismos IPC son más potentes y flexibles que los mecanismos vistos en la teoría, el llevar a cabo esta optimización es opcional.

Entienda el problema de control de procesos que se plantea

Cada vez que el servidor atiende una petición, debe crear un proceso hijo, esperar a que el nuevo proceso se ejecute y tras ello recoger información sobre cómo terminó el hijo. Estas actividades se resuelven con llamadas al sistema de UNIX, como son fork(), wait() y exit(). Para dominar estas herramientas, le recomendamos que implemente un programa sencillo que genere un proceso hijo, espere por su finalización y cuando ésta ocurra obtenga el código de finalización del proceso generado. Este programa le podrá servir de esqueleto para una parte del programa servidor.

Conozca los mecanismos para lanzar procesos

La ejecución de la función fork() produce la creación de un nuevo proceso que es imagen del proceso que la invocó originalmente (proceso padre). En nuestro caso el proceso hijo ejecutará un programa distinto al padre. Por tanto, habrá de reemplazar su código y sus datos por los del programa que se desea ejecutar. La familia de funciones exec... del UNIX se encargan de ello.

Basándose en el programa sencillo del apartado anterior, intente que el proceso hijo lance un programa, especificado con una cadena de caracteres. Una vez que lo anterior funcione, englóbelo en una función que reciba como parámetro un char* con la orden para ejecutar.

Conozca cómo se genera un proceso en segundo plano (demonio)

Como el proceso servidor deberá ejecutarse en forma de demonio, será necesario conocer los pasos necesarios para dejar un proceso en segundo plano y desvinculado de una terminal.

Diseñe los programas del servidor y del cliente

Observe que si ha seguido al pie de la letra las recomendaciones anteriores tendrá mucho camino recorrido. Dispone de una biblioteca de funciones que resuelven los problemas básicos de la práctica.

Realice un diseño general de los programas del servidor y del cliente. Exponga sus resultados al profesor tutor. Si éste le da el visto bueno, ya puede codificar la práctica y terminar su trabajo.

Consejos prácticos

Los semáforos son entidades que sobreviven a la muerte de sus procesos creadores, como ocurre con los ficheros. Por si algún programa se les queda colgado, deberían hacerse una utilidad para borrar los semáforos "basura" que les vayan surgiendo. Las órdenes UNIX para manipular semáforos y otros IPC son:

ipcs

visualiza los IPC activos
ipcrm
destruye IPC

Si un proceso se les queda colgado lo pueden matar con kill -9 pid, donde pid indica el identificador del proceso que quieren destruir. Para saber el pid de la víctima, tecleen ps y verán la lista de procesos asociados a la terminal.

La tecla de suspensión (suspend), que normalmente es ^Z, les puede ser de gran ayuda para mantener varios programas ejecutándose concurrentemente. Si pulsan ^Z, el proceso actual se "suspende", es decir, pasa al segundo plano mientras ustedes prosiguen con el shell. Para regresar un proceso suspendido al primer plano, tecleen fg. Con este mecanismo pueden probar el sistema con múltiples clientes sin tener que acaparar varias terminales, que son más bien escasas.