Cambiar a contenido.

OCW UNED

Secciones
Herramientas personales
Acciones de documento
Saltar a contenido principal

Mecanismos IPC del SYSTEM V

Consideraciones generales

Los mecanismos IPC descritos en la sección anterior no satisfacían los necesidades de muchas aplicaciones. Un gran avance llegó con el UNIX System V, que suministraba tres nuevos mecanismos: semáforos, colas de mensajes y memoria compartida, que se conocen de forma colectiva como mecanismos IPC del System V. Posteriormente estos mecanismos fueron implementados por la mayoría de las distribuciones de UNIX, incluso las BSD.

Características comunes de los mecanismos IPC del System V

Los mecanismos IPC del System V están implementados en el sistema como una unidad y comparten características comunes, entre las que se encuentran:

  1. Cada tipo de mecanismo IPC tiene asignada una tabla en el espacio de memoria del núcleo de tamaño fijo. Por lo tanto en el núcleo existen tres tablas relacionadas con los mecanismos IPC: una para semáforos, otra para mensajes y una tercera para la memoria compartida.
  2. Cada tabla asignada a un tipo de mecanismo IPC posee un número de entradas configurable. Cada entrada contiene información relativa a una instancia de dicho mecanismo IPC o canal IPC.
  3. Cada entrada de la tabla tiene asignada una llave numérica, que permite controlar el acceso a dicha instancia del mecanismo IPC.
  4. Cada entrada de la tabla asociada a un tipo de mecanismo IPC tiene asignado un índice IT para su localización dentro de la tabla.
  5. Cada entrada de la tabla asociada a un tipo de mecanismo IPC tiene almacenada una estructura ipc_perm que presenta la siguiente definición:

struct ipc_perm

{

 

ushort uid;flecha

( Identificador de usuario del proceso propietario del recurso.

ushort gid;flecha

( Identificador de grupo del proceso propietario del recurso.

ushort cuid;flecha

( Identificador de usuario del proceso creador del recurso

ushort cgid;flecha

( Identificador de grupo del proceso creador el recurso.

ushort mode;flecha

( Modo de acceso (permisos de lectura, escritura y ejecución para el usuario, el grupo y otros usuarios)

ushort seq;flecha

( Número de secuencia. Es un contador que lo mantiene el núcleo y que se incrementa siempre que se cierra una instancia o canal de un mecanismo IPC. Este contador es necesario para identificar los canales abiertos e impedir que mediante una elección aleatoria del identificador de canal, un proceso pueda adquirirlo.

key_t key;flecha

}

( Llave de acceso



  1. Cada entrada de la tabla asociada a un tipo de mecanismo IPC, además de la estructura ipc_perm, tiene almacenada también otras informaciones como por ejemplo el pid del último proceso que ha utilizado la entrada y la fecha de la última actualización o acceso.
  2. Cada instancia de un mecanismo IPC tiene asignado un descriptor numérico NIPC elegido por el núcleo, que la referencia de forma única y que será utilizado para localizar la instancia rápidamente cuando se realicen operaciones sobre ella.
  3. Cada tipo de mecanismo IPC dispone de una llamada al sistema tipo get [shmget (memoria compartida), semget (semáforos) y msgget (colas de mensajes)] que permite crear una nueva instancia de un determinado tipo de mecanismo IPC o acceder a alguna ya existente.
  4. Cada tipo de mecanismo IPC dispone de una llamada al sistema tipo ctl [shmctl (memoria compartida), semctl (semáforos) y msgctl (colas de mensajes)] que permite acceder a la información administrativa y de control de una instancia de un mecanismo IPC.

Asignación de un índice IT a una instancia NIPC

El núcleo calcula el descriptor numérico NIPC que asigna a una instancia de un mecanismo IPC usando la siguiente fórmula:

Descriptor numérico N IPC (1)

donde seq es el número de secuencia de la instancia, NT es el tamaño de la tabla asociada al mecanismo IPC, e IT es el índice de la instancia en la tabla. Esto asegura que un nuevo NIPC es generado si una entrada de la tabla de un cierto mecanismo IPC es reutilizada, puesto que seq es incrementado en una unidad. Asimismo se evita que los procesos accedan a una instancia usando un descriptor viejo.

El usuario pasa el NIPC como un argumento de las siguientes llamadas al sistema asociadas con la instancia del mecanismo IPC. El núcleo traduce el NIPC a la posición de la instancia en la tabla usando la fórmula:

El núcleo traduce el NIPC a la posición de la instancia (2)
  • Ejemplo 7.3: La tabla asociada a un determinado tipo de mecanismo IPC posee NT=100 entradas. Calcular IT de en los siguientes casos: a) NIPC=5. b)NIPC=30. c) NIPC=101. d) NIPC=303.

Aplicando la fórmula (2) se obtiene:

  1. IT= 5 mod (100) = 5 % 100 = 5
  2. IT= 30 mod (100) = 30 % 100 = 30
  3. IT= 101 mod (100) = 101 % 100 = 1
  4. IT= 303 mod (100) = 303 % 100 = 3
  • Ejemplo 7.4: La tabla asociada a un determinado tipo de mecanismo IPC posee NT=100 entradas. Calcular los descriptores posibles NIPC de la entrada IT =1 si el número de secuencia puede tomar como máximo el valor 3.

Los posibles valores NIPC de la entrada IT =1 se obtendrán usando la fórmula (1) para los valores seq=0, 1, 2 y 3.

seq=0 NIPC= 0*100+1 =1

seq=1 NIPC= 1*100+1 =101

seq=2 NIPC= 2*100+1 =201

seq=3 NIPC= 3*100+1 =301

Supóngase que el descriptor asociado a una instancia de un mecanismo IPC es NIPC=201. En un determinado instante dicha instancia es eliminada de la tabla. Cuando se vuelva a utilizar dicha entrada de la tabla, es decir, cuando se cree otra nueva instancia de un mecanismo IPC, el núcleo le asignara NIPC=301. Aquellos procesos que intenten acceder con NIPC=201 recibirán una señal de error ya que no es una entrada válida. Los descriptores NIPC son reciclados por el núcleo transcurrido un cierto intervalo de tiempo.

Creación de llaves

Cada entrada de una tabla de un determinado tipo de mecanismo IPC tiene una llave numérica, que permite controlar el acceso a dicha instancia del mecanismo IPC. La llamada al sistema ftok permite a un usuario crear una llave. Su sintaxis es la siguiente:

resultado=ftok(ruta,letra);

Esta llamada tiene dos parámetros de entrada, ruta que es la ruta de acceso de un fichero que debe existir dentro del sistema de archivos y letra que es un carácter. Si la llamada se ejecuta con éxito en resultado, que es una variable del tipo predefinido key_t, se almacenará una llave. En caso de error en resultado se almacenará el valor key_t-1.

En general ftok produce una llave de 32 bits combinando el parámetro letra con el número del nodo-i del fichero del parámetro ruta y con el número de dispositivo del sistema de archivos al que pertenece este fichero.

  • Ejemplo 7.5:

Las siguientes líneas de código C permiten crear una llave asociada al fichero "archivo1" y al carácter 'A':

#include <sys/types.h>

#include <sys/ipc.h>

...

key_t llave;

...

if((llave=ftok("archivo1",'A'))==(key_t)-1)

{

/* Tratamiento del error al crear una llave*/

}

Algunos comentarios sobre las llamadas al sistema tipo get

Un proceso adquiere una instancia de un mecanismo IPC haciendo una llamada al sistema del tipo get, pasándole una llave, ciertos indicadores y otros argumentos que dependen de cada mecanismo. Los indicadores permitidos son IPC_CREAT y IPC_EXCL. Su significado es el siguiente:

  • IPC_CREAT pide al núcleo que cree la instancia si ésta no existe ya.
  • IPC_EXCL es utilizado junto con IPC_CREAT y pide al núcleo que devuelva un error si la instancia ya existía.

Si no se especifica ningún indicador, el núcleo busca una instancia ya existente con la misma llave. Si la encuentra, y el proceso invocador tiene permiso de acceso, el núcleo devuelve el descriptor numérico NIPC de la instancia. En caso contrario devuelve el valor -1.

Si la llave toma el valor especial IPC_PRIVATE, el núcleo crea una nueva instancia. En este caso la instancia no podrá ser accedida a través de posteriores llamadas tipo get. Por lo tanto el proceso que invoca a la llamada al sistema con este argumento tiene propiedad exclusiva sobre la instancia. Eso si, el propietario puede compartir el recurso con sus hijos, que lo heredan cuando se realiza la llamada al sistema fork.

Algunos comentarios sobre las llamadas al sistema tipo ctl

Todos los tipos de mecanismos IPC poseen una llamada al sistema de control del tipo ctl que implementa diversos comandos. Estos comandos incluyen IPC_STAT y IPC_SET para obtener y configurar información del estado de un recurso, y IPC_RMID para eliminar un recurso. Los semáforos disponen de comandos adicionales para obtener y configurar los valores de un determinado semáforo perteneciente a un cierto conjunto.

Cada recurso IPC debe ser explícitamente eliminado mediante el uso del comando IPC_RMID. En caso contrario, el núcleo considera que se encuentra activo incluso aunque todos los procesos que lo estaban utilizando hayan terminado. Por lo tanto, un recurso IPC puede perdurar y ser utilizado más allá del tiempo de vida de los procesos que lo han estado utilizando. Esta propiedad puede ser bastante útil. Por ejemplo, un proceso puede escribir datos en una región de memoria compartida o un mensaje en una cola y después finalizar. Más tarde, otro proceso puede recuperar estos datos.

Únicamente el creador, el propietario actual o el superusuario pueden usar el comando IPC_RMID. La eliminación de un recurso afecta a todos los procesos que actualmente acceden a él, y el núcleo debe asegurarse de que todos estos procesos tratan este evento adecuadamente.

Semáforos

Los semáforos son objetos que pueden tomar valores enteros que soportan dos operaciones atómicas: P() y V()3. La operación P()decrementa en una unidad el valor del semáforo y bloquea al proceso que solicita la operación si su nuevo valor es menor que cero. La operación V()incrementa en una unidad el valor del semáforo; si el valor resultante es mayor o igual a cero, V()despierta a los procesos que estuvieran esperando por este evento.

Los semáforos pueden ser utilizados para implementar varios protocolos de sincronización. Por ejemplo, considérese el problema de administrar un cierto recurso limitado, es decir, un recurso con un número fijo de instancias. Los procesos intentan adquirir una instancia del recurso, y lo liberan cuando han terminado de utilizarlo. Este recurso puede ser representado por un semáforo que es inicializado al número de instancias. La operación P()es utilizada al intentar adquirir el recurso, decrementará el semáforo cada vez que tenga éxito. Cuando el semáforo alcanza el valor cero significará que no existen instancias libres, por lo que cualquier otra operación P()bloqueará al proceso. Liberar un recurso resulta en una operación V(), que incrementa el valor del semáforo, produciendo que los procesos bloqueados despierten

En el espacio de memoria del núcleo existe una tabla de semáforos con información de todos los semáforos existentes en el sistema. Cada entrada de esta tabla se denota por un identificador numérico semid que hace referencia a un conjunto de semáforos. Además cada entrada se implementa mediante la estructura semid_ds cuya definición es:

struct semid_ds {

struct ipc_perm sem_perm; flecha Estructura que contiene los permisos de acceso
struct sem *sem_base; flechaPuntero al primer semáforo del conjunto
ushort sem_nsems; flecha Número de semáforos en el conjunto
time_t sem_otime; flecha Fecha de la última operación
time_t sem_ctime; flecha Fecha del último cambio mediante semctl

}



Por otra parte, para cada semáforo perteneciente a un conjunto el núcleo guarda su valor y la información de sincronización en una estructura sem, cuya definición se muestra a continuación:

struct sem {

ushort semval; flecha Valor actual del semáforo
pid_t sempid; flecha pid del proceso que ha solicitado la última operación, llamando a semop.
ushort semncnt; flecha Número de procesos esperando a que semval se incremente ( >0 ).
ushort semzcnt; flecha Número de procesos esperando a que semval valga cero.

};



  • Ejemplo 7.6:

En la Figura 7.2 se representan las estructuras de datos del núcleo necesarias para el manejo de semáforos. A modo de ejemplo se han supuesto dos entradas activas en la tabla de semáforos, es decir, hay dos conjuntos de semáforos semid=0 y semid=1. La entrada semid=0 contiene información de un conjunto con 4 semáforos, mientras que la entrada semid=1 contiene información de un conjunto con 2 semáforos.

     
  Figura 7.2: Estructura de datos del núcleo necesarias para el manejo de semáforos  


Figura 7.2: Estructura de datos del núcleo necesarias para el manejo de semáforos

Obsérvese como en cada entrada de la tabla de semáforos hay almacenada una estructura semid_ds que entre otras informaciones (sem_perm, sem_nsems, sem_otime, sem_ctime) posee un puntero sem_base que apunta al primer semáforo de cada conjunto.

Por otra parte cada semáforo de un conjunto viene definido por una estructura sem que contiene la siguiente información: semval, sempid, semncnt y semzcnt.

Creación u obtención de un conjunto de semáforos

La llamada al sistema semget crea u obtiene un array o conjunto de semáforos. Su sintaxis es:

semid=semget(key,count,flags);

donde key es una llave numérica del tipo predefinido key_t o bien la constante IPC_PRIVATE, count es el número entero de semáforos del conjunto o array asociados a key y flags es una máscara de indicadores (máscara de bits). Estos indicadores permiten especificar, de forma similar a como se hace para los ficheros, los permisos de acceso al conjunto de semáforos. Asimismo en flags también se pueden introducir los indicadores IPC_CREAT e IPC_EXCL.

Si la llamada al sistema semget se ejecuta con éxito entonces en semid se almacenará el identificador entero de un array o conjunto de count semáforos asociados a la llave key. Si no existe un conjunto de semáforos asociado a la llave la orden fallará y en semid se almacenará el valor -1 a menos que se haya realizado con el indicador IPC_CREAT de flags activo, lo que fuerza a crear un nuevo conjunto de semáforos. También se crea un nuevo conjunto de semáforos si el parámetro key se configura al valor IPC_PRIVATE.

  • Ejemplo 7.7:

Las siguientes líneas de código C muestran como crear un nuevo conjunto de cinco semáforos, asociado a la llave creada a partir del fichero "ayudante" y la clave 'J'. Este conjunto de semáforos se va a crear con permisos de lectura y modificación para el usuario.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

...

int semid;

key_t llave;

...

llave=ftok("ayudante",'J');

if(llave==(key_t)-1)

{

/*Se ha producido un error al crear la llave.

Código de tratamiento del error*/

}

/*Creación del conjunto de semáforos*/

semid=semget(llave, 5, IPC_CREAT| 0600);

if (semid==-1)

{

/*Error al crear el conjunto de semáforos.

Código de tratamiento del error*/

}

Realización de operaciones con los elementos de un conjunto de semáforos

La llamada al sistema semop es utilizada para realizar operaciones sobre los elementos de un determinado conjunto de semáforos. Su sintaxis es:

resultado=semop(semid, sops, nsops);

donde semid es un identificador de un array o conjunto concreto de semáforos, sops es un puntero a un array de estructuras del tipo sembuf que indican las operaciones que se van a llevar a cabo sobre los semáforos, y nsops es el número total de elementos que tiene el array de operaciones, es decir, el número total de operaciones.

En general, el núcleo lee el array de operaciones sops del espacio de direcciones del usuario y verifica que los números de los semáforos son legales y que el proceso tiene los permisos necesarios para leer o cambiar los valores de los semáforos. Si no cuenta con los permisos adecuados la llamada semop falla y en resultado se almacena el valor -1.

La definición de la estructura del tipo sembuf utilizada es:

struct sembuf{

unsigned short sem_num;

short sem_op;

short sem_flg;

}

El significado de cada uno de los elementos de una estructura sembuf es el siguiente:

  • sem_num identifica a uno de los semáforos del conjunto semid. Su valor está comprendido entre 0 y N-1, donde N es el número total de semáforos en el conjunto.
  • sem_op especifica la acción a realizar en el semáforo elegido. Los valores de sem_op se interpretan de la siguiente manera:
    • sem_op > 0. Añadir sem_op al valor actual del semáforo. Los procesos que estaban durmiendo en espera de que el valor fuese incrementado serán despertados.
    • sem_op = 0. Bloquear el proceso hasta que el valor del semáforo sea cero.
    • sem_op < 0. Bloquear el proceso hasta que el valor del semáforo sea mayor o igual que el valor absoluto de sem_op, a continuación restar sem_op de dicho valor. Si el valor del semáforo ya es superior al valor absoluto de sem_op, el proceso que invoca esta llamada al sistema no se bloqueará.
  • sem_flg, permite suministrar dos indicadores a la llamada. El indicador IPC_NOWAIT pide al núcleo que devuelva un error en vez de bloquear al proceso. Asimismo, puede ocurrir un interbloqueo si un proceso que retiene un semáforo termina prematuramente sin liberarlo. Otros procesos esperando para adquirir dicho semáforo puede quedar para siempre bloqueados en la operación P(). Para evitar este problema, es posible pasar a semop el indicador SEM_UNDO para que el núcleo recuerde la operación y automáticamente la deshaga si el proceso termina.
  • Ejemplo 7.8:

Las siguientes líneas de código C muestran como realizar una operación P() y otra V() sobre los semáforos 2 y 4 respectivamente del conjunto de semáforos semid que agrupa un total de 5 semáforos.

struct sembuf operaciones[5];

...

operaciones[0].sem_num=2; /*Semáforo número 2*/

operaciones[0].sem_op=-1; /*Operación P*/

operaciones[0].sem_flg=0;

operaciones[1].sem_num=4; /*Semáforo número 4*/

operaciones[1].sem_op=1; /*Operación V*/

operaciones[1].sem_flg=0;

semop(semid,operaciones,2);

...

Finalmente comentar que el núcleo mantiene una lista para cada proceso que ha solicitado una operación sobre un semáforo con el indicador SEM_UNDO. Esta lista contiene un registro por cada operación que debe ser deshecha. Cuando un proceso termina, el núcleo chequea si tiene una lista de estas características, si la tiene el núcleo recorre la lista reconstruyendo todas las operaciones realizadas con anterioridad.

Acceso a la información administrativa y de control de un conjunto de semáforos

La llamada al sistema semctl permite acceder a la información administrativa y de control que posee el núcleo sobre un cierto conjunto de semáforos. Su declaración es:

resultado=semctl(semid, semnum, cmd, arg);

donde semid es el identificador de un array o conjunto de semáforos, semnum es el identificador de un semáforo concreto dentro del array, cmd es es un número entero o una constante simbólica (ver Tabla 7.1) que especifica la operación a efectuar que indica la operación que va a realizar la llamada al sistema semctl, y arg es una unión del tipo semun, que se define de la siguiente forma:

 

union semun

{

int val; flecha usado con SETVAL
struct semid_ds *buf; flecha usado por IPC_STAT y por IPC_SET
unsigned short* array; flecha usado por GETALL y SETALL.

}arg;



Si la llamada semctl tiene éxito, en resultado se almacenará un número entero cuyo valor depende del comando cmd. Si falla en resultado se almacenará el valor -1.

Comando

Significado

GETVAL

Se usa para leer el valor de un semáforo. Este número se almacena en resultado.

SETVAL

Permite inicializar un semáforo a un valor determinado que se especifica en arg.

GETPID

Se usa para leer el pid del último proceso que actuó sobre el semáforo. Este número se almacena en resultado.

GETNCNT

Permite leer el número de procesos que hay esperando a que se incremente el valor del semáforo. Este número se almacena en resultado.

GETZCNT

Permite leer el número de procesos que hay esperando a que el semáforo tome el valor cero. Este número se almacena en resultado.

GETALL

Permite leer el valor de todos los semáforos asociados a un identificador semid. Estos valores se almacenan en arg.

SETALL

Sirve para inicializar el valor de todos los semáforos asociados a un identificador semid. Los valores de inicialización deben estar en arg.

IPC_STAT, IPC_SET

Permiten leer y modificar la información administrativa asociada al identificador semid.

IPC_RMID

Indica al núcleo que debe borrar el conjunto de semáforos agrupados bajo el identificador semid. La operación no tendrá efecto mientras haya algún proceso que esté usando los semáforos.



Tabla 7.1: Valores posibles del parámetro cmd de la llamada semctl

  • Ejemplo 7.9:

Las siguientes líneas de código C muestran como crear un nuevo conjunto de cinco semáforos, asociado a la llave creada a partir del fichero "ayudante" y la clave 'J'. Este conjunto de semáforos se va a crear con permisos de lectura y modificación para el usuario. Además se inicializan los dos primeros semáforos con el valor 3 y los tres últimos con el valor 2. Finalmente se pregunta por el valor del semáforo número 2.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

...

int semid, valor;

ushort sem_conjunto[5];

...

/*Creación del conjunto de semáforos*/

semid=semget(ftok("ayudante",'J'), 5, IPC_CREAT | 0600);

if (semid==-1)

{

/*Código de tratamiento del error*/}

/*Inicialización de los semáforos*/

sem_conjunto[0]=3;

sem_conjunto[1]=3;

sem_conjunto[2]=2;

sem_conjunto[3]=2;

sem_conjunto[4]=2;

semctl(semid,0,SETALL,sem_conjunto);

...

/* Pregunta por el valor del semáforo número 2*/

valor=semctl(semid,2,GETVAL,0);

Colas de mensajes

Una cola de mensajes es una estructura de datos gestionada por el núcleo, en ella van a poder escribir varios procesos. Los mecanismos de sincronización para que no se produzca colisiones en el uso de la cola de mensajes son responsabilidad del núcleo. Los datos que se escriben en la cola deben tener formato de mensaje y son tratados como un todo indivisible, es decir, el proceso extrae o coloca la información en una única operación.

El mecanismo de comunicación de las colas de mensajes corresponde a la implementación del concepto de buzón, que permite la comunicación indirecta entre procesos. Un proceso tiene la posibilidad de depositar mensajes o extraerlos del buzón. Cada mensaje esta tipificado, y cada proceso extraerá de una cola de mensajes aquellos que quiera extraer.

En la implementación del UNIX System V todos los mensajes son almacenados en el espacio del núcleo y tienen asociado un identificador de cola de mensaje, denominado msqid. Los procesos pueden leer y escribir mensajes de cualquier cola.

Estructuras de datos asociadas a los mensajes

De forma general un mensaje se implementa mediante una estructura que consta de dos campos: el tipo del mensaje y el texto o cuerpo del mensaje. El tipo del mensaje es un entero positivo que permite identificar al mensaje de acuerdo con una tipificación previamente establecida por el programador. Por su parte, el texto del mensaje es un array de caracteres que contiene el mensaje propiamente dicho.

El núcleo mantiene básicamente tres tipos de estructuras de datos para implementar las colas de mensajes: la tabla de colas de mensajes, la lista enlazada de cabeceras de mensajes asociadas a una cola y a un área de datos.

El núcleo posee una tabla de colas de mensajes, cada entrada en dicha tabla está asignada a una única cola de mensajes que viene identificada por un descriptor numérico msqid. Además, cada entrada contiene una estructura del tipo msqid_ds:

struct msqid_ds {

struct ipc_perm msg_perm; flecha Estructura de los derechos de acceso.
struct msg *msg_first; flecha Puntero al primer mensaje.
struct msg *msg_last; flecha Puntero al último mensaje.
ushort msg_cbytes; flecha Número total de bytes en la cola.
ushort msg_qbytes; flecha Número máximo de bytes.
ushort msg_qnum; flecha Número de mensajes en la cola.
ushort msg_lspid; flecha pid del último proceso emisor.
ushort msg_lrpid; flecha pid del último proceso receptor.
time_t msg_stime; flecha Fecha del último envío de mensaje.
time_t msg_rtime; flecha Fecha de la última recepción del mensaje.
time_t msg_ctime; flecha Fecha del último cambio por msgctl.

};



A su vez cada cola de mensajes msqid tiene asociada una lista enlazada de cabeceras de mensajes pertenecientes a dicha cola. Cada cabecera viene descrita por una estructura msg, que presenta la siguiente definición:

struct msg{

struct msg *msg_sig; flechaPuntero al mensaje siguiente.
long msg_type; flechaTipo de mensaje.
short msg_ts; flechaTamaño del texto del mensaje.
char *msg_spot; flechaDirección del texto de mensaje en el área de datos del núcleo

}

 


Finalmente, el texto o cuerpo de cada mensaje perteneciente a una cola de mensajes se encuentra almacenado en un área de datos dentro del segmento del núcleo en memoria principal.

  • Ejemplo 7.10:
     
  Figura 7.3: Estructuras de datos utilizadas en la implementación del mecanismo IPC de cola de mensajes.  


Figura 7.3: Estructuras de datos utilizadas en la implementación del mecanismo IPC de cola de mensajes.

En la Figura 7.3 se muestran las estructuras de datos del núcleo utilizadas en la implementación del mecanismo IPC de cola de mensajes. Se observa como la tabla de cola de mensajes tiene dos entradas activas (msqid=0 y msqid=1). La cola msqid=0 posee una lista enlazada de tres cabeceras de mensajes, mientras que la cola msqid=1 posee una lista enlazada con una única cabecera de mensajes.

Cada entrada de la tabla contiene una estructura msqid_ds que aporta entre otras informaciones un puntero (msg_first) que apunta a la cabecera del primer mensaje de la cola y otro puntero (msg_last) que apunta a la cabecera del último mensaje de la cola. Además la cabecera de cada mensaje en una cola contiene un puntero (msg_sig) que apunta a la cabecera del siguiente mensaje en la cola. Se observa también como la cabecera de un mensaje contiene además el tipo de mensaje (msg_type), el tamaño del texto del mensaje (msg_ts) y la dirección del área de datos del núcleo donde se encuentra el texto de dicho mensaje (msg_spot).

Creación u obtención de una cola de mensajes

La llamada al sistema msgget crea una cola de mensajes o bien permite acceder a una cola ya existente usando una clave dada. Su sintaxis es:

msqid=msgget(key,flags)

donde key es la clave de la cola de mensaje y flags es una máscara de indicadores (similar a la descritas para los semáforos). Si la llamada al sistema msgget se ejecuta con éxito entonces en msqid se almacenará el identificador entero de una cola de mensajes asociada a la llave key. En caso contrario en msqid se almacenará el valor -1.

  • Ejemplo 7.11:

Las siguientes líneas de código C muestran como crear una cola de mensajes, asociada a la llave creada a partir del fichero "ayudante" y la clave 'J'. Esta cola se va a crear con permisos de lectura y modificación para el usuario.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

...

int msqid;

key_t llave;

...

llave=ftok("ayudante",'J');

msqid=msgget(llave, IPC_CREAT | 0600);

if (msqid==-1)

{

/* Error en la creación de la cola de mensajes.

Tratamiento del error*/

}

Envío de mensajes

La llamada al sistema msgsnd permite a un proceso enviar un mensaje desde su espacio de direcciones a una determinada cola de mensajes. Su sintaxis es:

resultado=msgsnd(msqid,&buffer,msgsz,msgflags);

donde msqid es un identificador de una cola de mensajes, buffer es la variable del espacio de direcciones del usuario que contiene el mensaje que se desea enviar, msgsz es la longitud del texto del mensaje en bytes, y msgflags es una máscara de indicadores que permite especificar el comportamiento del proceso emisor en caso de que no pueda enviarse el mensaje debido a una saturación del mecanismo de colas. Si la llamada al sistema tiene éxito en resultado se almacenará el valor 0, si falla se almacenará el valor -1.

Por defecto la llamada al sistema msgsnd es bloqueante, es decir, el proceso que la invoca pasará al estado dormido interrumpible por señales sino se puede escribir en la cola de mensajes, y se le despertará cuando se pueda escribir. También se le despertaría si la cola de mensajes fuese borrada, o recibiese una señal que no ignora. Es posible hacer que esta llamada sea no bloqueante para ello, hay que colocar el indicador IPC_NOWAIT en la máscara msgflags de msgsnd. En dicho caso sino se puede escribir en la cola la llamada devolverá el valor -1 y asignará a la variable errno el valor EAGAIN.

Cuando se realiza la llamada al sistema msgsnd el núcleo realiza la siguiente secuencia de acciones:

  1. Comprueba que el proceso emisor tiene permiso de escritura para la cola msqid.
  2. Comprueba que la longitud del mensaje no excede los límites del sistema y que no contiene demasiados bytes.
  3. Comprueba que el tipo de mensaje es un entero positivo.
  4. Si todos las comprobaciones anteriores son superadas con éxito, asigna espacio para el mensaje en el área de datos del núcleo y copia los datos desde el espacio de direcciones del usuario al espacio de direcciones del núcleo
  5. Asigna una cabecera de mensaje y la coloca al final de la lista enlazada de cabeceras de mensajes de la cola de mensajes msqid.
  6. Salva el tipo de mensaje y su tamaño en la cabecera del mensaje.
  7. Configura la cabecera del mensaje para que apunte al texto de mensaje en el área de datos del núcleo.
  8. Actualiza varios campos de tipo estadístico en la entrada de la tabla de colas asignada a la cola msqid.
  9. El núcleo despierta a los procesos que estaban dormidos esperando por la llegada de un mensaje en dicha cola.
  10. Si el número de bytes en la cola excede el límite de la cola, el proceso emisor dormirá hasta que otros mensajes sean eliminados de la cola.
  11. Si el proceso estableció en su llamada a msgsnd (indicador IPC_NOWAIT del campo msgflag) que no desea esperar, entonces la llamada devolverá el valor -1.

Recepción de mensajes

La llamada al sistema msgrcv permite que un proceso pueda extraer un mensaje de una determinada cola de mensajes. Su sintaxis es:

resultado=msgrcv(msqid,&buffer,msgsz,msgtipo,msgflags);

donde msqid es un identificador de una cola de mensajes, buffer es la variable del espacio de direcciones del usuario donde se va almacenar el mensaje, msgsz es la longitud del texto del mensaje en bytes, msgtipo indica el tipo del mensaje que se desea extraer y msgflags es una máscara de indicadores que permite especificar el comportamiento del proceso receptor en caso de que no pueda extraerse ningún mensaje del tipo especificado. Si la llamada al sistema tiene éxito en resultado se almacenará el número de bytes del mensaje recibido (este número no incluye los bytes asociados al tipo de mensaje). En caso de error en resultado se almacenará el valor -1.

El argumento msgtipo puede tomar los siguientes valores:

  • msgtipo = 0. Se extrae el primer mensaje que haya en la cola independientemente de su tipo. Corresponde al mensaje más viejo.
  • msgtipo > 0. Se extrae el primer mensaje del tipo msgtype que haya en la cola.
  • msgtipo < 0. Se extrae el primer mensaje que cumpla que su tipo es menor o igual al valor absoluto de msgtipo y a la vez sea el más pequeño de los que hay.
  • Ejemplo 7.12:

Supóngase que se tiene una cola que contiene tres mensajes cuyos tipos son 3, 1 y 2, respectivamente, y un usuario solicita un mensaje con msgtipo=-2, ¿Que tipo de mensaje extrae el núcleo?

Solución:

Se extrae el primer mensaje que cumpla que su tipo es menor o igual al valor absoluto de msgtipo y a la vez sea el más pequeño de los que hay. Es decir:

Tipo de mensaje devuelto = min {3,1,2} <= |-2|

Tipo de mensaje devuelto = 1

Por defecto la llamada al sistema msgrcv es bloqueante, es decir, el proceso receptor pasará al estado dormido interrumpible por señales sino se no puede extraer ningún mensaje del tipo especificado, y se le despertará cuando se pueda extraer. También se le despertaría si la cola de mensajes fuese borrada, o recibiese una señal que no ignora. Es posible hacer que esta llamada sea no bloqueante para ello, hay que colocar el indicador IPC_NOWAIT en la máscara msgflags de msgrcv. En dicho caso sino se puede escribir en la cola la llamada devolverá el valor -1 y asignará a la variable errno el valor ENOMSG.

Por otra parte, si se intenta extraer un mensaje de longitud mayor al tamaño especificado por el argumento msgsz de msgrcv se producirá un error, a menos que en el campo msgflags se coloque el indicador MSG_NOERROR. En este caso se extraerá únicamente los msgsz primeros bytes del mensaje.

  • Ejemplo 7.13:

Las siguientes líneas de código C muestran como enviar y recibir un mensaje del tipo 2, que se compone de una cadena de 50 caracteres:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

...

key_t llave;

int msqid;

struct

{

long tipo;

char cadena[50];

} mensaje;

int longitud=sizeof(mensaje)-sizeof(mensaje.tipo);

...

llave=ftok("ayudante",'J');

msqid=msgget(llave, IPC_CREAT | 0600);

...

...

/*Envio del mensaje*/

mensaje.tipo=2;

strcpy(mensaje.cadena,"MI PRIMER MENSAJE");

if(msgsnd(msqid, &mensaje,longitud,0)==-1)

{

/*Error durante el envío del mensaje.

Tratamiento del error.*/

}

...

/*Recepción del mensaje*/

mensaje.tipo=2;

if(msgrcv(msqid, &mensaje,longitud,2,0)==-1)

{

/*Error durante la recepción del mensaje.

Tratamiento del error.*/

}

Acceso a la información administrativa y de control de una cola de mensajes

La llamada al sistema msgctl permite leer y modificar la información estadística y de control de una cola de mensajes, su declaración es:

resultado=msgctl(msqid, cmd, &buffer);

donde msqid es el identificador de la cola, cmd es un número entero o una constante simbólica que especifica la operación a efectuar, y buffer es una estructura del tipo predefinido msqid_ds que contiene los argumentos de la operación. Si la llamada msgctl tiene éxito, en resultado se almacenará un número entero cuyo valor depende del comando cmd. Si falla en resultado se almacenará el valor -1.

Las operaciones que se pueden especificar con el argumento cmd de msgctl son:

  • IPC_RMID. Borra del sistema la cola de mensajes identificada por msqid. Si la cola está siendo usada por otros procesos, la eliminación de la cola no se hace efectiva hasta que todos los procesos terminan de utilizarla.
  • IPC_STAT. Lee el estado de la estructura msg_perm asociada a la entrada msqid de la tabla de colas y lo almacena en buff
  • IPC_SET. Modifica el valor de los campos de la estructura msg_perm asociada a la entrada msqid de la tabla de colas. Los nuevos valores para estos campos los toma de buff.

En la estructura msg_perm los campos modificables por el usuario son: msg_perm.uid, msg_perm.gid y msg_perm.mode. Mientras que, el superusuario puede modificar el campo msg_qbytes. Los demás campos o no son modificables o son manipulados por el sistema directamente.

  • Ejemplo 7.14:

La llamada al sistema

msgctl(msqid,IPC_RMID,0);

borra la cola de mensajes con identificador msqid.

Discusión

Las colas de mensajes suministran servicios similares a las tuberías. Sin embargo las colas de mensajes son más versátiles y no poseen las limitaciones de las tuberías. Las colas de mensajes transmiten datos como mensajes discretos, a diferencia de las tuberías que transmiten datos como un fllujo de bytes sin formato. Esto permite un mejor procesamiento de los datos. El campo tipo de mensaje de los mensajes permite asociar prioridades a los mensajes, lo que posibilita a un proceso receptor el poder comprobar antes los mensajes más urgentes. Asimismo en escenarios donde una cola de mensajes es compartida por múltiples procesos, el campo tipo de mensaje puede ser utilizado para designar un receptor.

Las colas de mensajes son útiles para transferir pequeñas cantidades de datos. Sin embargo si hay que transferir grandes cantidades de datos el rendimiento del sistema se deteriora. Esto es debido a que la transferencia de un mensaje requiere de dos operaciones de copia de datos en memoria: la primera del espacio de direcciones del proceso emisor a un buffer interno del núcleo, y la segunda de dicho buffer al espacio de direcciones del proceso receptor.

Otra limitación de las colas de mensajes es que no pueden especificar un determinado receptor. Cualquier proceso con los permisos apropiados puede recuperar mensajes de la cola. Aunque, como se mencionó con anterioridad, procesos cooperantes pueden acordar un protocolo para especificar receptores. Finalmente, otra limitación de las colas de mensajes es que no suministran un mecanismo de difusión, es decir, un proceso no puede enviar un único mensaje a varios receptores.

Debido a las limitaciones de las colas de mensajes, la mayoría de las aplicaciones de los sistemas UNIX más modernos encuentran en el uso de los streams un mecanismo más potente para implementar el paso de mensajes.

Memoria Compartida

La forma más rápida de comunicar dos procesos es hacer que compartan una zona de memoria. Para enviar datos de un proceso a otro, el proceso emisor solamente tiene que escribir en memoria y automáticamente esos datos estarán disponibles para que los lea otro proceso.

Es conocido que la memoria convencional que puede direccionar un proceso a través de su espacio de direcciones virtuales es un espacio local a dicho proceso y cualquier intento de direccionar esa memoria desde otro proceso va a provocar una violación de segmento.

El sistema UNIX System V soluciona este problema permitiendo crear regiones de memoria virtual que pueden ser direccionadas por varios procesos simultáneamente.

Estructuras de datos utilizadas para compartir memoria

El núcleo posee una tabla de memoria compartida, cada entrada en dicha tabla está asignada a una región de memoria compartida que viene identificada por un descriptor numérico shmid. Además, cada entrada contiene una estructura del tipo shmid_ds, que se define de la siguiente forma:

struct shmid_ds{

struct ipc_perm shm_perm;flechaEstructura que mantiene los permisos

int shm_segsz;flechaTamaño del segmento

ushort shm_lpid;flecha pid del proceso que realizó la ultima operación sobre la región de memoria compartida

ushort shm_cpid;flecha pid del proceso creador

ushort shm_nattch;flechaNúmero de procesos unidos a la región de memoria compartida

time_t shm_atime;flechaFecha de la última conexión

time_t shm_dtime;flechaFecha de la última desconexión

time_t shm_ctime;flechaFecha de la última operación shmctl

};



Creación u obtención de una región de memoria compartida

Para crear un segmento de memoria compartida o acceder a uno que ya existe, se utiliza la llamada al sistema shmget, cuya sintaxis es:

shmid=shmget(key,size,flags);

donde key es la clave de acceso a un segmento de memoria compartida, size especifica el tamaño en bytes del segmento de memoria solicitado y flags es una máscara de indicadores (similar a la descrita para los semáforos). Si la llamada al sistema shmget se ejecuta con éxito entonces en shmid se almacenará el identificador entero de la zona de memoria compartida asociada a la llave key. En caso contrario en shmid se almacenará el valor -1.

El identificador devuelto por shmget es heredado por los procesos descendientes del actual.

Cuando un proceso realiza la llamada al sistema shmget el núcleo realiza las siguientes acciones:

  1. Busca en la tabla de memoria compartida la región asociada con el parámetro key, si encuentra dicha región y el proceso tiene los permisos de acceso correctos entonces devuelve el descriptor shmid.
  2. Si no encuentra la región asociada con el parámetro key y el usuario ha configurado el indicador IPC_CREAT de flags para crear una nueva región entonces:
  3. Comprueba que el tamaño especificado size se encuentra entre los límites mínimo y máximo permitidos
  4. Asigna una región mediante el uso del algoritmo allocreg().
  5. Salva los permisos, tamaño y un puntero a la tabla de regiones dentro de la estructura shmid_ds asociada a la entrada shmid de la tabla de memoria compartida.
  6. Activa un bit en la entrada de la tabla de regiones asignada a la región de memoria compartida shmid para identificarla como una región de memoria compartida.
  7. También en la entrada de la tabla de regiones asignada a la región de memoria compartida shmid el núcleo activa un bit para indicar que dicha región no debe ser liberada cuando el último proceso que la comparta termine. Por lo tanto, los datos en una región de memoria compartida permanecerán intactos incluso aunque ningún proceso comparta ya dicha región.
  • Ejemplo 7.15

Las siguientes líneas de código C muestran como crear una zona de memoria compartida de tamaño 4096 bytes, sólo el usuario va a tener permisos de lectura y escritura.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

...

int shmid;

...

shmid=shmget(IPC_PRIVATE,4096,IPC_CREAT | 0600);

if (shmid==-1)

{

/* Error en la creación de la memoria compartida.

Tratamiento del error.*/

}

Ligar una región de memoria compartida al espacio de direcciones virtuales de un proceso

Antes de que un proceso pueda usar la región de memoria compartida shmid, es necesario asignarle un espacio de direcciones virtuales de dicho proceso. Esto es lo que se conoce como unirse o enlazarse al segmento de memoria compartida.

La llamada shmat asigna un espacio de direcciones virtuales al segmento de memoria cuyo identificador shmid ha sido dado por shmget. Por lo tanto shmat enlaza una región de memoria compartida de la tabla de regiones con el espacio de direcciones de un proceso (ver Figura 7.4)

     
  Figura 7.4: Estructura de datos para el mecanismo IPC de memoria compartida, una vez realizada la llamada al sistema shmat.  


Figura 7.4: Estructura de datos para el mecanismo IPC de memoria compartida, una vez realizada la llamada al sistema shmat.

La llamada al sistema shmat tiene la siguiente sintaxis:

resultado=shmat(shmid,shmdir,shmflags);

donde shmid es un identificador de una región de memoria compartida, shmdir es la dirección virtual del proceso donde se desea que empiece la región de memoria compartida, shmflags, es una máscara de bits que indica la forma de acceso a la memoria. Si el bit SHM_RDONLY está activo, la memoria será accesible para leer, pero no para escribir. Por defecto un segmento de memoria se comparte para lectura y escritura. Si la llamada al sistema shmat tiene éxito en resultado se almacena la dirección a la que está unido el segmento de memoria compartida shmid. En caso contrario en resultado se almacena el valor -1.

Las reglas que utiliza shmat para determinar la dirección son:

  • Si shmdir= 0, el sistema selecciona la dirección.
  • Si shmdirDistinto0, el valor de la dirección devuelto depende si se especificó o no el bit SHM_RND del parámetro shmflags. Si se especificó el segmento de memoria es enlazada en la dirección especificada por el parámetro shmdir redondeada por la constante SHMLBA (SHare Memory Lower Boundary Address). En caso contrario el segmento de memoria es enlazado en la dirección especificada por el parámetro shmdir

Obviamente para conseguir portabilidad lo mejor es dejar que el núcleo asigne la dirección (shmdir=0)

En el momento que una región de memoria compartida shmid se une a un proceso, está pasa a formar del espacio de direcciones virtuales de dicho proceso, siendo por tanto accesible de la misma forma (mediante el uso de punteros) que las restantes direcciones virtuales. Luego no es necesario invocar a ninguna llamada al sistema especial para acceder a los datos almacenados en un segmento de memoria compartida.

Desligar una región de memoria compartida del espacio de direcciones virtuales de un proceso

Cuando un proceso ha terminado de usar un segmento de memoria compartida shmid entonces debe desenlazarse o desunirse de él, para conseguirlo utiliza la llamada al sistema shmdt. Su sintaxis es:

resultado=shmdt(shmdir);

donde shmdir es la dirección virtual del segmento de memoria compartida que se quiere separar del proceso. Si la llamada tiene éxito en resultado se almacena el valor 0. En caso contrario se almacena el valor -1.

Acceso a la información administrativa y de control de una región de memoria compartida

La llamada al sistema shmctl permite realizar operaciones de control sobre una zona de memoria compartida creada previamente por shmget. Su sintaxis es:

resultado=shmctl(shmid,cmd,&buffer);

donde shmid es el identificador de una región de memoria compartida, cmd es un número entero o una constante simbólica (ver Tabla 7.2) que especifica la operación a efectuar, y buffer es una estructura del tipo predefinido shmid_ds que contiene los argumentos de la operación. Si la llamada shmctl tiene éxito, en resultado se almacenará un número entero cuyo valor depende del comando cmd. Si falla en resultado se almacenará el valor -1.

Valores

Significado

IPC_STAT

Lee el estado de la estructura de control shm_perm de la memoria compartida y lo devuelve en la zona apuntada por buffer.

IPC_SET

Inicializa alguno de los campos de la estructura de control de la memoria compartida shm_perm. El nuevo valor para estos campos los toma de la estructura apuntada por buffer.

IPC_RMID

Borra del sistema la región de memoria compartida identificada por shmid. Si existen varios procesos compartiendo la zona de memoria el borrado no se realiza hasta que todos los procesos liberen la memoria.

SHM_LOCK

Bloquea el segmento identificado por shmid. Esto implica que no se puede intercambiar a memoria secundaria. Solo se permite esta operación si el identificador de usuario efectivo es igual al del superusuario.

SHM_UNLOK

Desbloquea el segmento de memoria compartida shmid, permitiendo el intercambio con memoria secundaria. Solo se permite esta operación si el identificador de usuario efectivo es igual al del superusuario



Tabla 7.2: Valores posibles del parámetro cmd de la llamada shmctl

  • Ejemplo 7.16:

Las siguientes líneas de código C muestran como crear una zona de memoria compartida en la que se va almacenar un array unidimensional de 20 números reales. Tras manipular dicho array, la zona de memoria compartida es eliminada.

#include<sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#define MAX 20

int shmid, i;

float *array;

key_t llave;

...

/* Creación de una llave.*/

llave=ftok("prueba",'K');

/* Petición de una zona de memoria compartida */

shmid=shmget(llave,MAX*sizeof(float),IPC_CREAT | 0600);

/* Unión de la zona de memoria compartida a nuestro espacio de direcciones virtuales. */

array=shmat(shmid,0,0);

/* Manipulación de la zona de memoria compartida */

for (i=0; i<MAX; i++){

array[i]=i*i;

}

...

/* Separación de la zona de memoria compartida de nuestro espacio de direcciones virtuales. */

shmdt(array);

/* Borrado de la zona de memoria compartida */

shmctl(shmid, IPC_RMID,0);