EJEMPLOS
INTRODUCCION
En el presente capítulo se va a demostrar la potencia y versatilidad de la librería PPL, utilizando para ello dos aplicaciones que funcionan empleando el protocolo de comunicación entre ordenadores TCP/IP.
La primera aplicación consiste en un programa de chat completo con el proceso servidor y el cliente que pueden utilizar los usuarios para comunicarse con cualquier otra persona, de cualquier parte del mundo, que se encuentre conectada al mismo servidor de chat. La segunda aplicación consiste en un programa que permite llevar a cabo el suavizado binario de una imagen aprovechándose de la potencia combinada de los ordenadores de una red local o de conectados a Internet distribuidos por todo el planeta.
La primera de las aplicaciones se sirve de las funciones más básicas de la librería para su implementación, en ella se puede ver como, gracias a la librería PPL, se pueden construir aplicaciones en red sin mucha dificultad. Es la segunda aplicación la que hace un uso exhaustivo de la librería PPL y en la cual se demuestra todo su potencial y facilidad de uso.
EJEMPLO : PROGRAMA PARA CHAT
Los programas de chat permiten que personas localizadas en cualquier zona geográfica del planeta puedan comunicarse empleando para ello su teclado y su monitor. Lo único que tienen que hacer dos personas que quieren entablar una conversación a través del teclado es conectarse a un mismo servidor de chat, y dentro de este servidor a un canal concreto. Cada persona conectada al mismo canal podrá ver lo que escribe el resto de personas, y podrá tomar parte en la tertulia.
El servidor empleado en este ejemplo no maneja canales, ni dispone de las grandes posibilidades que proporcionan los modernos servidores de chat. Solo es un ejemplo ilustrativo del potencial que presenta PPL. Veremos como con relativamente poco código se pueden implementar aplicaciones distribuidas de gran potencia y sofisticación.
En primer lugar veremos el protocolo de aplicación con todos los servicios ideados para el servidor de chat. A continuación se explicará detalladamente el modo de operación de los procesos cliente - servidor, y por último se expondrá el código fuente de cada uno de estos procesos con comentarios detallados de cada una de las partes claves del código, de modo que el lector pueda apreciar la sencillez de implementación que permite la librería PPL.
Protocolo Chat
La cantidad de servicios que va a prestar el servidor de chat no es muy elevada. Además, el servidor no hace uso de los servicios PPL standard, sino que se han definido unos propios. A continuación se pasa a describir los servicios definidos para el protocolo chat :
Pasemos ahora a ver como funciona cada uno de los procesos, así como la implementación de los mismos.
Servidor Chat
Como todo proceso servidor, se encarga de atender las peticiones de servicio que recibe por parte de los clientes. El siguiente diagrama esquematiza el modo de funcionamiento del proceso servidor :

En la fase de inicio el servidor se sirve de la función PPL init_server, para la creación de la dirección de red en la cual quedará en espera de solicitudes de servicio. Recordar que esta función recibe como parámetro el número de puerto de protocolo en el cual va a operar el servidor y que devuelve el descriptor de socket para manejar las solicitudes de servicio recibidas por la dirección de red creada.
El servidor gestiona una lista del tipo t_host con las direcciones de red de los clientes que han iniciado un sesión chat. Cuando recibe una cadena de uno de los clientes, la retransmite a todos los clientes conectados a él (incluido el cliente que inicio el servicio TEXTO) para que la impriman en sus terminales.
Cliente Chat
Se encarga de interactuar con el usuario y transmitir las cadenas de texto introducidas al servidor, para que este las retransmita al resto de usuarios conectados al servidor de chat. La sintaxis que debe utilizar el usuario para ejecutar el cliente de chat es la siguiente :
clichat dirección_IP_servidor
Nada mas ser iniciada la ejecución del cliente por parte del usuario, este entra en una fase de inicio en la cual comprueba si el número de parámetros que recibe es correcto, así como si el valor de los parámetros recibidos es válido. A continuación obtiene información sobre la máquina local en la cual se está ejecutando y la muestra al usuario. Forma la dirección de red del servidor de chat, con la dirección IP especificada por el usuario y el puerto correspondiente para el servidor de chat, dicho puerto se ha definido en el fichero de cabecera chat.h, cuyo código fuente es :
#ifndef __CHAT_INCLUDE__
#define __CHAT_INCLUDE__
/* Numero de los puertos de protocolo a ser empleados por la aplicación. */
#define PUERTO_SERVIDOR_CHAT 6666
#define PUERTO_CLIENTE_CHAT 7777
/* Servicios de la aplicación. */
#define LOGIN 1000
#define TEXTO 2000
#define LOGOFF 3000
#endif
Acto seguido intenta establecer conexión con el servidor, si no existe ninguno en la dirección de red especificada se lo indica al usuario con un mensaje de error y finaliza la ejecución del cliente. Si la conexión con el servidor se establece satisfactoriamente, pasa a obtener información relativa al equipo en el cual se ejecuta el servidor y se la muestra al usuario. Luego solicita el servicio de inicio de sesión (LOGIN) para que el servidor actualice su lista de clientes. Por último se pide al usuario que especifique su nickname. Todo esto forma parte de la parte de la fase de inicio del cliente de chat.
El cliente de chat crea un proceso hijo encargado de mostrar en pantalla las cadenas remitidas por el servidor de chat en el cual ha iniciado la sesión. El proceso padre se encarga de la obtener las cadenas que teclea el usuario y las envía al servidor como formando parte del cuerpo de una trama con el servicio TEXTO.
Por lo tanto, el cliente de chat actúa a la vez como cliente y como servidor. El proceso hijo toma el papel de un servidor de terminal, ya atiende las peticiones del proceso que remite la cadena a ser mostrada en el terminal. Así, el proceso servidor también se comporta como un cliente cuando envía las cadenas a cada uno de los procesos hijos de los clientes que hay conectados a él. En el fichero de cabecera chat.h se definen los números de puerto de protocolo, tanto para el servidor de chat, como para el proceso hijo del cliente de chat :
#define PUERTO_SERVIDOR_CHAT 6666
#define PUERTO_CLIENTE_CHAT 7777
Por último, el usuario puede señalar que desea finalizar la sesión en el servidor de chat indicando la cadena FIN. El cliente mata a su proceso hijo y solicita el servicio LOGOFF para que el servidor de chat lo elimine de su lista de clientes.

Supongamos que tenemos dos procesos clientes que ya han iniciado una sesión en el servidor de chat. El gráfico de la página anterior muestra los pasos que se siguen cuando uno de los clientes transmite una cadena de caracteres al servidor de chat :
A continuación se expone el código fuente, tanto del proceso servidor de chat como del proceso cliente, explicando detalladamente como se ha llevado a cabo la implementación de cada una de las funciones descritas anteriormente.
Implementación Servidor Chat
El programa principal del servidor de chat ha sido implementado del siguiente modo :
#include <stdio.h>
#include "../ppl/ppl.h"
#include "chat.h"
/* Lista con los cliente conectados al servidor de chat. */
struct t_host* lista_hosts = NULL;
/* Prototipos de las funciones definidas por el servidor de chat. */
void atender_cliente(int nsfd);
void envia_cadena(struct t_host* host,char* cad);
/*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/
main(void)
{
struct sockaddr_in cli_addr;
struct t_trama trama;
int cli_addr_len = sizeof(struct sockaddr_in);
int sfd, nsfd;
/* Se forma e inicializa la dirección de red en la cual atenderá peticiones
el servidor de chat. Se emplea la función PPL init_server para llevar a cabo
dicha labor de inicialización. */
sfd = init_server(PUERTO_SERVIDOR_CHAT);
/* Bucle para procesar las peticiones de servicio. */
for(;;)
{
/* Bloqueo del proceso servidor en espera de peticiones. */
printf("\nSERVIDOR EN ESPERA......\n\n");
if ((nsfd=accept(sfd,(void*) &cli_addr,&cli_addr_len)) == -1)
error(DEP,"accept");
/* Se atiende el servicio solicitado por el cliente. */
atender_cliente(nsfd);
/* El nuevo descriptor de socket no puede utilizarse para
aceptar nuevas conexiones. Pasamos a cerrarlo. */
close(nsfd);
}
}
/*//////////////////////////////////////////////////////////////////////////*/
Como se puede apreciar la función main del proceso servidor de chat lleva a cabo todas las tareas descritas anteriormente. El servidor no funciona en modo concurrente ya que el servicio que lleva a cabo no suele requerir mucho tiempo de proceso y, además, como la función PPL init_server define una cola de cinco peticiones de servicio, es muy remota la posibilidad de que se pierda una solicitud de servicio. Dentro del propio código del servidor se han definido un par de funciones que llevan a cabo tareas concretas.
La función atender_cliente se invoca cada vez que un cliente conecta con el servidor. En ella se implementan los tres servicios chat descritos anteriormente. Recibe como parámetro el socket al cual está conectado el cliente que solicita el servicio.
/*--------------------------------------------------------------------------*/
void atender_cliente(int nsfd)
{
struct t_trama trama;
struct sockaddr_in dir;
struct hostent *cliente;
int long_dir_cli = sizeof(struct sockaddr_in);
struct t_host* host;
/* Solo se van a emplear tramas de control sin cuerpo. */
trama.head[BODY_SIZE] = 0;
/* Se obtiene la trama con el servicio solicitado por el cliente. */
recibe(nsfd,&trama);
/* Averiguamos cual es el servicio solicitado. */
switch(trama.head[SERVICIO])
{
case LOGIN:
printf("SERVICIO SOLICITADO: Inicio de sesion\n");
/* Obtenemos la direccion IP del cliente. */
getpeername(nsfd,(void*)&dir,&long_dir_cli);
/* Completamos la dirección de red del cliente incluyendo el puerto
en el cual va ha estar escuchando el proceso hijo del cliente. */
dir.sin_family = AF_INET;
dir.sin_port = htons(PUERTO_CLIENTE_CHAT);
/* Almacenamos la dirección en la lista de clientes. */
host = existe_host(dir.sin_addr.s_addr,lista_hosts);
if (host==NULL)
insert_host(&lista_hosts,make_host(&dir));
/* Se envia la respuesta al cliente. */
trama.head[SERVICIO] = OK;
envia(nsfd,&trama);
break;
case TEXTO:
printf("SERVICIO SOLICITADO: Cadena de caracteres\n");
printf("Cadena: %s \n",trama.body);
/* Se retransmite la cadena a cada uno de los clientes que tienen iniciada
la sesión en el servidor. */
host = lista_hosts;
while(host!=NULL)
{
envia_cadena(host,trama.body);
host=host->psig;
}
/* Se libera el cuerpo de la trama. Recordar que este es creado en
memoria dinámica por la función recibir. */
free(trama.body);
break;
case LOGOFF:
printf("SERVICIO SOLICITADO: Fin de sesion\n");
/* Obtenemos la dirección IP del cliente. */
getpeername(nsfd,(void*)&dir,&long_dir_cli);
/* Completamos la dirección de red del cliente para buscar en la lista de
clientes cual es el que vamos a eliminar. */
dir.sin_family = AF_INET;
dir.sin_port = htons(PUERTO_SERVIDOR_CHAT);
/* Se busca el cliente en la lista y se elimina de ella. */
host = existe_host(dir.sin_addr.s_addr,lista_hosts);
if (host!=NULL)
destroy_host(&lista_hosts,host);
/* Componemos la trama de respuesta para el cliente que solicito el servicio. */
trama.head[SERVICIO] = OK;
envia(nsfd,&trama);
break;
default:
printf("SERVICIO DESCONOCIDO\n");
break;
}
}
/*--------------------------------------------------------------------------*/
Por último, la siguiente función es empleada para establecer conexión con el proceso hijo de cliente cuya dirección de red está almacenada en la estructura host que recibe como parámetro, y enviarle una réplica de la cadena recibida por el servidor.
/*--------------------------------------------------------------------------*/
void envia_cadena(struct t_host* host, char* cad)
{
struct t_trama trama;
int sfd;
if((sfd=connect_server(host->dir)) == -1)
{
fprintf(stderr,"No se pudo establecer conexion con el servidor.\n");
return;
}
/* Composicion de la trama a enviar al cliente para que la muestre en
su terminal. */
trama.head[SERVICIO] = TEXTO;
trama.head[BODY_SIZE] = strlen(cad) + 1;
trama.body = cad;
envia(sfd,&trama);
close(sfd);
}
/*--------------------------------------------------------------------------*/
Implementación Cliente Chat
A continuación se muestra el código fuente que implementa la función principal del cliente de chat :
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include "../ppl/ppl.h"
#include "chat.h"
/* Variables globales del programa. */
struct t_host* server;
char nickname[80];
char cad[256]; /* Cadena a enviar al servidor. */
int pid; /* PID del proceso hijo. */
void inicio_sesion(char* IP);
void fin_sesion(void);
/*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/
main(argc, argv)
int argc;
char* argv[];
{
struct sockaddr_in cli_addr;
struct t_trama trama;
int cli_addr_len = sizeof(struct sockaddr_in);
int sfd, nsfd;
if (argc != 2)
{
fprintf(stderr,"\nSintaxis: %s direccion_IP_servidor\n\n",argv[0]);
exit(-1);
}
/* La siguiente función lleva a cabo todo el proceso de inicialización
descrito anteriormente. */
inicio_sesion(argv[1]);
/* Se crea el proceso encargado de imprimir en pantalla las cadenas
remitidas por se servidor de chat. */
if ((pid=fork()) == -1)
error(DEP,"fork");
else if (pid==0) /* Codigo para el proceso hijo. */
{
/* El hijo toma el papel de un proceso servidor cuyo único servicio es la
impresión en pantalla de la cadena de caracteres que recibe en la trama
enviada por el proceso servidor de chat. */
sfd = init_server(PUERTO_CLIENTE_CHAT);
/* Bucle para procesar las peticiones de servicio. */
for(;;)
{
/* Bloqueo del proceso servidor en espera de peticiones. */
if ((nsfd=accept(sfd,(void*) &cli_addr,&cli_addr_len)) == -1)
error(DEP,"accept");
/* El único servicio es la impresión del cuerpo de la trama. */
recibe(nsfd,&trama);
printf("%s\n",trama.body);
close(nsfd);
}
}
/* El siguiente bucle es empleado por el proceso padre para obtener
una cadena tecleada por el usuario y remitirla al servidor de chat. */
for (;;)
{
/* Cadena a mandar al servidor. */
gets(cad+strlen(nickname));
/* Se comprueba si la cadena es la cadena que indica el fin de sesión. */
if (!strcmp(cad+strlen(nickname),"FIN"))
fin_sesion();
/* A continuación se establece conexión con el servidor y se solicita el
servicio TEXTO para mandarle la cadena. */
/* Se establece la conexión con el servidor. */
if((sfd=connect_server(server->dir)) == -1)
{
fprintf(stderr,"No se pudo establecer conexion con el servidor.\n");
exit(-1);
}
/* Composición de la trama a enviar al servidor. */
trama.head[SERVICIO] = TEXTO;
trama.head[BODY_SIZE] = strlen(cad) + 1;
trama.body = cad;
/* Envío de la trama. */
if(envia(sfd,&trama) == -1)
{
fprintf(stderr,"No se pudo enviar la trama al servidor");
exit(-1);
}
/* Se cierra el socket, ya que no se va a emplear más. */
close(sfd);
}
}
/*//////////////////////////////////////////////////////////////////////////*/
El cliente de chat se sirve de dos funciones para llevar a cabo su labor. La función inicio_sesion lleva a cabo todas las tareas de inicialización explicadas anteriormente. Recibe como parámetro la dirección IP en la cual el se supone que se encuentra el servidor de chat.
/*--------------------------------------------------------------------------*/
void inicio_sesion(char* IP)
{
struct sockaddr_in ser_addr;
struct t_trama trama;
int sfd;
char hostname[80];
char hostIP[80];
int port;
/* Comprobamos si la dirección IP indicada por el usuario es valida. */
if (inet_addr(IP) == -1)
{
fprintf(stderr,"%s : No es una direccion IP valida.\n",IP);
exit(-1);
}
/* Información sobre el host en el cual se ejecuta el cliente. */
get_local_info(hostname,hostIP);
printf(">>>>> DATOS DE LA MAQUINA LOCAL <<<<<\n");
printf("NOMBRE: %s\n",hostname);
printf("DIRECCION IP: %s\n\n",hostIP);
/* Dirección de red del servidor. */
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = inet_addr(IP);
ser_addr.sin_port = htons(PUERTO_SERVIDOR_CHAT);
/* Se intenta realizar la conexión con el servidor que supuestamente se
encuentra esperando solicitudes de servicio en la dirección de red
formada anteriormente. */
if((sfd=connect_server(ser_addr)) == -1)
{
fprintf(stderr,"No se pudo establecer conexion con el servidor.\n");
exit(-1);
}
/* Información sobre el host en el cual se ejecuta el servidor. */
get_remote_info(sfd,hostname,hostIP,&port);
printf(">>>>> DATOS DE LA MAQUINA REMOTA <<<<<\n");
printf("NOMBRE: %s\n",hostname);
printf("DIRECCION IP: %s\n",hostIP);
printf("PUERTO: %d\n\n",port);
/* Composición de la trama a enviar al servidor solicitando el servicio
inicio de sesión. */
trama.head[SERVICIO] = LOGIN;
trama.head[BODY_SIZE] = 0;
envia(sfd,&trama);
recibe(sfd,&trama);
close(sfd);
printf(">>>>> CONEXION CON EL SERVIDOR DE CHAT ESTABLECIDA <<<<<\n");
/* Inicializamos la estructura que almacena la dirección del servidor. */
server = make_host(&ser_addr);
printf("NICKNAME: ");
gets(nickname);
strcpy(cad,"<");
strcat(cad,nickname);
strcat(cad,"> ");
strcpy(nickname,cad);
printf("\n>>>>> PUDE EMPEZAR A CHATEAR <<<<<\n");
}
/*--------------------------------------------------------------------------*/
La siguiente función se invoca cuando el usuario introduce la cadena FIN indicando que desea dar fin a su sesión de chat. Se encarga de eliminar al proceso hijo y de conectar con el servidor de chat para solicitar el servicio fin de sesión.
/*--------------------------------------------------------------------------*/
void fin_sesion()
{
struct sockaddr_in ser_addr;
struct t_trama trama;
int sfd;
/* Matamos el proceso hijo. */
kill(pid,SIGKILL);
if((sfd=connect_server(server->dir)) == -1)
{
fprintf(stderr,"No se pudo establecer conexion con el servidor.\n");
exit(-1);
}
/* Composición de la trama a enviar al servidor solicitando el servicio
fin de sesión. */
trama.head[SERVICIO] = LOGOFF;
trama.head[BODY_SIZE] = 0;
envia(sfd,&trama);
recibe(sfd,&trama);
/* Se concluye la ejecución del cliente de chat. */
close(sfd);
printf(">>>>> SESION CHAT CONCLUIDA <<<<<\n\n");
exit(0);
}
/*--------------------------------------------------------------------------*/
EJEMPLO : SUAVIZADO DE IMAGENES BINARIAS
Con el siguiente ejemplo se pretende demostrar el potencial de la librería PPL para la implementación de aplicaciones de procesamiento paralelo. El algoritmo paralelizable implementado es el suavizado de imágenes binarias.
El entorno de ejecución de la aplicación estará constituido por un conjunto de procesos servidores ejecutándose cada uno en una máquina o procesador distinto y encargados de llevar a cabo la tarea de suavizado sobre la partición asignada ; y un proceso cliente encargado de la interacción con el usuario, de las tareas de examen de la red en busca de servidores, particionado y asignación de particiones a cada proceso servidor, inicio de la ejecución y recopilación de los resultados.
A continuación vamos a describir como se ha implementado esta aplicación detalladamente. Debido a la extensión del código fuente no se va a mostrar aquí en su totalidad, solamente se mostrará el código de las funciones más relevantes.
Suavizado de Imágenes Binarias
La imágenes binarias resultan de usar luz posterior o luz estructurada, o de procesamientos tales como la detección de contornos o de umbrales. Usaremos el convenio de etiquetar los puntos oscuros con un 1 y los puntos iluminados con un 0. Así, ya que las imágenes binarias sólo pueden tomar dos valores, el ruido en este caso produce efectos tales como contornos irregulares, pequeños huecos, esquinas perdidas y puntos aislados.
La idea básica de los métodos explicados a continuación consiste en evaluar una función booleana específica sobre un entorno de vecindad centrado sobre un pixel p, y, dependiendo de la configuración espacial y los valores binarios de sus vecinos, asignarle a p un 0 o un 1. Debido a las limitaciones en el tiempo de procesamiento disponible para tareas de visión industrial, el análisis se limitará a menudo a los ocho vecinos de p, usando entonces la máscara 3 x 3 que se muestra en la siguiente figura :

El procedimiento de suavizado lleva a cabo las siguientes tareas :
Tomando como referencia la figura anterior, los dos primeros procesamientos de suavizado que se mencionan se llevan a cabo usando la expresión booleana :
![]()
donde "." y "+" representan a las funciones lógicas AND y OR, respectivamente. Siguiendo el convenio establecido anteriormente, a un pixel oscuro dentro del área de la máscara se le asigna un 1 lógico y a un pixel claro un 0 lógico. Así pues, si B1=1, asignaremos un 1 a p y en caso contrario este pixel tomará el valor 0. La ecuación anterior se aplica a todos los pixels simultáneamente, entendiéndose que se determina el siguiente valor para cada posición de pixel antes de que cualquiera de los pixels hayan sido actualizados.
Los pasos 3 y 4 del proceso de suavizado se llevan a cabo evaluando la expresión booleana :
![]()
simultáneamente para todos los pixels. Como se hizo anteriormente, haremos p=1 si B2=1 asignando a p un cero cuando B2=0.
La recuperación de los puntos de la esquina superior derecha se lleva a cabo usando la expresión :
![]()
donde la línea situada encima de la p y del paréntesis representa la negación lógica. De la misma forma, las esquinas inferior derecha, superior izquierda e inferior izquierda se recuperan usando las expresiones :
![]()
![]()
![]()
Estas cuatro últimas expresiones realizan el paso 5 del procedimiento de suavizado.
El proceso servidor, cuyo nombre es serimg, implementa una función encargada de aplicar las seis máscaras anteriores a la partición de imagen que el cliente le ha remitido previamente. El código fuente que implementa dicha función es :
/*--------------------------------------------------------------------------*/
void procesar (void)
{
imagen* imgsrc = (imagen*)data_source->data;
imagen* imgdst = (imagen*)data_result->data;
/* Se aplican los distintos tipos de suavizado binario sobre la imagen
que esta siendo procesada. */
suavizado_binario(imgsrc,imgdst,B1);
suavizado_binario(imgdst,imgsrc,B2);
suavizado_binario(imgsrc,imgdst,B3);
suavizado_binario(imgdst,imgsrc,B4);
suavizado_binario(imgsrc,imgdst,B5);
suavizado_binario(imgdst,imgsrc,B6);
}
/*--------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------*/
void suavizado_binario(imagen* imgsrc, imagen* imgdst, int tipo)
{
int i,j,fila_inicial,fila_final,ncol,nfil,pixelpos;
unsigned char a,b,c,d,e,f,g,h,p; /* Valores de pixel */
/* Posicion del pixel que esta siendo procesado. */
pixelpos = imgsrc->pixel_inicial;
/* Numero de columnas de la imagen a ser procesada. */
ncol = imgsrc->columnas;
/* Numero de filas de la imagen a ser procesada. */
nfil = imgsrc->filas;
/* Fila en que se encuentra el ultimo pixel a procesar. */
fila_final = imgsrc->pixel_final / imgsrc->columnas;
/* Fila en que se encuentra el primer pixel a procesar. */
fila_inicial = imgsrc->pixel_inicial / imgsrc->columnas;
/* Columna en que se encuentra el primer pixel a procesar. */
j = imgsrc->pixel_inicial % imgsrc->columnas;
/* Procesado de la imagen. */
for(i=fila_inicial;i<=fila_final;i++)
{
for(;j<ncol;j++)
{
/* Valor del pixel a procesar */
p = imgsrc->pixel[i][j];
/* Valor de pixel para los vecinos de p */
a = ((i-1)<0 || (j-1)<0) ? 0 : imgsrc->pixel[i-1][j-1];
b = ((i-1)<0) ? 0 : imgsrc->pixel[i-1][j];
c = ((i-1)<0 || (j+1)>=ncol) ? 0 : imgsrc->pixel[i-1][j+1];
d = ((j-1)<0) ? 0 : imgsrc->pixel[i][j-1];
e = ((j+1)>=ncol) ? 0 : imgsrc->pixel[i][j+1];
f = ((i+1)>=nfil || (j-1)<0) ? 0 : imgsrc->pixel[i+1][j-1];
g = ((i+1)>=nfil) ? 0 : imgsrc->pixel[i+1][j];
h = ((i+1)>=nfil || (j+1)>=ncol) ? 0 : imgsrc->pixel[i+1][j+1];
/* Valor de pixel en la imagen de resultados. */
switch(tipo)
{
case B1:
/* Rellena los huecos de un pixel en zonas oscuras y rellena
los pequenyos cortes y muescas en segmentos de lados rectos. */
imgdst->pixel[i][j] = p | b&g&(d|e) | d&e&(b|g);
break;
case B2:
/* Elimina los puntos aislados y las pequenyas protuberancias
a lo largo de los segmentos de lados rectos. */
imgdst->pixel[i][j] = p&((a|b|d)&(e|g|h)&(b|c|e)&(d|f|g));
break;
case B3:
/* Recupera esquina superior derecha. */
imgdst->pixel[i][j] = (~p) & (d&f&g) & (~(a|b|c|e|h)) | p;
break;
case B4:
/* Recupera esquina inferior derecha. */
imgdst->pixel[i][j] = (~p) & (a&b&d) & (~(c|e|f|g|h)) | p;
break;
case B5:
/* Recupera esquina superior izquierda. */
imgdst->pixel[i][j] = (~p) & (e&g&h) & (~(a|b|c|d|f)) | p;
break;
case B6:
/* Recupera esquina inferior izquierda. */
imgdst->pixel[i][j] = (~p) & (b&c&e) & (~(a|d|f|g|h)) | p;
break;
}
if (pixelpos==imgsrc->pixel_final)
break;
}
j = 0; /* Seguiente pixel en la columna cero. */
}
}
/*--------------------------------------------------------------------------*/
Algoritmo de Particionado y Planificación
El siguiente algoritmo es valido solamente para aquellas tareas paralelizables en las cuales el tiempo requerido para procesar una cierta cantidad de información es independiente de la semántica o significado de dicha información y además es proporcional a la cantidad de información a procesar. Este es el caso de, por ejemplo, el algoritmo de suavizado de imágenes binarias que se expuso en la sección anterior.
Imaginemos que tenemos una imagen binaria de 1024x768 pixeles (supongamos para simplificar que se emplea un byte por pixel, cada byte podrá tomar el valor 0xFF o 0x00, según que el pixel sea negro o blanco, respectivamente). Esto nos da un total de P=1024*768=786432 bytes de información.
Queremos aplicar el suavizado a dicha imagen binaria. Disponemos de una red local con un total de 4 nodos, cada uno de ellos con sus propias características en cuanto a hardware se refiere. Nuestro problema consiste en ver como distribuir la información a cada uno de los nodos, de tal modo que el tiempo requerido para procesar la imagen sea mínimo.
Debido a que tenemos nodos con distintas características, precisamos de un parámetro que nos permita medir la potencia de cada nodo. Dicho parámetro va a ser la velocidad de proceso. En nuestro caso la velocidad de proceso va a hacer referencia a la cantidad de bytes por segundo que es capaz de procesar uno de los nodos de la red. Supongamos las siguientes velocidades :
vp1 = 1.5 bytes/segundo
vp2 = 0.75 bytes/segundo
vp3 = 0.5 bytes/segundo
vp4 = 0.25 bytes/segundo
Como vemos la velocidad del nodo 1 es dos veces superior a la del nodo 2, tres veces superior a la del nodo 3 y seis veces superior a la del nodo 4.
Hemos de conseguir una distribución de información entre los nodos acorde con estos valores. Para ello, en primer lugar hemos de averiguar el mínimo tiempo necesario para procesar nuestro volumen de información, teniendo en cuenta la capacidad de proceso total de nuestro sistema.
![]()

En nuestro caso el tiempo mínimo estimado será :

A continuación hay que asignar a cada nodo la cantidad de información que va a tener que procesar, en función de su velocidad de proceso. Esto se lleva a cabo en base a la siguiente fórmula :
![]()
Con la formula anterior obtenemos la cantidad de bytes que uno de los nodos es capaz de procesar en el tiempo tmin, es decir obtenemos la el tamaño en bytes de la partición a asignar al nodo. Para el caso concreto que estamos considerando vemos que las particiones son:
p1 = 1.5 bytes/segundo * 262144 segundos = 393216 bytes
p2 = 0.75 bytes/segundo * 262144 segundos = 196608 bytes
p3 = 0.5 bytes/segundo * 262144 segundos = 131072 bytes
p4 = 0.25 bytes/segundo * 262144 segundos = 65536 bytes
Se puede comprobar que la suma de todas las particiones es igual al tamaño total de la imagen :
P = p1 + p2 + p3 + p4 = 393216 + 196608 + 131072 + 65536 = 786432 bytes
También podemos comprobar que la cantidad de información que maneja el nodo 1 es el doble que la que maneja el nodo 2, ya que la velocidad de proceso del nodo 1 es el doble que la del nodo 2. La relación entre las distintas particiones asignadas a cada nodo sería la siguiente :
p1 = 2*p2 = 3*p3 = 6*p4
p2 = 1.5*p3 = 3*p4
p3 = 2*p4
la cual esta acorde con la velocidad de proceso de cada uno de los nodos de la red.
Los nodos con mayor capacidad de proceso manejan mayor cantidad de información. Se puede hacer una estimación del tiempo necesario que se va a requerir para procesar toda una imagen :
t1 = p1 / vp1 = 393216 / 1.5 = 262144 segundos
t2 = p2 / vp2 = 196608 / 0.75 = 262144 segundos
t3 = p3 / vp3 = 131072 / 0.5 = 262144 segundos
t4 = p4 / vp4 = 65536 / 0.25 = 262144 segundos
que, como se puede ver, corresponde al tiempo mínimo estimado para procesar toda la imagen teniendo en cuenta la potencia de proceso del sistema.
Situaciones a Tener en Cuenta
Pueden surgir las siguientes situaciones especiales durante la ejecución de la aplicación paralela para el suavizado de imágenes :
Proceso Servidor
El servicio que ofrece el proceso serimg es la potencia de proceso del procesador de la máquina en la cual se está ejecutando. Su función consiste en quedarse en espera de que algún cliente tome control sobre él, y pase a transmitirle la zona de datos sobre la cual tiene que ejecutar la función de proceso que tiene implementada. Su funcionamiento se puede resumir en tres pasos :
En el ejemplo que estamos considerando la función de proceso sería el suavizado de imágenes binarias, mientras que la zona de datos estaría constituida por la partición de la imagen a procesar. Se sirve de los protocolos PPL standard para llevar a cabo su labor.
Proceso Cliente
Existen dos versiones del mismo, una en que funciona en modo terminal (cliimg) y otra que funciona bajo entorno XWindows (cliimgx).
El cliente mantiene una zona de datos en la cual se almacena tanto la imagen a ser procesada, como la imagen resultante del procesamiento. También gestiona una lista de estructuras del tipo t_host en la cual se almacenan las direcciones de red de los servidores activos en la red, así como la potencia de proceso estimada del equipo en el que se está ejecutando un servidor en concreto.
El esquema de funcionamiento del cliente se puede resumir con el siguiente gráfico :

En la fase de examen de la red se establece conexión con los procesos servidores activos, evaluándose su potencia de proceso. Con cada nuevo servidor se va actualizando la lista de servidores que mantiene el cliente. Se emplea la siguiente función para evaluar la potencia de proceso de cada servidor :
/*--------------------------------------------------------------------------*/
float speed_test(struct t_host* nodo)
{
struct data_zone* pdz; /* Puntero a la estructura que maneja la zona de datos */
struct t_trama trama;
struct timeval t_inicio,t_fin;
struct timezone tz;
struct time_dif t_dif;
int sfd;
double vp;
/* Solo se van a emplear tramas de control sin cuerpo. */
trama.head[BODY_SIZE] = 0;
pdz = crea_imagen(100,100);
init_imagen((imagen*)pdz->data,0,90);
/* En primer lugar se transmite una cantidad de bytes a procesar. */
sfd = connect_server(nodo->dir);
trama.head[SERVICIO] = TRANSFER_DATA;
trama.head[NUM_BYTES] = pdz->nbytes;
envia(sfd,&trama);
recibe(sfd,&trama);
if (trama.head[RESPUESTA] == OK)
envia_bytes(sfd,pdz->data,pdz->nbytes);
close(sfd);
/* A continuacion se ordena al nodo que procese la informacion transmitida
y se calcula el tiempo necesario para dicho proceso.
De este modo se evalua la velocidad de proceso del nodo. */
gettimeofday(&t_inicio,&tz);
sfd = connect_server(nodo->dir);
trama.head[SERVICIO] = INICIO_PROCESO;
envia(sfd,&trama);
recibe(sfd,&trama);
close(sfd);
gettimeofday(&t_fin,&tz);
tdif(&t_inicio,&t_fin,&t_dif);
vp = (((imagen*)pdz->data)->tam)/t_dif.seg;
destroy_data_zone(NULL,pdz);
return vp;
}
/*--------------------------------------------------------------------------*/
En la fases de particionado y asignación de particiones se aplica el algoritmo de particionado y planificación explicado anteriormente. La siguiente función es la encargada de implementar dichas tareas.
/*--------------------------------------------------------------------------*/
void particionar(struct data_zone* pdz, struct t_host* lista_hosts)
{
imagen* img; /* Imagen a ser partcionada. */
imagen* imgaux;
struct t_host* host; /* Host al cual vamos a asignar la particion. */
struct data_zone* pdzaux;
float tmin; /* Tiempo minimo para procesar toda la zona de datos. */
float vp_global = 0.0; /* Velocidad de proceso global del sistema. */
int p = 0; /* Tamanyo en bytes de la particion a asignar a un nodo. */
int pospixel1, pospixel2;
int fila_inicial, fila_final, filas, columnas, fila, i, j;
/* Imagen a ser procesada. */
img = (imagen*) pdz->data;
/* Calculamos la capacidad de procesamiento global de nuestra red. */
host = lista_hosts;
while(host!=NULL)
{
vp_global += host->vp;
host = host->psig;
}
/* Tiempo minimo estimado para procesar el total de la imagen. */
tmin = img->tam / vp_global;
/* Se establece el tamaño de cada una de las particiones a asignar a cada
uno de los nodos de la red. */
host = lista_hosts;
pospixel1 = img->pixel_inicial;
columnas = img->columnas;
while(host!=NULL)
{
/* Almacenamos el tiempo de respuesta previsto para el host. */
host->tfin = tmin;
/* Tamanyo de la particion. */
p = (host->vp * tmin) + 0.5; /* Se redondea la cantidad de bytes */
/* Posicion del pixel final que va a ser procesado. */
pospixel2 = pospixel1 + p - 1;
if (pospixel2 > img->pixel_final)
pospixel2 = img->pixel_final;
/* La particion se va a llevar a cabo por filas. */
fila = pospixel1 / columnas;
fila_inicial = (fila==0) ? 0 : fila-1;
fila = pospixel2 / columnas;
fila_final = ((fila+1)>=img->filas) ? (img->filas - 1) : fila+1;
filas = fila_final - fila_inicial + 1;
/* Obtenemos el espacio necesario para la particion y creamos la
estructura de la particion inicializando todos sus valores. */
pdzaux = crea_imagen(filas,columnas);
imgaux = (imagen*) pdzaux->data;
imgaux->pixel_inicial = pospixel1 - fila_inicial*columnas;
imgaux->pixel_final = img->pixel_inicial + p - 1;
for (i=fila_inicial;i<=fila_final;i++)
for (j=0;j<columnas;j++)
imgaux->pixel[i-fila_inicial][j] = img->pixel[i][j];
/* Asignamos dicha particion al host. */
/* Si el host ya tenia una particion asignada la eliminamos. */
if (host->ppart!=NULL)
destroy_data_zone(NULL,host->ppart);
host->ppart = pdzaux;
/* Actualizamos el valor de las variables de control. */
pospixel1 += p;
host = host->psig;
}
}
/*--------------------------------------------------------------------------*/
Una vez particionada la imagen original entre el conjunto de servidores activos en la red, se mandan los datos de cada partición a su correspondiente servidor, empleando para ello la función de la librería PPL send_data_to_servers.
La siguiente fase es iniciar el procesamiento en cada uno de los servidores. Las siguientes funciones implementan tal tarea. Se establece, por cada uno de los servidores activos, un contador de tiempo gracias al cual se pueden detectar las situaciones anómalas (caída de alguno de los servidores, merma en el rendimiento de alguno de los servidores, etc.) explicadas anteriormente.
/*--------------------------------------------------------------------------*/
void sigalarm_handler()
{
sprintf(cad,"ATENCION: El host %s no ha finalizado en el tiempo previsto",inet_ntoa(host->dir.sin_addr));
DrawCad(cad);
InsertCadBuf(cad);
switch(status(host))
{
case PROCESANDO:
sprintf(cad,"ESTADO DEL HOST %s: Procesando",inet_ntoa(host->dir.sin_addr));
DrawCad(cad);
InsertCadBuf(cad);
break;
case NO_PROCESANDO:
sprintf(cad,"ESTADO DEL HOST %s: Proceso concluido",inet_ntoa(host->dir.sin_addr));
DrawCad(cad);
InsertCadBuf(cad);
exit(0);
break;
case -1:
sprintf(cad,"ESTADO DEL HOST %s: Inoperativo",inet_ntoa(host->dir.sin_addr));
DrawCad(cad);
InsertCadBuf(cad);
exit(1);
break;
default:
sprintf(cad,"ESTADO DEL HOST %s: Indefinido",inet_ntoa(host->dir.sin_addr));
DrawCad(cad);
InsertCadBuf(cad);
}
}
/*--------------------------------------------------------------------------*/
/*--------------------------------------------------------------------------*/
void start_proces(void)
{
/* Le indicamos a cada uno de los procesos activos en la red que empiece
a procesar el fragmento de imagen que posee en su memoria. */
int pid,status;
struct t_trama trama;
/* Solo se van a emplear tramas de control sin cuerpo. */
trama.head[BODY_SIZE] = 0;
/* Host o nodo a tratar. */
host = lista_hosts;
while(host!=NULL)
{
if ((pid=fork()) == -1)
error(DEP,"fork");
else if (pid==0) /* Codigo para el proceso hijo. */
{
struct timeval t_inicio,t_fin;
struct timezone tz;
struct time_dif t_dif;
struct itimerval temp,temp_ant; /* Temporizadores */
float ttotal;
/* Se compone la trama de control para indicar al proceso servidor
que inicie el procesamiento de los datos que tiene almacenados en
su memoria. */
host->sfd = connect_server(host->dir);
trama.head[SERVICIO] = INICIO_PROCESO;
/* Definimos un temporizador con el tiempo en el cual se ha previsto
finalize el procesamiento del servidor. */
/* Se da un margen de 500 milisegundos al tiempo previsto para tener
en cuenta el tiempo que se consume en el envio y recepcion de las
tramas de control. */
ttotal = host->tfin + 0.5;
temp.it_value.tv_sec = ttotal;
temp.it_value.tv_usec = (ttotal-temp.it_value.tv_sec) * 1000000;
temp.it_interval.tv_sec = 0;
temp.it_interval.tv_usec = 0;
/* Se crea un proceso hijo encargado de manejar el temporizador */
if ((pid=fork()) == -1)
error(DEP,"fork");
else if (pid==0) /* Codigo para el proceso hijo. */
{
signal(SIGALRM,sigalarm_handler);
setitimer(ITIMER_REAL,&temp,&temp_ant);
/* Se espera la conlusion del procesamiento por parte del
servidor. */
recibe(host->sfd,&trama);
exit(0);
}
/* Empieza la contabilizacion del tiempo de proceso consumido por
el servidor. */
gettimeofday(&t_inicio,&tz);
/* Indicacamos al servidor que inicie el procesamiento y esperamos
su respuesta. */
envia(host->sfd,&trama);
/* Espera hasta que el proceso hijo encargado del temporizador
concluya su ejecucion. */
wait(&status);
status /= 256;
/* Se ignora la senyal originada por el temporizador activo. */
signal(SIGALRM,SIG_IGN);
/* Concluye la ejecucion del proceso encargado del temporizador. */
kill(pid,SIGTERM);
gettimeofday(&t_fin,&tz);
tdif(&t_inicio,&t_fin,&t_dif);
if (status==0)
{
sprintf(cad,"El host %s ha finalizado su ejecucion en %f segundos",inet_ntoa(host->dir.sin_addr),t_dif.seg);
DrawCad(cad);
InsertCadBuf(cad);
}
close(host->sfd);
exit(0);
}
host = host->psig;
}
/* Esperamos hasta que todos los procesos hijo finalizan su ejecucion. */
while((pid=wait(&status)) != -1)
;
FlushCadBuf();
}
/*--------------------------------------------------------------------------*/
Por último, lo único que queda por hacer es recopilar los resultados parciales que cada proceso servidor ha obtenido y formar la imagen resultado del suavizado binario. Para ello lo único que hay que hacer es utilizar la función de librería PPL get_data_from_servers.
La siguiente función se encarga de llevar a cabo todas las fases explicadas anteriormente.
/*--------------------------------------------------------------------------*/
void procesar(void)
{
struct t_host* host;
double vp_global, tmin;
double tpart, tsend, tpro, trec, tt, tcom;
struct timeval t_inicio,t_fin;
struct timezone tz;
struct time_dif t_dif;
/* Si la zona de datos a particionar esta vacia. */
if (data_source==NULL)
{
DrawCad("ERROR: Zona de datos vacia.");
return;
}
/* Si no se dispone de servidores para procesar la zona de datos. */
if (lista_hosts==NULL)
{
DrawCad("ERROR: Lista de servidores vacia.");
return;
}
/* Calculamos la capacidad de procesamiento global de nuestra red. */
host = lista_hosts;
while(host!=NULL)
{
vp_global += host->vp;
host = host->psig;
}
DrawCad("************************* INICIO PROCESO *************************");
sprintf(cad,"Velocidad de proceso estimada: %.2f bytes/segundo",vp_global);
DrawCad(cad);
/* Tiempo minimo estimado necesario para procesar el total de la
informacion, teniendo en cuenta la capacidad de procesamiento global
del sistema multicomputador. */
tmin = ((imagen*)data_source->data)->tam / vp_global;
sprintf(cad,"Tiempo de proceso estimado: %f segundos",tmin);
DrawCad(cad);
DrawCad("");
DrawCad(">>>>> CREANDO PARTICIONES <<<<<");
gettimeofday(&t_inicio,&tz);
particionar(data_source,lista_hosts);
gettimeofday(&t_fin,&tz);
tdif(&t_inicio,&t_fin,&t_dif);
tpart = t_dif.seg;
sprintf(cad,"Tiempo de particionado: %f segundos",tpart);
DrawCad(cad);
DrawCad("");
DrawCad(">>>>> TRANSFIRIENDO DATOS A LOS SERVIDORES <<<<<");
gettimeofday(&t_inicio,&tz);
send_data_to_servers(lista_hosts);
gettimeofday(&t_fin,&tz);
tdif(&t_inicio,&t_fin,&t_dif);
tsend = t_dif.seg;
sprintf(cad,"Tiempo de transferencia: %f segundos",tsend);
DrawCad(cad);
DrawCad("");
DrawCad(">>>>> PROCESANDO <<<<<");
gettimeofday(&t_inicio,&tz);
start_proces();
gettimeofday(&t_fin,&tz);
tdif(&t_inicio,&t_fin,&t_dif);
tpro = t_dif.seg;
sprintf(cad,"Velocidad de proceso real: %.2f bytes/segundo",((imagen*)data_source->data)->tam/t_dif.seg);
DrawCad(cad);
sprintf(cad,"Tiempo de proceso real: %f segundos",tpro);
DrawCad(cad);
DrawCad("");
DrawCad(">>>>> RECOPILANDO RESULTADOS <<<<<");
gettimeofday(&t_inicio,&tz);
get_data_from_servers(lista_hosts);
join_img(lista_hosts,(imagen*)data_source->data);
gettimeofday(&t_fin,&tz);
tdif(&t_inicio,&t_fin,&t_dif);
trec = t_dif.seg;
sprintf(cad,"Tiempo de transferencia: %f segundos",trec);
DrawCad(cad);
DrawCad("************************* FINAL PROCESO **************************");
DrawCad("");
}
/*--------------------------------------------------------------------------*/
La función DrawCad es empleada para imprimir cadenas en la ventana de información que utiliza el cliente que funciona sobre entorno XWindows.
[Indice] [Apéndice] [Bibliografía]
[Capítulo 1] [Capítulo 2] [Capítulo 3] [Capítulo 4] [Capítulo 5] [Capítulo 6] [Capítulo 7]
[Página principal] [Proyecto] [Sección FTP] [Libro de visitas] [Consulte mi libro de visitas]