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.
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.
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.
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.
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.
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.
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
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
.
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.
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:
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()
.
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.
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.