|
|
Este documento está disponible en los siguientes idiomas: English Castellano Deutsch Francais Nederlands Russian Turkce |
por Leonardo Giordani <leo.giordani(at)libero.it> Sobre el autor: Acaba de recibir el diploma de la Facultad de Ingeniería de Telecomunicaciones en el Politécnico de Milán. Interesado en programación (principalmente en Ensamblador y C/C++). Desde 1999 trabaja casi exclusivamente con Linux/Unix. Taducido al español por: Rafa Pereira <rptv2003(at)yahoo.com> Contenidos: |
Programación concurrente - Colas de mensajes (2)Resumen: Esta serie de artículos tiene el propósito de introducir al lector al concepto de multitarea y a su implementación en el sistema operativo Linux. Comenzando por los conceptos teóricos base de la multitarea, acabaremos escribiendo una aplicación completa mostrando la comunicación entre procesos, mediante un protocolo de comunicaciones sencillo pero eficiente. Los requisitos previos para entender este artículo son:
También podría ser buena idea leer antes alguno de los artículos previos de esta serie:
|
Ya dijimos que un protocolo es un conjunto de reglas que permite a las personas o a las máquinas hablar, incluso siendo diferentes. Por ejemplo, el uso de la lengua inglesa es un protocolo, porque me permite hablarles a mis lectores hindúes (que siempre están muy interesados en lo que escribo). Hablando en términos más relacionados con Linux, si recompilas el kernel (no temas, no es tan difícil), seguramente observarás la sección Networking, en la cual puedes hacer que tu kernel entienda varios protocolos de red, como TCP/IP.
A la hora de crear un protocolo tenemos que decidir qué clase de aplicación vamos a desarrollar. En esta ocasión vamos a escribir un simulador de conmutador telefónico sencillo. El proceso principal será el conmutador telefónico, y los procesos hijos actuarán como usuarios: permitiremos a los usuarios enviarse mensajes entre ellos a través del conmutador.
El protocolo va a cubrir tres situaciones distintas: el nacimiento de un usuario (esto es, el usuario existe y está conectado), el trabajo normal del usuario, y la muerte de un usuario (ya no está conectado). Hablemos de estas tres situaciones.
Cuando un usuario se conecta al sistema crea su propia cola de mensajes (no olvides que estamos hablando de procesos), sus identificadores tienen que ser enviados al conmutador para que éste sepa cómo llegar a cada usuario. En este punto, el usuario puede inicializar las estructuras o los datos que necesite. Por último, recibe del conmutador el identificador de una cola en la que puede escribir los mensajes que quiere enviar a otros usuarios a través del conmutador.
El usuario puede enviar y recibir mensajes. Cuando envía un mensaje a otro usuario se pueden dar dos situaciones: que el receptor esté conectado o que no lo esté. En ambos casos se enviará un mensaje de confirmación al emisor, para informarle de lo que ha ocurrido con su mensaje. Esto no supone ninguna acción por parte del receptor, es el conmutador quien hace el trabajo.
Cuando un usuario se desconecta del sistema debería notificárselo al conmutador, pero no es necesaria ninguna otra acción. El metacódigo que describe este modo de trabajo es el siguiente:
/* Nacimiento */ create_queue init send_alive send_queue_id get_switch_queue_id /* Trabajo */ while(!leaving){ receive_all if(<send condition>){ send_message } if(<leave condition>){ leaving = 1 } } /* Muerte */ send_dead
Ahora tenemos que definir el comportamiento de nuestro conmutador telefónico: cuando un usuario se conecta nos envía un mensaje que contiene el identificador de su cola de mensajes. A continuación, debemos almacenar el identificador, para poder enviar los mensajes dirigidos a este usuario, y responder enviándole el identificador de una cola en la que pueda escribir los mensajes que tengamos que enviar a otros usuarios. Además, tenemos que analizar todos los mensajes recibidos y comprobar si los receptores están conectados: si el receptor está conectado le enviaremos el mensaje, si el receptor no está conectado desecharemos el mensaje; en ambos casos enviaremos una confirmación al emisor. Cuando un usuario se desconecta, simplemente eliminaremos el identificador de su cola, de forma que no pueda recibir más mensajes.
La implementación en metacódigo es:
while(1){ /* Usuario nuevo */ if (<nacimiento de un usuario>){ get_queue_id send switch_queue_id } /* El usuario muere */ if (<muerte de un usuario>){ remove_user } /* Envío de mensajes */ check_message if (<usuario vivo>){ send_message ack_sender_ok } else{ ack_sender_error } }
Lo primero es definir una estructura para nuestro mensaje utilizando el prototipo de msgbuf del kernel.
typedef struct { int service; int sender; int receiver; int data; } messg_t; typedef struct { long mtype; /* Tipo de mensaje */ messg_t messaggio; } mymsgbuf_t;
Esto es algo general que podemos extender después: los campos sender y receiver contienen un identificador de usuario y el campo data contiene datos en general, mientras que el campo service se utiliza para solicitar un determinado servicio al conmutador. Por ejemplo, imaginemos que tenemos dos servicios: uno para entrega inmediata y otro para entrega con retraso, en cuyo caso el campo data podría transportar el número de segundos de retraso. Este es tan sólo un ejemplo, lo importante es que entendamos que el campo service nos ofrece muchas posibilidades.
Ahora podemos implementar algunas funciones para gestionar nuestras estructuras de datos, en particular para asignar y consultar el valor de los campos de los mensajes. Estas funciones son más o menos todas iguales, así que sólo mostraré dos de ellas, puedes encontrar el resto en los ficheros .h
void set_sender(mymsgbuf_t * buf, int sender) { buf->message.sender = sender; } int get_sender(mymsgbuf_t * buf) { return(buf->message.sender); }
El objetivo de estas funciones no es el de reducir el código (consisten solamente en una línea de código): están ahí simplemente para recordar su significado y hacer que la codificación del protocolo se acerque más al lenguaje humano y, por lo tanto, sea más fácil de usar.
Ahora tenemos que escribir funciones para generar claves IPC, crear y eliminar colas de mensajes y enviar y recibir mensajes. Construir una clave IPC es sencillo:
key_t build_key(char c) { key_t key; key = ftok(".", c); return(key); }
Entonces, la función para crear una cola es:
int create_queue(key_t key) { int qid; if((qid = msgget(key, IPC_CREAT | 0660)) == -1){ perror("msgget"); exit(1); } return(qid); }
como puedes ver, el tratamiento de errores es muy sencillo en este caso. La siguiente función destruye una cola:
int remove_queue(int qid) { if(msgctl(qid, IPC_RMID, 0) == -1) { perror("msgctl"); exit(1); } return(0); }
Y, por último, las funciones para recibir y enviar mensajes: para nosotros enviar un mensaje significa escribirlo en una determinada cola, en la especificada por el conmutador.
int send_message(int qid, mymsgbuf_t *qbuf) { int result, lenght; lenght = sizeof(mymsgbuf_t) - sizeof(long); if ((result = msgsnd(qid, qbuf, lenght, 0)) == -1){ perror("msgsnd"); exit(1); } return(result); } int receive_message(int qid, long type, mymsgbuf_t *qbuf) { int result, length; length = sizeof(mymsgbuf_t) - sizeof(long); if((result = msgrcv(qid, (struct msgbuf *)qbuf, length, type, IPC_NOWAIT)) == -1){ if(errno == ENOMSG){ return(0); } else{ perror("msgrcv"); exit(1); } } return(result); }
Y esto es todo. Encontrarás las funciones en el fichero layer1.h: intenta escribir algún programa (por ejemplo el del artículo anterior) utilizándolas. En el próximo artículo hablaremos sobre la capa 2 del protocolo y la implementaremos.
Como siempre, puedes enviarme comentarios, correcciones y preguntas a mi dirección de e-mail (leo.giordani(at)libero.it) or a través de la página "Talkback". Por favor, escríbeme en inglés, alemán o italiano.
|
Contactar con el equipo de LinuFocus
© Leonardo Giordani, FDL LinuxFocus.org |
Información sobre la traducción:
|
2003-05-06, generated by lfparser version 2.34