Home UC3M
Home IT
Home / Teaching / Telecommunication Engineering / Computer Architecture Laboratory  / Laboratory 3
anteriorsiguiente
 
Laboratorio de Arquitectura de Ordenadores
Práctica 3:
Inter Process Communication (IPC) mediante semáforos
Page navigator
   Nota
   Introducción
   Descripción de las funciones a implementar
   Comprobación de las funciones a implementar
   Archivos a enviar

Nota
Esta práctica se basa en la utilización de mecanismos de comunicación entre procesos (IPC, interprocess comunication), más concretamente, semáforos. En los siguientes apartados se proporciona un tutorial con toda la información necesaria para manejar semáforos que siguen la especificación de System V bajo Linux. Esto le permitirá, en concreto, realizar un ejercicio consistente en la finalización de una aplicación ya programada. El objetivo es comprender el funcionamiento de los semáforos, retener el código provisto para posibles aplicaciones futuras y entender y completar esta aplicación.


Introducción

IPC es un medio de comunicación entre procesos. Diferentes implementaciones de UNIX (y, por tanto, de Linux), soportan distintos tipos de IPC. Los tipos más comunes son tuberías (pipes), FIFOs (first in, first out), streamed pipes, named pipes, colas de mensajes (message queues), semáforos, memoria compartida (shared memory sockets / streams). Algunos de esto soportan full duplex, pero en caso de que la portabilidad sea importante se debe pensar en comunicación half duplex en general. Este tutorial se centra en el uso de semáforos según la especificación de System V.

Identificador de una estructura IPC

Un programa puede crear o acceder a una estructura IPC (como una cola de mensajes, un semáforo o memoria compartida) de distintas formas:

  1. Crear una estructura IPC utilizando la clave (key) IPC_PRIVATE, lo que garantiza trabajar con una estructura nueva. Un padre puede devolver un valor de retorno (el identificador de la estructura IPC) a un hijo cuando se efectúa una llamada a 'fork', o con la ayuda de un archivo para almacenar el identificador obtenido.
  2. En caso de que el identificador sea conocido desde el principio, la clave se puede establecer en un archivo de cabecera común mediante una macro, de modo que todos los procesos que utilicen la estructura compartan esa información. En consecuencia, no sería necesario pasar la clave del padre al hijo ni habría que buscar un identificador único en tiempo de ejecución. No obstante, el identificador se debe establecer al compilar, y se debe garantizar que sea único. Si se trata de crear una estructura ya existente, la función get correspondiente devuelve un error.
  3. Un identificador único se puede basar en la ruta de acceso y el identificador del proyecto (un valor entre 0 y 255), con la ayuda de la función ftok (ver man ftok).

La siguiente tabla presenta un resumen de las principales operaciones. En los siguientes puntos se entra en profundidad en el caso de los semáforos:

 
  Cola de mensajes Semáforos Memoria compartida
archivo de cabecera <sys/msg.h>  <sys/mem.h>  <sys/shm.h>
llamada al sistema para crear o abrir msgget() semget() shmget()
llamada al sistema para operaciones de control msgctl() semctl() shmctl()
llamada para operaciones de IPC msgsnd(); msgrcv() semop() shmat(); shmdt()
Tabla 1: Operaciones disponibles

Creación y control de semáforos

La función que permite crear o acceder a un semáforo es semget. Los parámetros son una clave (key), un entero nsems con el número de semáforos en el grupo (set), normalmente solo uno, y un entero flags con varios bits de control. A continuación presentamos la cabecera de la función:

#include <sys/sem.h>
int semget (key_t key, int nsems, int flags); 	// los flags se describen más abajo 
// Devuelve el identificador del semáforo o -1 si ocurre un error (dando un valor a errno)
/* errno = 	EACCESS (permiso denegado)
		EEXIST  (La estructura existe, no se puede crear)
		EIDRM   (La estructura está marcada para ser borrada)
		ENOENT  (La estructura no existe)
		ENOMEM  (Memoria insuficiente para crear la estructura)
		ENOSPC  (Excedido el número máximo de estructuras)
*/
Listado: semget
 

Las tres funciones  get disponibles para colas de mensajes, semáforos y memoria compartida (msgget, semget, and shmget) tienen dos parámetros similares: una clave (key) y un entero flagSe creará una nueva estructura IPC si se da alguno de los siguientes casos:

  • key es IPC_PRIVATE
  • key no está asociada a una estructura IPC del tipo correspondiente. El comportamiento en este caso depende del valor de flag según se muestra en la tabla:
parámetro flag key no existe key ya existe
ningún flag error, errno = ENOENT OK
IPC_CREAT OK, crea nueva estructura OK
IPC_CREAT | IPC_EXCL OK, crea nueva estructura error, errno = EEXIST
Table 2: Reglas para la creación de estructuras IPC

La función semget convierte la clave dada en un identificador que representa un grupo de semáforos, numerados comenzando por cero. No obstante, los semáforos no quedan listos para ser usados tras una llamada a semget. Necesitan ser inicializados con una llamada a semctl, una función diseñada para controlar el grupo de semáforos. A continuación se presenta esta función, junto con sus estructuras de datos asociadas, tal y como se definen en sem.h:

 
#include <sys/sem.h>
int semctl (int semid, int semnum, int cmd, union semun arg); 	// los flags se describen más abajo
// Devuelve el identificador del semáforo o -1 si ocurre un error (dando un valor a errno)
/* errno = 	EACCESS (permiso denegado)
		EEXIST  (La estructura existe, no se puede crear)
		EIDRM   (La estructura está marcada para ser borrada)
		ENOENT  (La estructura no existe)
		ENOMEM  (Memoria insuficiente para crear la estructura)
		ENOSPC  (Excedido el número máximo de estructuras)
*/

Listado: semctl

 
union semun {
     int val;               // entero
     struct semid_ds *buf;  // puntero a estructura
     unsigned short *array; // array
};
Listado: union semun
 

 

struct semid_ds {
      struct ipc_perm sem_perm; // estructura con los permisos
      unsigned short sem_nsems; // numero de semáforos en el grupo
      time_t sem_otime;         // fecha y hora de la última llamada a semop()
      time_t sem_ctime;         // fecha y hora de la última llamada a semctl()
};
Listado: struct semid_ds

Los parámetros de la función son, respectivamente, el identificador del grupo de semáforos, el número del semáforo sobre el que se actúa, el comando que se quiere realizar y una unión que contiene información (parámetros) para ese comando. Recuerde que una unión se diferencia de una estructura en que contiene la información correspondiente a uno y solo uno de sus campos. Los posibles comandos, es decir, los posibles valores de cmd son los siguientes:

cmd efecto
IPC_STAT Obtiene la estructura semid_ds de un grupo, y la almacena en la dirección del puntero buf definido en la unión semun.
IPC_SET Establece el valor de la estructura ipc_perm perteneciente a la estructura semid_ds de un grupo. Toma el valor a establecer de  buf en la unión semun.
IPC_RMID Elimina el grupo del kernel.
GETALL Devuelve los valores de todos los semáforos de un grupo. Los valores enteros obtenidos se almacenan en un array de unsigned short integers apuntado por el puntero array de la unión semun.
GETNCNT Devuelve el número de procesos que están esperando la asignación de recursos.
GETPID Devuelve el PID del proceso que realizó la ultima llamada a semop().
GETVAL Devuelve el valor de un semáforo individual perteneciente al grupo.
GETZCNT Devuelve el número de procesos que están esperando a que se utilicen el 100% de los semáforos.
SETALL Asigna valores a todos los semáforos del grupo utilizando los valores contenidos a partir de la dirección especificada por el puntero array definido en la unión.
SETVAL Establece el valor de un semáforo individual perteneciente al grupo a partir del valor del entero val definido en la unión.
Tabla 3: Valores de cmd

Estructura de permisos

Las colas de mensajes, los semáforos y la memoria compartida tienen varias características comunes. En primer lugar, el identificador de la estructura es siempre un entero positivo. Ese identificador debe ser conocido para poder trabajar con la estructura. Aunque muy similar, este procedimiento no es exactamente igual que el manejo de archivos, puesto que el identificador es un valor que se incrementa cada vez que se define una nueva estructura, hasta que alcanza el valor máximo y vuelve a un valor "próximo" a cero. Incluso si la estructura se elimina, el valor se incrementa para las próximas nuevas estructuras, con lo que el valor del identificador antiguo se mantiene. La información sobre la estructura IPC (por ejemplo un grupo de semáforos), se almacena en la estructura ipc_perm que se muestra a continuación:

struct ipc_perm
{
  key_t  key;   /* tipo de dato primitivo del sistema, normalmente definido como long integer en <sys/types.h> */
  ushort uid;   /* uid (usuario) efectiva y gid (grupo) efectiva el propietario */
  ushort gid;
  ushort cuid;  /* uid (usuario) efectiva y gid (grupo) efectiva del creador */
  ushort cgid;
  ushort mode;  /* modo de acceso */
  ushort seq;   /* slot usage sequence number */
};

Listado: ipc_perm
 

Operación de los semáforos

Ahora que somos capaces de crear, inicializar y realizar algunas operaciones de control sobre nuestros semáforos, es el momento de operar con ellos. Para ello se utiliza la función semop:

 
#include <sys/sem.h>
int semop (int semid, struct sembuf *sops, size_t nsops); 
// Devuelve 0 en caso de éxito y -1 en caso de error (dendo un valor a errno)

Listado: semop

	struct sembuf {
                unsigned short sem_num;   /* numero del semáforo */
                short sem_op;             /* operación sobre el semáforo */
                short sem_flg;            /* flags de la operación */
        };

Listado: struct sembuf

 
El primer parámetro de la función es el identificador del grupo de semáforos. Puesto que esta función actúa sobre cualquier número de semáforos, es necesario construir un array con las operaciones a realizar aunque solo se esté utilizando un semáforo del grupo. Este array es un array de  struct sembuf, y el segundo parámetro que recibe la función es un puntero a su primer elemento. Finalmente, nsops es el número de semáforos sobre los que se actúa, normalmente solo uno.
 
La estructura sembuf tiene un primer campo para el número del semáforo afectado y un segundo para la operación a realizar, con el siguiente significado:
 
Valor de sem_op Significado
> 0 El valor de sem_op se añade al valor del semáforo (operación 'release')
< 0 El valor absoluto de sem_op se resta del valor del semáforo, a no ser que esa operación haga que el valor sea negativo. En ese caso, la llamada se bloquea hasta que el valor pueda ser restado sin producir un valor negativo (operación 'wait')
== 0 La llamada se bloquea hasta que el valor del semáforo sea cero
Tabla 4: Operaciones con el seáforo

Limitaciones y comandos adicionales  

Las limitaciones de las estructuras IPC se deben principalmente al kernel del SO. Aun así, las estructuras IPC descritas aquí (por ejemplo, las colas de mensajes) no se eliminan automáticamente, sino que permanecen almacenadas en el kernel hasta que son eliminadas manualmente o el sistema se reinicia. Puede ser interesante, por tanto, acceder a las estructuras o incluso listarlas, para lo que algunos comandos adicionales son útiles.

El comando ipcs (consulte man ipcs) permitir obtener el status de todas las estructuras IPC System V:

ipcs -q Mostrar solo semáforos
ipcs -m    Mostrar solo colas de mensajes
ipcs -s Mostrar solo memoria compartida
ipcs --help Mostrar ayuda sobre otras opciones

Tabla 5: comando ipcs

El comando ipcrm  permite eliminar una estructura IPC del kernel. Aunque las estructuras se pueden eliminar mediante llamadas al sistema en un programa en C, al desarrollar un programa puede ser necesario hacerlo manualmente:

ipcrm <msg | sem | shm>  <IPC ID>
Simplemente se ha de especificar si el elemento es una cola de mensajes (msg), un grupo de semáforos (sem), o un segmento de memoria compartida (shm), junto con el identificador IPC ID que se puede obtener mediante el comando ipcs

Información adicional

Puede encontrar más información sobre semáforos y las funciones proporcionadas por Linux para manejarlos, junto con varios ejemplos, en este tutorial. Revise también las referencias bibliográficas que se proporcionan.


Descripción de las funciones a implementar
La implementación de la práctica corresponde a una aplicación cliente-servidor con las siguientes características:

General

  • El cliente y el servidor intercambian datos a través de colas de mensajes
  • El nombre de la cola de mensajes de establece al comienzo (en def.h)
  • El diseño no debe contener ningún array de tamaño preestablecido
  • El tipo de cualquier variable pasada por la cola de mensajes ha de ser puntero.

Servidor

  • El servidor intenta leer de la cola de mensajes, se arranca en primer lugar y queda bloqueado hasta que hay algún dato disponible en la cola.
  • El servidor espera que en cualquier línea impar haya un único entero.
  • El servidor va sumando los valores de los enteros de las líneas impares, y devuelve el valor resultante una vez que ha leído todos los mensajes de la cola de mensajes.
  • Además, el servidor devuelve el número total de líneas recibidas.
  • Los dos valores que se devuelven se escriben en una cola de mensajes que ha de ser leída por el cliente.
  • Al arrancar el servidor las colas de mensajes existentes (de ejecuciones anteriores) han de ser eliminadas, y la cola ha de ser reiniciada de forma que quede vacía.
  • El servidor elimina el mensaje de entrada (creado por el cliente) tras leer todos los elementos.

Cliente

  • Se ha de implementar el cliente (main_client.c) que lee la entrada de un archivo que se pasa como parámetro al llamar al cliente. Por ejemplo, main_client test_file_3.txt utilizará test_file_3.txt como archivo de entrada.
  • El cliente espera dos valores de retorno del servidor:
    • El número de líneas transferidas del cliente al servidor
    • La suma de todos los enteros situados en las líneas impares del archivo de entrada
  • El cliente elimina la cola de entrada del servidor tras leer estos valores de retorno o en caso de error en el archivo de entrada.
  • El control de errores de ficheros de entrada no válidos se realizará en el servidor.
     

Para esta práctica, se le proporciona una aplicación ya programada con la misma funcionalidad, pero con las siguientes diferencias en su diseño:

  • Hay dos semáforos, uno en el servidor y otro en el cliente, cuyos identificadores se obtienen de SENDKEY / RETURNKEY en def.h o del primer parámetro pasado al servidor en por línea de comandos.
  • Hay una única cola de mensajes, compartida por el cliente y el servidor.
  • La comunicación se basa en esperas no bloqueantes (opción IPC_NOWAIT)
     

Protocolo

Se utilizan dos semáforos, uno para el servidor y otro para el cliente, y una cola de mensajes. La idea es que el cliente escribe datos en la cola de mensajes, tras los que libera (operación 'release') el semáforo del servidor y empieza a esperar en el suyo (operación 'wait'). Una vez que el servidor lee el mensaje, libera el semáforo del cliente y empieza a esperar en el suyo. Tenga en cuenta que estas llamadas son no bloqueantes. Cuando el mensaje de tipo EOF type se lee, el servidor envía la suma y el número de líneas en un mensaje al cliente, tras lo que elimina la cola de mensajes.

Funciones a implementar

La aplicación se entrega completa, pero debe programar las funciones que la aplicación usa para manejar los semáforos, utilizando las tres funciones presentadas en este tutorial (semget, semctl y semop). Estas funciones se incluirán en un archivo llamado my_semaphore.c:

  • int createSem ( MySemaphore*, long );
    Esta función creará un semáforo. El primer parámetro es un puntero a struct MySemaphore y el segundo es el nombre del semáforo (un valor long). El valor de retorno es el identificador del semáforo en caso de éxito o -1 en caso de error.
     
  • int waitSem ( int );
    Cambia el valor del semáforo realizando la operación "-1". Puesto que el valor del semáforo alterna entre 0 y -1, una llamada a esta función siempre hace que el semáforo pasa al estado de espera.
     
  • int releaseSem ( int );
    Cambia el valor del semáforo realizando la operación "+1". Puesto que el valor del semáforo alterna entre 0 y -1, una llamada a esta función siempre hace que el semáforo pase al estado libre y el proceso que estaba esperando puede continuar ejecutándose.
     
  • void closeRemoveSem ( long );
    El semáforo identificado mediante el nombre (un valor long) se cierra y elimina del sistema. Un semáforo existente pero inaccesible provocará que el programa finalice. Los semáforos que no existen se pueden "cerrar y eliminar" sin problemas.
     
  • int getSemId ( long );
    Devuelve el identificador (ID) del semáforo a partir del "nombre" de éste (definido como un long). El programa finaliza si la llamada a semget produce un error.

Debe comentar todas estas funciones, e indicar todas las suposiciones que realiza al programarlas. Le resultará de ayuda revisar el código de cliente y servidor, para ver como se espera que operen estas funciones.

Esqueleto del programa

Para comenzar, debe descargar la aplicación ya programada para establecer un entorno de desarrollo apropiado:

  • def.h ; contiene las macros y definiciones generales, modifique sólo la línea con su NIA  (student ID)
  • def.c ; El fichero contiene distintas funciones usadas en algún lugar de la aplicación. Una descripción es incluida en el código fuente.
  • main_client.c ; el cliente (ya programado)
  • my_semaphore.h ; archivo de cabecera de my_semaphore.c
  • main_server; ejecutable del servidor.
  • Archivos auxiliares: ao.h and libao.a
     
  • Para compilar todo, utilice este sencillo makefile.

Aquí tiene un esqueleto para las funciones que debe programar para manejar los semáforos:

Notas

  • Cada archivo no erróneo debe tener un sólo valor entero en cada línea impar.
  • El cliente debe tratar el caso en el que el archivo de entrada sea erróneo.
  • El contenido de una línea par puede ser cualquiera, pero todas las líneas han de ser menores a 200 caracteres.
  • Cualquier tipo de error inesperado al invocar una llamada a sistema u otra función que devuelva un valor de retorno correspondiente a un error debe provocar el fin del programa, para lo que previamente se liberará toda la memoria reservada y se devolverá el control al proceso que llamó al programa de forma totalmente predecible.
  • El programa no debe entrar en bucles infinitos.
  • El programa no debe terminar con errores de segmentación (segmentation faults).
  • Los archivos de entrada con un número impar de líneas produces un error y la terminación ordenada de los programas del cliente y el servidor.
  • Sólo es necesaria una cola de mensaje.
  • Durante el desarrollo: cuando el cliente se cuelgue o termine con un error, el servidor se deberá reiniciar y las colas de mensajes borradas manualmente.
  • Las líneas impares contienen un número entero positivo mayor que cero.


Comprobación de las funciones a implementar

Comprobación

  1. Inicie el servidor con su NIA, por ejemplo: 10001234 (debe definir el mismo número para el cliente en def.h antes de compilar):
    ./main_server 10001234
     
  2. En una segunda ventana, arranque el cliente, por ejemplo usando el archivo de entrada test_file_example.txt (que no se proporciona):
    ./main_client test_file.txt

La salida debería ser como la siguiente:

 Ventana 1: (Servidor)

Ventana 2: (Cliente)

$ ./main_server 10001234
Server: Lines received: 10
Server: Sum of data passed: 55
$

$ ./main_client test_file.txt
Client: Lines passed by client: 10
Client: Data computed in server: 55
Client: Sum of lines received by server: 10

CPU Time CPU: 0 (milisecs.)

Memory Management Report
------------------------
* Memory allocated/deallocated correctly.

* Peak memory: 508 bytes.

$

  1. Se realizarán las siguientes pruebas:
    1. Leer un archivo de entrada que es mayor que el del ejemplo pero no contiene errores.
    2. Leer un archivo de entrada vacío.
    3. Leer un archivo de entrada con un número impar de líneas.
      La primera línea de la salida del cliente debe ser: Error: code line after line number is missing
    4. Leer un archivo de entrada que contiene una cadena en una línea impar cuyo primer carácter no es un dígito.
      La primera línea de la salida del cliente debe ser: Error: No valid line number detected
    5. Después de cada prueba (i-iv), se comprobará si:
      1. todos los semáforos y colas de mensajes se han eliminado. Esto implica que debe evitar que alguna estructura IPC permanezca en el sistema una vez que el cliente lee un archivo válido y finaliza, o una vez que el cliente finaliza porque se le ha pasado un archivo de entrada inválido. Para ello puede utilizar ipcs.
      2. el programa main_server ha finalizado.


Archivos a enviar
De acuerdo con la  fecha límite oficial, debe enviar el archivo my_semaphore.c conteniendo las funciones especificadas.

Página de entrega de IPC mediante semáforos

Notas de práctica de IPC mediante semáforos

Referencias bibliográficas
Aquí puede encontrar una guía general de la programación en Linux, bastante detallada  (copia local), con apartados concretos dedicados a comunicación entre procesos (interprocess comunication), y semáforos.

También puede consultar los siguientes libros:

  • Brian W. Kernighan, The Unix programming environment. Library: L/S 004.451.9 UNIX KER
  • Marc J. Rochkind, Advanced UNIX Programming, ISBN: 0-13-011818-4, Prentice-Hall Software Series, 1985.Library: L/S 004451.9 UNIX ROC
  • A. Silberschatz, P. Galvin, Operating Systems Concepts - 6th Edition/Windows XP Update, ISBN: 0-471-25060-0, John Wiley & Sons, 2002.
    Library: L/S 004.451 SIL

 

© Mario Ibañez Pérez, Pedro J. Muñoz Merino, Alfonso Rebolleda Sánchez, Ralf E. D. Seepold - last updated 02/04/2006

Location | Personnel | Teaching | Research | News | Intranet
inicio | mapa del web | contacta