Universidad Autónoma Metropolitana Iztapalapa Proyecto de Investigación I y II “Migración de datos entre nodos de un cluster” Ciencias Básicas e Ingeniería Licenciatura en Computación Autores: Fernando Fernández Vergara José Bernardo Hipatl Santiago Asesor: Dra. Graciela Román Alonso Este proyecto fue desarrollado en el marco del Proyecto CONACyT 342330-A Infraestructura para la construcción de aplicaciones fuertemente distribuidas. Duración del proyecto: 2 trimestres TRIMESTRE DE TERMINACIÓN: 03P Agradecimientos Agradecemos infinitamente a nuestros padres que nos hayan apoyado incondicionalmente a lo largo de nuestra carrera, dándonos palabras de aliento en momentos difíciles y que han hecho grandes sacrificios por que nosotros cumplamos nuestras metas. Agradecemos también a nuestros profesores que nos han transmitido su conocimiento adquirido a lo largo no sólo de su carrera sino de experiencias personales. También damos las gracias a nuestro asesor la Dra. Graciela Román Alonso por habernos tenido paciencia. Agradecemos a los Laboratorios de Docencia y Laboratorio de Súper cómputo a cargo del Ing. Juan Carlos Rosas Cabrera, por el apoyo proporcionado al facilitar los recursos necesarios para el desarrollo de gran parte de este proyecto. Así como al laboratorio de Sistemas Distribuidos del área de Computación de Sistemas de la UAM-I por facilitar sus recursos en el desarrollo de este proyecto. 2 Tipografía En este documento el código del lenguaje C y de las rutinas de MPI utilizadas en código en C, se representan en color azul. El uso de las negritas es utilizado en comandos y/o opciones de comandos en el sistema operativo, así como en los prototipos de las rutinas de MPI extraídos de los archivos de inclusión. Se incluyen algunos mensajes de error generados por el sistema en la línea de comandos, en color rojo, las letras cursivas sólo son usadas para hacer referencia a términos ya definidos o tratados en temas anteriores. 3 TABLA DE CONTENIDOS 1. INTRODUCCION .................................................................................................................................7 1.1. Objetivo ...........................................................................................................................................7 1.2. Justificación ....................................................................................................................................7 2. CONCEPTOS BÁSICOS ......................................................................................................................9 2.1. Clasificación de las arquitecturas de computadoras .......................................................................9 2.1.1. SISD (Single Instruction Single Data).....................................................................................9 2.1.2. SIMD (Single Instruction Multiple Data) ................................................................................9 2.1.3. MIMD (Multiple Instruction Multiple Data) ...........................................................................9 2.2. Clusters..........................................................................................................................................12 2.2.1. Definiciones básicas...............................................................................................................12 2.2.2. Funcionamiento de un Cluster ...............................................................................................13 2.2.3. Tipos de Clusters....................................................................................................................13 2.2.4. Características importantes de los Clusters............................................................................14 2.2.5. Middleware ............................................................................................................................14 2.3. Programación con MPI (Message Passing Interface) ...................................................................16 2.3.1. Introducción a MPI ................................................................................................................16 2.3.2. Características ........................................................................................................................16 2.3.3. Elementos Básicos de la Programación de MPI ....................................................................18 2.3.4. Compilación de programas MPI ............................................................................................27 2.3.5. Ejecución de Programas MPI.................................................................................................29 2.4. Balance de Carga ..........................................................................................................................31 3. MIGRACIÓN DE DATOS..................................................................................................................34 3.1. Definición e Implementación de un TDA lista balanceable ........................................................35 3.1.1. Implementación......................................................................................................................36 3.2. Envío/Recepción de un nodo de un TDA lista..............................................................................38 3.2.1. Envío/Recepción del nodo Campo por Campo (Versión 1 “camxcam.c”)...........................39 3.2.2. Envío/Recepción nodo empaquetado (Versión 2.1 “empaketado.c”)....................................41 3.2.3. Envío/Recepción TDA empaquetado (Versión 2.2 “empaketado_b.c”) ...............................44 3.2.4. Envío/Recepción de un nodo usando uniones (Versión 3.1 “uniones.c”) .............................47 3.2.5. Envío/Recepción de un nodo usando uniones (Versión 3.2 “uniones_b.c”) .........................49 4. RECONOCIMIENTO DEL TDA LISTA BALANCEABLE.............................................................53 4.1 Algoritmo .......................................................................................................................................54 4.2. Implementación.............................................................................................................................55 4.2.1. Obtención de los bloques .......................................................................................................55 4.2.2. Verificación de los bloques....................................................................................................55 4.2.3. Registro de la Estructura ........................................................................................................56 4.2.4. Creación del archivo de Salida...............................................................................................56 5. MIGRACIÓN DE DATOS COMPARTIDOS POR HILOS...............................................................58 5.1. Uso de Hilos en MPI .....................................................................................................................58 5.1.1. hilos_1.c .................................................................................................................................59 5.1.2. hilos_2.c .................................................................................................................................61 5.2. Sincronización para la lista balanceable .......................................................................................63 5.2.1. hilos_3.c .................................................................................................................................64 5.2.2. hilo_3.c 2ª versión..................................................................................................................70 4 5.3. Rutinas para el balance de carga (versión final) ...........................................................................75 6. RESULTADOS....................................................................................................................................77 6.1. Desempeño del programa hilos_3.c 1ª versión.............................................................................77 6.2. Desempeño del programa hilos_3.c 2ª versión.............................................................................77 7. CONCLUSIONES Y PERSPECTIVAS ..............................................................................................79 8. BIBLIOGRAFIA .................................................................................................................................82 9. ANEXOS .............................................................................................................................................84 9.1. Anexo A (Codigos Fuentes para la migración de datos) ..............................................................84 Listado “protos.h” ............................................................................................................................84 Listado “operlis.c” ...........................................................................................................................85 Listado “ent_sal.c” ...........................................................................................................................87 Listado “val_azar.c”.........................................................................................................................88 Listado “camxcamp.c” .....................................................................................................................89 Listado “empaketado.c” ...................................................................................................................91 Listado “empaketado_b.c” ...............................................................................................................94 Listado “uniones.c” ..........................................................................................................................98 Listado “uniones_b.c” ....................................................................................................................100 9.2. Anexo B (Códigos Fuentes para el Reconocimiento del TDA Lista).........................................103 Listado “scan.c” .............................................................................................................................103 9.3. Anexo C (Códigos Fuentes para Migración de datos compartidos con hilos)............................112 Listado “hilos_1.c”........................................................................................................................112 Listado “hilos_2.c”.........................................................................................................................115 Listados “Primera versión” ............................................................................................................119 Listado “protos.h” ..........................................................................................................................120 Listado “def_TDA.h”.....................................................................................................................122 Listado “balance.c” ........................................................................................................................122 Listado “hilos_3.c”.........................................................................................................................125 Listado “operlis.c” .........................................................................................................................129 Listado “ent_sal.c” .........................................................................................................................131 Listados “Segunda Versión” ..........................................................................................................132 Listado “protos.h” ..........................................................................................................................132 Listado “def_TDA.h”.....................................................................................................................132 Listado “balance.c” ........................................................................................................................132 Listado “Hilos_3.c”........................................................................................................................134 Listado “operlis.c” .........................................................................................................................135 Listado “ent_sal.c” .........................................................................................................................135 Listado “MPI_lista.c”.....................................................................................................................135 Listado “scan.c” .............................................................................................................................137 9.4. Anexo D (Códigos Fuentes Versión final)..................................................................................147 Listado “protos.h” ..........................................................................................................................147 Listado “MPI_lista.orig” ................................................................................................................148 Listado “balance.c” ........................................................................................................................149 Listado “scan.c” .............................................................................................................................152 9.4. Anexo E.......................................................................................................................................162 Manual de usuario “Rutinas de Balance” ......................................................................................162 9.5. Anexo F.......................................................................................................................................165 Manual de usuario programa “scan” ..............................................................................................165 5 1. INTRODUCCIÓN 6 1. INTRODUCCIÓN 1.1. Objetivo Realizar un trabajo de investigación en el área de sistemas distribuidos, con el fin de dar una propuesta de un mecanismo de balance de datos haciendo uso de la herramienta MPI. 1.2. Justificación En la actualidad los grandes volúmenes de información en formato digital, así como los pesados cálculos para la investigación científica que llevan a cabo las universidades y empresas en nuestro país, han obligado a éstas a buscar herramientas que hagan más rápidas y por tanto eficientes sus investigaciones. Una alternativa es el uso de las supercomputadoras, el cual representa un problema, ya que el costo de las mismas es muy elevado, costo que muchas instituciones no pueden solventar. Pero, no es la única opción, ni mucho menos la mejor, por lo menos en el aspecto económico. Otra alternativa lo son los cluster, que en muchos aspectos y en gran medida son la mejor opción para la investigación que se desarrolla en las universidades como la nuestra. Pero esta opción, también tiene sus desventajas, entre las más importantes se encuentra la necesidad de personal calificado para lograr un buen desempeño en la paralelización de aplicaciones, personal que es capacitado casi en su totalidad por instituciones educativas como la nuestra. Pero el buen desempeño de una aplicación paralela no depende sólo de una buena codificación, además esta en gran medida en función de una buena administración del cluster, para que le mismo rinda al máximo. El principal problema de un cluster en una institución como la nuestra es que no sólo es utilizado para el procesamiento de una aplicación paralela, lo cual sería absurdo, ya que el cluster no sería aprovechado a su máximo, también se da el caso que hay clusters que también son usados para el desempeño de las actividades docentes. Es aquí cuando surgen nuevos problemas, ya que el buen desempeño de las aplicaciones paralelas, depende en gran medida de la carga de trabajo de los nodos del cluster. Existen herramientas que auxilian en la administración de un cluster, y entre las tareas que desempeñan están, la detección de nodos con una gran carga de trabajo, para que los procesos sean lanzados en los nodos con menor carga. Pero, ¿qué pasa si se lanza una aplicación en un nodo que inicialmente no se encuentra cargado, pero que posteriormente se carga, ya sea por el uso del nodo como estación de trabajo o por la gran cantidad de datos de nuestra misma aplicación? Pues la respuesta es obvia, nuestro proceso paralelo se verá afectado por la sobrecarga. Por otra parte nosotros al codificar una aplicación paralela, si pensamos en prever la situación anterior, nos haremos complicada nuestra tarea de paralelización más de lo que ya es. Ya que no sólo habría que codificar rutinas para censar la carga del nodo, sino además la forma de migrar nuestros datos para que el procesamiento llevado hasta el momento no sea en vano. Por tanto el objetivo de este proyecto es contribuir al desarrollo de las rutinas encargadas del balance de la carga de datos para nuestras aplicaciones paralelas. 7 2. Conceptos Básicos 8 2. CONCEPTOS BÁSICOS 2.1. Clasificación de las arquitecturas de computadoras [2] La clasificación de las arquitecturas de computadoras fue propuesta por Michael Flynn y se basa en basa en el número de instrucciones y de la secuencia de datos que la computadora utiliza para procesar información. Puede haber secuencias de instrucciones sencillas o múltiples y secuencias de datos sencillas o múltiples. Esto da lugar a 4 tipos de computadoras, de las cuales solamente dos son aplicables a las computadoras paralelas. 2.1.1. SISD (Single Instruction Single Data) Este es el modelo tradicional de computación secuencial donde una unidad de procesamiento recibe una sola secuencia de instrucciones que operan en una secuencia de datos. Por ejemplo para procesar la suma de N números a1 , a2 ,... aN, el procesador necesita acceder a memoria N veces consecutivas (para recibir un número) También son ejecutadas en secuencia N-1 adiciones. Es decir los algoritmos para las computadoras SISD no contienen ningún paralelismo, éstas están constituidas de un procesador. 2.1.2. SIMD (Single Instruction Multiple Data) A diferencia de SISD, en este caso se tienen múltiples procesadores que sincronizadamente ejecutan la misma secuencia de instrucciones, pero en diferentes datos. El tipo de memoria que estos sistemas utilizan es distribuida. Aquí hay N secuencias de datos, una por procesador, así que diferentes datos pueden ser utilizados en cada procesador. Los procesadores operan sincronizadamente y un reloj global se utiliza para asegurar esta operación. Es decir, en cada paso todos lo procesadores ejecutan la misma instrucción, cada uno en diferente dato. 2.1.3. MIMD (Multiple Instruction Multiple Data) Este tipo de computadora es paralela al igual que las SIMD, la diferencia con estos sistemas es que MIMD es asíncrono. No tiene un reloj central. Cada procesador en un sistema MIMD puede ejecutar su propia secuencia de instrucciones y tener sus propios datos. Esta 9 característica es la más general y poderosa de esta clasificación. Se tienen N procesadores, N secuencias de instrucciones y N secuencias de datos. Si el sistema de multiprocesamiento posee procesadores de aproximadamente igual capacidad, estamos en presencia de multiprocesamiento simétrico, en el otro caso hablamos de multiprocesamiento asimétrico. Cada procesador opera bajo el control de una secuencia de instrucciones, ejecutada por su propia unidad de control, es decir cada procesador es capaz de ejecutar su propio programa con diferentes datos. Esto significa que los procesadores operan asincrónicamente, o en términos simples, pueden estar haciendo diferentes cosas en diferentes datos al mismo tiempo. Los sistemas MIMD se clasifican en: • Sistemas de Memoria Compartida. • Sistemas de Memoria Distribuida. • Sistemas de Memoria Compartida Distribuida En este tipo de sistemas cada procesador tiene acceso a toda la memoria, es decir hay un espacio de direccionamiento compartido. Se tienen tiempos de acceso a memoria uniformes ya que todos los procesadores se encuentran igualmente comunicados con la memoria principal y las lecturas y escrituras de todos los procesadores tienen exactamente las mismas latencias; Y además el acceso a memoria es por medio de un conducto común. En esta configuración, debe asegurarse que los procesadores no tengan acceso simultáneamente a regiones de memoria de una manera en la que pueda ocurrir algún error. Desventajas: • El acceso simultáneo a memoria es un problema. • En PC’s y estaciones de trabajo: Todos los CPU’s comparten el camino a memoria. Un CPU que acceda la memoria, bloquea el acceso de todos los otros CPU’s. • En computadoras vectoriales como Crays, etc. Todos los CPU’s tienen un camino libre a la memoria. No hay interferencia entre CPU’s. • La razón principal por el alto precio de Cray es la memoria. Ventaja: La facilidad de la programación. Es mucho más fácil programar en estos sistemas que en sistemas de memoria distribuida. Sistemas de Memoria Distribuida Estos sistemas tienen su propia memoria local. Los procesadores pueden compartir información solamente enviando mensajes, es decir, si un procesador requiere los datos contenidos en la memoria de otro procesador, deberá enviar un mensaje solicitándolos. Esta comunicación se le conoce como Paso de Mensajes. Ventajas: La escalabilidad. Las computadoras con sistemas de memoria distribuida son fáciles de escalar, mientras que la demanda de los recursos crece, se puede agregar más memoria y procesadores. Desventajas: 10 • • El acceso remoto a memoria es lento. La programación puede ser complicada. Las computadoras MIMD de memoria distribuida son conocidas como sistemas de procesamiento en paralelo masivo (MPP) donde múltiples procesadores trabajan en diferentes partes de un programa, usando su propio sistema operativo y memoria. Además se les llama multicomputadoras, máquinas libremente juntas o cluster. Sistemas de Memoria Compartida Distribuida Es un cluster o una partición de procesadores que tienen acceso a una memoria compartida común pero sin un canal compartido. Esto es, físicamente cada procesador posee su memoria local y se interconecta con otros procesadores por medio de un dispositivo de alta velocidad, y todos ven las memorias de cada uno como un espacio de direcciones globales. El acceso a la memoria de diferentes clusters se realiza bajo el esquema de Acceso a Memoria No Uniforme (NUMA), la cual toma menos tiempo en acceder a la memoria local de un procesador que acceder a memoria remota de otro procesador. Ventajas: • Presenta escalabilidad como en los sistemas de memoria distribuida. • Es fácil de programar como en los sistemas de memoria compartida. • No existe el cuello de botella que se puede dar en máquinas de sólo memoria compartida. La arquitectura que nos interesa es la MIMD con memoria distribuida, en la cual se encuentran clasificados los clusters, en el siguiente apartado da una breve descripción de lo que es un cluster. 11 2.2. Clusters [5] El término cluster es usado generalmente para referirnos a un grupo de componentes distribuidos que colaboran en conjunto para presentarse al usuario como un solo sistema. Un Cluster es un conjunto o conglomerado de computadoras interconectadas entre sí de alguna manera, que trabajan en conjunto, distribuyéndose las tareas entre ellas, logrando que el usuario lo vea como una sola, simulando una sola computadora muy potente. Son construidos utilizando componentes de hardware comunes, lo que los hace un poco más accesibles y con el software que en su mayoría se apega a la licencia GNU. El principal papel que juegan es el de proporcionar soluciones a la investigación científica, que requieren mucho tiempo de procesamiento y de cálculos pesados, aunque también son usados en las ingenierías y en las aplicaciones comerciales. 2.2.1. Definiciones básicas Rendimiento: Es la efectividad del desempeño de una computadora, sobre una aplicación o un benchmark en particular. Flops: Es una medida de la velocidad del procesamiento numérico del procesador. Son operaciones de punto flotante por segundo. Alto Rendimiento (HPC): Gran demanda de procesamiento de datos en procesadores, memoria y otros recursos de hardware, donde la comunicación entre ellos es muy rápida. Latencia: Tiempo de transferencia de mensajes de una interfaz a otra. Ancho de Banda: Capacidad de transferencia que tiene un canal de comunicaciones en una unidad de tiempo. 12 Switches: Es un conjunto de puertos de entrada, un conjunto de puertos de salida y una red cruzada interna (crossbar) que conecta cada entrada a cada salida, el buffering interno, y el control lógico para efectuar la conexión de entrada y salida en cada punto de tiempo (malla) Hub: es un conjunto de puertos de entrada y salida multiplexado (un alambre) Ethernet: Protocolo de comunicación basado en el estándar IEEE. VIA: Protocolo de comunicación con características de baja latencia. 2.2.2. Funcionamiento de un Cluster En su parte central, la tecnología de Clusters consta de dos partes. La primera componente, consta de un sistema operativo confeccionado especialmente para esta tarea, un conjunto de compiladores y aplicaciones especiales, que permiten que los programas que se ejecutan sobre esta plataforma tomen las ventajas de esta tecnología de Clusters. La segunda componente es la interconexión de hardware entre las máquinas (nodos) del Cluster. Se han desarrollado interfaces de interconexión especiales muy eficientes, pero comúnmente las interconexiones se realizan mediante una red Ethernet dedicada de alta velocidad. Es mediante esta interfaz que los nodos del Cluster intercambian entre sí asignación de tareas, actualizaciones de estado y datos del programa. Existe otra interfaz de red que conecta al Cluster con el mundo exterior. Un nodo esencialmente se compone de: • Procesador. • Memoria. • Dispositivos de almacenamiento. • Tarjeta de Red. 2.2.3. Tipos de Clusters [5] Beowulf Concepto de los primeros clusters (componentes COTS, filosofía “Hágalo usted mismo”). En 1994, se integró el primer cluster de PC’s en el Centro de Vuelos Espaciales Goddard de la NASA, para resolver problemas computacionales que aparecen en las ciencias de la Tierra y el Espacio. Los pioneros de este proyecto fueron Thomas Sterling, Donald Becker y otros científicos de la NASA. El cluster de PC’s desarrollado tuvo una eficiencia de 70 megaflops (millones de operaciones de punto flotante por segundo). Los investigadores de la NASA le dieron el nombre de Beowulf a este cluster, en honor del héroe de las leyendas medievales, quien derrotó al monstruo gigante Grendel. Nombre de un héroe de la mitología danesa relatado en el libro La Era de las Fábulas, del autor norteamericano Thomas Bulfinch (1796-1867). Clusters de Alto Rendimiento (HPC) Dedicados a tareas que requieran de muchos recursos (CPU, memoria, comunicaciones). La tecnología de Clusters de Alto Rendimiento para Linux más conocida es la tecnología Beowulf. Esta tecnología puede proporcionar potencial de cómputo del tipo de una supercomputadora utilizando computadoras personales sencillas. Al conectar estas entre sí mediante una red Ethernet de alta velocidad, las computadoras personales se combinan para lograr la potencia de una supercomputadora. Clusters de Alta Disponibilidad Garantizan que un determinado recurso este disponible por mayor tiempo posible. Por ejemplo, los servicios de TCP/IP (Web, Telnet, etc.) pueden estar 13 asegurados por medio del Sist. Operativo. La alta disponibilidad en tareas de cómputo en general tiene que estar proporcionada por el mismo programa. Los servidores de un Cluster de Alta Disponibilidad normalmente su función es la de esperar listos para entrar inmediatamente en funcionamiento en el caso de que falle algún otro servidor. Clusters Homogéneos Son aquellos en los cuales la tecnología es la misma (PC’s, SCSI, etc.). Clusters Heterogéneos Aquellos en los que la tecnología puede ser diferente o hasta pueden usarse diferentes plataformas Clusters Dedicados Son maquinas que se usan específicamente como un cluster. En la fotografía se muestra un cluster dedicado con 20 nodos, cada nodo cuenta con dos procesadores Intel Pentium III a 1 GHz y 1 GB en RAM, una interfaz de red para la administración del sistema Ethernet 10/100 Mb y otra para las aplicaciones paralelas Giganet a 1Gb. Este se encuentra en el “Laboratorio de Súper cómputo y visualización paralela” de la UAM-Iztapalapa. Clusters No Dedicados Trabajan como un cluster solo una parte del tiempo. 2.2.4. Características importantes de los Clusters • • • Uso de Hardware convencional. PC’s Servidores. Memoria Distribuida. Cada procesador tiene acceso sólo a la memoria local, se necesitan usar bibliotecas del tipo (DSM-Dynamic Shared Memory). Uso de software abierto. En el 95% de Clusters al momento, se usa Linux como sistema operativo. Solo el 0.01% de Clusters hace uso de Windows. 2.2.5. Middleware Es aquel modulo que interactúa como conductor entre sistemas permitiendo a cualquier usuario de sistemas de información comunicarse con varias fuentes de información que se encuentran interconectadas a través de una red. Interfases más usadas: • MPI (Message Passing Interface) Interfaz mas usada para paso de mensajes, existen varias implementaciones: MPICH, LAM-MPI, VAMPIR. Entre las ventajas que tiene esta interfaz se encuentran: distintos modos de comunicación, la sincronización de procesos, el traslape de procesos de computo con procesos de comunicación, entre otras. • PVM (Parallel Virtual Machine) Desarrollada para NOW, totalmente libre, capaz de trabajar en redes homogéneas y heterogéneas, realiza un manejo transparente del ruteo de los mensajes, conversión da datos, calendarización de tareas a través de una red de arquitecturas incompatibles. • DSM (Memoria Compartida Distribuida): Es un modelo que hace que la memoria distribuida de todos los nodos aparezca como memoria compartida desde el punto de vista de programador. 14 2.3. Programación Paralela [S2001] Para paralelizar una aplicación es necesario contar con un lenguaje o una biblioteca que brinde las bibliotecas necesarias para esto. Dependiendo de la herramienta con que se cuente, se particionará el código en piezas para que se ejecute en paralelo en varios procesadores. Es aquí donde entra el termino granularidad. Granularidad es el tamaño de las piezas en que se divide una aplicación. Dichas piezas pueden ser una sentencia de código, una función o un proceso en si que se ejecutara en paralelo. Granularidad es categorizada en paralelismo de grano fino y paralelismo de grano grueso. De grano fino es cuando el código se divide en una gran cantidad de piezas pequeñas. Es a nivel de sentencia donde un ciclo se divide en varios subciclos que se ejecutaran en paralelo. Se le conoce además como paralelismo de datos. De grano grueso es a nivel de subrutinas o segmentos de código, donde las piezas son pocas y de cómputo más intensivo que las de grano fino. Se le conoce como paralelismo de tareas. En el paralelismo de grano grueso en el nivel más alto se presenta cuando en la aplicación se detectan tareas independientes y estas se ejecutan en procesos independientes en, más de un procesador. En los modelos de memoria distribuida (paso de mensajes) sólo se implementa paralelismo de grano grueso. Un programador puede desarrollar aplicaciones paralelas con código fuente en C, C++ y FORTRAN, mediante el uso de paradigmas o modelos provistos por los Sistemas Operativos como lo es en los sistemas tipo Unix. Con la clonación de procesos o creación de hilos, o usando directivas a nivel sentencia, las cuales, también son provistas por los Sistemas Operativos, pero cuando se cuenta con un paralelismo real, es decir, a nivel de Hardware. Además del paso de mensajes, el cual es usado para sistemas de memoria distribuida en donde cada procesador tiene su propia memoria. Este modelo es apropiado para la comunicación y sincronización entre los procesos que se ejecutan de manera independiente (con su propio espacio de direcciones) en diferentes computadoras. La programación usando el modelo de paso de mensajes consiste en enlazar y hacer llamadas dentro del programa, a unas bibliotecas que manejarán el intercambio de datos entre los procesadores. Existen principalmente dos bibliotecas para la programación bajo este esquema: • MPI (Message Passing Interface). • PVM (Parallel Virtual Machine). El esquema de implementación de paralelismo mediante el paso de mensajes es a nivel de grano grueso dada la generación de procesos o tareas independientes que se ejecutan en varios procesadores. En MPI y PVM, uno de los esquemas que se emplean en la comunicación y generación de los procesos es el modelo Maestro-Esclavo. Un proceso maestro divide u distribuye el trabajo en subtareas, que las asigna a cada nodo conocido como “esclavo”. Terminando cada “esclavo” su parte, envían los resultados al “maestro” para que este recopile la información y la presente. Otro esquema utilizado en este modelo es el de “todos esclavos” que consiste en que todos los procesos no son generados por un “maestro” y se ejecutan independientemente sin notificar a un proceso maestro sus resultados. 15 2.3. Programación con MPI (Message Passing Interface) 2.3.1. Introducción a MPI [3] MPI es un estándar para el paradigma de programación de paso de mensajes. En este paradigma el programador se imagina varios procesadores, cada uno con su propio espacio de memoria, y escribe un programa para correr en cada procesador. Pero esto no es programación en paralelo, no mientras que dichos procesos no intercambien información, es decir, no tengan una coparticipación. Por lo que debe de haber una coparticipación entre los procesadores para el intercambio de información, y la forma en que ocurra este intercambio es mediante los mensajes. Así el punto principal del paradigma de paso de mensajes es el que los procesos se comuniquen mediante el envió y recepción de mensajes. De esta forma en el paso de mensajes no hay una concepción sobre memoria compartida o procesos que acceden la memoria de otros procesadores. Por lo que no hay preocuparse por la consistencia de los datos, es decir, que no se escriba un dato mientras se esta leyendo por otro proceso, el cual puede llevar a resultados no deseados. El hecho de que para cada procesador se tenga un proceso corriendo, el cual tiene interacción con otros procesos corriendo en procesadores distintos, no quiere decir que el programador tenga que escribir una serie de programas secuénciales que sean capaces de comunicarse entre sí, sino que el programador sea capaz de crear código, que dependiendo sobre que y/o cuantos procesadores se encuentre corriendo, el proceso aplique el mismo funcionamiento a cada parte de los datos que le corresponda, para que posteriormente los resultados individuales de cada procesador sean unificados y evaluados en un solo proceso para llegar al fin deseado. Así que lo que hace un típico programa paralelo es “dividir el problema en problemas chiquitos y después reunir los resultados parciales y obtener un resultado final”. Es así como con MPI se crea paralelismo de grano grueso ya que esta herramienta provee funciones que determinan que fragmento de código serán ejecutados por cada procesador. MPI no provee paralelismo de datos, sino paralelismo de tareas. 2.3.2. Características [3] MPI es un estándar para la comunicación inter-proceso en un esquema de multiprocesador de memoria-distribuida. El estándar ha sido desarrollado por un comité de proveedores, laboratorios del gobierno y universidades. La implementación del estándar es usualmente dejada para los diseñadores de sistemas en el cual MPI corre, pero una implementación de dominio público está disponible en http://www-unix.mcs.anl.gov/mpi/. MPI es un conjunto de rutinas de librería para C/C++ y FORTRAN. Además MPI es un conjunto de rutinas de biblioteca para C/C++ y FORTRAN. Cuando un programa bajo MPI comienza, el programa se descompone en un número de procesos especificados por el usuario. Cada proceso corre y se comunica con otras instancias del programa, posiblemente corriendo en el mismo procesador o en diferentes. La comunicación básica consiste en enviar y recibir datos de un proceso a otro. Esta comunicación toma lugar en una red de trabajo muy rápida, que conecta a los procesadores en un sistema de memoria-distribuida. 16 Un paquete de datos enviados con MPI requiere bastantes piezas de información: el proceso de envío, el proceso de recepción, la dirección de comienzo en memoria a ser enviados, el número de datos que han sido enviados, un identificador de mensajes, y el grupo de todos los procesos que puede recibir el mensaje. Todos estos objetos están disponibles para ser enviados por el programador. En uno de los programas más simples en MPI, un proceso maestro despacha trabajo a los procesos esclavos. Esos procesos reciben los datos, realizan tareas en ellos, y envían los resultados al proceso maestro, el que combinará los resultados. Los otros procesos corren continuamente desde el comienzo del programa. Las características de MPI son: § Generales - Los comunicadores combinan procesamiento en contexto y en grupo para seguridad de los mensajes. - Seguridad en las aplicaciones con hebrado. § Manejo de ambiente. MPI incluye definiciones para: - Temporizadores y sincronizadores. - Inicializar y finalizar. - Control de Errores. - Interacción con el ambiente de ejecución. § Comunicación punto a punto. - Heterogeneidad para el buffer y tipos de datos derivados. - Varios modos de Comunicación. § Comunicaciones colectivas: - Capacidad de manipulación de operaciones colectivas con operaciones propias o definidas por el usuario. - Gran número de rutinas para el movimiento de datos. - Los subgrupos pueden definirse directamente o por la topología. § Topologías por procesos: - Soporte incluido para las topologías virtuales para procesos. § Caracterización de la interfase: - Se permite al usuario interceptar llamadas MPI para instalar sus propias herramientas. 17 2.3.3. Elementos Básicos de la Programación de MPI Iniciación y terminación de MPI [4] Lo primero que necesita hacer el programador para paralelizar sus programas con MPI es iniciar MPI. Por lo tanto la rutina a ser llamada por cualquier programa hecho con MPI es la rutina de iniciación, antes de cualquier otra rutina de MPI. Para la codificación en C, dicha rutina acepta los argumentos del main (argc y argv) para que estos puedan ser pasados a cada uno de los procesos, lo cual es una ventaja sobre la codificación en FORTRAN, ya que podemos crear ejecutables que reciba argumentos. La rutina de iniciación es la siguiente: int MPI_Init(int *argc, char ***argv); Un programa con MPI debe de llamar a la rutina: int MPI_Finalize(void); Cuando todas las comunicaciones son completadas. Esta rutina limpia todas las estructuras de datos de MPI, entre otras cosas. Esta rutina no cancela el estado las comunicaciones, es responsabilidad del programador completar y/o terminar con todas las comunicaciones. Esta rutina sólo puede ser llamada una vez, después de la llamada de dicha rutina ninguna otra rutina de MPI puede ser llamada, incluso MPI_Init, solo otro proceso nuevo lo podrá hacer. Terminación anormal de procesos de MPI En ocasiones hay que prever una posible situación que pueda bloquear la terminación de nuestros procesos que se encuentran corriendo en paralelo. Un ejemplo de este tipo de situaciones que podrían presentarse, es el hecho de que uno de nuestros procesos se quede esperando la recepción de un mensaje, cuando todos los demás ya han terminado, esta situación impedirá a nuestra aplicación paralela terminar. Claro que esta es una situación que no se debe de presentar si el programa esta bien hecho, pero en ocasiones es posible que se presente dicha situación. Otra situación que puede provocar una terminación anormal de nuestros procesos es que en algún nodo se presente una situación que pueda afectar el desempeño de la aplicación en general, por lo que sería conveniente terminar nuestros procesos. Para esto se tiene la siguiente rutina que trata de terminar la aplicación paralela. int MPI_Abort(MPI_Comm comm, int codigoerror); Esta aplicación trata de terminar todos los procesos contenidos en comm, para que el programa paralelo termine. Información del Ambiente Cuando el programador ejecuta sus programas con MPI, hay información que podría a llegar a ser relevante para nuestros propósitos, como lo es el número de procesos corriendo, numero (identificador) de proceso que se trata y/o en donde se encuentra corriendo. MPI provee rutinas que ayudan en la obtención de dichos datos. El valor para el número de procesos se obtiene con la rutina: int MPI_Comm_size(MPI_Comm comm, int *numproc); comm Es el comunicador, utilizado en la comunicación de los proceso, este parámetro es usado pero no modificado por la rutina. numproc Es el número de proceso en el grupo del comunicador, debe ser un apuntador ya que la rutina modifica dicho parámetro. El identificador del proceso, es el número o rango del proceso, es un número entre cero y el total de procesos menos uno (n-1). La rutina para la obtención del identificador es: 18 int MPI_Comm_rank(MPI_Comm comm, int *identificador); comm Es el comunicador, utilizado en la comunicación de los proceso, este parámetro es usado pero no modificado por la rutina. identificador Es el rango del proceso en el grupo del comunicador, debe ser un apuntador ya que la rutina modifica dicho parámetro. El nombre de la estación donde se encuentra corriendo lo proporciona la rutina: int MPI_Get_processor_name(char *nombre, int *longitud); nombre Es el apuntador de la cadena donde se guardara el nombre del procesador, este parámetro es modificado por la rutina. longitud Es la longitud de la cadena nombre, este parámetro es modificado por la rutina. Es importante hacer notar que al pasarle un apuntador a una cadena, hay que reservar el espacio necesario que pudiese ocupar el nombre de la estación de trabajo. Para esto MPI define la macro MPI_MAX_PROCESSOR_NAME la cual es igual 256. Dicha macro esta definida en “mpi.h”. Comunicadores y Etiquetas Entre las rutinas de iniciación que lleva a cavo MPI_Init esta la definición de algo llamado MPI_COMM_WORLD para cada proceso que llama dicha macro. MPI_COMM_WORLD es un comunicador. Todas las comunicaciones en MPI realizan llamadas a rutinas que requieren de un argumento comunicador, y los procesos en MPI sólo pueden comunicarse si comparten el mismo comunicador, ya que éste especifica un dominio de comunicación. Cada comunicador contiene un grupo que es una lista de procesos. Un proceso puede tener varios comunicadores y por consiguiente pertenecer a varios grupos. MPI_COMM_WORLD es el comunicador de todos los procesos en MPI. El procesamiento por grupos permite a MPI cubrir una serie de debilidades presentes en algunas bibliotecas de paso de mensajes e incrementa sus capacidades de portabilidad, eficiencia y seguridad. El procesamiento por grupos agrega elementos como: división de procesos, transmisión de mensajes sin conflictos, extensibilidad para los usuarios (para la creación de bibliotecas) y seguridad. MPI_GROUP_EMPTY sirve para denotar aquel grupo que no tiene miembros., La constante MPI_GROUP_NULL es el valor usado para referirse a un grupo no valido. Para crear un nuevo comunicador se debe partir de un comunicador ya existente. La rutina que implementa la creación de un comunicador es: int MPI_Comm_dup(MPI_Comm comm, MPI_Comm *newcomm); comm Comunicador a partir del cual se generara uno nuevo, la rutina sólo hace uso de este argumento. newcomm Nuevo comunicador, la rutina modifica dicho parámetro. Esta rutina le permite crear un nuevo comunicador (newcomm) compuesto de los mismos procesos que hay en comm pero con un nuevo contexto para asegurar que las comunicaciones efectuadas para distintos propósitos no sean confundidas. Finalmente cuando se han efectuado todas las comunicaciones asociadas al nuevo comunicador, este deberá ser destruido la rutina para la liberación del comunicador es como sigue, esta no implica mayor dificultad por lo que no requiere mayor explicación. MPI_Comm_free(MPI_Comm *comm); En cuanto a las etiquetas, la función principal de estas es que un proceso tenga la opción de recibir o no, mensajes dependiendo de la “etiqueta” asociada a dicho mensaje, que no es otra cosa que 19 un número entero. Con esto MPI implementa la recepción de mensajes selectiva por parte de los procesos receptores. Medición de tiempo Medir el tiempo que dura un programa es importante para establecer su eficiencia y para fines de depuración, sobre todo cuando éste es paralelo. Para esto MPI ofrece las funciones MPI_Wtime y MPI_Wtick. Ambas proveen alta resolución y poco costo. double MPI_Wtime(void); MPI_Wtime devuelve un punto flotante que representa el número de segundos transcurridos a partir de cierto tiempo pasado, el cual se garantiza que no cambia durante la vida del proceso. Es responsabilidad del usuario hacer la conversión de segundos a otras unidades de tiempo: horas, minutos, etc. double MPI_Wtick(void); MPI_Wtick permite saber cuantos segundos hay entre tic's sucesivos del reloj. Por ejemplo, si el reloj esta implementado en hardware como un contador que se incrementa cada milisegundo, entonces MPI_Wtick debe devolver 10-3 Tipos de datos en MPI Para el envió y recepción de mensajes es muy importante indicarle exactamente a las rutinas encargadas de ello, los tipos de datos que serán enviados, por lo que MPI establece una equivalencia entre los datos definidos en C/C++ y los tipos de datos de MPI. La siguiente tabla muestra las macros definidas para los tipos de datos de MPI y su equivalente en C, así como el valor de la macro. Tipo de Dato en C Macro Valor de la Macro char MPI_CHAR 1 unsigned char MPI_UNSIGNED_CHAR 2 MPI_BYTE 3 short MPI_SHORT 4 unsigned short MPI_UNSIGNED_SHORT 5 int MPI_INT 6 unsigned MPI_UNSIGNED 7 long MPI_LONG 8 unsigned long MPI_UNSIGNED_LONG 9 float MPI_FLOAT 10 double MPI_DOUBLE 11 long double MPI_LONG_DOUBLE 12 long long int MPI_LONG_LONG_INT 13 MPI_PACKED 14 MPI_LB 15 MPI_UB 16 MPI_FLOAT_INT 17 MPI_DOUBLE_INT 18 long int MPI_LONG_INT 19 short int MPI_SHORT_INT 20 MPI_2INT 21 20 Tipo de Dato en C Macro Valor de la Macro MPI_LONG_DOUBLE_INT 22 Algunas de las macros definidas por MPI hacen referencia a datos que no son tipos de datos en C. En algunos casos simplemente se tratan de estructuras con la siguiente forma: struct { double var; int loc; } Como lo son las macros MPI_FLOAT_INT, MPI_DOUBLE_INT y MPI_LONG_DOUBLE_INT. Estos casos en particular al ser declarados en código C puro generaría el siguiente error en la compilación “too many types in declaration”(demasiados tipos en la declaración). Algunos otros son tipos simples como MPI_BYTE, el cual no requiere mayor explicación. Otros son más complejos como el MPI_PACKED, el cual será tratado más adelante. Comunicación punto a punto Una comunicación punto-a-punto siempre involucra solamente dos procesos. Un proceso que envía el mensaje y otro que lo recibe. En el envío el proceso fuente hace una llamada a una rutina especificándole el proceso destino dentro del rango del comunicador. El proceso receptor sólo hace la llamada a la rutina de MPI para la recepción del mensaje, dentro del rango del comunicador, no es necesario especificar la fuente de quien se espera el mensaje. Existen cuatro modos de comunicación provistas por MPI: estándar, sincrono, buffered y lectura. Los cuatro tipos se refieren a cuatro tipos de envío, no es significante el modo de recepción de los mensajes en los modos de comunicación. El envío de un mensaje se completa cuando el buffer, el cual es el canal de comunicación, puede ser reutilizado. Los modos estándar, sincrono y buffered difieren en el envío, sólo en un aspecto. Como se completa el envío dependiendo de cómo se recibe el mensaje. La siguiente tabla muestra los cuatro tipos de envío, así como la rutina encargada de ella y una descripción. Modo de Rutina de Descripción Comunicación MPI Estándar MPI_Send Se completa cuando el mensaje puede ser enviado, no implica que el mensaje pueda o no ser recibido. Sincrono MPI_Ssend Sólo se completa cuando la recepción se ha completado Buffered MPI_Bsend Siempre se completa (a menos que ocurra un error), sin considerar si la recepción se ha completado. El mensaje se copia a un sistema de buffer para después transmitir si es necesario. Lectura MPI_Rsend Siempre se completa (a menos que ocurra un error), sin considerar si la recepción se ha completado. El envío de mensajes involucra el intercambio de información entre los proceso. Pero, ¿Qué es un mensaje? 21 Un mensaje es un arreglo de elementos de un tipo de dato particular de MPI. Para todos los mensajes en MPI deben especificarse el tipo en el envío y la recepción. Los aspectos que MPI provee para el envío de mensajes y ganar portabilidad son los siguientes: • Especificación de los datos a enviar con tres campos: dirección de inicio, tipo de dato y contador. • Los tipos de datos son construidos recursivamente por las rutinas de MPI • La especificación de tipos de datos permite comunicaciones heterogéneas. • Se elimina la longitud de un mensaje a favor de un contador. Cualquiera de las rutinas de envío de mensajes sigue el patrón de la rutina MPI_Send, el cual es el siguiente: MPI_Send(void *buf, int count, MPI_Datatype tipo, int dest, int etiqueta, MPI_Comm comunicador); donde: buf count tipo destino etiqueta comunicador Es la dirección del buffer de envío. Es un entero que indica el número de elementos a recibir Tipo de datos de MPI de los elementos en el buffer de envío. Identificador del proceso destino. Etiqueta de un mensaje. Comunicador al cual pertenece el proceso que envía y recibe mensaje. La recepción de un mensaje se realiza mediante la rutina: MPI_Recv(void *buf, int count, MPI_Datatype tipo, int fuente, int etiqueta, MPI_Comm comunicador, MPI_Status estado); donde: buf count tipo fuente etiqueta comunicador estado Es la dirección del buffer de envío. Es un entero que indica el número de elementos a recibir Tipo de datos de MPI de los elementos en el buffer de envío. Identificador del proceso fuente o MPI_ANY_SOURCE (reciba de cualquier fuente) Etiqueta de un mensaje o MPI_ANY_TAG (cualquier etiqueta). Comunicador al cual pertenece el proceso que envía y recibe mensaje. Estructura de tipo MPI_Status con información variada. La estructura MPI_Status es usada para la información del mensaje recibido, esto en caso de que se hallan utilizado algunas de las macros MPI_ANY_TAG o MPI_ANY_SOURCE, que posteriormente puedan necesitarse, e incluso tiene un campo que indica el error que ocurrió en caso de que la recepción no se logre completar. La estructura es la siguiente: typedef struct { int count; int MPI_SOURCE; int MPI_TAG; int MPI_ERROR; int private_count; }MPI_Status; Esta estructura se encuentra declarada dentro de “mpi.h” 22 Comunicaciones colectivas Además de las comunicaciones punto a punto MPI provee rutinas para las comunicaciones colectivas, por lo que permite la comunicación entre varios tipos de procesos. Las comunicaciones colectivas permiten la transferencia de datos entre procesos que tienen el mismo canal de comunicación, es decir, comunicador. En el caso de dichas comunicaciones se usan etiquetas para los mensajes, se hace uso del comunicador. [S2001] Las comunicaciones colectivas pueden ser clasificadas en tres clases: • Sincronización: barreras para sincronizar. • Movimiento (transferencia) de datos: operaciones para difundir, recolectar y esparcir datos. • Cálculos colectivos: operaciones para reducción global, tales como suma, multiplicación, máximo o mínimo o cualquier función definida por el usuario. Sincronización Una operación de barrera sincroniza a todos los procesos que compartan el mismo canal de comunicación, es decir, que pertenezcan al mismo grupo. No hay intercambio de datos. La rutina encargada de dicha operación es: int MPI_Barrier(MPI_Comm comunicador); Movimiento (transferencia) de datos Para la transferencia de datos, en primer lugar se tiene a la comunicación uno-a-muchos en este el proceso raíz difunde el mismo mensaje a múltiples procesos con una simple operación. La rutina de MPI que implementa dicho broadcast es: int MPI_Bcast(void *inbuf, int numelem, MPI_Datatype tipo, int raiz, MPI_Comm comunicador); donde: 23 inbuf numelem tipo raiz comunicador Dirección del buffer de recepción, o buffer de envío si se trata del proceso raíz Numero de elementos en el buffer de envío. Tipo de dato de los elementos en el buffer. Rango del proceso raíz, es decir, el encargado de difundir los datos. Canal de comunicación. Otras rutinas para las comunicaciones colectivas son: int MPI_Gather(void *inbuf, int numelem, MPI_Datatype intipo, void *outbuf, int outelem, MPI_Datatype outtipo, int raiz, MPI_Comm comunicador); La rutina anterior hace que cada proceso, incluyendo el proceso raíz, envié el contenido de su buffer de envío (inbuf) al proceso raíz. El proceso raíz recibe los mensajes y los almacena en orden (según el rango del proceso que realiza el envío) en el buffer de recepción (outbuf). int MPI_Scatter(void *inbuf, int numelem, MPI_Datatype intipo, void *outbuf, int outelem, MPI_Datatype outtipo, int raiz, MPI_Comm comunicador); En esta rutina el proceso raíz envié un trozo de información a cada proceso incluyéndolo a él. El proceso raíz enviará u trozo del contenido del buffer de envío (inbuf). Comenzando desde la dirección inicial de dicho buffer se desplazara una cantidad (numelem) para realizar el siguiente envío. donde: inbuf Dirección del buffer envío. numelem Numero de elementos a enviar a cada proceso. intipo Tipo de dato de los elementos en el buffer de entrada. outbuf Dirección del buffer de recepción. outelem Número de elementos a recibir cada uno outtipo Tipo de elementos en el buffer de recepción. raiz Rango del proceso raíz, es decir, el encargado de difundir los datos. comunicador Canal de comunicación. Cálculos colectivos Básicamente las operaciones colectivas son operaciones de reducción. Una operación de reducción toma datos de diferentes procesos y los reduce a un simple dato. El orden de evaluación canónico de una reducción esta determinado por el rango de los procesos. La operación de reducción puede ser una operación predefinida o una definida por el usuario, Las operaciones definidas en MPI se muestran en la siguiente tabla: Nombre de Operación operación en MPI MPI_MAX Máximo MPI_MIN Mínimo MPI_PROD Producto MPI_SUM Suma MPI_LAND Y lógico MPI_LOR O lógico MPI_LXOR XOR lógico MPI_BAND Y bit a bit MPI_BOR OR bit a bit 24 Nombre de operación en Operación MPI MPI_BXOR XOR bit a bit MPI_MAXLOC Máximo y su posición MPI_MINLOC Mínimo y su posición Las rutinas que realizan las operaciones de reducción se explican a continuación. La diferencia entre estas dos rutinas es que en la primera sólo el proceso raíz tiene el resultado de la operación en su buffer de recepción, la segunda todos los procesos cuentan con el resultado en su buffer de recepción. int MPI_Reduce(void *inbuf, void *outbuf, int numelem, MPI_Datatype tipo, MPI_Op operacion, int root, MPI_Comm comunicador); int MPI_Allreduce(void *inbuf, void *outbuf, int numelem, MPI_Datatype tipo, MPI_Op operacion, int root, MPI_Comm comunicador); donde: inbuf Dirección del buffer de envío. outbuf Dirección del buffer de recepción. numelem Numero de elementos en el buffer de envío. tipo Tipo de datos de los elementos del buffer de envío. operacion Operación a realizar sobre los elementos del buffer de envío. root Rango del proceso raíz. comunicador Canal de comunicación. Comunicaciones sin bloqueo El rendimiento de muchas operaciones se puede mejorar si se logran solapar las comunicaciones y el cálculo. La forma de solapar las comunicaciones es la creación de hilos de ejecución, con esto se puede crear una hilo que se encargue de las comunicaciones, mientras que el proceso siga realizando su trabajo. En este caso no habría problema al utilizar comunicaciones con bloqueo ya que el hilo encargado de las comunicaciones podría permanecer bloqueado, esperando la terminación de las mismas. La comunicación sin bloqueo es una alternativa, que usualmente da un buen resultado. La forma en que operan las comunicaciones es como sigue: para enviar y/o recibir un mensaje la rutina encargada de ello hace la solicitud y regresa antes de que el canal de comunicación se encuentre libre y/o ocupado, es así, como la rutina regresa antes de que el buffer de envío sea reutilizable y/o utilizable, por lo que hay que realizar una llamada a una rutina de verificación, para ver si el envío y/o recepción se han completado. Hasta el momento se han hablado de comunicaciones con bloqueo, esto quiere decir que la rutina llamada para la transmisión regresa si se han completado las operaciones, por lo que el proceso es detenido en cuanto las comunicaciones no terminen. Pero, MPI implementa estas mismas comunicaciones, sin bloqueo, las rutinas siempre regresan directamente y permiten que el proceso continué realizando su trabajo y después de un lapso de tiempo realice un test para verificar la terminación de la operación de envío y/o recepción. Las rutinas de MPI para la comunicación sin bloqueo tienen la misma sintaxis que las rutinas con bloqueo, excepto por el prefijo I (Inmediato), además las rutinas de envío se les agrega un parámetro en la llamada, dicho parámetro es un tipo de dado de MPI que posteriormente será utilizado para determinar si el mensaje se ha completado. Las rutinas de envío sin bloqueo son: 25 MPI_Isend MPI_Issend MPI_Ibsend MPI_Irsend La sintaxis de estas rutinas tienen la misma sintaxis que la de MPI_Isend que es como sigue: int MPI_Isend(void *buf, int count, MPI_Datatype tipo, int dest, int etiqueta, MPI_Comm comunicador, MPI_Request *request); donde: buf count tipo destino etiqueta comunicador request Es la dirección del buffer de envío. Es un entero que indica el número de elementos a recibir Tipo de datos de MPI de los elementos en el buffer de envío. Identificador del proceso destino. Etiqueta de un mensaje. Comunicador al cual pertenece el proceso que envía y recibe mensaje. Estructura de tipo MPI_Request con información que ayuda a determinar si las comunicaciones se han completado. La rutina de recepción tiene la siguiente sinopsis: MPI_Recv(void *buf, int count, MPI_Datatype tipo, int fuente, int etiqueta, MPI_Comm comunicador, MPI_Request *request); donde: buf count tipo fuente etiqueta comunicador request Es la dirección del buffer de envío. Es un entero que indica el número de elementos a recibir Tipo de datos de MPI de los elementos en el buffer de envío. Identificador del proceso fuente o MPI_ANY_SOURCE (reciba de cualquier fuente) Etiqueta de un mensaje o MPI_ANY_TAG (cualquier etiqueta). Comunicador al cual pertenece el proceso que envía y recibe mensaje. Estructura de tipo MPI_Request con información que ayuda a determinar si las comunicaciones se han completado. Como ya se menciono anteriormente las rutinas de envío y recepción sin bloqueo no completan las comunicaciones, sólo hacen la solicitud de envío o recepción según sea el caso. Las funciones para completar el envío o la recocción son MPI_Wait y MPI_Test, la primera permite esperar por la finalización de una operación y la otra verifica si se ha completado dicha operación. La sinopsis de dichas rutinas es: int MPI_Wait(MPI_Request *request, MPI_Status *estado); int MPI_Test(MPI_Recuest *request, int *bandera, MPI_Status *estado); donde: request Tipo de dato de MPI_Recuest que determina la operación que se trata, envío o recepción. bandera Bandera que especifica si la operación ha sido contemplada. estado Tipo de dato MPI_Status en donde se gurda toda la información sobre la operación completada. En MPI existen otras rutinas que ayudan a la comunicación sin bloqueo, por ejemplo, en ocasiones no importa que un proceso se espere a que termine de enviar un mensaje, sino que hay muchos que esperan un mensaje que posiblemente no llegará, esto se presenta cuando no se sabe en realidad como se presentaran las comunicaciones entre los proceso, como lo es en este proyecto. Para 26 esto existen dos rutinas que ayudan a que un proceso no se bloquee esperando un mensaje, lo que se hace es verificar si se ha recibido un mensaje de una fuente especifica y con una etiqueta determinada. Las rutinas que realizan dicha verificación son: int MPI_Probe(int fuente, int etiqueta, MPI_Comm comunicador, MPI_Status *estado) ; int MPI_Iprobe(int fuente, int etiqueta, MPI_Comm comunicador, int *bandera, MPÌ_Status*estado) ; donde: fuente Identificador rango del proceso enviador. etiqueta Etiqueta del mensaje. comunicador Canal de comunicación. bandera Entero que determina si hay un mensaje con la respectiva etiqueta y fuente. estado Tipo de dato de MPI_Status en donde se guarda información del mensaje recibido. La primera es una verificación con bloqueo, es decir, que esta se bloquea hasta que se presente un mensaje con las características especificadas. La segunda es sin bloqueo, esta rutina verifica si hay un mensaje que se ajuste a los parámetros y regresa, si se presenta esta situación modifica el parámetro bandera=true. Una vez que se sabe que llegó el mensaje, se puede recibir usando MPI_Recv. Con estas rutinas no sólo se puede evitar el bloqueo de un proceso además se pueden establecer comunicaciones sin que se tenga que establecer previamente una comunicación para establecer la longitud del mensaje a enviar, Esto se debe a que una vez que MPI_Probe o MPI_Iprobe hayan sido exitosas, se puede revisar la estructura estado con MPI_Get_count para obtener la información respectiva y usarla en MPI_Recv. La sinopsis esta rutina es: int MPI_Get_count(MPI_Status estado, MPI_Datatype tipo, int *contador); donde: estado Tipo de dato MPI_Status donde se guarda información de la comunicación establecida. tipo Tipo de dato de los elementos recibidos. contador Número de elementos recibidos. 2.3.4. Compilación de programas MPI [1] Para compilar los programas MPI, usualmente se utiliza el comando mpicc (o mpif77 en el caso de FORTRAN) cuyo funcionamiento y parámetros son similares a la del compilador gcc, no existe un manual del comando mpicc la siguiente sinopsis y opciones del compilador son tomadas del manual del gcc, pero fueron probadas exitosamente con programas MPI. Sinopsis de mpicc mpicc [opciones] <fuente | Fuentes> [opciones] Opciones: Algo muy importante que hay que tomar en cuenta es que las opciones van separadas una de otra, por ejemplo, si se tienen las opciones v y o no es posible ponerlas de la siguiente forma: $: mpicc -vo hola holamundo.c Esto es incorrecto y el compilador marcará un error. La forma correcta de poner estas opciones es: $: mpicc -v -o hola holamundo.c Las opciones más comunes son: 27 -o file Coloca la salida del proceso en un archivo con el nombre especificado especificando en file. Si no se utiliza esta opción la salida por default dependerá de la etapa en la que se encuentre, es decir, si se está en el preprocesado, compilado o ensamblado la salida será el nombre del fuente con la extensión correspondiente a la etapa de que se trate; si se trata del ejecutable la salida será a.out. -v Imprime en la salida estándar del error la ejecución de la instrucción al recorrer cada una de las etapas de la compilación. También imprime el número de versión del compilador y del preprocesador -Dmacro=valor Esta es utilizada para la creación de macros que no fueron declarados en el código fuente, por ejemplo, el archivo “prueba.c” con el siguiente código. #include <stdio.h> #include “mpi.h” int id, proc, longitud; char nombre[50]; int main (int argc, char *argv[]) { MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &longitud); printf("hola Mundo , estoy en el procesador %s\n", nombre); printf(“Macro definida : %d\n”, MCR); MPI_Finalize(); } Hay que observar que la macro MCR no es declarada en el código fuente, para que nuestra compilación no tenga errores hay que realizarla de con la siguiente instrucción: $: mpicc -o salida prueba.c -DMCR=1 Y no tendremos ningún problema en obtener nuestro ejecutable. -include file Esta opción debe ser utilizada para indicarle un archivo de cabecera que no halla sido declarado en el código fuente. Por ejemplo, si tenemos el archivo holamundo.c con el siguiente código: #include <stdio.h> int id, proc, longitud; char nombre[50]; int main (int argc, char *argv[]) { MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &longitud); printf("hola Mundo , estoy en el procesador %s\n", nombre); MPI_Finalize(); } 28 Hay que observar que no se ha incluido la biblioteca “mpi.h” y es necesario incluirla para poder compilar este programa. Pero la podemos incluir desde la línea de comandos como sigue: $: mpicc holamundo.c -o salida -include /usr/share/mpi/include/mpi.h -static Esta opción es muy impórtate cuando uno crea los ejecutables en un sistema Linux y se los lleva para correrlos en otra distribución o en otro Unix, ya que el enlazado es simbólico. ¿Que quiere decir esto?, pues muy simple, en los ejecutables en los sistema tipo Unix no se inserta el código de las bibliotecas que se utilizan durante el programa, solamente se crea un enlace hacia la biblioteca necesaria para la corrida de nuestros ejecutables, esto hace a nuestros ejecutables compactos, pero, ¿Qué sucede si corremos nuestro ejecutable en una distribución que no tiene la biblioteca a la que se hace referencia?, pues se tendrán problemas para correr nuestra aplicación. Pero eso se soluciona muy fácilmente con esta opción, aunque se tiene un costo en el tamaño de nuestros ejecutables. -w Deshabilita el desplegado de todos los mensajes warnigs(advertencias). Estas son sólo algunas de las opciones que posiblemente se lleguen a utilizar para la compilación de programas MPI, este compilador también acepta la compilación paso por paso, pero en tal caso hay que tener cuidado principalmente al enlazar los programas ya que hay que conocer los parámetros utilizados para la llamada del enlazador. 2.3.5. Ejecución de Programas MPI MPI no especifica como serán arrancados los procesos MPI. Cada una de las implementaciones de MPI se encarga de definir los mecanismos de arranque de los procesos; para tales fines algunas versiones coinciden en el uso de un programa especial denominado mpirun. Dicho programa no es parte del estándar MPI. [1] Para ver las opciones del comando mpirun solo hay que poner la siguiente instrucción: $: mpirun -h Esto mostrará las opciones de dicho comando. A continuación se presenta la sinopsis del comando y alguna de las opciones más importantes, si se desea conocer el total de estas deberá verificar la ayuda de este comando. Sinopsis. mpirun [opciones_mpirun...] <nombre_programa> [opciones_programa...] opciones_mpirun: -arch <arquitectura> Especifica la arquitectura donde serán lanzados los programas. -h 29 Muestra la ayuda. -machine <nombre_maquina> Se usa para poner en marcha el proceso en la maquina especificada. -machinefile <nombre del machine_file> Toma la lista de las maquinas en donde el proceso será lanzado del archivo machine_file Este archivo es una lista de todas la maquinas disponibles. Si o se usa esta opción se toma el archivo por default machines.LINUX (en el caso de LINUX) que se encuentra en mpi/share/ la ubicación de la carpeta mpi variara dependiendo del sistema. En esta ubicación se encuentra un ejemplo de como debe de ser este archivo, ya que al instalarlo no se tiene configurado, o bien se puede utilizar el comando mpi/sbin/tstmachines el cual realiza una prueba sobre las posibles maquinas que pueden formar parte del cluster, este mismo comando configura el archivo machinefile que será usado por default. -np <numero_procesos> Especifica el número de procesos que serán lanzados de la aplicación paralela, estos no necesariamente se encontrara corriendo uno por maquina. -nodes <nodos> Especifica el numero de nodos que serán tomados del archivo machinesfile para lanzar los procesos -nolocal Especifica que el proceso no deberá ser lanzado en la machina local. -stdin archivo Usa el archivo como entrada estándar para la aplicación en paralelo. -v Imprime algunos mensajes en pantalla del trabajo que realiza. Al terminar mpirun regresa un cero, a menos que mpirun haya detectado un problema en alguno de los procesos, en tal caso regresa un valor distinto de cero. 30 2.4. Balance de Carga Uno de los principales aspectos para obtener un buen rendimiento en los sistemas distribuidos es el balance de carga. Es un componente muy importante, ya que de él depende el buen uso que se hace de la capacidad global de rendimiento del sistema (eficiencia). El rendimiento global depende en gran medida del algoritmo elegido para el balance de carga . El balance de carga es el método para que los sistemas distribuidos obtengan el mayor grado de eficiencia posible. El balance de carga consiste en el reparto de la carga entre los nodos del sistema para que la eficiencia sea mayor. La carga se distribuye y se traslada de los nodos más cargados a los nodos menos cargados del sistema. Con esto se consigue una reducción del tiempo de ejecución de las tareas en el sistema distribuido, y se consigue aproximar el tiempo de finalización de la ejecución de las tareas de cada uno de los nodos, es decir, se consigue que los nodos terminen de ejecutar "al mismo tiempo", y que no haya grandes diferencias entre el término de uno de ellos y él de los demás. Con todo esto se consigue que no haya nodos sobrecargados, y otros libres de trabajo, porque estos nodos sobrecargados trasladarían parte de su trabajo a esos otros nodos más libres de trabajo. Los algoritmos de distribución de carga se basan en unos componentes, que marcan las diferencias entre unos algoritmos y otros: • Política de información: La cuál decide qué tipo de información debe recoger de los nodos, de qué nodos debe recoger ésta información, y cuándo debe recogerla. • Política de transferencia: Decide si cada uno de los nodos es apto o no para llevar a cabo una transferencia, tanto de emisor como de receptor. • Política de selección: Decide cuál es la carga que se va a transferir en una transferencia. • Política de localización: Localiza el nodo adecuado para realizar una determinada transferencia. De éste aspecto depende en gran medida el rendimiento global, ya que dependiendo de la elección del algoritmo de balance, se puede conseguir que todos los nodos del sistema estén trabajando en todo momento para minimizar el tiempo de ejecución de los procesos, o una mala elección del algoritmo de balance puede hacer que tan solo unos pocos nodos estén sobrecargados mientras que otros no tengan nada de trabajo. Esa es la función principal que debe llevar a cabo el algoritmo de balance de carga, hacer que todos los nodos estén trabajando mientras que haya algún proceso pendiente, y sea posible ejecutar dicho proceso en varios nodos consiguiendo una disminución en el tiempo de proceso. Hay algunos casos en los que el algoritmo de distribución no debe repartir carga entre los demás nodos ya que el tiempo de ejecución de dicho proceso puede ser tan pequeño, que si se repartiera entre los nodos se perdería demasiado tiempo en la comunicación y el resultado sería un tiempo de ejecución mayor. El algoritmo encargado del balance debe tener en cuenta todos estos detalles. Pero además existen varios políticas que hay que definir para llegar a elegir un algoritmo de distribución eficiente: 31 • • Centralizado vs. Distribuido: Las políticas centralizadas son aquellas en las que la información se concentra en una única ubicación física, que toma todas las decisiones de planificación. Esta solución presenta problemas de cuellos de botella, y tiene un límite en su grado de escalabilidad. Por otro lado las políticas distribuidas son aquellas en las que la información está repartida entre los distintos nodos, y las decisiones son tomadas entre todos. Los problemas son que la información puede no ser coherente, se replica la información y necesita más comunicaciones. Estáticas vs. Dinámicas: Las políticas estáticas toman decisiones de forma determinista o probabilística sin tener en cuenta el estado actual del sistema. Esta solución puede ser efectiva cuando la carga se puede caracterizar suficientemente bien antes de la ejecución, pero falla al ajustar las fluctuaciones del sistema. Las políticas dinámicas utilizan información sobre el estado del sistema para tomar decisiones, por lo que potencialmente mejoran a las políticas estáticas mejorando la calidad de las decisiones. Incurren en mayor sobrecarga al tener que recoger información de estado en tiempo real. Todos estos conceptos y otros más son los que habrá que tener en cuenta para llegar a diseñar un algoritmo de balance de carga, para un sistema distribuido, que consiga obtener el mayor rendimiento global posible. 32 3. Migración de datos 3. MIGRACIÓN DE DATOS [S2001] En el modelo de envió y recepción de mensajes, los mensajes son datos que un proceso envía a otro o a otros, los datos es la información guardada en variables del proceso en cuestión. Estas variables pueden ser de diferente tipo, es decir, se puede tratar de un entero, una flotante, un carácter, una cadena, inclusive variables que guarden un conjunto de elementos de información conformados por alguno de los anteriores, etc. Pero no sólo se encuentra involucrado el tipo de información que se enviara, sino además a quien y/o a quienes les será enviado, y por supuesto la cantidad que se enviara de información. Estos parámetros son importantes para el envió de información. MPI implementa rutinas para el envió y recepción de mensajes. De tal manera MPI provee tres aspectos para el envío de datos, con los cuales gana portabilidad. La especificación de los datos a enviar con tres campos. 1. Dirección de inicio 2. Tipo de dato. 3. Contador. Con las especificaciones de datos se permiten las comunicaciones heterogéneas. Las funciones básicas de MPI para el envío y recepción de datos son: MPI_Send(void *buf, int count, MPI_Datatype tipo, int dest, int etiqueta, MPI_Comm comunicador); MPI_Recv(void *buf, int count, MPI_Datatype tipo, int fuente, int etiqueta, MPI_Comm comunicador, MPI_Status estado); Ya mencionadas en el apartado “Comunicación punto a punto” Además se establecen los cuatro tipo de comunicación en MPI: Estándar, Sincrono, Buffered y de Lectura mencionados en el mismo apartado. De tal manera que para enviar, por ejemplo el contenido de la variable x del proceso 0 al proceso 1, la forma de invocar la rutina para el envío seria como sigue: MPI_Send(&x, 1, MPI_INT, 1, 99, MPI_COMM_WORLD); Y el proceso receptor realizaría la llamada a la rutina de recepción de la siguiente forma, suponiendo que el resultado lo desea guardar en otra variable, digamos y: MPI_Recv(&y, 1, MPI_INT, 0, 99, MPI_COMM_WORLD, &status); Es importante que al enviar un dato se haga uso del equivalente declarado en MPI, ya que la utilización de cualquier otro provoca que MPI, cometa errores en la recepción y por consiguiente quede bloqueado o sea abortado en el peor de los casos. Aunque para la recepción de un mensaje no necesariamente se necesite saber de quien viene o la etiqueta del mismo, por lo que otra forma de recibir un mensaje es como sigue: MPI_Recv(&y, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); La migración de datos no queda sólo en el envío, para que se hable de migración de información el elemento enviado a otro proceso debe de desaparecer del proceso que lo envía. Esto se muestra en la figura. Que finalmente es lo que se pretende con este proyecto. 34 3.1. Definición e Implementación de un TDA lista balanceable [AJ1994] Existen básicamente dos formas de implementar un TDA lista, una de ellas hace uso de arreglos, lo cual especifica una lista estática. Supongamos que deseamos realizar una lista estática de elementos los cuales contienen los siguientes datos: matricula, nombre, edad y sexo de un alumno. Lo primero que se pensaría en hacer es la creación de una estructura que tuviera tales elementos, esto se lograría de la siguiente forma: struct registro { long matricula; char nombre[30]; int edad; char sexo; }; El siguiente paso es la creación de la lista y como dicha lista es estática, lo más lógico es el uso de arreglos, así que el siguiente paso es: struct registro lista[50]; De tal forma que cada no de los elementos se encontraría uno seguido de otro en memoria como se muestra en la siguiente figura. El enunciado anterior declara una lista con 50 elementos, lo cual es un inconveniente, ya que lo más probable es que se tengan más o menos elementos, por lo que se tendrían dos situaciones, la primera y no tan grave, es el desperdicio de memoria, la segunda sería fatal y provocaría una terminación anormal de nuestro proceso, al no tener memoria para guardar más elementos para los cuales reservamos memoria. La principal limitante es que se debe conocer el número de elementos que se tendrán, aunque por otro lado es de fácil manejo. [AJ1994] La otra forma de crear la lista es de forma dinámica, es decir en tiempo de ejecución pedir la memoria para que ésta sea tan grande como nos lo permita la memoria asignada a nuestro proceso. Además de esta forma nuestra lista puede estar vacía sin ocupar memoria. Esta forma de crear las listas nos trae el concepto de “listas ligadas”, lo cual se explica a continuación. Ya observamos que el uso información de múltiples tipos involucra las estructuras. Pues a la estructura utilizada para la creación de la lista estática le agregamos un campo quedándonos de la siguiente forma: struct registro { long matricula; char nombre[30]; int edad; char sexo; struct registro *sig; }; 35 El campo agregado es un apuntador al tipo de dato creado para manejar los datos. En otras palabras, la estructura contiene un apuntador a su propio tipo. Esto significa que una instancia de tipo registro puede apuntar a otra instancia del mismo tipo. Esta es la menara en que se crean los enlaces en una lista ligada. Cada elemento de la lista tiene un campo que apunta al siguiente elemento de la lista como lo ilustra la siguiente figura. Cada lista tiene un principio y un final. [MJ1995] El inicio de la lista está marcado por el apuntador a la cabeza, el cual apunta a la primera estructura de la lista. El apuntador de cabeza no es una instancia de la estructura, sino simplemente un apuntador al tipo de dato que forma la lista. El final de la lista está marcado por una estructura la cual tiene su campo apuntador igual a NULL. Debido a que todos los demás elementos de la lista tienen en su campo apuntador un valor distinto a NULL, que apunta al siguiente elemento de la lista, un valor de apuntador de NULL es una manera inequívoca para indicar el final de la lista. La estructura de la lista ligada, con su apuntador de cabeza y si último elemento se muestra en la siguiente figura. Entre las ventajas significativas que se tienen con esta forma de realizar la implementación la lista, es en la inserción y eliminación de elementos de la lista, lo cual es una tarea común de programación. Mientras que con los arreglos, la eliminación de elementos así como la inserción, cuando se requiere preservar un orden, involucra gran cantidad de procesamiento al realizar corrimientos de los elementos, con las listas ligadas todo lo que se requiere es un manejo adecuado de apuntadores, de hecho no se necesitan mover los datos. Pero la principal ventaja es el espacio de almacenamiento como se explico anteriormente. 3.1.1. Implementación [AJ1994] Primeramente se hará uso de la palabra clave typedef para crear un sinónimo de la estructura que se pretende crear. Por ejemplo, si tomamos el ejemplo anterior: typedef struct registro nodo; El que se haga uso de typedef o de la etiqueta de la estructura para declarar a las estructuras casi no tiene diferencia práctica, sólo da como resultado un código más conciso. Enseguida se establece la estructura que será utilizada para guardar la información que se necesita guardar, anexando un campo, el cual deberá ser un apuntador al mismo tipo de la estructura, como se muestra a continuación: struct registro { 36 long matricula; char nombre[30]; int edad; char sexo; nodo *sig; }; Una vez establecidas nuestras estructuras se procede a la creación de la lista, la cual en un principio se encuentra vacía, con el siguiente enunciado ilustra la acción a tomar. nodo *lista=NULL; Una vez creada nuestra lista vacía, para agregar elementos a la misma se deberá solicitar memoria para la creación de un elemento y una vez creado se deberá insertar en la lista. La forma de pedir memoria para un elemento de la lista se ilustra en las siguientes líneas: nodo *aux=NULL; aux=(nodo *)malloc(sizeof(nodo)); if(aux!=NULL) { /**** Se inicializa el nodo con los elementos necesarios ****/ /**** El apuntador al siguiente elemento deberá ser inicializado a NULL *****/ aux->sig=NULL; } Las funciones de creación de nodos, la inserción y eliminación de nodos se dejan a consideración del usuario. Es importante declarar un apuntador al tipo de elemento con el cual se formará la lista y no una instancia de la misma. Uno de los motivos es que si se declara una instancia de la misma, en realidad no se llegará gamas a tener una lista vacía además todo el manejo de apuntadores será peligroso. 37 3.2. Envío / recepción de un nodo de un TDA lista Como ya se mencionó uno de los objetivos de este proyecto es el envío y la recepción de uno o varios nodos de un TDA lista, sin importar los campos que contenga cada nodo de dicha lista. Para tal motivo se creará un TDA lista cuyos nodos poseen los siguientes datos de un alumno, como lo son: nombre, edad, matricula y sexo. El código de las funciones para el manejo de la lista se muestra en el Anexo A listado “operlis.c”, se encuentran debidamente documentadas, pero se considera que estas no son de gran importancia ya que se pretende que el usuario cree sus propias funciones de manejo de la lista y sólo haga uso de las funciones creadas para la migración de nodos del TDA. Se usan otras funciones que solamente son auxiliares en el desplegado de información en pantalla, creación del TDA a partir de un archivo y obtención de valores al azar para la simulación de una petición para enviar nodos del TDA a otro nodos del cluster, con lo cual se lograría un balance de carga, el cual es el objetivo principal de dicho proyecto. Dichos códigos se encuentran en el Anexo A. MPI implementa rutinas para el envío y recepción de datos primitivos de C. En el caso de un TDA, el contenido de los nodos puede llegar a ser tan sencillo como enviar un dato nativo de C. Ahora el problema con el TDA que se pretende manejar, es que además de ser un TDA creado dinámicamente, pueden estar involucrados el manejo de otras estructuras. Para lograr enviar nodos de un TDA lista de un nodo del cluster a otro, se exploran las diferentes opciones mediante las cuales se puede realizar dicha tarea. Todas las versiones aquí presentadas tienen la misma función principal. A continuación se presenta el código fuente del modulo principal de cada uno de los programas, el cual es idéntico para todas las formas en que se envían los nodos de TDA. main(int argc, char *argv[]) { nodo *lista=crea_lista(); MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); if(id==COORDINADOR) { int destino=0, num_elem=0, lon_tda=0; printf("Soy el coordinador desde %s\n", nombre); lista=obten_datos("/tmp/regis.txt"); //Creación del TDA a partir de un archivo. Printf(lista); //Impresión del TDA lon_tda=longitud_TDA(lista); //Se prepara para migrar elementos valores_azar(proc, lon_tda, &destino, &num_elem); lista=Envia_TDA(lista, destino, num_elem); //Envía elementos printf("%d elementos son enviados a: %d\n", num_elem, destino); printf("Soy el coordinador desde %s\n", nombre); Printf(lista); //Función que imprime la lista } else { printf("\n Hola Mundo, soy el proceso %d, desde %s\n", id, nombre); lista=Recive_TDA(lista); if(!es_vacia(lista)) Printf(lista); printf("\n"); } MPI_Finalize(); return 0; 38 //Función que imprime la lista } Cabe recordar que el desarrollo del proyecto para el balance de carga se basa en un “problema de juguete”, el cual nos ayudara a ver como y en que forma debemos utilizar para la migración de los nodos del TDA lista de una forma más general, es decir, que nuestro código sea capaz de migrar nodos de una lista sin tener que conocer los elementos que conforman dicho nodo. 3.2.1. Envío / recepción del nodo Campo por Campo (Versión 1 “camxcam.c”) El primer acercamiento que se tiene para migrar los nodos del TDA lista es el más “sencillo”, pero es el menos conveniente para ser utilizado. Este consiste en que el proceso enviador envié cada nodo del TDA lista, campo por campo, esto claro trae repercusiones en las comunicaciones y en el caso de un gran manejo de información llegar a saturar el canal de comunicación, ya que las comunicaciones en el modelo de paso de mensajes utiliza el protocolo TCP/IP. Bueno para realizar el envío campo por campo solamente se realiza un ciclo para cumplir con el envío de los nodos solicitados. En cada iteración se envía cada uno de los campos al proceso destino. Es un procedimiento que no requiere mayores explicaciones, pero si hay que realizar ciertas precisiones: 1.Se debe tener cuidado con enviar y recibir el tipo de dato especificado en C con su equivalente en MPI, es decir, que si yo declaro un double x; para el envío de este dato de debe especificar MPI_DOUBLE como parámetro de tipo de dato en la función de envío y recepción, cualquier otro parámetro diferente al equivalente de MPI puede causar resultados inesperados. 2.Antes de comenzar a enviar cada uno de los nodos se debe de mandar un mensaje al proceso receptor indicándole cuantos nodos se le van a enviar, para que este realice las iteraciones necesarias para la recepción de todos los nodos que le sean enviados. 3.En caso de que el dato a enviar, sea un arreglo, se pueden presentar dos situaciones, una que se envié el numero de elementos del arreglo antes que el arreglo mismo, de esta forma el proceso receptor sabrá la longitud del siguiente mensaje; la segunda es hacer uso de las rutinas MPI_Probe y MPI_Get_count en conjunto para la recepción del arreglo. En esta versión se opta por la primera opción. 4.Si el nodo del TDA lista hace usos de estructuras como campos, se le deberá dar el mismo tratamiento a dichos campos para enviarlos. El código de la función de envío es el siguiente: /***** En este primer acercamiento se envía campo por campo ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0; int longitud=0; nodo *ap=lista; //Primero se manda el numero de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i++) { 39 ap=lista; longitud=strlen(ap->nombre)+1; //longitud de la cadena // Se envía campo por campo MPI_Send(&ap->matricula, 1, MPI_UNSIGNED, destino, etiqueta, MPI_COMM_WORLD); MPI_Send(&longitud, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); // Se envía la longitud de la cadena antes de enviarla MPI_Send(&ap->nombre, longitud, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD); MPI_Send(&ap->edad, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); MPI_Send(&ap->sexo, 1, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD); // Se elimina del TDA el nodo enviado lista=lista->sig; free(ap); } return lista; } El código de la función de recepción es el siguiente: /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0; nodo *ap=NULL; unsigned matricula=0; char nombre[40], sexo; int edad; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { MPI_Recv(&matricula, 1, MPI_UNSIGNED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); // Se recibe la longitud del campo siguiente M PI_Recv(&longitud, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); MPI_Recv(nombre, longitud, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); MPI_Recv(&edad, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, & estado); MPI_Recv(&sexo, 1, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); // Reconstrucción del TDA ap=crea_nodo(matricula, nombre, edad, sexo); lista=inserta_nodo(lista, ap); } return lista; } El inconveniente con esta versión del programa es que genera una gran cantidad de mensajes, lo que tiene gran repercusión en las comunicaciones. Pero el problema fundamental es que no es un código genérico, esto trae como consecuencia que, si se maneja un TDA lista diferente al usado para el desarrollo de esta versión, este código no funcionará. 40 3.2.2. Envío / recepción nodo empaquetado (Versión 2.1 “empaketado.c”) Para la segunda versión el problema a vencer es la gran cantidad de mensajes que se generan al enviar los nodos del TDA lista campo por campo. La solución a este problema la provee MPI mediante el empaquetado de datos. En el apartado “Tipos de dados de MPI” del capitulo 2.2. se menciono un tipo de dato de MPI, este es el MPI_PACKED. MPI provee mecanismos para el empaquetamiento de datos. Empacar datos es “almacenar datos no continuos en un buffer continuo, para que sean enviados y una vez recibidos nuevamente almacenarlos en localidades discontinuas”. Las rutinas encargadas del empaquetamiento y desempaquetamiento de datos son: int MPI_Pack(void *inbuff, int num_elem, MPI_Datatype tipo, void *outbuff, int outtam, int *posicion, MPI_Comm comuicador); donde: inbuff buffer de entrada num_elem Número de elementos a ser empaquetados. tipo Tipo de dato en la entrada outbuff buffer de salida, donde se almacenarán los datos en forma continua. outtam Tamaño del buffer de salida. posicion Indicador de posición actual en el buffer de salida. comunicador Canal de comunicación. MPI_Pack permite copiar en un buffer (outbuff) continuo de memoria datos almacenados en el buffer (inbuff) de entrada. Cada llamada a la rutina MPI_Pack modificará el valor del parámetro posicion incrementándolo según el valor de num_elem. int MPI_Unpack(void *inbuff, int intam, int *posicion, void *outbuff, int outelem, MPI_Datatype tipo, MPI_Comm comuicador); donde: inbuff buffer de entrada intam Tamaño del buffer de entrada en bytes, posicion Indicador de posición actual en el buffer de salida. outbuff buffer de salida, donde se almacenarán los datos en forma continua. outelem Número de elementos a ser desempaquetados. tipo Tipo de dato en la salida comunicador Canal de comunicación. MPI_Unpack permite copiar en un buffer (outbuff) los datos almacenados en el buffer continuo (inbuff) de entrada. Cada llamada a la rutina MPI_Unpack modificará el valor del parámetro posicion incrementándolo según el valor de outelem. Para esta versión se crea un paquete por cada nodo del TDA lista y es enviado. El proceso receptor se encarga de desempaquetar la información contenida en el buffer de recepción y reconstruye el nodo del TDA y lo enlista. 41 El código de la función de envío es: nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0, posicion=0; int longitud=0; nodo * ap=lista; size_t tam=0; char *buffer; //Primero se manda el numero de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i++) { ap=lista; longitud=strlen(ap->nombre)+1; //longitud de la cadena posicion=0; //Se inicializa el indicador de la posición en el buffer //Se calcula el tamaño de la memoria a solicitar tam=sizeof(int)+sizeof(char)*longitud+sizeof(unsigned)+sizeof(int)+sizeof(char); // También se empaqueta la longitud de la cadena para ser enviada buffer=(char *)malloc(tam); if(buffer!=NULL) { //Se envía el tamaño del paquete a ser enviado. MPI_Send(&tam, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); //Comienza el empaquetado de la información MPI_Pack(&longitud, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->nombre, longitud, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->matricula, 1, MPI_UNSIGNED, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->edad, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->sexo, 1, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); //Fin del empaquetado. //Se envía el paquete. MPI_Send(buffer, posicion, MPI_PACKED, destino, etiqueta, MPI_COMM_WORLD); // Se elimina del TDA el nodo enviado lista=lista->sig; free(ap); free(buffer); } else { perror("Memoria Insuficiente..."); //Se envía un cero para identificar que hubo un error y se rompe el ciclo MPI_Send(&cero, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); break; } } return lista; } 42 El código de la función de recepción es: ***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0, posicion=0; nodo *ap=NULL; unsigned matricula; char nombre[40], sexo; int edad; size_t tam; char *buffer; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { posicion=0; //Se inicializa la posición en el buffer MPI_Recv(&tam, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); if(tam<0) //Si se recibe un cero es que hubo un error y se rompe el ciclo break; buffer=(char *)malloc(tam); if(buffer!=NULL) { //Se recibe el paquete. MPI_Recv(buffer, tam, MPI_PACKED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //Se comienza a desempaquetar // Se recibe la longitud del campo siguiente MPI_Unpack(buffer, tam, &posicion, &longitud, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, nombre, longitud, MPI_CHAR, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &matricula, 1, MPI_UNSIGNED, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &edad, 1, M PI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &sexo, 1, MPI_CHAR, MPI_COMM_WORLD); // Reconstrucción del TDA ap=crea_nodo(matricula, nombre, edad, sexo); lista=inserta_nodo(lista, ap); free(buffer); } else perror("Memoria Insuficiente..."); } return lista; } Con esta versión se logra eliminar un gran número de mensajes, hay que considerar que aún se debe de enviar un mensaje al proceso receptor antes de mandar los nodos del TDA lista, para que sepa cuantos nodos serán recibidos. 43 3.2.3. Envío / recepción TDA empaquetado (Versión 2.2 “empaketado_b.c”) La siguiente versión es aún mejor ya que se solo se envían dos mensajes para migrar todos los nodos de TDA lista, uno con el tamaño del paquete y el paquete en sí. En esta versión del programa se crea un paquete que contiene todos los nodos de la lista. El código para la función de envío es: ***** En este acercamiento se envía un paquete para todos los nodos del TDA lista ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0, posicion=0; int longitud=0; nodo *ap=lista; size_t tam=calcula_tam_buffer(lista, num_elem); char *buffer; //Primero se manda el numero de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } buffer=(char *) malloc(tam); if(buffer!=NULL) { ap=lista; //Se envía el tamaño del paquete a ser enviado. MPI_Send(&tam, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); //Este ciclo crea un solo paquete para todos los nodos. for(i=0; i<num_elem; i++) { ap=lista; longitud=strlen(ap->nombre)+1; //longit ud de la cadena MPI_Pack(&longitud, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->nombre, longitud, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->matricula, 1, MPI_UNSIGNED, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->edad, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->sexo, 1, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); // Se elimina del TDA el nodo enviado lista=lista->sig; free(ap); } //Se envía el paquete. MPI_Send(buffer, posicion, MPI_PACKED, destino, etiqueta, MPI_COMM_WORLD); free(buffer); //se libera el espacio asignado. } else { perror("Memoria Insuficiente..."); //Se envía un cero para indicar que hubo un error. MPI_Send(&cero, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); } return lista; } 44 [MJ1995] En esta versión se utiliza una función la cual calcula el tamaño del paquete a ser enviado, el código de dicha función es: size_t calcula_tam_buffer(nodo *lista, int num_elem) { nodo *aux=lista; int i=0, longitud=0; size_t tama=0; for(i=0; i<num_elem; i++) { longitud=strlen(aux->nombre)+1; tama+=sizeof(int)+sizeof(char)*longitud+sizeof(unsigned)+sizeof(int)+sizeof(char); aux=aux->sig; } return tama; } Es importante hacer el cálculo correcto del tamaño del buffer para enviar, un cálculo mal hecho provocaría que los procesos se pasmaran, ya sea porque el proceso encargado de enviar el mensaje escriba fuera de los límites establecidos para el buffer o por que el proceso receptor se quede esperando a que el canal de comunicación este vacío. Cabe mencionar que si se tiene que calcular el tamaño de una estructura como lo es en este caso, no es lo mismo realizar el cálculo de la estructura que de todos sus campos en conjunto, es decir, si tenemos la siguiente estructura: struct Nodo_A { int x; double y; char array[20]; } Tenemos las siguientes formas de calcular su tamaño: size_a=sizeof(struct Nodo_a); size_b=sizeof(int)+sizeof(bouble)+20*sizeof(char); Esto no necesariamente da el mismo resultado. La forma correcta de realizar el calculo, es hacerlo campo por campo y la razón es muy obvia. Nosotros no necesitamos el espacio que ocupa la estructura, ya que no guardamos la estructura en el buffer de envío, nosotros guardaremos los campos de la estructura en el buffer, por tal motivo lo que necesitamos es calcular el espacio de los campo en conjunto. 45 El código para la función de recepción es: /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0, posicion=0; nodo *ap=NULL; unsigned matricula; char nombre[40], sexo; int edad; size_t tam=0; char *buffer; /*** Se recibe el numero de elementos a recib ir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); if(num_elem>0) MPI_Recv(&tam, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); if(tam>0) { buffer=(char *)malloc(tam); if(buffer!=NULL) { //Se recibe el paquete. MPI_Recv(buffer, tam, MPI_PACKED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //Se comienza a desempaquetar for(i=0; i<num_elem; i++) { // Se recibe la longitud del campo siguiente MPI_Unpack(buffer, tam, &posicion, &longitud, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, nombre, longitud, MPI_CHAR, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &matricula, 1, MPI_UNSIGNED, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &edad, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &sexo, 1, MPI_CHAR, MPI_COMM_WORLD); // Reconstrucción del TDA ap=crea_nodo(matricula, nombre, edad, s exo); lista=inserta_nodo(lista, ap); } free(buffer); } else perror("Memoria Insuficiente..."); } return lista; } En estas versiones se soluciona el problema del envío de múltiples mensajes, pero no se tiene ganancia alguna en cuanto a la generalidad de las funciones, se sigue teniendo el problema al manejar los campos de los nodos del TDA lista. Además se presenta otro problema, que es el manejo de la memoria para la versión 2.2, se puede presentar el caso que la memoria no sea asignada al ser de un gran tamaño. 46 3.2.4. Envío / recepción de un nodo usando uniones (Versión 3.1 “uniones.c”) Hasta el momento en las versiones anteriores se tiene el problema que para migrar un nodo de un TDA lista se debe conocer los campos del nodo, lo cual es un problema, ya que lo que se pretende que la migración sea independiente de dichos campos. [AJ1994] En C existe un tipo de estructuras especiales para guardar diferentes tipos de datos en un momento dado, estas estructuras son las uniones, las cuales permiten solapar sobre una misma dirección de memoria distintos tipos de datos, ya sea de datos primitivos de C o de datos derivados. Supongamos que definimos una unión de la siguiente forma: union prueba { char letra; int x; double y; }var; En el ejemplo anterior se tienen 3 tipos de datos, los cuales tienen la misma dirección de memoria, pero cada uno ocupa un espacio diferente en la misma; la variable de tipo char ocupa 1 byte , la variable tipo int ocupa 2 bytes y la variable de tipo double ocupa 4 bytes (puede variar dependiendo del compilador). Es así como el espacio que ocupe en memoria la variable “var” de tipo unión declarada, será del tamaño del dato más grande, en este ejemplo será de 4 bytes. Esta unión permite guardar tres tipos de datos diferentes en una sola dirección de memoria. Esta característica se usa en MPI para evitar el empaquetamiento y desempaquetamiento de datos, los que se hace es crear una unión con dos campos, uno de ellos es el tipo de dato que se desea enviar, en este caso el nodo de un TDA lista, el otro campo es un arreglo de tipo char con la longitud equivalente al tamaño del nodo del TDA lista. En una versión anterior se calculó el tamaño de los datos a enviar campo por campo, en esta ocasión si necesitamos el tamaño de la estructura, ya que realmente lo que haremos es enviar la estructura tal cual y no campo por campo. La forma de hacer uso de esta unión es muy sencilla, lo que hay que hacer es copiar el nodo que se desea migrar a la unión, enseguida se manda como un mensaje de longitud igual al tamaño del nodo del TDA lista, y se recibe de la misma forma, una vez recibido se puede hacer uso normalmente de la estructura para manejar el nodo del TDA lista. La unión utilizada se presenta a continuación, así mismo se presenta la estructura de datos declara para el desarrollo de esta parte del proyecto, dicha estructura se encuentra declarada en el archivo “protos.h”. typedef union Nodos_TDA e_TDA; union Nodos_TDA { nodo ap; char mensaje[STRUCTSIZE]; }; typedef struct Registro nodo; struct Registro 47 { char nombre[40]; unsigned matricula; int edad; char sexo; nodo *sig; }; Enseguida se muestra el código para la función de envío del nodo del TDA lista haciendo uso de las uniones de C. /***** En esta v ersión se realiza el envío mediante el uso de uniones ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0; int longitud=0; e_TDA elem; nodo *ap=NULL; //Primero se manda el número de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i++) { ap=lis ta; strcpy(elem.ap.nombre, ap->nombre); elem.ap.matricula=ap->matricula; elem.ap.edad=ap->edad; elem.ap.sexo=ap->sexo; MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD); lista=lista->sig; free(ap); } return lis ta; } El siguiente código corresponde a la función de recepción del nodo TDA lista. /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0; e_TDA elem; nodo *ap=NULL; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); ap=crea_nodo(elem.ap.matricula, elem.ap.nombre, elem.ap.edad, elem.ap.sexo); lista=inserta_nodo(lista, ap); } return lista; 48 } Cabe realizar unas observaciones para esta versión. 1. Es claro que el código es mucho más compacto que en las versiones anteriores. 2. Hay que mandar un mensaje por cada nodo a enviar. 3. Solamente se logra eliminar el manejo de los capos en el código de recepción del nodo del TDA lista. El problema persiste en la función de envío. [MJ1995] Por ultimo es importante que en la unión se declare una variable del tipo que se desea migrar y no un apuntador a una variable de dicho tipo. ¡Forma incorrecta! union Nodos_TDA { nodo *ap; char mensaje[STRUCTSIZE]; }mensg; Esta forma se ve muy conveniente, ya que para copiar el nodo que se va a migrar, solo habría que igualar sus apuntadores de la siguiente forma, por ejemplo: mensg=lista; lista=lista->sig; De esta forma se evitaría la copia de campo por campo, pero esto es un error fatal, para que podamos realizar el envío de un nodo del TDA lista, utilizando uniones, debemos de garantizar que la información del nodo se encuentre en el lugar que ocupa la unión en memoria, para poder solaparla con el arreglo de tipo char. Lo cual no se logra igualando apuntadores, no de esta forma, lo que logramos hacer con esto es hacer que el apuntador de la unión apunte al apuntador del nodo del TDA lista, pero eso no quiere decir que la información ya se encuentra donde queremos. 3.2.5. Envío / recepción de un nodo usando uniones (Versión 3.2 “uniones_b.c”) El problema de la versión anterior es que en la función de envío aún se necesitaba manejar los campos de cada uno de los nodos del TDA lista, la forma en que se soluciona este problema es usando apuntadores, pero no como se indico anteriormente. Sino de la siguiente forma: Primero declaramos la unión de la forma correcta, que es como sigue: union Nodos_TDA { nodo ap; char mensaje[STRUCTSIZE]; }mensg; [AJ1994] Hay que hacer la observación que dentro de la estructura se tiene declarada una variable del tipo que se desea migrar y no un apuntador, pues como ya mencionamos necesitamos garantizar que la información del nodo a migrar se encuentre en el lugar que ocupa la unión en memoria. Como segundo paso realizaremos la copia, mediante los apuntadores de la siguiente forma: ap=lista; elem.ap=*ap; Las variables ap y lista son nodos del TDA lista, la variable elem es la variable de tipo unión, de tal forma que elem.ap es del tipo nodo del TDA lista. Lo que se hace con la segunda instrucción es la copia del nodo hacia la unión, es decir, copia lo que se encuentra en la dirección *ap en elem.ap. 49 Con esto se soluciona por completo el problema del manejo de los campos de los nodos. Por lo que en esta versión se tienen las funciones de envío y recepción como sigue: nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0; nodo *ap=NULL; e_TDA elem; //Primero se manda el número de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i+ +) { ap=lista; //Se realiza una copia del nodo en el elemento de la unión para poder ser transferido. elem.ap=*ap; //Es importante inicializar el campo a nulo elem.ap.sig=NULL; MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, destino, etiqueta, MPI_ COMM_WORLD); // Se elimina el nodo de la lista lista=lista->sig; free(ap); } return lista; } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0; e_TDA elem; nodo *ap=NULL; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); *ap=elem.ap; lista=inserta_nodo(lista, ap); } else perror("Memoria Insuficiente..."); } return lista; } Lo único que hay que considerar en esta versión es cuando se vallan a migrar los nodos del TDA lista se deberá asignar el valor NULL al apuntador al siguiente elemento, de la misma forma se 50 deberá hacer en una lista doblemente ligada. La razón es que en la practica después de migrar algunos nodos del TDA lista el proceso receptor se confunde al recibir valores que no tiene sentido para él. 51 4. RECONOCIMIENTO AUTOMÁTICO DEL TDA LISTA BALANCEABLE 52 4. RECONOCIMIENTO DEL TDA LISTA BALANCEABLE En esta sección se pretende crear un programa capaz de reconocer una estructura dentro de un archivo, y guardar dicha estructura en un archivo con extensión .h. Para el desarrollo de este programa hay que tener en consideración lo siguiente: • El parámetro que se le pasará al programa. El usuario puede pasar el nombre de la estructura, o una redefinición de la misma. • No se conoce el formato (aspecto) que tiene definido la estructura, por lo que hay que garantizar que la búsqueda no dependa del formato que tiene al ser declarada en el archivo. • Una vez que la estructura se ha encontrado, es posible que dicha estructura contenga algunos otros tipos de datos derivados, es decir, otras estructuras. Por tal motivo se debe de realizar una verificación de cada uno de los campos de la estructura, de no reconocer algún campo como básico, deberá ser buscado recursivamente. El modelo conceptual del programa esta representado por la siguiente figura: 53 4.1 Algoritmo Establece_flujo(archivo, TDA) com flujo=abre_archivo(archivo) Cola_estrutras=Busca_Estructura(flujo, TDA) Crea def_TDA(Cola_Estructuras) term. Busca_Estructuras(flujo, TDA): Cola_estructuras com Mientras(salida=Falso) com bloque=Obten_bloque(flujo, TDA) sig_paso=Verifica_bloque(bloque) Caso sig_paso: com Encontrada: Cola_estruturas=Registra_TDA(bloque, Cola_estructuras) salida=Termina. Redefinición_busca: Cola_estruturas=Registra_TDA(bloque, Cola_estructuras) salida=Sigue_buscando Redefinición_busca_original: Cola_estruturas=Registra_TDA(bloque, Cola_estructuras) TDA=nueva_TDA salida=Sigue_buscando term term Regresa Cola_estructuras term Registra_TDA(bloque, Cola_estructuras): Cola_estructuras com Haz campo=Verifica_Campos(bloque) Si(campo=No_Reconocido) entonces Cola_Estructuras=Busca_Estructuras(flujo, campo) Mientras(campo=No_Reconocido) Regresa Cola_estructuras term 54 4.2. Implementación 4.2.1. Obtención de los bloques El programa al comenzar a buscar la estructura, lo que hace es localizar bloques que pudiesen tratarse de la definición de la estructura buscada. Primero se establecen tres criterios para descartar un bloque. El primer criterio es la identificación de un carácter “#” el cual es utilizado en el lenguaje C para la identificación de las directivas de procesamiento; el segundo criterio es la existencia de los prototipos de función que están caracterizados por seguir el patrón <nombre_funcion>([argumentos]); si dicho patrón es identificado también es descartado; También se considera si se trata del cuerpo de una función, ya que generalmente una estructura no es declarada para una sola función. Las funciones tienen el siguiente patrón: [Tipo del valor de retorno] <nombre_funcion>([argumentos]){[cuerpo función]} Además se utiliza el patrón que tiene la declaración de una estructura que es como sigue: struct <nombre_estructura>{<campo;|campos>}[declaración_variable]; Donde: campos=<campo; [campos]> campo=<tipo_dato> < variable> La función “obten_bloque”, es la encargada de identificar estos patrones, ignora espacios en blanco, tabuladores y saltos de línea innecesarios y regresa el bloque que posiblemente sea el TDA buscado, el cual es una cadena de texto. 4.2.2. Verificación de los bloques El siguiente paso es verificar si realmente se trata de la estructura buscada. Supongamos que tenemos la siguiente estructura con la siguiente redefinición. typedef struct registro nodo: struct registro { int edad; char nombre[]; char sexo; double saldo; nodo *sig; }; En este caso se pueden llegar a presentar las siguientes tres situaciones: • Que el argumento pasado al programa sea el nombre de la estructura a buscar en este caso seria registro. Si no existe la redefinición, se ha encontrado la estructura (RE_END Registra Estructura y Termina). • Que el argumento pasado al programa sea el nombre de la estructura como en el caso anterior, pero en esta ocasión se encuentre primero la redefinición. Si este el caso, se debe registrar la línea y seguir buscando (RL_SBE Registra Línea y Sigue Buscando Estructura.). • Que el argumento pasado al programa sea el de la redefinición de la estructura, es decir, nodo. En tal caso se tendría que registrar la línea, obtener el nombre real de la estructura y buscar la nueva estructura (RL_BNE Registra Línea y Busca Nueva Estructura.). 55 4.2.3. Registro de la Estructura El registro de las redefiniciones y las estructuras se realiza en un TDA cola, con el objetivo de llevar un registro de los tokens buscados y la estructura guardada con la búsqueda de dicho término y evitar de esta forma la búsqueda de términos que ya han sido localizados, esto cuando se estén verificado los campos de la estructura encontrada. Después de encontrar la estructura, el paso a realizar es verificar cada uno de los campos, ya que la estructura puede contener tipos de datos derivados, que posteriormente sean necesarios. Si un campo no es reconocido, se llama a la función de búsqueda de estructura, con el nuevo dato a buscar. La función regresa la cola que contiene las definiciones de los tipos de datos buscados una vez que se ha encontrado la estructura y que sus campos han sido verificados. 4.2.4. Creación del archivo de Salida Finalmente la cola obtenida es registrada en el archivo “def_TDA.h”, para su posterior uso. 56 5. Migración de datos compartidos por hilos 57 5. MIGRACIÓN DE DATOS COMPARTIDOS POR HILOS [S2001] El uso de hilos de ejecución involucra necesariamente la concurrencia entre ellos, es decir, que ambos existan realmente como programas que compiten por recursos de procesador, por ende cuando se hace uso de hilos de ejecución, se pueden presentar dos situaciones, la primera es que los hilos se ejecuten de manera independiente, la otra situación es que dichos hilos estén relacionados, esto implica que en ambos se haga uso de los mismos datos, ya sea leyendo y/o sobrescribiendo éstos. El principal problema se presenta cuando se hace uso de hilos de ejecución y estos interactúan entre ellos, ya que en dos procesos corriendo concurrentemente, se puede presentar que éstos requieran acceder al mismo tiempo la misma variable o el mismo recurso, lo cual traería consigo inconsistencia en los datos. Por ejemplo, imaginemos que los procesos son de reservación de boletos para aerolíneas. Sin un adecuado control, dos procesos pudieran requerir el leer y modificar la variable BoletosDisponibles al mismo tiempo. El proceso A leería la variable, y antes de poder modificarla, B también la leería. A guardaría entonces BoletosDisponibles-1, y B guardaría ese mismo valor. De esta manera, parecería que se hubiera vendido un solo boleto en lugar de dos. De esta forma el decrementar la variable, en este caso se considera una sección crítica, dentro del código del programa. Por tal motivo hay que asegurar a los hilos de ejecución la exclusión mutua cuando se encuentre dentro de una sección crítica. Es decir, hay que asegurar que cuando un hilo entre a su sección crítica ningún otro hilo pueda acceder a su propia sección crítica, hasta que el proceso que estaba en su sección crítica la abandone. El uso de la exclusión mutua como consecuencia que los hilos que entren en su sección critica, mantengan bloqueados al resto de los hilos que quieran acceder a su sección. Así, no es práctico incluir a todo un hilo en una sección crítica, ya que los demás hilos permanecerían bloqueados y el multihebrado perdería su concurrencia. Es importante el incluir sólo lo indispensable en la sección crítica, el resto del hilo deberá permanecer fuera de la sección. En el caso de este proyecto la sección crítica se presenta durante el manejo de los datos y el momento de migrar la información. Los datos no deberán poder ser migrados a otro nodo del cluster si en el momento se están realizando operaciones sobre ellos y no se podrán agregar elementos nuevos mientras se estén realizando operaciones sobre los mismos. Por lo que las rutinas encargadas de realizar la migración (envió y recepción) deberán ser tomadas como una sección crítica, en cuanto al código que pretenda hacer uso de las rutinas de balance, la sección crítica estará determinada por las operaciones que sobre la lista se efectúen, es importante hacer notar que la lectura en los elementos de la lista también debe ser considerada una sección crítica ya que la lectura se puede ver afectada por el proceso de balanceo, ya sea por la inserción de elementos, lo cual no es tan grabe, como la eliminación de nodos del TDA lista, lo cual involucra el libramiento de memoria que conlleva a una perdida en los apuntadores, que acarrearía una terminación anormal del proceso. 5.1. Uso de Hilos en MPI En esta parte del proyecto se pretende ver el funcionamiento de los hilos en MPI. El objetivo es crear dos hebras de un programa, mientras el núcleo del programa realiza sus tareas correspondientes sobre los datos, una de las hebras se encargará de migrara datos, la otra se encargará de verificar si se han recibido datos de otro nodo. 58 Se deben de tener en consideración dos situaciones: la primera es que se deben de garantizar la integridad de los datos, es decir se debe de evitar que un dato sea escrito mientras es leído; la otra situación es que los hilos deben de hacer el manejo de los datos de manera independiente al núcleo, se deberán crear “procedimientos” y no funciones, para que en el manejo de los datos no se tenga que terminar el hilo para que los datos sean modificados, es decir, se deberán pasar por referencia. En esta primera fase sobre el manejo de hilos se trata con un “problema de juguete” para ver la forma de proceder para el uso con el TDA lista. 5.1.1. hilos_1.c Para el desarrollo del proyecto en esta fase se plantea el siguiente problema: se tiene dos procesos corriendo uno de los procesos (Coordinador –proceso 0- ) inicializa un arreglo con números al azar, creara dos hilos, uno para la migración del arreglo y el otro que realizara la impresión de los datos en pantalla, el proceso receptor sólo esperará por lo elementos que le sean enviados. El objetivo de esta fase es ver el desempeño de los hilos en procesos que corren en paralelo así como garantizar la integridad de los datos. Las tareas del proceso cero se muestran en el siguiente código. inicializa(arreglo); fprintf(stdout,"Proceso %d, desde %s\n", id, nombre); //Creación del hilo monitor encargado de migrar elementos del arreglo pthread_create(&hilo1, NULL, (void *)&monitor, (void *)1); //Creación del hilo que realiza el desplegado en pantalla pthread_create(&hilo2, NULL, (void *)&proceso, (void *)2); //Espera por la terminación de los hilos. pthread_join(hilo1, (void *)&vr); pthread_join(hilo2, (void *)&vr); printf("Los hilos del proceso %s han terminado...\n", id); Una vez creados lo hilos el proceso debe esperan la terminación de los mismos, de lo contrario no se observará resultado alguno del lado de este proceso. La tarea del monitor es realizar la petición del número de elementos que serán enviados al proceso 1. void monitor(void *datos) { int hilo=(int)datos; int num_nodos=0, i=0, etiqueta=0; if(id==0) { sleep(5); //Lapso de espera antes de comenzar el envío pthread_mutex_lock(&candado); //Cierra el candado para garantizar la integridad de los datos. fprintf(stdout,"Hola soy el hilo %d\n",hilo); fprintf(stdout,"del proceso %d\n",id); fprintf(stdout,"Desde el procesador %s\n",nombre); //Este ciclo valida que el usuario no exceda el límite del arreglo. do { printf("Dime cuantos nodos quieres migrar: "); scanf("%d", &num_nodos); }while(num_nodos<0 || num_nodos>20); printf("\n los elementos a migrar son %d\n",num_nodos); //Manda el mensaje con el número de elementos que enviará al proceso 1 MPI_Send(&num_nodos, 1, MPI_INT, 1, etiqueta, MPI_COMM_WORLD); //Envía cada uno de los elementos(envía los últimos). for(i=20-num_nodos; i< 20 ; i++) 59 { MPI_Send(&arreglo[i], 1, MPI_INT, 1, etiqueta, MPI_COMM_WORLD); arreglo[i]=-1; //Borra el elemento del arreglo. } pthread_mutex_unlock(&candado); //Abre el candado. } pthread_exit((void *)hilo); } El hilo espera un momento antes de comenzar el envío, cuando pretenda iniciar la transferencia se tendrá que garantizar la integridad de los datos, es por lo que se hace uso de los candados, los cuales no permiten que otro hilo y/o el proceso mismo tenga acceso a los datos una vez cerrado el candado, [S2001] el candado es una variable de tipo pthread_mutex_t, debe ser global, para su uso la instrucción que establece la sección de exclusión es pthread_mutex_lock, y recibe como parámetro la dirección de la variable usada para la exclusión. Una vez enviado el dato, debe ser eliminado, y una vez terminado el envío se debe liberar la sección. La función que libera la sección de la exclusión es pthread_mutex_unlock, el parámetro que recibe la misma es la dirección de la variable usada para la exclusión. También el proceso que haga uso de los datos debe contar con dichas instrucciones para que de la misma forma no sean enviados y eliminados mientras se están usando, ya que generaría valores incorrectos en la lectura y/o operación de los mismos. En este programa la operación de los datos es la impresión, en el listado de la función encargada del manejo de los datos de observa dicho procedimiento. void proceso(void *numero) { int i=0, count=0; int hilo=(int)numero; printf("\n Soy el hilo %d del procesador %s\n", hilo, nombre); while(count<5) { pthread_mutex_lock(&candado); //Se cierra el candado para garantizar la integridad. for(i=0; (arreglo[i]!=-1 && i<20); i++) printf("%3d",arreglo[i]); printf("\n"); pthread_mutex_unlock(&candado); //Se abre el candado sleep(2); count ++; } pthread_exit((void *)hilo); } Es importante que la exclusión comience antes de la petición de los elementos a migrar, ya que posiblemente el proceso que haga uso de ello posiblemente se encuentre agregando o incluso eliminado datos, lo que acarrearía un problema si se quisieran enviar menos elementos de los que se tienen. Claro que en este programa no se da el caso como antes de ser enviados todos los datos se le manda un mensaje al proceso receptor indicándole la cantidad de elementos que se le enviran, el proceso receptor podría recibir basura o en el peor de los casos quedarse esperando por un elemento que no llegará. En esta fase el proceso receptor no tiene mayor tarea que la de recibir los datos del proceso enviador en un arreglo que se encuentra inicialmente “vacío”, por lo que no requiere mayor explicación. fprintf(stdout,"Proceso %d, desde %s\n", id, nombre); //Iniciación del arreglo for(i=0;i<20; i++) array[i]=-1; 60 //Recibe el mensaje con la cantidad de elementos que recibirá. MPI_Recv(&num_dat, 1, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //ciclo para la Recepción de los elementos del arreglo. for(i=0; i<num_dat; i++) MPI_Recv(&array[i], 1, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //Desplegado del arreglo en este proceso. for(i=0; (array[i]!=-1 && i<20); i++) printf("%3d",array[i]); El listado de este programa se encuentra en el Anexo C, el programa es el hilo_1.c 5.1.2. hilos_2.c Hasta ahora se ha visto la forma de hacer uso de los hilos en MPI, garantizando la exclusión mutua. Pero el programa anterior cuenta con muchas restricciones, una de ellas es que sólo hay comunicación entre dos procesos, si se corren más éstos no hacen nada. También se tiene la situación de espera de un mensaje. Uno de los objetivos del proyecto es crear una rutina que reciba una señal de otro proceso o un hilo del mismo programa, el cual se esta ejecutando en el mismo nodo de la máquina y su trabajo consiste en censar la carga del procesador y si este determina que el nodo del cluster esta sobrecargado, manda la señal para que este mande sus datos a otro nodo. Lo anterior implica que todos los procesos son susceptibles de enviar o recibir información, es así como todos los procesos deberían crear los hilos encargados del envío y la recepción de datos. La siguiente parte de código del listado “hilos_2.c”, realiza dicha tarea, con el problema del ejemplo anterior. Ahora se pretende hacer desarrollar el mismo problema pero de una forma más general. //Se inicializan los arreglos que posteriormente serán enviados inicializa(arreglo); //Se crean los hilos encargados del balance de carga. pthread_create(&envio, NULL, (void *)&Envia, (void *)1); pthread_create(&recepcion, NULL, (void *)&Recibe, (void *)salir); //Función que opera con los datos. proceso(); //Crea la señal para la terminación de los hilos MPI_Term_hilo(signal); //Se espera la terminación de los hilos para el balance de carga. //Y se guarda el valor de retorno. pthread_join(envio,(void *)&vr_env); pthread_join(recepcion, (void *)&vr_rec); fprintf(stdout, "Proceso %d, desde %s ---->", id, nombre); if(vr_env && vr_rec) fprintf(stdout, " El balance ha terminado sin problemas.\n"); Esta parte del problema no requiere mayor explicación. Lo interesante se presenta en las funciones de envío y recepción de datos. Hay que observar que los hilos que estén encargados del balance, no es posible determinar durante cuanto tiempo se necesite estén corriendo, quien determinará que el balance se ha terminado es el proceso principal, y el momento para finalizar el balance es la terminación de los cálculos. Así que los hilos tendrán que esperar por la señal que les dé la hebra principal. En el caso del envío no se sabe en que momento se deberán enviar datos, inclusive cuantos datos serán enviados, en este programa se hace uso de una espera de 0 a 10 segundo al azar para enviar de igual forma un numero de elementos a un destinatario en las mismas condiciones. Lo anterior con el fin de realizar el desarrollo de las rutinas de balance en condiciones extremas y las condiciones extremas en este caso es que la migración de datos se de cada cierto tiempo. A continuación se presenta 61 el cuerpo de la función void Envia_TDA(void *num) que sería el hilo encargado de enviar elementos del arreglo a otro nodo del cluster. /***** Función encargada del envío de elementos del arreglo, se hace al azar *****/ void Envia(void *num) { int destino=0, num_elem=0; int vr=(int)num; int tam=0, i=0; time_t semilla; //Iniciación de la semilla. srand((unsigned) semilla +id); while(MPI_Signal_term_hilo) { sleep(rand()%10); //Espera un tiempo al azar antes de realizar un envío valores_azar(proc,20,&destino,&num_elem); if(destino!=id && num_elem) { fprintf(stdout,"\n proceso %d -----> Hilo envía %d datos a %d...\n",id, num_elem, destino); //se cierra el candado para garantizar la integridad de los datos pthread_mutex_lock(&candado); //Se manda el mensaje que indica cuantos elementos se van a migrar. MPI_Send(&num_elem, 1, MPI_INT, destino, CANTIDAD, MPI_COMM_WORLD); //Ciclo encargado del envío de los elementos. //Se obtiene el tamaño actual del arreglo para enviar los últimos elementos. tam=calcula_tam(arreglo); for(i=tam-num_elem; i<tam; i++) { MPI_Send(&arreglo[i], 1, MPI_INT, destino, ELEMENTO, MPI_COMM_WORLD); arreglo[i]=-1; } //Se abre el candado para que el proceso que los opera pueda hacer uso de estos. pthread_mutex_unlock(&candado); } } pthread_exit((void *)&vr); } En este caso se ha decidido enviar los últimos elementos del arreglo lo cual es lo más conveniente para evitar que se estén realizando corrimientos. En la parte del envío de datos no se presenta mayor problema que garantizar la integridad de los datos, ya que si no se recibe la señal de enviar, no se realizará envío alguno, por lo que no hay la posibilidad de bloqueo por este lado. En el caso de la recepción de un dato o mejor dicho de un mensaje indicando cuantos elementos le serán enviados, la situación es muy diferente. En realidad el hilo encargado de la recepción de datos no sabe si le serán enviados datos, por lo que esperar por un mensaje que posiblemente no llegue provocaría un posible bloqueo de la aplicación paralela. La forma de evitar el bloqueo es hacer uso de las rutinas de MPI para las comunicaciones sin bloqueo. Es así como la forma de proceder del hilo receptor es verificar si se ha recibido un mensaje sin realmente recibirlo, con ciertas características, como lo son la fuente y la etiqueta del mensaje. Pero como no se sabe con certeza de quien esperar mensajes, pues sólo hay que tomar en cuenta la etiqueta del mensaje, por lo que se establecieron dos etiquetas para el envío y recepción de datos. Una vez comprobada la recepción del mensaje, es cuando se pasa realmente a recibirlo, en este mensaje se indicara cuantos elementos le serán enviados entonces se tendrá la certeza de cuantos elementos se recibirán y de quien. 62 Es importante considerar que se debe de tener espacio para recibir los elementos que le sean enviados y en caso de no tenerlo evitar que se bloque el proceso por no tener el mismo, aunque esto signifique perdida de datos. Como resultado se tiene la siguiente función de recepción de datos. void Recibe(void *salida) { int *termina=(int *)salida; int bandera=0, fuente=0, elem=0, i=0, basura=0, vr=0, tam=0; int count =0; MPI_Status estado; while(MPI_Signal_term_hilo) { //Se realiza una prueba, para ver si se han recibido mensajes MPI_Iprobe(MPI_ANY_SOURCE, CANTIDAD, MPI_COMM_WORLD, &bandera, &estado); if(bandera)//Verificación de recepción de mensajes. { //Obtención de la fuente que envío el mensaje. fuente=estado.MPI_SOURCE; //Recepción de la cantidad de elementos a recibir. MPI_Recv(&elem, 1, MPI_INT, fuente, CANTIDAD, MPI_COMM_WORLD, &estado); //Se cierra el candado para garantizar la integridad. pthread_mutex_lock(&candado); tam=calcula_tam(arreglo); //Obtención del numero de elementos en el arreglo. fprintf(stdout, "\n El proceso %d recibe señal de %d ---> datos a recibir %d\n",id, fuente, elem); for(i=0; i<elem; i++,tam++) { //Recepción de los elementos enviados. if(tam<100) MPI_Recv(&arreglo[tam], 1, MPI_INT, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); //Esto garantiza que no se sobrepase del tamaño del arreglo, aunque provoca perdida de datos. else MPI_Recv(&basura, 1, MPI_INT, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); } //Se abre el candado nuevamente. pthread_mutex_unlock(&candado); } } pthread_exit((void *)&vr); } Se establecieron dos etiquetas, CANTIDAD la cual indica que el mensaje recibido nos dice la cantidad de elementos que debemos esperar y ELEMENTO el cual indica que es un dato. En este programa el proceso realiza la impresión del arreglo cada cierto tiempo en archivo llamado como el nodo del cluster donde esta activo este proceso y el número de proceso que corresponde. El listado de este programa se encuentra en el Anexo C. 5.2. Sincronización para la lista balanceable Hasta ahora se ha logrado que dos hilos de un proceso trabajen sin problemas, ya que no requieren mayor sincronización que la que proporciona la exclusión mutua, para el manejo de los datos. En la recepción de datos, la rutina de recepción realiza una verificación para ver si han llegado 63 mensajes, si se han recibido pues comienza su trabajo, pero, ¿Qué pasa con la rutina de envió?, pues hasta el momento ella misma decide cuando y cuanto enviar, cuando en realidad este debe de estar en espera de la señal que le indique comenzar a trabajar. Así esta debe de responder a otra rutina que le indique empezar a trabajar. Se contempla que la rutina encargada de dar esta señal es otro el cual se encarga de monitorear la carga del procesador y si se encuentra muy cargado comenzar a migrara la información. Pues bien, para llevar a cabo la sincronización entre el hilo que monitorea la carga del procesador y el hilo encargado del envió de los datos se deberá hacer uso de los semáforos. [S2001] Los semáforos son un mecanismo de sincronización entre procesos e hilos de ejecución. Los semáforos son variables enteras que no aceptan valores negativos. Si el semáforo tiene una valor de cero indica que no se puede continuar, es decir esta en rojo, si el valor es mayor que cero indica que se puede seguir, es decir, esta en verde. Para que los hilos hagan uso de semáforos para poder sincronizarse, existen dos operaciones básicas en los semáforos, las cuales son la espera por el verde (sem_wait) y da el verde (sem_post). Cuando un hilo da luz verde al semáforo, es decir, realizar sem_post a una variable de tipo semáforo, lo que se hace es incrementar en uno a la variable, de tal forma que si algún otro hilo esperaba por el mismo semáforo, es decir, esta bloqueado en una llamada a sem_wait, éste podrá continuar su ejecución ya que el semáforo es mayor que cero, pero una vez que se deja de esperar por la variable semáforo, la llamada a sem_wait lo que hará es decrementar el valor en uno de dicho semáforo. Un valor de uno dejara continuar a un proceso que espera por el semáforo, este lo decrementará y si en el momento otro hilo espera por el mismo semáforo éste no podrá continuar, ya que el semáforo sólo le dio luz verde a uno. Es así como el valor que tenga la variable de tipo semáforo le dará luz verde a la cantidad de esperas por el verde que indica el valor de la variable. Es así como la rutina encargada del envió de datos deberá realizar la espera por la señal que le indique que comience a hacer su trabajo, esta señal es la luz verde de un semáforo. El hilo encargado del monitoreo deberá ser el encargado de dar dicha señal, por tanto en el monitor se deberá hacer una llamada sem_post para dar luz verde y comenzar el envió de datos. 5.2.1. hilos_3.c En dos casos anteriores se manejaba un arreglo global, lo cual simplificaba en gran medida las cosas ya que los hilos deben tener acceso a éste, en el caso anterior además cada uno de los hilos encargados del balance hace uso de algunos parámetros como lo son, la señal de terminación, y en el caso del envío, el destino y el número de elementos para realizar un envío. En el programa anterior, algunos de estos parámetros son declarados dentro de la función misma, lo cual no es lo que se busca, se busca que la función de envío responda a los parámetros que le sea pasados de algún otro hilo que se encuentre permanentemente censando la carga del nodo del cluster. Para que los parámetros que necesitan los hilos sean pasazos sin que el usuario tenga que estar declarando cada uno de ellos y además no tengan que ser globales, ya que una buena practica de programación es hacer el menor uso de variables globales, se crea una estructura que contenga cada uno de los campos, para que el usuario al hacer usos de las rutinas de balance sólo tenga que declarar una variable de este tipo y pasar por referencia dicha variable a las rutinas de balanceo. La estructura creada para dicho propósito es como sigue: typedef struct Parametros_Balance { nodo **TDA; int MPI_DEST; int MPI_ELEM; 64 int MPI_SIGNAL_TERM; }MPI_Balance; Como tanto la rutina de envío como la de recepción hacen uso de la lista creada dinámicamente, es necesario pasarle la lista, por lo que el campo nodo **TDA es utilizado para hacer referencia a la dirección del apuntador a la cabeza de la lista y de esta forma pueda ser modificada la lista. [AJ1994] Por ejemplo, la siguiente declaración int *ptr; Declara un apuntador, llamado ptr, que puede apuntar a una variable de tipo int. Posteriormente se tendrá que inicializar ese apuntador haciendo uso del carácter de in dirección & para hacer que el apuntador apunte a una variable específica del tipo correspondiente, Suponiendo que la variable x ha sido declarada como variable de tipo int, el enunciado de inicialización sería como sigue: ptr = &x; Es así como se le asigna la dirección de x a ptr y hace que ptr apunte a x. Una vez hecho esto se tiene dos formas de modificar el valor de la variable x=12; *ptr=12; Ambos enunciados asignaran el valor de 12 a la variable, Usando el carácter de in dirección en el apuntador se logra el mismo efecto sobre la variable, de esta forma se pasan los argumentos a las funciones para que estas puedan modificar el parámetro que les es pasado. Pero, en el caso de las listas ligadas dinámicamente lo que se declara inicialmente es un apuntador, que no apunta a la dirección de memoria de una variable ya que no es declarada ninguna instancia de nuestro tipo de dato. El apuntador es inicializado con la memoria asignada dinámicamente. Ahora como el apuntador en sí mismo es una variable numérica, es guardada en una dirección en particular de la memoria. Por lo tanto, se puede crear un apuntador a un apuntador, la cual es una variable cuyo valor es la dirección de un apuntador. La manera de hacer esto es: int x=12; int *ptr=&x; int **ptr_a_ptr=&ptr ; Para el caso de las listas ligadas dinámicamente lo que se necesita es hacer uso de un doble apuntador y de esta forma poder modificar la lista. La figura “Ilustración de un apuntador a un apuntador” de la pagina anterior ilustra de mejor manera la forma de operar de un doble apuntador. Así para que le sea pasada la lista ligada dinámicamente a los hilos encargados del balaceo y que estos puedan modificarla se tendrá que iniciar este doble apuntador a la dirección de la cabeza de la lista de la siguiente forma: nodo *lista: MPI_Balance elem; 65 elem.TDA=&lista; El resto de los campos de la estructura MPI_Balance no requiere mayor explicación ya que cuentan con un nombre muy descriptivo. El envío de los nodos del TDA lista se hará por medio de una estructura auxiliar (unión) como se realizo en la versión 3.2 (uniones_b.c) de la sección “Envío y Recepción de un nodo de un TDA lista” la estructura es como sigue: typedef union Nodos_TDA e_TDA; union Nodos_TDA { nodo ap; char mensaje[STRUCTSIZE]; }; Ya se observo la forma de hacer uso de la misma en la sección antes mencionada. Ahora no se pretende que el usuario de las rutinas tenga que estar realizando la creación de los hilos encargados del balance y ni que tenga que estar codificando las instrucciones para la finalización de las mismas, el objetivo principal es que el usuario realice una llamada a las rutinas de balance de carga y una llamada a la finalización de las mismas. Para eso se crean las rutinas MPI_Init_balance y MPI_Finaliza_balance las cuales serán las encargadas de iniciar la estructura con los parámetros necesarios para los hilos, crear los hilos y esperar por la terminación de los mismos respectivamente de tal forma que el usuario de las rutinas las invoque como si realizará una invocación a la función printf por poner un ejemplo. Las rutinas de iniciación y de finalización son las siguientes: void MPI_Init_balance(MPI_Balance *par_bal, nodo **lista) { //Se inicia el semáforo el cual indicará el momento para comenzar el balance sem_init(&signal,0,0); //Iniciación de la estructura //Se indica la dirección de los datos que podrían ser enviados a otro proceso par_bal->TDA=lista; //Se inicia el parámetro destino, con un valor igual al proceso par_bal->MPI_DEST=id; //Se inicia el número de elementos a enviar par_bal->MPI_ELEM=0; //Se inicia la señal que indica a los hilos que terminen par_bal->MPI_SIGNAL_TERM=0; //Iniciación de los hilos para el balance pthread_create(&envio, NULL, (void *)&Envia_TDA, (void *)par_bal); pthread_create(&recepcion, NULL, (void *)&Recibe_TDA, (void *)par_bal); //Esta Función no va a ser necesaria para el uso en general //Este hilo es el encargado de determinar a quien, cuantos y en que momento le serán enviados datos pthread_create(&monitor, NULL, (void *)&Monitor,(void *)par_bal); } void MPI_Finaliza_balance(MPI_Balance *par_bal) { int vr_env=0, vr_rec=0, vr_mon=0; //Se manda la señal a los hilos para que terminen su trabajo par_bal->MPI_SIGNAL_TERM=1; //Se espera la terminación de los hilos que realizan el balance de carga. 66 //Y se guarda el valor de retorno. pthread_join(monitor,(void *)&vr_mon); pthread_join(envio,(void *)&vr_env); pthread_join(recepcion, (void *)&vr_rec); if(vr_env || vr_rec) fprintf(stdout, "Proceso %d, desde %s ----> si hubo balance \n", id, nombre); else fprintf(stdout, "Proceso %d, desde %s ----> no hubo balance \n", id, nombre); } Esta rutinas no requieren mayor explicación más que la línea 3 del la rutina de iniciación del balance, Este enunciado es la inicialización de un variable de tipo sem_t, la cual es un semáforo usada para la coordinación de la rutina de envío de datos (Envia_TDA) y la rutina encargada del monitoreo de la carga del procesador. El monitor deberá de dar un sem_post para darle luz verde a la rutina de envío la cual realizará un sem_trywait la cual es una espera del semáforo, pero sin bloqueo para evitar una señal que posiblemente no llegue y entrar nuevamente al mismo problema que se tiene al recibir datos. Las rutinas que son invocadas en la iniciación son las siguientes: void Envia_TDA(void *datos) { int i=0, vr=0; nodo *ap=NULL, *aux=NULL, *aux_2=NULL; e_TDA elem; MPI_Balance *param_bal=(MPI_Balance *)datos; while(!param_bal->MPI_SIGNAL_TERM) { if(sem_trywait(&signal)==0) { //Se realiza una validación para no enviarse a sí mismo los datos y/o evitar enviar un mensaje innecesario if(param_bal->MPI_DEST!=id && param_bal->MPI_ELEM) {//Se indica al proceso principal que si hubo balance vr=1; fprintf(stdout,"\n proceso %d -----> Hilo envía %d datos a %d...\n",id, param_bal>MPI_ELEM, param_bal->MPI_DEST); //se cierra el candado para garantizar la integridad de los datos Cierra_candados(); //Se manda el mensaje que indica los elementos a migrar. MPI_Send(&param_bal->MPI_ELEM, 1, MPI_INT, param_bal->MPI_DEST, CANTIDAD, MPI_COMM_WORLD); for(i=0; i<param_bal->MPI_ELEM; i++) { //Se obtiene el ultimo elemento de la lista para su envío. ap=aux=*param_bal->TDA; while(ap->sig!=NULL) ap=ap->sig; //Se realiza una copia del nodo en el elemento de la unión para poder ser transferido. elem.ap=*ap; //Es importante inicializar el campo a nullo elem.ap.sig=NULL; //Se envía el nodo MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, param_bal>MPI_DEST, ELEMENTO, MPI_COMM_WORLD); //Se inicializa el campo del anterior a NULL while(aux->sig!=ap) aux=aux->sig; // Se elimina el nodo de la lista 67 if(aux!=ap) aux->sig=NULL; else param_bal->TDA=&aux_2; free((void *)ap); } Abre_candados(); } } } pthread_exit((void *)&vr); } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ void Recibe_TDA(void *datos) { int num_elem=0, i=0, fuente=0, vr=0, bandera=0; e_TDA elem; nodo *ap=NULL; MPI_Balance *param_bal=(MPI_Balance *)datos; MPI_Status estado; do { //Se realiza una prueba, para ver si se han recibido mensajes MPI_Iprobe(MPI_ANY_SOURCE, CANTIDAD, MPI_COMM_WORLD, &bandera, &estado); if(bandera)//Verificación de recepción de mensajes. {//Se indica al proceso principal que si hubo balance vr=1; //Obtención de la fuente que envío el mensaje. fuente=estado.MPI_SOURCE; //Recepción de la cantidad de elementos a recibir. MPI_Recv(&num_elem, 1, MPI_INT, fuente, CANTIDAD, MPI_COMM_WORLD, &estado); fprintf(stdout, "\n El proceso %d recibe señal de %d ---> datos a recibir %d\n",id, fuente, num_elem); //Se cierra el candado para garantizar la integridad. Cierra_candados(); for(i=0; i<num_elem; i++) { ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); *ap=elem.ap; /***********Aquí el usuario pondrá su función para la inserción de la lista *****/ inserta_nodo(param_bal->TDA, ap); } else perror("Error... Memoria Insuficiente, imposible recibir los elementos..."); } //Se habré el candado nuevamente. Abre_candados(); } }while(!param_bal->MPI_SIGNAL_TERM); pthread_exit((void *)&vr); 68 } El código de dichas rutinas funciona de la misma manera que en el programa de la primera fase (hilos_2.c) pero ahora sobre el tipo de dato TDA lista. Se le incorporaron las rutinas: /***** Función que cierra los candados para garantizar la integridad de los datos. void Cierra_candados(void) { pthread_mutex_lock(&candado); pthread_mutex_lock(&candado_estructura); } *****/ /***** Función que libera los candados *****/ void Abre_candados(void) { pthread_mutex_unlock(&candado); pthread_mutex_unlock(&candado_estructura); } Las cuales garantizan la integridad de los datos, esta mismas deberán ser invocadas en el código del usuario en el momento de realizar las operaciones sobre sus datos ya que no se deberá poder recibir y/o enviar éstos mientras se este operando sobre los mismos. En la rutina de iniciación de balance se crea un hilo más, el cual funge como el monitor del sistema, no es objetivo de este proyecto desarrollar dicha rutina, pero se crea una que deberá actuar de manera muy similar a esta, al igual que en el caso anterior se toman las condiciones extremas, en las cuales se envían datos cada cierto tempo. El tiempo y los elementos a migrar son al azar así como el destinatario. Una vez determinados estos elementos los cuales son parte de una variable de tipo MPI_Balance, la cual le es pasada por referencia, se dará la señal para comenzar el envío de datos. void Monitor(void *datos) { MPI_Balance *par_bal=(MPI_Balance *)datos; time_t tiempo; int count=0, long_lista=0, vr=0; //Se inicializa la semilla para realizar las acciones al azar. srand((unsigned)time(&tiempo)+id); //Verifica que el proceso no haya indicado el final de las operaciones. while(!par_bal->MPI_SIGNAL_TERM) { Cierra_candados(); //Determina el destino al azar. par_bal->MPI_DEST=rand()%proc; //Se obtiene el tamaño de la lista para, ver cuantos elementos se pueden enviar long_lista=longitud_TDA(*par_bal->TDA); par_bal->MPI_ELEM=rand()%(long_lista/2); //Da la señal para que el proceso enviador comience el envío de los datos sem_post(&signal); Abre_candados(); //Espera máximo 10 segundos antes de realizar otro envío sleep((rand()%5)+5); } pthread_exit((void *)&vr); } 69 Haciendo uso de estas rutinas de balance el código de la función principal quedaría como el listado “hilos_3.c” version1. main(int argc, char *argv[]) { nodo *lista; //Estructura que contiene los elementos necesarios para llevar a cabo el balance de carga MPI_Balance elem_bal; //Rutinas de iniciación para MPI. MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); crea_lista(&lista); lista=obten_datos("/tmp/regis.txt"); //Se inicializa el balance de carga MPI_Init_balance(&elem_bal,&lista); //Creación del TDA a partir de un archivo. proceso(&elem_bal); //Se termina el balance de carga MPI_Finaliza_balance(&elem_bal); MPI_Finalize(); return 0; } En este programa la rutina de proceso() lo único que realiza es la impresión de los datos cada cierto tiempo durante 30 ocasiones, para ver los listados completos de esta primera prueba ir al Anexo C 1ª versión . 5.2.2. hilo_3.c 2ª versión En esta fase del proyecto el problema a vencer son las líneas que hacen referencia al campo de la estructura, el cual es un apuntador al siguiente elemento y no necesariamente se llama de la forma que se llamo en el desarrollo del proyecto. Para esto hay que observar que las rutinas Envia_TDA y Recibe_TDA hacen uso dos rutinas para el manejo de la lista una es la “inserción de nodos” y la “eliminación de nodos” y una más cuyo objetivo es inicializar el campo al siguiente elemento a NULL. Estas tres funciones son iguales para cualquier TDA lista que sea manejado por las rutinas de balance. Para solucionar dicho problema se le agregan algunas rutinas al programa “scan.c” de la sección “Reconocimiento automático del TDA lista balanceable”, que nos auxiliaran a conocer el nombre del campo el cual es un apuntador al siguiente elemento de la lista. Las funciones implementadas en el programa “scan.c” son: /***** Esta función obtiene el nombre del campo, el cual es un apuntador a un elemento del mismo tipo, para la creación del TDA lista *****/ void busca_ptr_sig(char *struc, char *ptr_sig) { char *aux, *aux2; aux=strtok(struc,";"); while(aux!=NULL) { 70 necesario aux=strtok(NULL,";"); if(existe_asterisco(aux)) { aux2=strtok(aux,"*"); aux2=strtok(NULL,"*"); strcpy(ptr_sig,aux2); break; } } } /***** Verifica que en la línea se encuentre el * el cual es el indicador del campo que apunta al siguiente en una lista. *****/ int existe_asterisco(char *ptr_sig) { int i=0, vr=0; elemento while(ptr_sig[i]!='\0') if(ptr_sig[i++]=='*') { vr=1; break; } return vr; } Una vez encontrado el campo, a partir de un archivo llamado “MPI_lista.orig” generar el archivo “MPI_lista.c” el cual contiene las funciones mencionadas anteriormente. El listado completo del archivo “scan.c” se encuentra en el Anexo B “listados Segunda versión.” El contenido del archivo “MPI_lista.orig” es como sigue: /***** Archivo obtenido de la búsqueda realizada para la obtención de la estructura necesaria para la creación del TDA lista *****/ #include "protos.h" /***** Inserción de nodo en la lista *****/ void inserta_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista; if(es_vacia(ap)) *lista=elem; else { while(ap->@!=NULL) ap=ap->@; ap->@=elem; } } /***** Elimina un nodo de la lista *****/ void elimina_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista, *aux=NULL; 71 if(!es_vacia(*lista)) { if(ap=elem) { aux=*lista; *lista=aux->@; free(aux); } else while(ap->@!=NULL) { if(ap->@==elem->@) { aux=ap->@; ap->sig=aux->@; free(aux); break; } ap=ap->@; } } } /***** Inicia el apuntador al siguiente elemento de la lista con NULL *****/ void Asigna_null_asig(nodo *ap) { ap->@=NULL; } El programa “scan” leerá este archivo y cuando encuentre el carácter @ lo sustituirá por el nombre del campo al siguiente elemento de la lista y el resultado lo guardará en el archivo “MPI_lista.c” el cual si existe será destruido. En el caso de la estructura utilizada en este caso el resultado es el siguiente: /***** Archivo obtenido de la búsqueda realizada para la obtención de la estructura necesaria para la creación del TDA lista *****/ #include "protos.h" /***** Inserción de nodo en la lista *****/ void inserta_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista; if(es_vacia(ap)) *lista=elem; else { while(ap->sig!=NULL) ap=ap->sig; ap->sig=elem; } } /***** Elimina un nodo de la lista *****/ void elimina_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista, *aux=NULL; 72 if(!es_vacia(*lista)) { if(ap=elem) { aux=*lista; *lista=aux->sig; free(aux); } else while(ap->sig!=NULL) { if(ap->sig==elem->sig) { aux=ap->sig; ap->sig=aux->sig; free(aux); break; } ap=ap->sig; } } } /***** Inicia el apuntador al siguiente elemento de la lista con NULL *****/ void Asigna_null_asig(nodo *ap) { ap->sig=NULL; } Haciendo uso de esta herramienta, nuestras rutinas Envia_TDA y Recibe_TDA quedan de la siguiente forma, también es corregido el proceso de enviar los últimos nodos de la lista, ahora se envían los primeros: /***** En este primer acercamiento se envía campo por campo ****/ void Envia_TDA(void *datos) { int i=0, vr=0; nodo *ap=NULL, *aux=NULL, *aux_2=NULL; e_TDA elem; MPI_Balance *param_bal=(MPI_Balance *)datos; int longitud_lista=0; while(!param_bal->MPI_SIGNAL_TERM) { if(sem_trywait(&signal)==0) { //Se realiza una validación para no enviarse a sí mismo los datos y/o evitar enviar un mensaje innecesario if(param_bal->MPI_DEST!=id && param_bal->MPI_ELEM) {//Se indica al proceso principal que si hubo balance vr=1; fprintf(stdout,"\n proceso %d -----> Hilo envía %d datos a %d...\n",id, param_bal>MPI_ELEM, param_bal->MPI_DEST); //se cierra el candado para garantizar la integridad de los datos Cierra_candados(); //Se manda el mensaje que indica los elementos a migrar. MPI_Send(&param_bal->MPI_ELEM, 1, MPI_INT, param_bal->MPI_DEST, CANTIDAD, MPI_COMM_WORLD); for(i=0; i<param_bal->MPI_ELEM; i++) 73 { ap=*param_bal->TDA; //Se realiza una copia del nodo en el elemento de la unión para poder ser transferido. elem.ap=*ap; //Es importante inicializar el campo a nullo Asigna_null_asig(&elem.ap); //Se envía el nodo MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, param_bal>MPI_DEST, ELEMENTO, MPI_COMM_WORLD); //Se elimina el nodo de la lista elimina_nodo_lista_MPI(param_bal->TDA,ap->sig); } Abre_candados(); } } } pthread_exit((void *)&vr); } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ void Recibe_TDA(void *datos) { int num_elem=0, i=0, fuente=0, vr=0, bandera=0; e_TDA elem; nodo *ap=NULL; MPI_Balance *param_bal=(MPI_Balance *)datos; MPI_Status estado; do { //Se realiza una prueba, para ver si se han recibido mensajes MPI_Iprobe(MPI_ANY_SOURCE, CANTIDAD, MPI_COMM_WORLD, &bandera, &estado); if(bandera)//Verificación de recepción de mensajes. {//Se indica al proceso principal que si hubo balance vr=1; //Obtención de la fuente que envío el mensaje. fuente=estado.MPI_SOURCE; //Recepción de la cantidad de elementos a recibir. MPI_Recv(&num_elem, 1, MPI_INT, fuente, CANTIDAD, MPI_COMM_WORLD, &estado); fprintf(stdout, "\n El proceso %d recibe señal de %d ---> datos a recibir %d\n",id, fuente, num_elem); //Se cierra el candado para garantizar la integridad. Cierra_candados(); for(i=0; i<num_elem; i++) { ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); *ap=elem.ap; inserta_nodo_lista_MPI(param_bal->TDA, ap); } else perror("Error... Memoria Insuficiente, imposible recibir los elementos..."); } 74 //Se abre el candado nuevamente. Abre_candados(); } }while(!param_bal->MPI_SIGNAL_TERM); pthread_exit((void *)&vr); } 5.3. Rutinas para el balance de carga (versión final) 1. 2. 3. 4. 5. 6. Las rutinas encargadas del balance de carga hacen uso de los siguientes archivos: scan.c: este es el código fuente encargado del reconocimiento automático para el balance de la lista balanceable. def_TDA.h (generado por el programa “scan”) balance.c: archivo que contiene todas las rutinas de necesarias para el balance de carga. MPI_lista.orig (archivo que contiene la estructura general de las funciones inserta_nodo_lista_mpi(), elimina_nodo_lista_mpi() y Asiga_null_asig()) MPI_lista.c (archivo generado a partir de “MPI_lista.orig” por el programa “scan”) protos.h: este contiene la definición de las estructuras de datos y los prototipos utilizados pro las rutinas de balance. Los listados de dichos archivos se pueden consultar en el Anexo D. 75 6. RESULTADOS 76 6. RESULTADOS 6.1. Desempeño del programa hilos_3.c 1ª versión En esta versión se presento principalmente el siguiente problema, en una de las primeras versiones, una vez que era invocada la rutina MPI_Init_balance, al invocar a la rutina proceso() pasándole como argumento la dirección de la lista de la siguiente forma: nodo *lista; proceso(lista); //Declaración de la lista. //Invocación del proceso que maneja las operaciones sobre la lista. Se presentaba lo siguiente: si el hilo tenía que mandar ciertos elementos, que inicialmente eran tomados de la cabeza de la lista los elementos no se podían eliminar y causaban conflictos que eran arrastrados, esto se debe a que los hilos operan con un doble apuntador modificando la cabeza de la lista, por lo que la cabeza de la lista dejaba de ser la que se le pasaba al proceso, en un principio se decidió enviar a los últimos elementos. Pero esto no soluciona por completo el error, ya que ¿Qué pasa si se envía la lista por completo? pues la respuesta es sencilla, la lista no era eliminada y provocaba que los apuntadores se perdieran, marcando un numero de elementos inexistentes. Es como se toma la determinación de pararle la estructura completa a MPI_balance la cual contiene la cabeza de la lista que es manejada, aunque en realidad esto no es necesario bastaría con enviar el campo TDA de dicha estructura de la siguiente forma: nodo *lista; MPI_Balance elem; //Declaración de la lista. MPI_Init_balance(&elem, &lista); proceso(*elem.TDA); //Invocación del proceso que maneja las operaciones sobre la lista. Otro problema que se presenta es que en las rutinas Envia_TDA y Recibe_TDA existen instrucciones que hace referencia la campo de la estructura que apunta al siguiente elemento de la lista ligada, lo cual es un problema ya que se pretende que la rutina sea lo más general posible. Vea las líneas que se muestran a continuación y que son parte de la rutina de envío. //Se obtiene el último elemento de la lista para su envío. ap=aux=*param_bal->TDA; while(ap->sig!=NULL) ap=ap->sig; Esto provocaría que el usuario de la rutina tenga que modificar la misma para poder hacer uso del balance. 6.2. Desempeño del programa hilos_3.c 2ª versión Esta versión no tiene mayores problemas que la saturación del canal de recepción de mensajes, ya que es probado en condiciones extremas, se puede presentar que algún nodo del cluster reciba varios mensajes de diferentes nodos, lo que provoca una saturación del buffer de recepción lo cual es un error y consecuentemente que el programa sea abortado por el sistema operativo. Se realizaron pruebas haciendo uso del envío sin bloqueo así como el envío sincrono pero el resultado es el mismo. 77 7. CONCLUSIONES Y PERSPECTIVAS 78 7. CONCLUSIONES Y PERSPECTIVAS En este proyecto se crearon unas rutinas para el balance de carga capaces de enviar y/o recibir registros (estructuras de datos del TDA lista), garantizando la integridad de los mismos. Estas rutinas sólo se encargan del envió y/o recepción de los registros, para realizar su trabajo requieren de un monitor que se encargué del momento, la cantidad de registros, además del origen y el destino de dichos registros, todo mediante macros definidas para el funcionamiento de dichas rutinas. El usuario de las rutinas para el Balance de Carga deberá hacer uso de unas rutinas creadas para garantizar la integridad de sus datos, estas rutinas son: Cierra_candados(), Abre_candados(), estas rutinas deben de ser usadas en secciones criticas y es responsabilidad del usuario determinar cuales son sus secciones criticas y de esa forma proteger sus datos ante una posible migración de los mismos. La metodología empleada en este proyecto nos llevó a tomar diferentes opciones de cómo debían de ser migrados los nodos de un TDA, como parte del mismo se realizo un analizador léxico, capaz de identificar un registro. La función de este analizador es: una vez identificado el registro, éste deberá crear un alias del mismo registro, con el fin de que todas las rutinas diseñadas sean capaces de reconocer el tipo de dato que están manejando. Este analizador léxico fue creado a partir de la necesidad de conocer que tipo de dato se va a enviar de un nodo del cluster a otro. Las rutinas de balance de carga fueron diseñadas a partir de pruebas realizadas a los diferentes modos de enviar un nodo de un TDA lista, cada una de las formas en que se puede realizar el envío fue implementada y fueron analizados los pros y los contras, sobre la base de estos se llegó a una versión final. Tomando además las siguientes consideraciones: Las rutinas generadas no debían involucrar la modificación de las mismas por las personas que hicieran uso de estas; deberían de ser capaces de soportar cualquier tipo de datos definidos por el usuario de las mismas; deberían de garantizar la integridad de los datos al migrar los nodos del TDA, es decir, deberían de evitar la perdida de los datos y/o incluso la duplicidad de los mismos; y sobre todo ser fáciles de utilizar, tan fáciles como llegar a realizar las llamadas de las mismas. Estas rutinas de balance de carga, para su buen funcionamiento hacen uso de un monitor capaz de realizar una auditoria de los nodos del cluster, en cuanto a carga de procesamiento y a memoria, dicho monitor debe ser el encargado de determinar en que momento, cuantos nodos del TDA serán migrados y el nodo del cluster origen y el destino, además para garantizar que no halla perdida de datos, este monitor es el que debe determinar en que momento se han de terminar cada uno de los procesos corriendo. No es uno de los objetivos de este proyecto la creación de dicho monitor, pero, se creó una rutina que realiza dichas funciones sin realizar la auditoria de cada uno de los nodos del cluster, el momento, la cantidad de nodos del TDA y el nodo del cluster origen y destino son determinados al azar lo que lleva a un extremo la migración de datos, presentándose la saturación de los búferes de recepción si en un momento dado se migran datos de múltiples nodos del cluster aun un sólo nodo del mismo. Las rutinas del balance de carga desarrolladas en este proyecto se encuentra dentro de un margen aceptable de operatividad, son confiables, no se presentan perdidas de datos en cuanto al envío y/o recepción de información, si no hay saturación del canal las comunicaciones no representan un tiempo muerto de procesamiento. Por otro lado en cuanto a los resultados no satisfactorios obtenidos con las pruebas realizadas, la saturación del canal se da en un cluster no dedicado, es un cluster que además de correr las aplicaciones en paralelo cada uno de sus nodos es utilizado como estación de trabajo, con toda la carga en comunicaciones que esto representa, como lo es el acceso a Internet, la 79 exportación de sesiones gráficas, etc. Así como los servicios utilizados por el sistema como lo son NIS, NFS entre otros y la inexistente administración de tareas del cluster; la saturación resulta obvia. Las rutinas representan una buena opción en el balance de carga dinámico, resultan lo más general posible para la migración de datos y son de fácil preparación para la utilización en proyectos. Tal vez no representen una buena opción en cuanto a la administración de clusters, pero si representan una buena opción en cuanto al desempeño de las aplicaciones paralelas que hagan uso de éstas. Además probablemente sea posible hacerlas totalmente genéricas con el uso de apuntadores a función, o con la programación orientada a objetos. Pueden hacerse mejoras de las mismas con facilidad. 80 8. BIBLIOGRAFÍA 81 8. BIBLIOGRAFIA & [MJ1995] H. M. Deitel, P. J. Deitel “Como programar en C/C++”. Segunda Edición. Editorial Pearson Educación. Págs. 259-285, 317-346, 395-402, 467-478. Año 1995 & [AJ1994] Peter Aitken, Bradley Jones. “Aprendiendo C en 21 días” Editorial Pearson Educación. Págs. 189-206, 267-271, 272-276, 545-550, Año 1994 & [S2001] William Stallings. “Sistemas Operativos” 4a edición. Editorial Prentice Hall. Págs. 150-161, 163-165, 191-220, 231-235. Año 2001 & [1] Manuales en línea de Linux (man). & [2] Apuntes de “Programación Paralela” & [3] http://www-unix.mcs.anl.gov/mpi/. & [4] http://www.mpi-forum.org/. & [5] http://www.ldc.usb.ve/~ibanez/docencia/MPI/ 82 9. ANEXOS 83 9. ANEXOS 9.1. Anexo A (Códigos Fuentes para la migración de datos) Listado “protos.h” En este archivo se encuentran todos los prototipos así como la definición de las estructuras usadas para el desarrollo del la primera fase del proyecto. #include <stdlib.h> #include <stdio.h> #include <math.h> #include "mpi.h" #define COORDINADOR 0 /***** Variables globales utilizadas para la identificación de los procesos. *****/ int id, proc, long_nom; char nombre[50]; MPI_Status estado; typedef struct Registro nodo; struct Registro { char nombre[40]; unsigned matricula; int edad; char sexo; nodo *sig; }; /***** Prototipos para el manejo del TDA lista *****/ nodo *crea_lista(void); int es_vacia(nodo *lista); nodo *crea_nodo( unsigned matricula, char *nombre, int edad, char sexo); nodo *inserta_nodo(nodo *lista, nodo *elem); nodo *elimina_nodo(nodo *lista, nodo *elem); nodo *elimina_lista(nodo *lista); int longitud_TDA(nodo *lista); /***** Varias *****/ void Printf(nodo *lista); nodo *obten_datos(char *archivo); void valores_azar(int tot_proc, int tot_ele, int *destino, int *migrar); 84 Listado “operlis.c” En este apartado se presenta el contenido del archivo “operlis.c” el cual contiene las operaciones que se utilizan en el desarrollo de este proyecto. No se pretende que el usuario haga uso de dichas funciones para la codificación de los programas que realice, el usuario deberá de implementar sus propias funciones para el manejo de la lista. #include "protos.h" /***** Inicializa la lista *****/ nodo *crea_lista(void) { return NULL; } /***** Verifica si la lista se encuentra vacía *****/ int es_vacia(nodo *lista) { if(lista==NULL) return 1; else return 0; } /**** Creación de nodo de la lista ******/ nodo *crea_nodo(unsigned matricula, char *nombre, int edad, char sexo) { nodo *ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { ap->matricula=matricula; strcpy(ap->nombre, nombre); ap->edad=edad; ap->sexo=sexo; ap->sig=NULL; } else perror("Error... Memoria insuficiente"); return ap; } /***** Inserción de nodo en la lista *****/ nodo *inserta_nodo(nodo *lista, nodo *elem) { nodo *ap=lista; if(es_vacia(ap)) lista=elem; else { while(ap->sig!=NULL) ap=ap->sig; ap->sig=elem; 85 } return lista; } /***** Elimina un nodo de la lista *****/ nodo *elimina_nodo(nodo *lista, nodo *elem) { nodo *ap=lista; nodo *aux=NULL; if(!es_vacia(ap)) { if(ap->matricula==elem->matricula) { aux=ap; ap=ap->sig; free(aux); } else while(ap->sig!=NULL) { if(ap->sig->matricula==elem->matricula) { aux=ap->sig; ap->sig=aux->sig; free(aux); break; } ap=ap->sig; } } return lista; } /***** Elimina la lista completa *****/ nodo *elimina_lista(nodo *lista) { nodo *ap=lista; while(!es_vacia(lista)) { ap=lista; lista=elimina_nodo(lista, ap); } return lista; } /***** Calcula el numero de nodos del TDA para su posterior uso *****/ int longitud_TDA(nodo *lista) { int cont=0; nodo *ap=lista; while(!es_vacia(ap)) { cont++; 86 ap=ap->sig; } return cont; } Listado “ent_sal.c” En este apartado se encuentran las funciones utilizadas para la creación de la lista a partir de un archivo, una función que se encarga del desplegado de la lista dándole cierto formato. EL archivo donde se encuentran tales funciones es “ent_sal.c”. Ninguna de estas funciones que serán usadas en el proyecto final, estas sólo sirven para el desarrollo del mismo. La obtención del TDA lista es a criterio de los usuarios del código para el balance de cargas. #include "protos.h" nodo *obten_datos(char *archivo) { char nombre[40], sexo, basura; int edad; unsigned matricula; nodo *ap=NULL; nodo *lista=crea_lista(); FILE *leer; leer = fopen (archivo,"r"); if (leer != NULL) { do { fscanf (leer, "%d", &matricula); fscanf (leer, "%[^\n]s", nombre); fscanf (leer, "%2d", &edad); fscanf (leer, "%c%*c", &sexo); ap=crea_nodo(matricula, nombre, edad, sexo); lista=inserta_nodo(lista, ap); }while (!feof(leer)); fclose(leer); } else perror("Error... No se pudor abrir el archivo"); return lista; } void Printf(nodo *lista) { nodo *ap=lista; printf("\n%10s | %-50s|%5s|%5s\n", "Matricula","Nombre", "Edad","Sexo"); printf("---------------------------------------------------------------------------\n"); while(ap!=NULL) { printf("%10u | %-50s|%5d|%5c\n", ap->matricula, ap->nombre, ap->edad, ap->sexo); ap=ap->sig; } } 87 Listado “val_azar.c” Otra función utilizada para la realización del proyecto es valores_azar, dicha función es utilizada para dar una simulación más cercana al proyecto final en conjunto, ya que el balance de carga es uno de los módulos, existe otro modulo que se encargará de determinar cuando el nodo se encuentre cargado y por tanto deba de migrarse su información a otro con menos carga, lo cual queda fuera de los objetivos del proyecto actual. Esta función genera valores al azar que le son pasados a las funciones de envío, se toman las precauciones para que dichos valores no estén dentro de los rangos permitidos. La función se encuentra dentro del archivo “val_azar.c”. #include "protos.h" /***** Esta función es utilizada para la creación de los parámetros que determinan a quien y cuanto se va a ser enviado, *****/ void valores_azar(int tot_proc, int tot_ele, int *destino, int *migrar) { time_t tiempo; srand((unsigned)time(&tiempo)); *destino=(rand()%tot_proc)+1; *migrar=(rand()%tot_ele)+1; } 88 Listado “camxcamp.c” /***** En esta primera versión el programa, el único encargado de crear el TDA a partir del archivo es el proceso "Maestro" o "Coordinador" el cual se encargará de enviar elementos al azar de dicha estructura a un proceso igualmente al azar. El envío se realiza campo por campo, es decir, cada nodo del TDA es enviado uno por uno y a su vez cada campo de cada nodo, por lo que el proceso receptor, deberá reconstruir el TDA. Proyecto: camxcam.c ent_sal.c operlist.c val_azar.c *****/ #include "protos.h" nodo *Envia_TDA(nodo *lista, int destino, int num_elem); nodo *Recive_TDA(nodo *lista); main(int argc, char *argv[]) { nodo *lista=crea_lista(); MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); if(id==COORDINADOR) { int destino=0, num_elem=0, lon_tda=0; printf("Soy el coordinador desde %s\n", nombre); lista=obten_datos("/tmp/regis.txt"); //Creación del TDA a partir de un archivo. Printf(lista); //Impresión del TDA lon_tda=longitud_TDA(lista); //Se prepara para migrar elementos valores_azar(proc, lon_tda, &destino, &num_elem); lista=Envia_TDA(lista, destino, num_elem); //Envía elementos printf("%d elementos son enviados a: %d\n", num_elem, destino); printf("Soy el coordinador desde %s\n", nombre); Printf(lista); //Función que imprime la lista } else { printf("\n Hola Mundo, soy el proceso %d, desde %s\n", id, nombre); lista=Recive_TDA(lista); if(!es_vacia(lista)) Printf(lista); //Función que imprime la lista printf("\n"); } MPI_Finalize(); return 0; } /***** En este primer acercamiento se envía campo por campo ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { 89 int i=0, etiqueta=99, cero=0; int longitud=0; nodo *ap=lista; //Primero se manda el número de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i++) { ap=lista; longitud=strlen(ap->nombre)+1; //longitud de la cadena // Se envía campo por campo MPI_Send(&ap->matricula, 1, MPI_UNSIGNED, destino, etiqueta, MPI_COMM_WORLD); MPI_Send(&longitud, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); // Se envía la longitud de la cadena antes de enviarla MPI_Send(&ap->nombre, longitud, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD); MPI_Send(&ap->edad, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); MPI_Send(&ap->sexo, 1, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD); // Se elimina del TDA el nodo enviado lista=lista->sig; free(ap); } return lista; } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0; nodo *ap=NULL; unsigned matricula=0; char nombre[40], sexo; int edad; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { MPI_Recv(&matricula, 1, MPI_UNSIGNED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); // Se recibe la longitud del campo siguiente MPI_Recv(&longitud, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); MPI_Recv(nombre, longitud, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); MPI_Recv(&edad, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); MPI_Recv(&sexo, 1, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); // Reconstrucción del TDA 90 ap=crea_nodo(matricula, nombre, edad, sexo); lista=inserta_nodo(lista, ap); } return lista; } Listado “empaketado.c” /***** En esta versión el programa, es el único encargado de crear el TDA a partir del archivo es el proceso "Maestro" o "Coordinador" el cual se encargara de enviar elementos al azar de dicha estructura a un proceso igualmente al azar. Para enviar los datos crea un paquete por cada nodo el que contiene toda la información, con esto se evita el envío de mensajes por campo de la estructura. Así es enviado un paquete por cada nodo EL proceso receptor se encarga de desempaquetar la información recibida, reconstruir el nodo y enlistarlo. Proyecto: empaketado.c ent_sal.c operlist.c val_azar.c *****/ #include "protos.h" nodo *Envia_TDA(nodo *lista, int destino, int num_elem); nodo *Recive_TDA(nodo *lista); main(int argc, char *argv[]) { nodo *lista=crea_lista(); MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); if(id==COORDINADOR) { int destino=0, num_elem=0, lon_tda=0; printf("Soy el coordinador desde %s\n", nombre); lista=obten_datos("/tmp/regis.txt"); //Creación del TDA a partir de un archivo. Printf(lista); //Impresión del TDA lon_tda=longitud_TDA(lista); //Se prepara para migrar elementos valores_azar(proc, lon_tda, &destino, &num_elem); lista=Envia_TDA(lista, destino, num_elem); //Envía elementos printf("%d elementos son enviados a: %d\n", num_elem, destino); printf("Soy el coordinador desde %s\n", nombre); Printf(lista); //Función que imprime la lista } else { printf("\n Hola Mundo, soy el proceso %d, desde %s\n", id, nombre); lista=Recive_TDA(lista); if(!es_vacia(lista)) Printf(lista); //Función que imprime la lista printf("\n"); } 91 MPI_Finalize(); return 0; } /***** En este acercamiento se crea un paquete de cada uno de los nodos y se envía el paquete ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0, posicion=0; int longitud=0; nodo *ap=lista; size_t tam=0; char *buffer; //Primero se manda el numero de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i++) { ap=lista; longitud=strlen(ap->nombre)+1; //longitud de la cadena posicion=0; //Se inicializa el indicador de la posición en el buffer //Se calcula el tamaño de la memoria a solicitar tam=sizeof(int)+sizeof(char)*longitud+sizeof(unsigned)+sizeof(int)+sizeof(char); // También se empaqueta la longitud de la cadena para ser enviada buffer=(char *)malloc(tam); if(buffer!=NULL) { //Se envía el tamaño del paquete a ser enviado. MPI_Send(&tam, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); //Comienza el empaquetado de la información MPI_Pack(&longitud, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->nombre, longitud, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->matricula, 1, MPI_UNSIGNED, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->edad, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->sexo, 1, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); //Fin del empaquetado. //Se envía el paquete. MPI_Send(buffer, posicion, MPI_PACKED, destino, etiqueta, MPI_COMM_WORLD); // Se elimina del TDA el nodo enviado lista=lista->sig; free(ap); free(buffer); } else { perror("Memoria Insuficiente..."); //Se envía un cero para identificar que hubo un error y se rompe el ciclo MPI_Send(&cero, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); 92 break; } } return lista; } /***** Él(los) proceso(s) receptor(es) desempaqueta(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0, posicion=0; nodo *ap=NULL; unsigned matricula; char nombre[40], sexo; int edad; size_t tam; char *buffer; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { posicion=0; //Se inicializa la posición en el buffer MPI_Recv(&tam, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); if(tam<0) //Si se recibe un cero es que hubo un error y se rompe el ciclo break; buffer=(char *)malloc(tam); if(buffer!=NULL) { //Se recibe el paquete. MPI_Recv(buffer, tam, MPI_PACKED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //Se comienza a desempaquetar // Se recibe la longitud del campo siguiente MPI_Unpack(buffer, tam, &posicion, &longitud, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, nombre, longitud, MPI_CHAR, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &matricula, 1, MPI_UNSIGNED, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &edad, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &sexo, 1, MPI_CHAR, MPI_COMM_WORLD); // Reconstrucción del TDA ap=crea_nodo(matricula, nombre, edad, sexo); lista=inserta_nodo(lista, ap); free(buffer); } else perror("Memoria Insuficiente..."); } return lista; } 93 Listado “empaketado_b.c” /***** En esta versión el programa, es el único encargado de crear el TDA a partir del archivo es el proceso "Maestro" o "Coordinador" el cual se encargara de enviar elementos al azar de dicha estructura a un proceso igualmente al azar. Para enviar los datos crea un paquete que contiene toda la información de todos los nodos a ser migrados para evitar el envío de múltiples mensajes. EL proceso receptor se encarga de desempaquetar la información recibida reconstruir el nodo y enlistarlo Proyecto: empaketado.c ent_sal.c operlist.c val_azar.c *****/ #include "protos.h" nodo *Envia_TDA(nodo *lista, int destino, int num_elem); nodo *Recive_TDA(nodo *lista); size_t calcula_tam_buffer(nodo *lista, int num_elem); main(int argc, char *argv[]) { nodo *lista=crea_lista(); MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); if(id==COORDINADOR) { int destino=0, num_elem=0, lon_tda=0; printf("Soy el coordinador desde %s\n", nombre); lista=obten_datos("/tmp/regis.txt"); //Creación del TDA a partir de un archivo. Printf(lista); //Impresión del TDA lon_tda=longitud_TDA(lista); //Se prepara para migrar elementos valores_azar(proc, lon_tda, &destino, &num_elem); lista=Envia_TDA(lista, destino, num_elem); //Envia elementos printf("%d elementos son enviados a: %d\n", num_elem, destino); printf("Soy el coordinador desde %s\n", nombre); Printf(lista); //Función que imprime la lista } else { printf("\n Hola Mundo, soy el proceso %d, desde %s\n", id, nombre); lista=Recive_TDA(lista); if(!es_vacia(lista)) Printf(lista); //Función que imprime la lista printf("\n"); } MPI_Finalize(); return 0; } 94 /***** En este acercamiento se envía un paquete para todos los nodos del TDA lista ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0, posicion=0; int longitud=0; nodo *ap=lista; size_t tam=calcula_tam_buffer(lista, num_elem); char *buffer; //Primero se manda el número de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } buffer=(char *) malloc(tam); if(buffer!=NULL) { ap=lista; //Se envía el tamaño del paquete a ser enviado. MPI_Send(&tam, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); //Este ciclo crea un solo paquete para todos los nodos. for(i=0; i<num_elem; i++) { ap=lista; longitud=strlen(ap->nombre)+1; //longitud de la cadena MPI_Pack(&longitud, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->nombre, longitud, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->matricula, 1, MPI_UNSIGNED, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->edad, 1, MPI_INT, buffer, tam, &posicion, MPI_COMM_WORLD); MPI_Pack(&ap->sexo, 1, MPI_CHAR, buffer, tam, &posicion, MPI_COMM_WORLD); // Se elimina del TDA el nodo enviado lista=lista->sig; free(ap); } //Se envía el paquete. MPI_Send(buffer, posicion, MPI_PACKED, destino, etiqueta, MPI_COMM_WORLD); free(buffer); //se libera el espacio asignado. } else { perror("Memoria Insuficiente..."); //Se envía un cero para indicar que hubo un error. MPI_Send(&cero, 1, MPI_INT, destino, etiqueta, MPI_COMM_WORLD); } return lista; } 95 /***** Él(los) proceso(s) receptor(es) desempaqueta(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0, posicion=0; nodo *ap=NULL; unsigned matricula; char nombre[40], sexo; int edad; size_t tam=0; char *buffer; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); if(num_elem>0) MPI_Recv(&tam, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); if(tam>0) { buffer=(char *)malloc(tam); if(buffer!=NULL) { //Se recibe el paquete. MPI_Recv(buffer, tam, MPI_PACKED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //Se comienza a desempaquetar for(i=0; i<num_elem; i++) { // Se recibe la longitud del campo siguiente MPI_Unpack(buffer, tam, &posicion, &longitud, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, nombre, longitud, MPI_CHAR, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &matricula, 1, MPI_UNSIGNED, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &edad, 1, MPI_INT, MPI_COMM_WORLD); MPI_Unpack(buffer, tam, &posicion, &sexo, 1, MPI_CHAR, MPI_COMM_WORLD); // Reconstrucción del TDA ap=crea_nodo(matricula, nombre, edad, sexo); lista=inserta_nodo(lista, ap); } free(buffer); } else perror("Memoria Insuficiente..."); } return lista; } size_t calcula_tam_buffer(nodo *lista, int num_elem) { nodo *aux=lista; int i=0, longitud=0; 96 size_t tama=0; for(i=0; i<num_elem; i++) { longitud=strlen(aux->nombre)+1; tama+=sizeof(int)+sizeof(char)*longitud+sizeof(unsigned)+sizeof(int)+sizeof(char); aux=aux->sig; } return tama; } 97 Listado “uniones.c” /***** En esta versión el programa, es el único encargado de crear el TDA a partir del archivo es el proceso "Maestro" o "Coordinador" el cual se encargara de enviar elementos al azar de dicha estructura a un proceso igualmente al azar. El envío se realiza por medio de uniones, es decir, cada nodo del TDA es enviado uno por uno mediante el uso de una unión, el proceso receptor, solo deberá agregar a la lista el nodo recibido. Proyecto: uniones.c ent_sal.c operlist.c val_azar.c *****/ #include "protos.h" #define STRUCTSIZE sizeof(nodo) nodo * Envia_TDA(nodo *lista, int destino, int num_elem); nodo *Recive_TDA(nodo *lista); typedef union Nodos_TDA e_TDA; union Nodos_TDA { nodo ap; char mensaje[STRUCTSIZE]; }; main(int argc, char *argv[]) { nodo *lista=crea_lista(); MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); if(id==COORDINADOR) { int destino=0, num_elem=0, lon_tda=0; printf("Soy el coordinador desde %s\n", nombre); lista=obten_datos("/tmp/regis.txt"); //Creación del TDA a partir de un archivo. Printf(lista); //Impresión del TDA lon_tda=longitud_TDA(lista); //Se prepara para migrar elementos valores_azar(proc, lon_tda, &destino, &num_elem); lista=Envia_TDA(lista, destino, num_elem); //Envía elementos printf("%d elementos son enviados a: %d\n", num_elem, destino); printf("Soy el coordinador desde %s\n", nombre); Printf(lista); //Función que imprime la lista } else { printf("\n Hola Mundo, soy el proceso %d, desde %s\n", id, nombre); lista=Recive_TDA(lista); if(!es_vacia(lista)) Printf(lista); //Función que imprime la lista printf("\n"); 98 } MPI_Finalize(); return 0; } /***** En esta versión se realiza el envío mediante el uso de uniones ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0; int longitud=0; e_TDA elem; nodo *ap=NULL; //Primero se manda el numero de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i++) { ap=lista; strcpy(elem.ap.nombre, ap->nombre); elem.ap.matricula=ap->matricula; elem.ap.edad=ap->edad; elem.ap.sexo=ap->sexo; MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD); lista=lista->sig; free(ap); } return lista; } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0; e_TDA elem; nodo *ap=NULL; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); ap=crea_nodo(elem.ap.matricula, elem.ap.nombre, elem.ap.edad, elem.ap.sexo); lista=inserta_nodo(lista, ap); } return lista; } 99 Listado “uniones_b.c” /***** En esta primera versión el programa, el único encargado de crear el TDA a partir del archivo es el proceso "Maestro" o "Coordinador" el cual se encargara de enviar elementos al azar de dicha estructura a un proceso igualmente al azar. El envío se realiza por medio de uniones, es decir, cada nodo del TDA es enviado uno por uno mediante el uso de una unión. La diferencia con el anterior es la forma de llenar los campos de la estructura, en este no hay que llenar campo por campo, esto hace el código más general, de esta forma no hay que meter en la codificación el llenado de la estructura en la unión para el envío. El proceso receptor, solo deberá agregar a la lista el nodo recibido. Proyecto: uniones_b.c ent_sal.c operlist.c val_azar.c *****/ #include "protos.h" #define STRUCTSIZE sizeof(nodo) nodo *Envia_TDA(nodo *lista, int destino, int num_elem); nodo *Recive_TDA(nodo *lista); typedef union Nodos_TDA e_TDA; union Nodos_TDA { nodo ap; char mensaje[STRUCTSIZE]; }; main(int argc, char *argv[]) { nodo *lista=crea_lista(); MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); if(id==COORDINADOR) { int destino=0, num_elem=0, lon_tda=0; printf("Soy el coordinador desde %s\n", nombre); lista=obten_datos("/tmp/regis.txt"); //Creación del TDA a partir de un archivo. Printf(lista); //Impresión del TDA lon_tda=longitud_TDA(lista); //Se prepara para migrar elementos valores_azar(proc, lon_tda, &destino, &num_elem); lista=Envia_TDA(lista, destino, num_elem); //Envía elementos printf("%d elementos son enviados a: %d\n", num_elem, destino); printf("Soy el coordinador desde %s\n", nombre); Printf(lista); //Función que imprime la lista } else { printf("\n Hola Mundo, soy el proceso %d, desde %s\n", id, nombre); lista=Recive_TDA(lista); if(!es_vacia(lista)) 100 Printf(lista); printf("\n"); //Función que imprime la lista } MPI_Finalize(); return 0; } /***** En este primer acercamiento se envía campo por campo ****/ nodo *Envia_TDA(nodo *lista, int destino, int num_elem) { int i=0, etiqueta=99, cero=0; nodo *ap=NULL; e_TDA elem; //Primero se manda el numero de elementos a migrar for(i=1; i<proc; i++) { if(i==destino) MPI_Send(&num_elem, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); else //Si se trata de alguien diferente al destino MPI_Send(&cero, 1, MPI_INT, i, etiqueta, MPI_COMM_WORLD); } for(i=0; i<num_elem; i++) { ap=lista; //Se realiza una copia del nodo en el elemento de la unión para poder ser transferido. elem.ap=*ap; //Es importante inicializar el campo a nulo elem.ap.sig=NULL; MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, destino, etiqueta, MPI_COMM_WORLD); // Se elimina el nodo de la lista lista=lista->sig; free(ap); } return lista; } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ nodo *Recive_TDA(nodo *lista) { int num_elem=0, i=0, longitud=0; e_TDA elem; nodo *ap=NULL; /*** Se recibe el numero de elementos a recibir ***/ MPI_Recv(&num_elem, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); printf("El mensaje recibido es: %d \n", num_elem); for(i=0; i<num_elem; i++) { ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); *ap=elem.ap; lista=inserta_nodo(lista, ap); 101 } else perror("Memoria Insuficiente..."); } return lista; } 102 9.2. Anexo B (Códigos Fuentes para el Reconocimiento del TDA Lista) Listado “scan.c” #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXCAD 20000 #define RE_END 0 #define RL_SBE 1 #define RL_BNE 2 #define NR_SBE 4 //Registra Estructura Termina //Registra Línea y Sigue Buscando Estructura //Registra Línea y Busca Nueva Estructura //No Registres y Sigue Buscando Estructura /***** Estructura auxiliar para la identificación del registro de los TDA's *****/ typedef struct estruct_reg reg; struct estruct_reg { char *str_reg; char *token; reg *sig; }; /***** Funciones para la búsqueda de la estructura *****/ reg *busca_estructuras(char *tda, FILE *flujo, reg *col_dat); char *obten_bloque(FILE *flujo); void descarta_funcion(FILE *flujo); /***** Funciones de verificación *****/ int existe(char *linea,reg *cola); int token_reg(char *linea, reg *cola); int dato_c(char *decl); int verifica_exist(char *linea, char *tda, char *nueva_str); int verifica_campos(char *estruc, reg *cola, char *ntok); /***** Funciones para el manejo de los TDA's utilizados para el registro *****/ reg *crea_nodo(char *linea, char *token); reg *encola(reg *cola, reg *ap); reg *registra_TDA(char *linea, char *aux, reg *col_dat, FILE *flujo); void crea_arout(reg *estructuras, FILE *salida, char *tda); main(int argc, char *argv[]) { FILE *flujo, *salida; reg *estructuras=NULL; if(argc>2) { if((flujo=fopen(argv[1],"r"))!=NULL) { printf("\t Escaneando archivo \"%s\" \n", argv[1]); printf("\t\t Buscando TDA --> %s... \n", argv[2]); printf("\t\t Creando archivo de salida...\n"); if((salida=fopen("./def_TDA.h","w"))!=NULL) 103 { //La función busca la estructura y regresa un TDA con toda la información de la misma estructuras=busca_estructuras(argv[2], flujo, estructuras); //Se guardan las estructuras en un archivo y se cierran los flujos crea_arout(estructuras, salida, argv[2]); fclose(flujo); fclose(salida); } else perror("Error... No se puede crear el archivo de salida"); } else perror("Error... Al abrir el archivo, verifique ruta y nombre"); } else { if(argc==2) perror("Error... Argumentos insuficientes"); else perror("Error... sin argumentos en main()\n"); printf("\t Prueba con: %s <archivo.c> <tda>\n\n",argv[0]); printf("\t<archivo.c>: archivo fuente en donde se deberá buscar la estructura \n"); printf("\t <tda>: es el nombre de la estructura a ser buscada \n"); } return 0; } /********** Funciones que auxilian a la búsqueda del TDA *********/ //Esta función es la encargada de recorrer todo el archivo en busca del TDA //lo hace por bloques que pudiesen ser una estructura reg *busca_estructuras(char *tda, FILE *flujo, reg *col_dat) { char linea[MAXCAD]="", nueva_str[MAXCAD]="", aux[MAXCAD]; int vr=0, salir=0, k=0; fpos_t *inicio; reg *ap=NULL; strcpy(aux,tda); while(!feof(flujo)) { //La función regresa un bloque que pudiera ser una estructura strcpy(linea, obten_bloque(flujo)); // Busca en el bloque el tda que solicitado vr=verifica_exist(linea, aux, nueva_str); switch(vr) { case RE_END: //Esta opción indica que el TDA ha sido encontrado col_dat=registra_TDA(linea, aux, col_dat, flujo); salir=1; break; 104 case RL_SBE: //Esta opción indica que se trata de una redefinición, //pero se sigue buscando la misma estructura col_dat=registra_TDA(linea, aux, col_dat, flujo); salir=0; break; case RL_BNE: //Esta indica que se trata de una redefinición y se buscara la original col_dat=registra_TDA(linea, aux, col_dat, flujo); strcpy(aux, nueva_str); salir=0; break; default: //no se ha encontrado nada relacionado con la estructura break; } if(salir) break; } return col_dat; } //Esta función es la encargada de buscar en el archivo, lo hace por bloques que pudiesen ser //declaraciones de estructuras, sólo regresan los posibles candidatos. //Es capas de ignorar prototipos, archivos de inclusión y funciones. char *obten_bloque(FILE *flujo) { int llave=0, term_lin=0, paren_a=0, paren_c=0, i=0; char linea[MAXCAD]; /*El siguiente ciclo es capaz de encontrar bloques que puedan ser considerados como una estructura.*/ while(!feof(flujo)) { linea[i]=fgetc(flujo); switch(linea[i]) { //Ignora los archivos de cabecera case '#': while(!feof(flujo)) if(fgetc(flujo)=='\n') break; i=0; break; //verifica si no hay llaves abiertas para salir case ';': if(paren_a && paren_c)//Descarta los prototipos paren_a=paren_c=i=0; else { if(llave==0) term_lin=1; i++; } break; //Elimina los saltos de línea y tabuladores innecesarios case '\n': break; case '\t': break; //Registra si se han abierto o cerrado llaves case '{': if(paren_a && paren_c) 105 { //Evita buscar la definición dentro de una función descarta_funcion(flujo); paren_a=paren_c=i=llave=0; } else llave++; i++; break; case '}': llave--; i++; break; //Registra si se encuentra posiblemente una función case '(': paren_a++; i++; break; case ')': paren_c++; i++; break; default: i++; } if(term_lin) //Pone el fin de línea { linea[i]='\0'; break; } } return linea; } //Esta función se encarga de eliminar lo que se considera como el cuerpo de la función void descarta_funcion(FILE *flujo) { int llave=1; char basura; while(!feof(flujo)) { switch(fgetc(flujo)) { case '{': llave++; break; case '}': llave--; break; default: break; } if(!llave) break; } } /****** Funciones que auxilian en la identificación, registro y verificación del TDA y sus campos *****/ //Esta función se encarga de verificar si la línea ya fue registrada, para evitar duplicarla int existe(char *linea, reg *cola) { reg *ap=cola; int vr=0, iguales=0; while(ap!=NULL) { iguales=strcmp(linea,ap->str_reg); if(iguales==0) { vr=1; 106 break; } ap=ap->sig; } return vr; } //Verifica si un token ya fue buscado, para evitar buscarlo nuevamente al verificar los campos de la estructura. int token_reg(char *linea, reg *cola) { reg *ap=cola; int vr=0; while(ap!=NULL) { vr=strcmp(linea,ap->token); if(vr==0) { vr=1; break; } ap=ap->sig; } return vr; } //Esta función es la encargada de verificar si el bloque que le es pasado //es el TDA buscado int verifica_exist(char *linea, char *tda, char *nueva_str) { int salida=NR_SBE,j=0,i=0; char typedef_[]="typedef"; //cadena para determinar si la estructura pasada es un redefinición. size_t tam_sub=strlen(typedef_), tam_cad=strlen(linea); //Esta función es la encargada de determinar si el TDA buscado se encuentra en //en el bloque, si no se encuentra regresa un NULL char *tipo=strstr(linea,tda); size_t tam_tipo=0; //Posiblemente se trata de la definición if(tipo!=NULL) { tam_tipo=strlen(tipo); //Verifica si se trata de una redefinición if(strstr(linea, typedef_)!=NULL) { //verifica si es el nombre de la estructura // o es la redefinición. if(tam_tipo-1==strlen(tda)) { for(i=tam_sub+1, j=0; i<tam_cad-tam_tipo-1; i++, j++) nueva_str[j]=linea[i]; nueva_str[i]='\0'; salida=RL_BNE; } 107 else { //Si la redefinición esta junto con la estructura if(strstr(linea,"{")) salida=RE_END; //Solamente se trata de la redefinición. else salida=RL_SBE; } } //La estructura ha sido encontrada. else { printf("\t Estructura encontrada \n\t"); printf("\t\t%s\n",linea); salida=RE_END; } } return salida; } //Verifica que los campos del TDA sean tipos de datos reconocidos una vez que esta ha sido encontrado, //si alguno de los campos no es reconocido realiza la búsqueda de este mismo int verifica_campos(char *estruc, reg *cola, char *ntok) { char *aux, *cadmod, *tipo; char delim[]=";"; int i=0, j=0, vr=1; //pide memoria para la realización de su trabajo cadmod=(char *)malloc(sizeof(char)*strlen(estruc)+1); tipo=(char *)malloc(sizeof(char)*strlen(estruc)+1); if(cadmod != NULL && tipo!=NULL) { //Busca el cuerpo de la estructura, para obtener los campos while(estruc[i++]!='{'); //Copia los campos de la estructura a una nueva cadena for(j=0; i<strlen(estruc)-2; j++, i++) cadmod[j]=estruc[i]; cadmod[j]='\0'; //Obtiene campo por campo mediante el delimitador ";" aux=strtok(cadmod, delim); while(aux!=NULL) { if(aux!=NULL) { i=0; //Este ciclo obtiene el tipo de dato de la declaración while(aux[i++]!=' ') tipo[i-1]=aux[i-1]; tipo[i-1]='\0'; if(!dato_c(tipo)) //verifica si se trata de un tipo de dato de C 108 { //Verifica si se trata de una estructura ya registrada if(!token_reg(tipo, cola)) { //Copia el nuevo token para seguir buscando for(i=0; tipo[i]!='\0'; i++) ntok[i]=tipo[i]; vr=0; //Regresa un valor de error. break; } } } aux=strtok(NULL, delim); } } else perror("Error... Memoria insuficiente"); return vr; } //Determina si el tipo de dato que se le pasa es un tipo de dato de C int dato_c(char *decl) { char *tipo=NULL; if(strstr(decl,"char")!=NULL) return 1; if(strstr(decl,"int")!=NULL) return 1; if(strstr(decl,"unsigned")!=NULL) return 1; if(strstr(decl,"long")!=NULL) return 1; if(strstr(decl,"float")!=NULL) return 1; if(strstr(decl,"double")!=NULL) return 1; if(tipo==NULL) return 0; } /***** Funciones para el de los TDA utilizados en la búsqueda de los TDA's ****/ reg *crea_nodo(char *linea, char *token) { reg *ap=NULL; ap=(reg *)malloc(sizeof(ap)); if(ap!=NULL) { ap->str_reg=(char *)malloc(strlen(linea)+1); ap->token=(char *)malloc(strlen(token)+1); if(ap->str_reg!=NULL && ap->token!=NULL) { strcpy(ap->str_reg, linea); strcpy(ap->str_reg, linea); 109 ap->sig=NULL; } else perror("Error... Memoria insuficiente"); } else perror("Error... Memoria insuficiente"); return ap; } reg *encola(reg *cola, reg *ap) { reg *aux=cola; if(aux==NULL) cola=ap; else { while(aux->sig!=NULL) aux=aux->sig; aux->sig=ap; } return cola; } //Función encargada de registrar lo relacionado con la búsqueda //verifica primero si no ha sido registrada, para evitar duplicidad reg *registra_TDA(char *linea, char *aux, reg *col_dat, FILE *flujo) { reg *ap=NULL; int campo=1; //Verifica el registro de la linea if(!existe(linea, col_dat)) { ap=crea_nodo(linea, aux); col_dat=encola(col_dat, ap); } do { //Si alguno de los campos no existe se llama a la función para su búsqueda desde el inicio campo=verifica_campos(linea, col_dat, aux); if(!campo) { rewind(flujo); col_dat=busca_estructuras(aux, flujo, col_dat); } }while(campo==0); return col_dat; } //Función encargada de escribir en el archivo de salida el resultado de la búsqueda 110 void crea_arout(reg *estructuras, FILE *salida, char *tda) { reg *ap=NULL; if(estructuras!=NULL) { //Escribe un redefinición del TDA para que sea usado por el programa realizado //en la parte I fprintf(salida,"\n typedef struct %s nodo;\n", tda); do { ap=estructuras; fprintf(salida, "%s\n", ap->str_reg); estructuras=estructuras->sig; free(ap); }while(estructuras!=NULL); } else printf("La estructura no ha sido encontrada...\n"); } 111 9.3. Anexo C (Códigos Fuentes para Migración de datos compartidos con hilos) Listado “hilos_1.c” /***** Programa que ejemplifica la forma de usar los hilos en MPI, el proceso 0 realiza el desplegado de los datos(arreglo de enteros) en pantalla con un hilo, mientras otro espera un lapso de tiempo y pide al usuario el número de elementos a migrar al proceso 1, el cual como única función tiene la recepción de los datos. El arreglo utilizado es global. archivo: hilos_1.c *****/ #include <semaphore.h> #include <pthread.h> #include <stdio.h> #include "mpi.h" //Prototipos de las funciones utilizadas void proceso(void *numero); void monitor(void *numero); void inicializa(int *arr); int id; int proc; int longitud; int hilo; char nombre[MPI_MAX_PROCESSOR_NAME]; pthread_mutex_t candado; //Candado utilizado para garantizar la integridad del arreglo. int arreglo[20]; main (int argc, char *argv[]) { int vr=0; int array[20]; pthread_t hilo1, hilo2; int i=0; long num_dat; time_t semilla; MPI_Status estado; //Hilos utilizados por el Coordinador(proceso 0) //Objeto para la verificación de la recepción de mensajes //Rutinas de iniciación para MPI MPI_Init (&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Get_processor_name(nombre, &longitud); switch(id) { case 0: //Proceso coordinador inicializa(arreglo); fprintf(stdout,"Proceso %d, desde %s\n", id, nombre); //Creación del hilo monitor encargado de migrar elementos del arreglo pthread_create(&hilo1, NULL, (void *)&monitor, (void *)1); //Creación del hilo que realiza el desplegado en pantalla pthread_create(&hilo2, NULL, (void *)&proceso, (void *)2); //Espera por la terminación de los hilos. 112 pthread_join(hilo1, (void *)&vr); pthread_join(hilo2, (void *)&vr); printf("Los hilos del proceso %s han terminado...\n", id); break; case 1: //Proceso receptor printf("\n"); fprintf(stdout,"Proceso %d, desde %s\n", id, nombre); //Iniciación del arreglo for(i=0;i<20; i++) array[i]=-1; //Recibe el mensaje con la cantidad de elementos que recibirá. MPI_Recv(&num_dat, 1, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //ciclo para la Recepción de los elementos del arreglo. for(i=0; i<num_dat; i++) MPI_Recv(&array[i], 1, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &estado); //Desplegado del arreglo en este proceso. for(i=0; (array[i]!=-1 && i<20); i++) printf("%3d",array[i]); printf("\n\n"); break; default: break; //Otro proceso... } //Finalización del paralelismo. MPI_Finalize(); return 0; } /**** Iniciación del arreglo con valores al azar *****/ void inicializa(int *arr) { int i=0; time_t semilla; time(&semilla); srand((unsigned)semilla); //Iniciación de la semilla for(i=0;i<20;i++) arr[i]=rand()%10; } /***** Proceso encargado de la migración de los datos. Pide el numero de elementos del arreglo a ser enviados al proceso 1 y los envía *****/ void monitor(void *datos) { int hilo=(int)datos; int num_nodos=0, i=0, etiqueta=0; if(id==0) { sleep(5); //Lapso de espera antes de comenzar el envío pthread_mutex_lock(&candado); //Cierra el candado para garantizar la integridad de los datos. fprintf(stdout,"Hola soy el hilo %d\n",hilo); fprintf(stdout,"del proceso %d\n",id); fprintf(stdout,"Desde el procesador %s\n",nombre); 113 //Este ciclo valida que el usuario no exceda el límite del arreglo. do { printf("Dime cuantos nodos quieres migrar: "); scanf("%d", &num_nodos); }while(num_nodos<0 || num_nodos>20); printf("\n los elementos a migrar son %d\n",num_nodos); //Manda el mensaje con el número de elementos que enviará al proceso 1 MPI_Send(&num_nodos, 1, MPI_INT, 1, etiqueta, MPI_COMM_WORLD); //Envía cada uno de los elementos (envía los últimos). for(i=20-num_nodos; i< 20 ; i++) { MPI_Send(&arreglo[i], 1, MPI_INT, 1, etiqueta, MPI_COMM_WORLD); arreglo[i]=-1; //Borra el elemento del arreglo. } pthread_mutex_unlock(&candado); //Abre el candado. } pthread_exit((void *)hilo); } /***** Proceso encargado de imprimir el arreglo en pantalla cada 2 segundos, aquí se ve que el arreglo disminuye cuando se envían al proceso 1 *****/ void proceso(void *numero) { int i=0, count=0; int hilo=(int)numero; printf("\n Soy el hilo %d del procesador %s\n", hilo, nomb re); while(count<5) { pthread_mutex_lock(&candado); //Se cierra el candado para garantizar la integridad. for(i=0; (arreglo[i]!=-1 && i<20); i++) printf("%3d",arreglo[i]); printf("\n"); pthread_mutex_unlock(&candado); //Se habré el candado sleep(2); count ++; } pthread_exit((void *)hilo); } 114 Listado “hilos_2.c” /***** Programa que ejemplifica la forma de usar los hilos en MPI. Cada proceso inicializa el arreglo con valores al azar posteriormente crea un par de hilos, el de envío y el de recepción y comienza el procesamiento de sus datos el cual consiste en la impresión del arreglo en un archivo con nombre igual a la maquina donde se encuentra corriendo y al proceso correspondiente, cada 2 segundos en 15 ocasiones. El hilo encargado del envío lo hace cada cierto tiempo al azar al igual que la cantidad de elemento que envía. El arreglo utilizado es global, archivo: hilos_2.c *****/ #include <pthread.h> #include <stdio.h> #include "mpi.h" #define CANTIDAD 0 #define ELEMENTO 1 //Prototipos de las funciones que el proceso necesita para el manejo de los datos void proceso(void); char *num_proc(int id); char da_digit(int num); void inicializa(int *arr); void valores_azar(int tot_proc, int tot_ele, int *destino, int *migrar); //Prototipos de las funciones necesarias para el envío y recepción. void Envia(void *num); void Recibe(void *salida); void MPI_Term_hilo(int *signal); int id; int proc; int longitud; char nombre[MPI_MAX_PROCESSOR_NAME]; pthread_mutex_t candado; //Candado para garantizar la exclusión mutua. int arreglo[100]; //Esta es la señal para la terminación de los hilos. int MPI_Signal_term_hilo=1; main (int argc, char *argv[]) { int vr_env=0, vr_rec=0, salir=0; pthread_t envio, recepcion; //hilo para el manejo de los datos MPI_Status estado; //Objeto para la verificación de la recepción de mensajes. int *signal=&MPI_Signal_term_hilo; //Rutinas de iniciación de MPI MPI_Init (&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Get_processor_name(nombre, &longitud); //Se inicializan los arreglos que posteriormente serán enviados inicializa(arreglo); //Se crean los hilos encargados del balance de carga. pthread_create(&envio, NULL, (void *)&Envia, (void *)1); pthread_create(&recepcion, NULL, (void *)&Recibe, (void *)salir); 115 //Función que opera con los datos. proceso(); //Crea la señal para la terminación de los hilos MPI_Term_hilo(signal); //Se espera la terminación de los hilos para el balance de carga. //Y se guarda el valor de retorno. pthread_join(envio,(void *)&vr_env); pthread_join(recepcion, (void *)&vr_rec); fprintf(stdout, "Proceso %d, desde %s ---->", id, nombre); if(vr_env && vr_rec) fprintf(stdout, " El balance ha terminado sin problemas.\n"); MPI_Finalize(); return 0; } /***** Esta función es utilizada para indicar a los hilos encargados del balance de la carga que terminen. *****/ void MPI_Term_hilo(int *signal) { *signal=0; } /***** Función encargada del envío de elementos del arreglo, se hace al azar *****/ void Envia(void *num) { int destino=0, num_elem=0; int vr=(int)num; int tam=0, i=0; time_t semilla; //Iniciación de la semilla. srand((unsigned) semilla +id); while(MPI_Signal_term_hilo) { sleep(rand()%10); //Espera un tiemp o al azar antes de realizar un envío valores_azar(proc,20,&destino,&num_elem); if(destino!=id && num_elem) { fprintf(stdout,"\n proceso %d -----> Hilo envía %d datos a %d...\n",id, num_elem, destino); //se cierra el candado para garantizar la integridad de los datos pthread_mutex_lock(&candado); //Se manda el mensaje que indica cuantos elementos se van a migrar. MPI_Send(&num_elem, 1, MPI_INT, destino, CANTIDAD, MPI_COMM_WORLD); //Ciclo encargado del envío de los elementos. //Se obtiene el tamaño actual del arreglo para enviar los últimos elementos. tam=calcula_tam(arreglo); for(i=tam-num_elem; i<tam; i++) { MPI_Send(&arreglo[i], 1, MPI_INT, destino, ELEMENTO, MPI_COMM_WORLD); arreglo[i]=-1; } //Se habré el candado para que el proceso que los opera pueda hacer uso de estos. 116 pthread_mutex_unlock(&candado); } } pthread_exit((void *)&vr); } void Recibe(void *salida) { int *termina=(int *)salida; int bandera=0, fuente=0, elem=0, i=0, basura=0, vr=0, tam=0; int count =0; MPI_Status estado; while(MPI_Signal_term_hilo) { //Se realiza una prueba, para ver si se han recibido mensajes MPI_Iprobe(MPI_ANY_SOURCE, CANTIDAD, MPI_COMM_WORLD, &bandera, &estado); if(bandera)//Verificación de recepción de mensajes. { //Obtención de la fuente que envío el mensaje. fuente=estado.MPI_SOURCE; //Recepción de la cantidad de elementos a recibir. MPI_Recv(&elem, 1, MPI_INT, fuente, CANTIDAD, MPI_COMM_WORLD, &estado); //Se cierra el candado para garantizar la integridad. pthread_mutex_lock(&candado); tam=calcula_tam(arreglo); //Obtención del numero de elementos en el arreglo. fprintf(stdout, "\n El proceso %d recibe señal de %d ---> datos a recibir %d\n",id, fuente, elem); for(i=0; i<elem; i++,tam++) { //Recepción de los elementos enviados. if(tam<100) MPI_Recv(&arreglo[tam], 1, MPI_INT, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); //Esto garantiza que no se sobrepase del tamaño del arreglo, aunque provoca perdida de datos. else MPI_Recv(&basura, 1, MPI_INT, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); } //Se habré el candado nuevamente. pthread_mutex_unlock(&candado); } } pthread_exit((void *)&vr); } /***** Esta función calcula el numero de elementos en el arreglo *****/ int calcula_tam(int *arr) { int i=0; for(i=0; (arr[i]!=-1 && i<100); i++); return i; } 117 /***** Función de iniciación para el arreglo *****/ void inicializa(int *arr) { int i=0; time_t semilla; time(&semilla); srand((unsigned)semilla+id); //Se crean valores aleatorios para el arreglo. for(i=0;i<20;i++) arr[i]=rand()%10; //El resto se inicia con un valor de -1 for(;i<100; i++) arr[i]=-1; } /***** Esta función es utilizada para la creación de los parámetros que determinan a quien y cuanto se va a ser enviado, *****/ void valores_azar(int tot_proc, int tot_ele, int *destino, int *migrar) { time_t tiempo; srand((unsigned)time(&tiempo)+id); *destino=(rand()%tot_proc); //Para que cada uno de los proceso envié sólo una cuarta parte de sus elementos *migrar=(rand()%(tot_ele/4)); } /***** Este es el proceso encargado de las operaciones de los datos, Las operaciones que realiza son la escritura del arreglo cada cierto tiempo, en un archivo con el nombre del procesador y el proceso correspondiente. *****/ void proceso(void) { int i=0, count=0; FILE *arch; char nom_arch[20]; //Estas tres funciones crean el nombre del archivo en el cual guardaran el arreglo cada cierto tiempo. strcpy(nom_arch,"./"); strcat(nom_arch,nombre); strcat(nom_arch,"_"); strcat(nom_arch,num_proc(id));//num_proc regresa el numero de proceso en una cadena. arch=fopen(nom_arch,"w"); if(arch!=NULL) { fprintf(arch,"Soy el Proceso:%d desde el procesador %s\n", id, nombre); while(count<15) { //El hilo encargado de la migración de los datos no debe poder hacerlo mientras //se este operando sobre los mismos. pthread_mutex_lock(&candado); for(i=0; (arreglo[i]!=-1 && i<100); i++) fprintf(arch,"%3d",arreglo[i]); fprintf(arch,"\n"); pthread_mutex_unlock(&candado); 118 sleep(2); count ++; } } } /***** Funciones auxiliares para la creación del nombre del archivo para la salida. *****/ /***** Recibe el numero de proceso y lo convierte a cadena para ser agregado al nomb re del archivo *****/ char *num_proc(int id) { char pross[4]; int dec=0, uni=0; pross[0]=da_digit(id/100); dec=id%100; pross[1]=da_digit(dec/10); uni=dec%10; pross[2]=da_digit(uni); pross[3]='\0'; return pross; } /***** Convierte números en caracteres *****/ char da_digit(int num) { char dig_let; switch(num) { case 0: dig_let='0'; break; case 1: dig_let='1'; break; case 2: dig_let='2'; break; case 3: dig_let='3'; break; case 4: dig_let='4'; break; case 5: dig_let='5'; break; case 6: dig_let='6'; break; case 7: dig_let='7'; break; case 8: dig_let='8'; break; case 9: dig_let='9'; break; default: break; } return dig_let; } Listados “Primera versión” Es esta sección quedan establecidas las rutinas para el balance de carga, se incluyen los listados de dichas rutinas y los listados de las rutinas necesarias para el desarrollo del programa de prueba, el cual maneja una lista de registros de alumnos, que es formada a partir de un archivo de texto. Las rutinas de balance sólo responden a la indicación de un hilo que les indica a donde y cuantos elementos se deberán migrar a otro nodo del cluster. 119 Listado “protos.h” #include <semaphore.h> #include <pthread.h> #include <stdlib.h> #include <stdio.h> #include <math.h> #include "mpi.h" #include "def_TDA.h" //Este archivo de inclusión si será incluido en el proyecto final //Este archivo de inclusión si será incluido en el proyecto final //Este archivo de inclusión si será incluido en el proyecto final //Este archivo de inclusión si será incluido en el proyecto final, //incluye la definición del TDAS que se va a utilizar. /***** Variables globales utilizadas para la identificación de los procesos. *****/ int id, proc, long_nom; char nombre[50]; #define CANTIDAD 0 #define ELEMENTO 1 #define COORDINADOR 0 #define STRUCTSIZE sizeof(nodo) /***** Estructura utilizada para lograr el balance del TDA *****/ typedef union Nodos_TDA e_TDA; union Nodos_TDA { nodo ap; char mensaje[STRUCTSIZE]; }; /***** Estructura de datos necesaria para que los hilos puedan manejar el TDA y la migración de datos a otros nodos del cluster *****/ typedef struct Parametros_Balance { nodo **TDA; int MPI_DEST; int MPI_ELEM; int MPI_SIGNAL_TERM; }MPI_Balance; sem_t signal; //Señal para indicar al hilo que envié los datos a otros nodos. pthread_t envio, recepcion, monitor; //hilo para el manejo de los datos pthread_mutex_t candado, candado_estructura; //Candado para garantizar la exclusión mutua. /***** Prototipos de las funciones encargadas del balance de la carga ****/ void MPI_Init_balance(MPI_Balance *par_bal, nodo **lista); void MPI_Finaliza_balance(MPI_Balance *par_bal); void Envia_TDA(void *datos); void Recibe_TDA(void *datos); void Monitor(void *datos); void Cierra_candados(void); void Abre_candados(void); /***** Estas funciones son obtenidas mediante el ejecutable scan de la sección 2 ****/ 120 /*************************************************************************************************** *************/ /*Las siguientes funciones tendrán que ser declaradas por el usuario de la rutina de balance, no será necesario*/ /*que sean utilizadas las rutinas declaradas para esta prueba, y tampoco es necesario que sean incluidas en este*/ /*archivo de cabecera, éste sólo es una prueba */ /*************************************************************************************************** *************/ /***** Prototipos para el manejo del TDA lista *****/ void crea_lista(nodo **lista); int es_vacia(nodo *lista); nodo *crea_nodo( unsigned matricula, char *nombre, int edad, char sexo); void inserta_nodo(nodo **lista, nodo *elem); void elimina_nodo(nodo **lista, nodo *elem); void elimina_lista(nodo **lista); int longitud_TDA(nodo *lista); /***** Varias *****/ void Printf(nodo *lista, FILE *ap); nodo *obten_datos(char *archivo); 121 Listado “def_TDA.h” En este archivo se encuentra la definición de la estructura utilizada para la creación del TDA lista, se pretende que el usuario haga uso del programa “scan.c” de la sección “Búsqueda de una estructura” para la obtención del mismo, además se crea una redefinición de la estructura la cual es manejada por las rutinas de balance de carga para su trabajo. /***** Este archivo es obtenido con el ejecutable de la parte 2. El cual realiza una redefinición de la estructura para que los hilos hagan uso de la misma *****/ typedef struct Registro nodo; struct Registro { char nombre[40]; unsigned matricula; int edad; char sexo; nodo *sig; }; Listado “balance.c” Este es el listado el cual contiene las rutinas necesarias para el balance de carga. /***** Esta es el archivo que lleva a cabo el balance de carga, no requiere mayores cambios por el usuario que los indicados en la documentación misma, esto para darle mayor libertad al usuario de realizar sus funciones para el manejo del TDA. *****/ #include "protos.h" void MPI_Init_balance(MPI_Balance *par_bal, nodo **lista) { //Se inicia el semáforo el cual indicará el momento para comenzar el balance sem_init(&signal,0,0); //Iniciación de la estructura //Se indica la dirección de los datos que podrían ser enviados a otro proceso par_bal->TDA=lista; //Se inicia el parámetro destino, con un valor igual al proceso par_bal->MPI_DEST=id; //Se inicia el número de elementos a enviar par_bal->MPI_ELEM=0; //Se inicia la señal que indica a los hilos que terminen par_bal->MPI_SIGNAL_TERM=0; //Iniciación de los hilos para el balance pthread_create(&envio, NULL, (void *)&Envia_TDA, (void *)par_bal); pthread_create(&recepcion, NULL, (void *)&Recibe_TDA, (void *)par_bal); //Esta Función no va a ser necesaria para el uso en general //Este hilo es el encargado de determinar a quien, cuantos y en que momento le serán enviados datos pthread_create(&monitor, NULL,(void *)&Monitor,(void *)par_bal); } void MPI_Finaliza_balance(MPI_Balance *par_bal) { 122 int vr_env=0, vr_rec=0, vr_mon=0; //Se manda la señal a los hilos para que terminen su trabajo par_bal->MPI_SIGNAL_TERM=1; //Se espera la terminación de los hilos que realizan el balance de carga. //Y se guarda el valor de retorno. pthread_join(monitor,(void *)&vr_mon); pthread_join(envio,(void *)&vr_env); pthread_join(recepcion, (void *)&vr_rec); if(vr_env || vr_rec) fprintf(stdout, "Proceso %d, desde %s ----> si hubo balance \n", id, nombre); else fprintf(stdout, "Proceso %d, desde %s ----> no hubo balance \n", id, nombre); } /***** En este primer acercamiento se envía campo por campo ****/ void Envia_TDA(void *datos) { int i=0, vr=0; nodo *ap=NULL, *aux=NULL, *aux_2=NULL; e_TDA elem; MPI_Balance *param_bal=(MPI_Balance *)datos; while(!param_bal->MPI_SIGNAL_TERM) { if(sem_trywait(&signal)==0) { //Se realiza una validación para no enviarse a sí mismo los datos y/o evitar enviar un mensaje innecesario if(param_bal->MPI_DEST!=id && param_bal->MPI_ELEM) {//Se indica al proceso principal que si hubo balance vr=1; fprintf(stdout,"\n proceso %d -----> Hilo envía %d datos a %d...\n",id, param_bal>MPI_ELEM, param_bal->MPI_DEST); //se cierra el candado para garantizar la integridad de los datos Cierra_candados(); //Se manda el mensaje que indica los elementos a migrar. MPI_Send(&param_bal->MPI_ELEM, 1, MPI_INT, param_bal->MPI_DEST, CANTIDAD, MPI_COMM_WORLD); for(i=0; i<param_bal->MPI_ELEM; i++) { //Se obtiene el último elemento de la lista para su envío. ap=aux=*param_bal->TDA; while(ap->sig!=NULL) ap=ap->sig; //Se realiza una copia del nodo en el elemento de la unión para poder ser transferido. elem.ap=*ap; //Es importante inicializar el campo a nullo elem.ap.sig=NULL; //Se envía el nodo MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, param_bal>MPI_DEST, ELEMENTO, MPI_COMM_WORLD); //Se inicializa el campo del anterior a NULL while(aux->sig!=ap) aux=aux->sig; // Se elimina el nodo de la lista 123 if(aux!=ap) aux->sig=NULL; else param_bal->TDA=&aux_2; free((void *)ap); } Abre_candados(); } } } pthread_exit((void *)&vr); } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ void Recibe_TDA(void *datos) { int num_elem=0, i=0, fuente=0, vr=0, bandera=0; e_TDA elem; nodo *ap=NULL; MPI_Balance *param_bal=(MPI_Balance *)datos; MPI_Status estado; do { //Se realiza una prueba, para ver si se han recibido mensajes MPI_Iprobe(MPI_ANY_SOURCE, CANTIDAD, MPI_COMM_WORLD, &bandera, &estado); if(bandera)//Verificación de recepción de mensajes. {//Se indica al proceso principal que si hubo balance vr=1; //Obtención de la fuente que envió el mensaje. fuente=estado.MPI_SOURCE; //Recepción de la cantidad de elementos a recibir. MPI_Recv(&num_elem, 1, MPI_INT, fuente, CANTIDAD, MPI_COMM_WORLD, &estado); fprintf(stdout, "\n El proceso %d recibe señal de %d ---> datos a recibir %d\n",id, fuente, num_elem); //Se cierra el candado para garantizar la integridad. Cierra_candados(); for(i=0; i<num_elem; i++) { ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); *ap=elem.ap; /***********Aquí el usuario pondrá su función para la inserción de la lista *****/ inserta_nodo(param_bal->TDA, ap); } else perror("Error... Memoria Insuficiente, imposible recibir los elementos..."); } //Se habré el candado nuevamente. Abre_candados(); } }while(!param_bal->MPI_SIGNAL_TERM); pthread_exit((void *)&vr); 124 } /***** Función que cierra los candados para garantizar la integridad de los datos. void Cierra_candados(void) { pthread_mutex_lock(&candado); pthread_mutex_lock(&candado_estructura); } *****/ /***** Función que libera los candados *****/ void Abre_candados(void) { pthread_mutex_unlock(&candado); pthread_mutex_unlock(&candado_estructura); } Listado “hilos_3.c” Este es el listado del programa de prueba utilizado para la creación de las rutinas de balance de carga. /***** Esta es una prueba con la versión final de la rutina de balance, la cual incluye el balance de cargas mediante hilos, la forma en que se realiza el balance, es mediante el uso de uniones como se hizo en la versión ("uniones_b.c"). La forma de operar de este programa es similar a la versión anterior ("hilos_2.c"). Es decir, cada proceso obtiene su lista a partir de un archivo, después de unos lapsos de tiempo, se comenzara una simulación del balance de la carga, el momento del envió y el destino se hacen al azar. Cada proceso creará un archivo, en el cual imprime la lista, este proceso es el proceso de operaciones sobre los datos, en el cual se debe de garantizar la integridad de los mismos. Los archivos que componen el proyecto en esta fase de prueba son: hilos_3.c balance.c operlis.c ent_sal.c protos.h *****/ #include "protos.h" /***** Prototipos auxiliares del proceso *****/ void proceso(MPI_Balance *elem); char *num_proc(int id); char da_digit(int num); main(int argc, char *argv[]) { nodo *lista; //Estructura que contiene los elementos necesarios para llevar a cabo el balance de carga MPI_Balance elem_bal; //Rutinas de iniciación para MPI. MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); 125 crea_lista(&lista); lista=obten_datos("/tmp/regis.txt"); //Se inicializa el balance de carga MPI_Init_balance(&elem_bal,&lista); //Creación del TDA a partir de un archivo. proceso(&elem_bal); //Se termina el balance de carga MPI_Finaliza_balance(&elem_bal); MPI_Finalize(); return 0; } void Monitor(void *datos) { MPI_Balance *par_bal=(MPI_Balance *)datos; time_t tiempo; int count=0, long_lista=0, vr=0; //Se inicializa la semilla para realizar las acciones al azar. srand((unsigned)time(&tiempo)+id); while(!par_bal->MPI_SIGNAL_TERM)//Verifica que el proceso no halla indicado el final de las operaciones. { Cierra_candados(); //Determina el destino al azar. par_bal->MPI_DEST=rand()%proc; //Se obtiene el tamaño de la lista para, ver cuantos elementos se pueden enviar long_lista=longitud_TDA(*par_bal->TDA); par_bal->MPI_ELEM=rand()%(long_lista/2); //Da la señal para que el proceso enviador comience el envío de los datos sem_post(&signal); Abre_candados(); //Espera máximo 10 segundos antes de realizar otro envío sleep((rand()%5)+5); } pthread_exit((void *)&vr); } /****** Este es el proceso encargado de las operaciones de los datos, Las operaciones que realiza son la escritura del arreglo cada cierto tiempo, en un archivo con el nombre del procesador y el proceso correspondiente. *****/ void proceso(MPI_Balance *elem) { int i=0, count=0; FILE *arch; char nom_arch[20]; //Estas tres funciones crean el nombre del archivo en el cual guardaran el arreglo cada cierto tiempo. strcpy(nom_arch,"./"); strcat(nom_arch,nombre); strcat(nom_arch,"_"); strcat(nom_arch,num_proc(id));//num_proc regresa el numero de proceso en una cadena. arch=fopen(nom_arch,"w"); if(arch!=NULL) 126 { count=0; fprintf(arch,"Soy el Proceso:%d desde el procesador %s\n", id, nombre); while(count++<30) { //El hilo encargado de la migración de los datos no debe poder hacerlo mientras //se este operando sobre los mismos. fprintf(arch,"\n--------------------->iteración %d", count); //El usuario de las rutinas de balance deberá de implementar el candado en las operaciones de sus datos. Cierra_candados(); Printf(*elem->TDA, arch); fprintf(arch,"\t\t\t Número de elementos: %d\n",longitud_TDA(*elem->TDA)); Abre_candados(); sleep(2); } } else perror("Error no se pudo generar el archivo de salida..."); } /***** Funciones auxiliares para la creación del nombre del archivo para la salida. *****/ /***** Recibe el numero de proceso y lo convierte a cadena para ser agregado al nombre del archivo *****/ char *num_proc(int id) { char pross[4]; int dec=0, uni=0; pross[0]=da_digit(id/100); dec=id%100; pross[1]=da_digit(dec/10); uni=dec%10; pross[2]=da_digit(uni); pross[3]='\0'; return pross; } /***** Convierte números en caracteres *****/ char da_digit(int num) { char dig_let; switch(num) { case 0: dig_let='0'; break; case 1: dig_let='1'; break; case 2: dig_let='2'; break; case 3: dig_let='3'; break; case 4: dig_let='4'; break; case 5: dig_let='5'; break; case 6: dig_let='6'; break; case 7: dig_let='7'; break; case 8: dig_let='8'; break; case 9: dig_let='9'; break; default: break; } 127 return dig_let; } 128 Listado “operlis.c” En este archivo se encuentras las funciones necesarias para la operación de la lista, éstas sufrieron algunas modificaciones, fueron convertidas en procedimientos para que no tuviesen que regresar la lista como era el caso en la sección “Envío y Recepción de un nodo TDA lista”. /*** En este archivo se incluyen todas las funciones para el manejo de un TDA lista. Por las situaciones que se presentan en la forma de enviar y recibir los datos, éstas deben ser procedimientos, es decir, deben de recibir la lista por referencia, para que pueda ser modificada sin tener que regresar la lista. ***/ #include "protos.h" /***** Inicializa la lista *****/ void crea_lista(nodo **lista) { *lista=NULL; } /***** Verifica si la lista se encuentra vacía *****/ int es_vacia(nodo *lista) { if(lista==NULL) return 1; else return 0; } /**** Creación de nodo de la lista ******/ nodo *crea_nodo(unsigned matricula, char *nombre, int edad, char sexo) { nodo *ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { ap->matricula=matricula; strcpy(ap->nombre, nombre); ap->edad=edad; ap->sexo=sexo; ap->sig=NULL; } else perror("Error... Memoria insuficiente"); return ap; } /***** Inserción de nodo en la lista *****/ void inserta_nodo(nodo **lista, nodo *elem) { nodo *ap=*lista; if(es_vacia(ap)) *lista=elem; else { while(ap->sig!=NULL) ap=ap->sig; ap->sig=elem; 129 } } /***** Elimina un nodo de la lista *****/ void elimina_nodo(nodo **lista, nodo *elem) { nodo *ap=*lista, *aux=NULL; if(!es_vacia(*lista)) { if(ap->matricula==elem->matricula) { aux=*lista; *lista=aux->sig; free(aux); } else while(ap->sig!=NULL) { if(ap->sig->matricula==elem->matricula) { aux=ap->sig; ap->sig=aux->sig; free(aux); break; } ap=ap->sig; } } } /***** Elimina la lista completa *****/ void elimina_lista(nodo **lista) { nodo *ap=*lista; while(!es_vacia(*lista)) { ap=*lista; elimina_nodo(lista, ap); } *lista=NULL; } /***** Calcula el numero de nodos del TDA para su posterior uso *****/ int longitud_TDA(nodo *lista) { int cont=0; nodo *ap=lista; while(!es_vacia(ap)) { cont++; ap=ap->sig; } return cont; } 130 Listado “ent_sal.c” #include "protos.h" /***** Función encargada de crear el TDA lista a partir del archivo que se le pasa por parámetro.*/ nodo *obten_datos(char *archivo) { char nombre[40], sexo; int edad; unsigned matricula; nodo *ap=NULL; nodo *lista; FILE *leer; crea_lista(&lista); leer=fopen(archivo,"r"); if (leer!=NULL) { do { fscanf (leer, "%d", &matricula); fscanf (leer, "%[^\n]s", nombre); fscanf (leer, "%2d", &edad); fscanf (leer, "%c%*c", &sexo); ap=crea_nodo(matricula, nombre, edad, sexo); inserta_nodo(&lista, ap); }while (!feof(leer)); fclose(leer); } else perror("Error... No se pudor abrir el archivo"); return lista; } /***** Función encargada de la impresión en un archivo para que quede constancia del balance de carga *****/ void Printf(nodo *lista, FILE *arch) { nodo *ap=lista; fprintf(arch,"\n%10s | %-50s|%5s|%5s\n", "Matricula","Nombre", "Edad","Sexo"); fprintf(arch,"---------------------------------------------------------------------------\n"); while(ap!=NULL) { fprintf(arch,"%10u | %-50s|%5d|%5c\n", ap->matricula, ap->nombre, ap->edad, ap->sexo); ap=ap->sig; } } 131 Listados “Segunda Versión” Estos son los listados de las rutinas de balance de carga y las del programa de prueba, con las modificaciones hechas para la primera versión. Para los listados de algunos de los archivos no hay diferencia por lo que no son incluidos, sólo se hace la referencia de que no hubo cambio alguno. Listado “protos.h” No hay cambio, ver listado primera versión. Listado “def_TDA.h” No hay cambio, ver listado primera versión. Listado “balance.c” /***** Esta es el archivo que lleva a cabo el balance de carga, no requiere mayores cambios por el usuario que los indicados en la documentación misma, esto para darle mayor libertad al usuario de realizar sus funciones para el manejo del TDA. *****/ #include "protos.h" void MPI_Init_balance(MPI_Balance *par_bal, nodo **lista) { //Se inicia el semáforo el cual indicará el momento para comenzar el balance sem_init(&signal,0,0); //Iniciación de la estructura //Se indica la dirección de los datos que podrían ser enviados a otro proceso par_bal->TDA=lista; //Se inicia el parámetro destino, con un valor igual al proceso par_bal->MPI_DEST=id; //Se inicia el número de elementos a enviar par_bal->MPI_ELEM=0; //Se inicia la señal que indica a los hilos que terminen par_bal->MPI_SIGNAL_TERM=0; //Iniciación de los hilos para el balance pthread_create(&envio, NULL, (void *)&Envia_TDA, (void *)par_bal); pthread_create(&recepcion, NULL, (void *)&Recibe_TDA, (void *)par_bal); //Esta Función no va a ser necesaria para el uso en general //Este hilo es el encargado de determinar a quien, cuantos y en que momento le serán enviados datos pthread_create(&monitor, NULL,(void *)&Monitor,(void *)par_bal); } void MPI_Finaliza_balance(MPI_Balance *par_bal) { int vr_env=0, vr_rec=0, vr_mon=0; //Se manda la señal a los hilos para que terminen su trabajo par_bal->MPI_SIGNAL_TERM=1; //Se espera la terminación de los hilos que realizan el balance de carga. //Y se guarda el valor de retorno. 132 pthread_join(monitor,(void *)&vr_mon); pthread_join(envio,(void *)&vr_env); pthread_join(recepcion, (void *)&vr_rec); if(vr_env || vr_rec) fprintf(stdout, "Proceso %d, desde %s ----> si hubo balance \n", id, nombre); else fprintf(stdout, "Proceso %d, desde %s ----> no hubo balance \n", id, nombre); } /***** En este primer acercamiento se envía campo por campo ****/ void Envia_TDA(void *datos) { int i=0, vr=0; nodo *ap=NULL, *aux=NULL, *aux_2=NULL; e_TDA elem; MPI_Balance *param_bal=(MPI_Balance *)datos; int longitud_lista=0; while(!param_bal->MPI_SIGNAL_TERM) { if(sem_trywait(&signal)==0) { //Se realiza una validación para no enviarse a sí mismo los datos y/o evitar enviar un mensaje innecesario if(param_bal->MPI_DEST!=id && param_bal->MPI_ELEM) {//Se indica al proceso principal que si hubo balance vr=1; fprintf(stdout,"\n proceso %d -----> Hilo envia %d datos a %d...\n",id, param_bal>MPI_ELEM, param_bal->MPI_DEST); //se cierra el candado para garantizar la integridad de los datos Cierra_candados(); //Se manda el mensaje que indica los elementos a migrar. MPI_Send(&param_bal->MPI_ELEM, 1, MPI_INT, param_bal->MPI_DEST, CANTIDAD, MPI_COMM_WORLD); for(i=0; i<param_bal->MPI_ELEM; i++) { ap=*param_bal->TDA; //Se realiza una copia del nodo en el elemento de la unión para poder ser transferido. elem.ap=*ap; //Es importante inicializar el campo a nullo elem.ap.sig=NULL; //Se envía el nodo MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, param_bal>MPI_DEST, ELEMENTO, MPI_COMM_WORLD); //Se elimina el nodo de la lista elimina_nodo_lista_MPI(param_bal->TDA, ap->sig); } Abre_candados(); } } } pthread_exit((void *)&vr); } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ void Recibe_TDA(void *datos) { 133 int num_elem=0, i=0, fuente=0, vr=0, bandera=0; e_TDA elem; nodo *ap=NULL; MPI_Balance *param_bal=(MPI_Balance *)datos; MPI_Status estado; do { //Se realiza una prueba, para ver si se han recibido mensajes MPI_Iprobe(MPI_ANY_SOURCE, CANTIDAD, MPI_COMM_WORLD, &bandera, &estado); if(bandera)//Verificación de recepción de mensajes. {//Se indica al proceso principal que si hubo balance vr=1; //Obtención de la fuente que envío el mensaje. fuente=estado.MPI_SOURCE; //Recepción de la cantidad de elementos a recibir. MPI_Recv(&num_elem, 1, MPI_INT, fuente, CANTIDAD, MPI_COMM_WORLD, &estado); fprintf(stdout, "\n El proceso %d recibe señal de %d ---> datos a recibir %d\n",id, fuente, num_elem); //Se cierra el candado para garantizar la integridad. Cierra_candados(); for(i=0; i<num_elem; i++) { ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); *ap=elem.ap; inserta_nodo_lista_MPI(param_bal->TDA, ap); } else perror("Error... Memoria Insuficiente, imposible recibir los elementos..."); } //Se abre el candado nuevamente. Abre_candados(); } }while(!param_bal->MPI_SIGNAL_TERM); pthread_exit((void *)&vr); } void Cierra_candados(void) { pthread_mutex_lock(&candado); pthread_mutex_lock(&candado_estructura); } void Abre_candados(void) { pthread_mutex_unlock(&candado); pthread_mutex_unlock(&candado_estructura); } Listado “Hilos_3.c” No hay cambio, ver listado primera versión. 134 Listado “operlis.c” No hay cambio, ver listado primera versión. Listado “ent_sal.c” No hay cambio, ver listado primera versión. Listado “MPI_lista.c” Este archivo contiene las funciones de inserción y eliminación de nodos las cuales son llamadas en algún momento por las rutinas de balance de carga, se pretende que éste sea obtenido a partir del programa “scan” el cual fue modificado para la generación de estas funciones en esta sección. /***** Archivo obtenido de la búsqueda realizada para la obtención de la estructura necesaria para la creación del TDA lista *****/ #include "protos.h" /***** Inserción de nodo en la lista *****/ void inserta_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista; if(es_vacia(ap)) *lista=elem; else { while(ap->sig!=NULL) ap=ap->sig; ap->sig=elem; } } /***** Elimina un nodo de la lista *****/ void elimina_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista, *aux=NULL; if(!es_vacia(*lista)) { if(ap=elem) { aux=*lista; *lista=aux->sig; free(aux); } else while(ap->sig!=NULL) { if(ap->sig==elem->sig) { aux=ap->sig; ap->sig=aux->sig; free(aux); break; } 135 ap=ap->sig; } } } 136 Listado “scan.c” Esta es la versión final del programa “scan” el cual es capaza obtener el nombre del campo en la estructura el cual es el apuntador al siguiente elemento. #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXCAD 20000 #define RE_END 0 #define RL_SBE 1 #define RL_BNE 2 #define NR_SBE 4 //Registra Estructura t Termina //Registra Linea y Sigue Buscando Estructura //Regis tra Linea y Busca Nueva Estructura //No Registres y Sigue Buscando Estructura /***** Estructura auxiliar para la identificación del registro de los TDA's *****/ typedef struct estruct_reg reg; struct estruct_reg { char *str_reg; char *token; reg *sig; }; /***** Funciones para la búsqueda de la estructura *****/ reg *busca_estructuras(char *tda, FILE *flujo, reg *col_dat); char *obten_bloque(FILE *flujo); void descarta_funcion(FILE *flujo); /***** Funciones de verificación *****/ int existe(char *linea,reg *cola); int token_reg(char *linea, reg *cola); int dato_c(char *decl); int verifica_exist(char *linea, char *tda, char *nueva_str); int verifica_campos(char *estruc, reg *cola, char *ntok); int existe_asterisco(char *ptr_sig); /***** Funciones para el manejo de los TDA's utilizados para el registro *****/ reg *crea_nodo(char *linea, char *token); reg *encola(reg *cola, reg *ap); reg *registra_TDA(char *linea, char *aux, reg *col_dat, FILE *flujo); void crea_arout(reg *estructuras, FILE *salida, char *tda); void busca_ptr_sig(char *struc, char *ptr_sig); void crea_func(char *ptr_sig); main(int argc, char *argv[]) { FILE *flujo, *salida; reg *estructuras=NULL; if(argc>2) { if((flujo=fopen(argv[1],"r"))!=NULL) { printf("\t Escaneando archivo \"%s\" \n", argv[1]); printf("\t\t Buscando TDA --> %s... \n", argv[2]); 137 printf("\t\t Creando archivo de salida...\n"); if((salida=fopen("./def_TDA.h","w"))!=NULL) { //La función busca la estructura y regresa un TDA con toda la información de la misma estructuras=busca_estructuras(argv[2], flujo, estructuras); //Se guardan las estructuras en un archivo y se cierran los flujos crea_arout(estructuras, salida, argv[2]); fclose(flujo); fclose(salida); } else perror("Error... No se puede crear el archivo de salida"); } else perror("Error... Al abrir el archivo, verifique ruta y nombre"); } else { if(argc==2) perror("Error... Argumentos insuficientes"); else perror("Error... sin argumentos en main()\n"); printf("\t Prueba con: %s <archivo.c | archivo.h> <tda>\n\n",argv[0]); printf("\t<archivo.c | archivo.h>: archivo fuente en donde se deberá buscar la estructura \n"); printf("\t <tda>: es el nombre de la estructura a ser buscada \n"); } return 0; } /********** Funciones que auxilian a la búsqueda del TDA *********/ //Esta función es la encargada de recorrer todo el archivo en busca del TDA //lo hace por bloques que pudiesen ser una estructura reg *busca_estru cturas(char *tda, FILE *flujo, reg *col_dat) { char linea[MAXCAD]="", nueva_str[MAXCAD]="", aux[MAXCAD]; int vr=0, salir=0, k=0; fpos_t *inicio; reg *ap=NULL; strcpy(aux,tda); while(!feof(flujo)) { //La función regresa un bloque que pudiera ser una estructura strcpy(linea, obten_bloque(flujo)); // Busca en el bloque el tda que solicitado vr=verifica_exist(linea, aux, nueva_str); switch(vr) { case RE_END: //Esta opción indica que el TDA ha sido encontrado col_dat=registra_TDA(linea, aux, col_dat, flujo); 138 salir=1; break; case RL_SBE: //Esta opción indica que se trata de una redefinición, //pero se sigue buscando la misma estructura col_dat=registra_TDA(linea, aux, col_dat, flujo); salir=0; break; case RL_BNE: //Esta indica que se trata de una redefinición y se buscara la original col_dat=registra_TDA(linea, aux, col_dat, flujo); strcpy(aux, nueva_str); salir=0; break; default: //no se ha encontrado nada relacionado con la estructura break; } if(salir) break; } return col_dat; } //Esta función es la encargada de buscar en el archivo, lo hace por bloques que pudiesen ser //declaraciones de estructuras, sólo regresa los posibles candidatos. //Es capas de ignorar prototipos, archivos de inclusión y funciones. char *obten_bloque(FILE *flujo) { int llave=0, term_lin=0, paren_a=0, paren_c=0, i=0; char linea[MAXCAD]; /*El siguiente ciclo es capaz de encontrar bloques que puedan ser considerados como una estructura.*/ while(!feof(flujo)) { linea[i]=fgetc(flujo); switch(linea[i]) { //Ignora los archivos de cabecera case '#': while(!feof(flujo)) if(fgetc(flujo)=='\n') break; i=0; break; //verifica si no hay llaves abiertas para salir case ';': if(paren_a && paren_c)//Descarta los prototipos paren_a=paren_c=i=0; else { if(llave==0) term_lin=1; i++; } break; //Elimina los saltos de línea y tabuladores innecesarios case '\n': break; case '\t': break; 139 //Registra si se han abierto o cerrado llaves case '{': if(paren_a && paren_c) { //Evita buscar la definición dentro de una función descarta_funcion(flujo); paren_a=paren_c=i=llave=0; } else llave++; i++; break; case '}': llave--; i++; break; //Registra si se encuentra posiblemente una función case '(': paren_a++; i++; break; case ')': paren_c++; i++; break; default: i++; } if(term_lin) //Pone el fin de linea { linea[i]='\0'; break; } } return linea; } //Esta función se encarga de eliminar lo que se considera como el cuerpo de la función void descarta_funcion(FILE *flujo) { int llave=1; char basura; while(!feof(flujo)) { switch(fgetc(flujo)) { case '{': llave++; break; case '}': llave--; break; default: break; } if(!llave) break; } } /****** Funciones que auxilian en la identificación, registro y verificación del TDA y sus campos *****/ //Esta función se encarga de verificar si la linea ya fue registrada, para evitar duplicarla int existe(char *linea, reg *cola) { reg *ap=cola; int vr=0, iguales=0; while(ap!=NULL) { iguales=strcmp(linea,ap->str_reg); if(iguales==0) 140 { vr=1; break; } ap=ap->sig; } return vr; } //Verifica si un token ya fue buscado, para evitar buscarlo nuevamente al verificar los campos de la estructura. int token_reg(char *linea, reg *cola) { reg *ap=cola; int vr=0; while(ap!=NULL) { vr=strcmp(linea,ap->token); if(vr==0) { vr=1; break; } ap=ap->sig; } return vr; } //Esta función es la encargada de verificar si el bloque que le es pasado //es el TDA buscado int verifica_exist(char *linea, char *tda, char *nueva_str) { int salida=NR_SBE,j=0,i=0; char typedef_[]="typedef"; //cadena para determinar si la estructura pasada es un redefinición. size_t tam_sub=strlen(typedef_), tam_cad=strlen(linea); //Esta función es la encargada de determinar si el TDA buscado se encuentra en //en el bloque, si no se encuentra regresa un NULL char *tipo=strstr(linea,tda); size_t tam_tipo=0; //Posiblemente se trata de la definición if(tipo!=NULL) { tam_tipo=strlen(tipo); //Verifica si se trata de una redefinición if(strstr(linea, typedef_)!=NULL) { //verifica si es el nombre de la estructura // o es la redefinición. if(tam_tipo-1==strlen(tda)) { for(i=tam_sub+1, j=0; i<tam_cad-tam_tipo-1; i++, j++) nueva_str[j]=linea[i]; 141 nueva_str[i]='\0'; salida=RL_BNE; } else { //Si la redefinición esta junto con la estructura if(strstr(linea,"{")) salida=RE_END; //Solamente se trata de la redefinición. else salida=RL_SBE; } } //La estructura ha sido encontrada. else { printf("\t Estructura encontrada \n\t"); printf("\t\t%s\n",linea); salida=RE_END; } } return salida; } //Verifica que los campos del TDA sean tipos de datos reconocidos una vez que esta ha sido encontrado, //si alguno de los campos no es reconocido realiza la búsqueda de este mismo int verifica_campos(char *estruc, reg *cola, char *ntok) { char *aux, *cadmod, *tipo; char delim[]=";"; int i=0, j=0, vr=1; //pide memoria para la realización de su trabajo cadmod=(char *)malloc(sizeof(char)*strlen(estruc)+1); tipo=(char *)malloc(sizeof(char)*strlen(estruc)+1); if(cadmod != NULL && tipo!=NULL) { //Busca el cuerpo de la estructura, para obtener los campos while(estruc[i++]!='{'); //Copia los campos de la estructura a una nueva cadena for(j=0; i<strlen(estruc)-2; j++, i++) cadmod[j]=estruc[i]; cadmod[j]='\0'; //Obtiene campo por campo mediante el delimitador ";" aux=strtok(cadmod, delim); while(aux!=NULL) { if(aux!=NULL) { i=0; //Este ciclo obtiene el tipo de dato de la declaración while(aux[i++]!=' ') 142 tipo[i-1]=aux[i-1]; tipo[i-1]='\0'; if(!dato_c(tipo)) //verifica si se trata de un tipo de dato de C { //Verifica si se trata de una estructura ya registrada if(!token_reg(tipo, cola)) { //Copia el nuevo token para seguir buscando for(i=0; tipo[i]!='\0'; i++) ntok[i]=tipo[i]; vr=0; //Regresa un valor de error. break; } } } aux=strtok(NULL, delim); } } else perror("Error... Memoria insuficiente"); return vr; } //Determina si el tipo de dato que se le pasa es un tipo de dato de C int dato_c(char *decl) { char *tipo=NULL; if(strstr(decl,"char")!=NULL) return 1; if(strstr(decl,"int")!=NULL) return 1; if(strstr(decl,"unsigned")!=NULL) return 1; if(strstr(decl,"long")!=NULL) return 1; if(strstr(decl,"float")!=NULL) return 1; if(strstr(decl,"double")!=NULL) return 1; if(tipo==NULL) return 0; } /***** Funciones para el de los TDA utilizados en la búsqueda de los TDA's ****/ reg *crea_nodo(char *linea, char *token) { reg *ap=NULL; ap=(reg *)malloc(sizeof(ap)); if(ap!=NULL) { ap->str_reg=(char *)malloc(strlen(linea)+1); ap->token=(char *)malloc(strlen(token)+1); if(ap->str_reg!=NULL && ap->token!=NULL) 143 { strcpy(ap->str_reg, linea); strcpy(ap->str_reg, linea); ap->sig=NULL; } else perror("Error... Memoria insuficiente"); } else perror("Error... Memoria insuficiente"); return ap; } reg *encola(reg *cola, reg *ap) { reg *aux=cola; if(aux==NULL) cola=ap; else { while(aux->sig!=NULL) aux=aux->sig; aux->sig=ap; } return cola; } //Función encargada de registrar lo relacionado con la búsqueda //verifica primero si no ha sido registrada, para evitar duplicidad reg *registra_TDA(char *linea, char *aux, reg *col_dat, FILE *flujo) { reg *ap=NULL; int campo=1; //Verifica el registro de la linea if(!existe(linea, col_dat)) { ap=crea_nodo(linea, aux); col_dat=encola(col_dat, ap); } do { //Si alguno de los campos no existe se llama a la función para su búsqueda desde el inicio campo=verifica_campos(linea, col_dat, aux); if(!campo) { rewind(flujo); col_dat=busca_estructuras(aux, flujo, col_dat); } }while(campo==0); return col_dat; } 144 //Función encargada escribir en el archivo de salida el resultado de la búsqueda void crea_arout(reg *estructuras, FILE *salida, char *tda) { reg *ap=NULL; int encon_ptr_sig=0; char ptr_sig[20]=""; if(estructuras!=NULL) { //Escribe un redefinición del TDA para que sea usado por el programa realizado //en la parte I fprintf(salida,"\n typedef struct %s nodo;\n", tda); do { ap=estructuras; fprintf(salida, "%s\n", ap->str_reg); if(!encon_ptr_sig) { encon_ptr_sig=existe_asterisco(ap->str_reg); if(encon_ptr_sig) { busca_ptr_sig(ap->str_reg, ptr_sig); printf("\t El apuntador al siguiente nodo es: %s\n",ptr_sig); } } estructuras=estructuras->sig; free(ap); }while(estructuras!=NULL); crea_func(ptr_sig); } else printf("La estructura no ha sido encontrada...\n"); } /***** Verifica que en la línea se encuentre el * el cual es el indicador del campo que apunta al siguiente elemento en una lista. *****/ int existe_asterisco(char *ptr_sig) { int i=0, vr=0; while(ptr_sig[i]!='\0') if(ptr_sig[i++]=='*') { vr=1; break; } return vr; } /***** Esta función obtiene el nombre del campo, el cual es un apuntador a un elemento del mismo tipo, necesario para la creación del TDA lista *****/ void busca_ptr_sig(char *struc, char *ptr_sig) 145 { char *aux, *aux2; aux=strtok(struc,";"); while(aux!=NULL) { aux=strtok(NULL,";"); if(existe_asterisco(aux)) { aux2=strtok(aux,"*"); aux2=strtok(NULL,"*"); strcpy(ptr_sig, aux2); break; } } } /***** Esta función crea el archivo que contiene las funciones para la inserción y eliminación de nodos que son llamadas por las funciones Envia_TDA y Recibe_TDA respectivamente *****/ void crea_func(char *ptr_sig) { FILE *fuente, *salida; char caracter; fuente=fopen("./MPI_lista.orig","rb"); salida=fopen("./MPI_lista.c","wb"); //Archivo que contiene la estructura principal de las funciones. if(fuente!=NULL && salida!=NULL) { while(!feof(fuente)) { fread(&caracter, sizeof(char), 1, fuente); //El carácter @ indica que este será substituido por el campo que es un apuntador if(caracter!='@') fwrite(&caracter, sizeof(char), 1, salida); else fwrite(ptr_sig, sizeof(char),strlen(ptr_sig),salida); } fseek(salida,-1,SEEK_END); //Esta ultima escritura es para evitar que sea repetido el ultimo carácter, //es posible que no sea necesaria, si se tiene problemas se deberá comentar. caracter=' '; fwrite(&caracter, sizeof(char), 1, salida); fclose(fuente); fclose(salida); } else perror("No se puede crear el archivo de salida necesario para el manejo del TDA lista...."); } 146 9.4. Anexo D (Códigos Fuentes Versión final) Listado “protos.h” #include <semaphore.h> #include <pthread.h> #include "mpi.h" #include "def_TDA.h" //Este archivo de inclusión si será incluido en el proyecto final, //incluye la definición del TDAS que se va a utilizar. #define CANTIDAD 0 #define ELEMENTO 1 #define COORDINADOR 0 #define STRUCTSIZE sizeof(nodo) /***** Estructura utilizada para lograr el balance del TDA *****/ typedef union Nodos_TDA e_TDA; union Nodos_TDA { nodo ap; char mensaje[STRUCTSIZE]; }; /***** Estructura de datos necesaria para que los hilos puedan manejar el TDA y la migración de datos a otros nodos del cluster *****/ typedef struct Parametros_Balance { nodo **TDA; int MPI_DEST; int MPI_ELEM; int MPI_SIGNAL_TERM; }MPI_Balance; sem_t signal; //Señal para indicar al hilo que envié los datos a otros nodos. pthread_t envio, recepción, monitor; //hilo para el manejo de los datos pthread_mutex_t candado, candado_estructura; //Candado para garantizar la exclusión mutua. /***** Prototipos de las funciones encargadas del balance de la carga ****/ void MPI_Init_balance(MPI_Balance *par_bal, nodo **lista); void MPI_Finaliza_balance(MPI_Balance *par_bal); void Envia_TDA(void *datos); void Recibe_TDA(void *datos); void Monitor(void *datos); void Cierra_candados(void); void Abre_candados(void); void Inicia_balance(void); /***** Funciones obtenidas con el programa scan de la sección 2 *****/ void inserta_nodo_lista_MPI(nodo **lista, nodo *elem); void elimina_nodo_lista_MPI(nodo **lista, nodo *elem); 147 void Asigna_null_asig(nodo *ap); Listado “MPI_lista.orig” /***** Archivo obtenido de la búsqueda realizada para la obtención de la estructura necesaria para la creación del TDA lista *****/ #include "protos.h" /***** Inserción de nodo en la lista *****/ void inserta_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista; if(es_vacia(ap)) *lista=elem; else { while(ap->@!=NULL) ap=ap->@; ap->@=elem; } } /***** Elimina un nodo de la lista *****/ void elimina_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista, *aux=NULL; if(!es_vacia(*lista)) { if(ap=elem) { aux=*lista; *lista=aux->@; free(aux); } else while(ap->@!=NULL) { if(ap->@==elem->@) { aux=ap->@; ap->sig=aux->@; free(aux); break; } ap=ap->@; } } } /***** Inicia el apuntador al siguiente elemento de la lista con NULL *****/ void Asigna_null_asig(nodo *ap) { ap->@=NULL; } 148 Listado “balance.c” /***** Esta es el archivo que lleva a cabo el balance de carga, no requiere mayores cambios por el usuario que los indicados en la documentación misma, esto para darle mayor libertad al usuario de realizar sus funciones para el manejo del TDA. *****/ #include "protos.h" void MPI_Init_balance(MPI_Balance *par_bal, nodo **lista) { //Se inicia el semáforo el cual indicará el momento para comenzar el balance sem_init(&signal,0,0); //Iniciación de la estructura //Se indica la dirección de los datos que podrían ser enviados a otro proceso par_bal->TDA=lista; //Se inicia el parámetro destino, con un valor igual al proceso par_bal->MPI_DEST=0; //Se inicia el número de elementos a enviar par_bal->MPI_ELEM=0; //Se inicia la señal que indica a los hilos que terminen par_bal->MPI_SIGNAL_TERM=0; //Iniciación de los hilos para el balance pthread_create(&envio, NULL, (void *)&Envia_TDA, (void *)par_bal); pthread_create(&recepcion, NULL, (void *)&Recibe_TDA, (void *)par_bal); //Esta Función no va a ser necesaria para el uso en general //Este hilo es el encargado de determinar a quien, cuantos y en que momento le serán enviados datos pthread_create(&monitor, NULL,(void *)&Monitor,(void *)par_bal); } void MPI_Finaliza_balance(MPI_Balance *par_bal) { int vr_env=0, vr_rec=0, vr_mon=0; //Se manda la señal a los hilos para que terminen su trabajo par_bal->MPI_SIGNAL_TERM=1; //Se espera la terminación de los hilos que realizan el balance de carga. //Y se guarda el valor de retorno. pthread_join(monitor,(void *)&vr_mon); pthread_join(envio,(void *)&vr_env); pthread_join(recepcion, (void *)&vr_rec); } /***** En este primer acercamiento se envía campo por campo ****/ void Envia_TDA(void *datos) { int i=0, vr=0; nodo *ap=NULL, *aux=NULL, *aux_2=NULL; e_TDA elem; MPI_Balance *param_bal=(MPI_Balance *)datos; int longitud_lista=0; while(!param_bal->MPI_SIGNAL_TERM) 149 { if(sem_trywait(&signal)==0) { //Se realiza una validación para no enviarse a sí mismo los datos y/o evitar enviar un mensaje innecesario if(param_bal->MPI_DEST!=id && param_bal->MPI_ELEM) {//Se indica al proceso principal que si hubo balance vr=1; //se cierra el candado para garantizar la integridad de los datos Cierra_candados(); //Se manda el mensaje que indica los elementos a migrar. MPI_Send(&param_bal->MPI_ELEM, 1, MPI_INT, param_bal->MPI_DEST, CANTIDAD, MPI_COMM_WORLD); for(i=0; i<param_bal->MPI_ELEM; i++) { ap=*param_bal->TDA; //Se realiza una copia del nodo en el elemento de la unión para poder ser transferido. elem.ap=*ap; //Es importante inicializar el campo a nullo Asigna_null_asig(&elem.ap); //Se envía el nodo MPI_Send(elem.mensaje, STRUCTSIZE, MPI_CHAR, param_bal>MPI_DEST, ELEMENTO, MPI_COMM_WORLD); //Se elimina el nodo de la lista elimina_nodo_lista_MPI(param_bal->TDA, ap->sig); } Abre_candados(); } } } pthread_exit((void *)&vr); } /***** Él(los) proceso(s) receptor(es) reconstruye(n) el TDA que les es enviado *****/ void Recibe_TDA(void *datos) { int num_elem=0, i=0, fuente=0, vr=0, bandera=0; e_TDA elem; nodo *ap=NULL; MPI_Balance *param_bal=(MPI_Balance *)datos; MPI_Status estado; do { //Se realiza una prueba, para ver si se han recibido mensajes MPI_Iprobe(MPI_ANY_SOURCE, CANTIDAD, MPI_COMM_WORLD, &bandera, &estado); if(bandera)//Verificación de recepción de mensajes. {//Se indica al proceso principal que si hubo balance vr=1; //Obtención de la fuente que envío el mensaje. fuente=estado.MPI_SOURCE; //Recepción de la cantidad de elementos a recibir. MPI_Recv(&num_elem, 1, MPI_INT, fuente, CANTIDAD, MPI_COMM_WORLD, &estado); //Se cierra el candado para garantizar la integridad. Cierra_candados(); for(i=0; i<num_elem; i++) { 150 ap=(nodo *)malloc(sizeof(nodo)); if(ap!=NULL) { MPI_Recv(elem.mensaje, STRUCTSIZE, MPI_CHAR, fuente, ELEMENTO, MPI_COMM_WORLD, &estado); *ap=elem.ap; inserta_nodo_lista_MPI(param_bal->TDA, ap); } else perror("Error... Memoria Insuficiente, imposible recibir los elementos..."); } //Se habré el candado nuevamente. Abre_candados(); } }while(!param_bal->MPI_SIGNAL_TERM); pthread_exit((void *)&vr); } void Cierra_candados(void) { pthread_mutex_lock(&candado); pthread_mutex_ lock(&candado_estructura); } void Abre_candados(void) { pthread_mutex_unlock(&candado); pthread_mutex_unlock(&candado_estructura); } void Inicia_balance(void) { sem_post(&signal); } 151 Listado “scan.c” #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXCAD 20000 #define RE_END 0 #define RL_SBE 1 #define RL_BNE 2 #define NR_SBE 4 //Registra Estructura t Termina //Registra Línea y Sigue Buscando Estructura //Registra Línea y Busca Nueva Estructura //No Registres y Sigue Buscando Estructura /***** Estructura auxiliar para la identificación del registro de los TDA's *****/ typedef struct estruct_reg reg; struct estruct_reg { char *str_reg; char *token; reg *sig; }; /***** Funciones para la búsqueda de la estructura *****/ reg *busca_estructuras(char *tda, FILE *flujo, reg *col_dat); char *obten_bloque(FILE *flujo); void descarta_funcion(FILE *flujo); /***** Funciones de verificación *****/ int existe(char *linea,reg *cola); int token_reg(char *linea, reg *cola); int dato_c(char *decl); int verifica_exist(char *linea, char *tda, char *nueva_str); int verifica_campos(char *estruc, reg *cola, char *ntok); int existe_asterisco(char *ptr_sig); /***** Funciones para el manejo de los TDA's utilizados para el registro *****/ reg *crea_nodo(char *linea, char *token); reg *encola(reg *cola, reg *ap); reg *registra_TDA(char *linea, char *aux, reg *col_dat, FILE *flujo); void crea_arout(reg *estructuras, FILE *salida, char *tda); void busca_ptr_sig(char *struc, char *ptr_sig); void crea_func(char *ptr_sig); main(int argc, char *argv[]) { FILE *flujo, *salida; reg *estructuras=NULL; if(argc>2) { if((flujo=fopen(argv[1],"r"))!=NULL) { printf("\t Escaneando archivo \"%s\" \n", argv[1]); printf("\t\t Buscando TDA --> %s... \n", argv[2]); printf("\t\t Creando archivo de salida...\n"); if((salida=fopen("./def_TDA.h","w"))!=NULL) 152 { //La función busca la estructura y regresa un TDA con toda la información de la misma estructuras=busca_estructuras(argv[2], flujo, estructuras); //Se guardan las estructuras en un archivo y se cierran los flujos crea_arout(estructuras, salida, argv[2]); fclose(flujo); fclose(salida); } else perror("Error... No se puede crear el archivo de salida"); } else perror("Error... Al abrir el archivo, verifique ruta y nombre"); } else { if(argc==2) perror("Error... Argumentos insuficientes"); else perror("Error... sin argumentos en main()\n"); printf("\t Prueba con: %s <archivo.c | archivo.h> <tda>\n\n",argv[0]); printf("\t<archivo.c | archvio.h>: archivo fuente en donde se deberá buscar la estructura \n"); printf("\t <tda>: es el nombre de la estructura a ser buscada \n"); } return 0; } /********** Funciones que auxilian a la búsqueda del TDA *********/ //Esta función es la encargada de recorrer todo el archivo en busca del TDA //lo hace por bloques que pudiesen ser una estructura reg *busca_estructuras(char *tda, FILE *flujo, reg *col_dat) { char linea[MAXCAD]="", nueva_str[MAXCAD]="", aux[MAXCAD]; int vr=0, salir=0, k=0; fpos_t *inicio; reg *ap=NULL; strcpy(aux,tda); while(!feof(flujo)) { //La función regresa un bloque que pudiera ser una estructura strcpy(linea, obten_bloque(flujo)); // Busca en el bloque el tda que solicitado vr=verifica_exist(linea, aux, nueva_str); switch(vr) { case RE_END: //Esta opción indica que el TDA ha sido encontrado col_dat=registra_TDA(linea, aux, col_dat, flujo); salir=1; break; 153 case RL_SBE: //Esta opción indica que se trata de una redefinición, //pero se sigue buscando la misma estructura col_dat=registra_TDA(linea, aux, col_dat, flujo); salir=0; break; case RL_BNE: //Esta indica que se trata de una redefinición y se buscara la original col_dat=registra_TDA(linea, aux, col_dat, flujo); strcpy(aux, nueva_str); salir=0; break; default: //no se ha encontrado nada relacionado con la estructura break; } if(salir) break; } return col_dat; } //Esta función es la encargada de buscar en el archivo, lo hace por bloques que pudiesen ser //declaraciones de estructuras, sólo regresa los posibles candidatos. //Es capas de ignorar prototipos, archivos de inclusión y funciones. char *obten_bloque(FILE *flujo) { int llave=0, term_lin=0, paren_a=0, paren_c=0, i=0; char linea[MAXCAD]; /*El siguiente ciclo es capaz de encontrar bloques que puedan ser considerados como una estructura.*/ while(!feof(flujo)) { linea[i]=fgetc(flujo); switch(linea[i]) { //Ignora los archivos de cabecera case '#': while(!feof(flujo)) if(fgetc(flujo)=='\n') break; i=0; break; //verifica si no hay llaves abiertas para salir case ';': if(paren_a && paren_c)//Descarta los prototipos paren_a=paren_c=i=0; else { if(llave==0) term_lin=1; i++; } break; //Elimina los saltos de línea y tabuladores innecesarios case '\n': break; case '\t': break; //Registra si se han abierto o cerrado llaves case '{': if(paren_a && paren_c) 154 { //Evita buscar la definición dentro de una función descarta_funcion(flujo); paren_a=paren_c=i=llave=0; } else llave++; i++; break; case '}': llave--; i++; break; //Registra si se encuentra posiblemente una función case '(': paren_a++; i++; break; case ')': paren_c++; i++; break; default: i++; } if(term_lin) //Pone el fin de línea { linea[i]='\0'; break; } } return linea; } //Esta función se encarga de eliminar lo que se considera como el cuerpo de la función void descarta_funcion(FILE *flujo) { int llave=1; char basura; while(!feof(flujo)) { switch(fgetc(flujo)) { case '{': llave++; break; case '}': llave--; break; default: break; } if(!llave) break; } } /****** Funciones que auxilian en la identificación, registro y verificación del TDA y sus campos *****/ //Esta función se encarga de verificar si la línea ya fue registrada, para evitar duplicarla int existe(char *linea, reg *cola) { reg *ap=cola; int vr=0, iguales=0; while(ap!=NULL) { iguales=strcmp(linea,ap->str_reg); if(iguales==0) { vr=1; 155 break; } ap=ap->sig; } return vr; } //Verifica si un token ya fue buscado, para evitar buscarlo nuevamente al verificar los campos de la estructura. int token_reg(char *linea, reg *cola) { reg *ap=cola; int vr=0; while(ap!=NULL) { vr=strcmp(linea,ap->token); if(vr==0) { vr=1; break; } ap=ap->sig; } return vr; } //Esta función es la encargada de verificar si el bloque que le es pasado //es el TDA buscado int verifica_exist(char *linea, char *tda, char *nueva_str) { int salida=NR_SBE,j=0,i=0; char typedef_[]="typedef"; //cadena para determinar si la estructura pasada es un redefinición. size_t tam_sub=strlen(typedef_), tam_cad=strlen(linea); //Esta función es la encargada de determinar si el TDA buscado se encuentra en //en el bloque, si no se encuentra regresa un NULL char *tipo=strstr(linea,tda); size_t tam_tipo=0; //Posiblemente se trata de la definición if(tipo!=NULL) { tam_tipo=strlen(tipo); //Verifica si se trata de una redefinición if(strstr(linea, typedef_)!=NULL) { //verifica si es el nombre de la estructura // o es la redefinición. if(tam_tipo-1==strlen(tda)) { for(i=tam_sub+1, j=0; i<tam_cad-tam_tipo-1; i++, j++) nueva_str[j]=linea[i]; nueva_str[i]='\0'; salida=RL_BNE; 156 } else { //Si la redefinición esta junto con la estructura if(strstr(linea,"{")) salida=RE_END; //Solamente se trata de la redefinición. else salida=RL_SBE; } } //La estructura ha sido encontrada. else { printf("\t Estructura encontrada \n\t"); printf("\t\t%s\n",linea); salida=RE_END; } } return salida; } //Verifica que los camp os del TDA sean tipos de datos reconocidos una vez que esta ha sido encontrado, //si alguno de los campos no es reconocido realiza la búsqueda de este mismo int verifica_campos(char *estruc, reg *cola, char *ntok) { char *aux, *cadmod, *tipo; char delim[]=";"; int i=0, j=0, vr=1; //pide memoria para la realización de su trabajo cadmod=(char *)malloc(sizeof(char)*strlen(estruc)+1); tipo=(char *)malloc(sizeof(char)*strlen(estruc)+1); if(cadmod != NULL && tipo!=NULL) { //Busca el cuerpo de la estructura, para obtener los campos while(estruc[i++]!='{'); //Copia los campos de la estructura a una nueva cadena for(j=0; i<strlen(estruc)-2; j++, i++) cadmod[j]=estruc[i]; cadmod[j]='\0'; //Obtiene campo por campo mediante el delimitador ";" aux=strtok(cadmod, delim); while(aux!=NULL) { if(aux!=NULL) { i=0; //Este ciclo obtiene el tipo de dato de la declaración while(aux[i++]!=' ') tipo[i-1]=aux[i-1]; tipo[i-1]='\0'; 157 if(!dato_c(tipo)) //verifica si se trata de un tipo de dato de C { //Verifica si se trata de una estructura ya registrada if(!token_reg(tipo, cola)) { //Copia el nuevo token para seguir buscando for(i=0; tipo[i]!='\0'; i++) ntok[i]=tipo[i]; vr=0; //Regresa un valor de error. break; } } } aux=strtok(NULL, delim); } } else perror("Error... Memoria insuficiente"); return vr; } //Determina si el tipo de dato que se le pasa es un tipo de dato de C int dato_c(char *decl) { char *tipo=NULL; if(strstr(decl,"char")!=NULL) return 1; if(strstr(decl,"int")!=NULL) return 1; if(strstr(decl,"unsigned")!=NULL) return 1; if(strstr(decl,"long")!=NULL) return 1; if(strstr(decl,"float")!=NULL) return 1; if(strstr(decl,"double")!=NULL) return 1; if(tipo==NULL) return 0; } /***** Funciones para el de los TDA utilizados en la búsqueda de los TDA's ****/ reg *crea_nodo(char *linea, char *token) { reg *ap=NULL; ap=(reg *)malloc(sizeof(ap)); if(ap!=NULL) { ap->str_reg=(char *)malloc(strlen(linea)+1); ap->token=(char *)malloc(strlen(token)+1); if(ap->str_reg!=NULL && ap->token!=NULL) { strcpy(ap->str_reg, linea); 158 strcpy(ap->str_reg, linea); ap->sig=NULL; } else perror("Error... Memoria insuficiente"); } else perror("Error... Memoria insuficiente"); return ap; } reg *encola(reg *cola, reg *ap) { reg *aux=cola; if(aux==NULL) cola=ap; else { while(aux->sig!=NULL) aux=aux->sig; aux->sig=ap; } return cola; } //Función encargada de registrar lo relacionado con la búsqueda //verifica primero si no ha sido registrada, para evitar duplicidad reg *registra_TDA(char *linea, char *aux, reg *col_dat, FILE *flujo) { reg *ap=NULL; int campo=1; //Verifica el registro de la linea if(!existe(linea, col_dat)) { ap=crea_nodo(linea, aux); col_dat=encola(col_dat, ap); } do { //Si alguno de los campos no existe se llama a la función para su búsqueda desde el inicio campo=verifica_campos(linea, col_dat, aux); if(!campo) { rewind(flujo); col_dat=busca_estructuras(aux, flujo, col_dat); } }while(campo==0); return col_dat; } 159 //Función encargada escribir en el archivo de salida el resultado de la búsqueda void crea_arout(reg *estructuras, FILE *salida, char *tda) { reg *ap=NULL; int encon_ptr_sig=0; char ptr_sig[20]=""; if(estructuras!=NULL) { //Escribe un redefinición del TDA para que sea usado por el programa realizado //en la parte I fprintf(salida,"\n typedef struct %s nodo;\n", tda); do { ap=estructuras; fprintf(salida, "%s\n", ap->str_reg); if(!encon_ptr_sig) { encon_ptr_sig=existe_asterisco(ap->str_reg); if(encon_ptr_sig) { busca_ptr_sig(ap->str_reg, ptr_sig); printf("\t El apuntador al siguiente nodo es: %s\n",ptr_sig); } } estructuras=estructuras->sig; free(ap); }while(estructuras!=NULL); crea_func(ptr_sig); } else printf("La estructura no ha sido encontrada...\n"); } /***** Verifica que en la línea se encuentre el * el cual es el indicador del campo que apunta al siguiente elemento en una lista. *****/ int existe_asterisco(char *ptr_sig) { int i=0, vr=0; while(ptr_sig[i]!='\0') if(ptr_sig[i++]=='*') { vr=1; break; } return vr; } /***** Esta función obtiene el nombre del campo, el cual es un apuntador a un elemento del mismo tipo, necesario para la creación del TDA lista *****/ void busca_ptr_sig(char *struc, char *ptr_sig) { 160 char *aux, *aux2; aux=strtok(struc,";"); while(aux!=NULL) { aux=strtok(NULL,";"); if(existe_asterisco(aux)) { aux2=strtok(aux,"*"); aux2=strtok(NULL,"*"); strcpy(ptr_sig, aux2); break; } } } /***** Esta función crea el archivo que contiene las funciones para la inserción y eliminación de nodos que son llamadas por las funciones Envia_TDA y Recibe_TDA respectivamente *****/ void crea_func(char *ptr_sig) { FILE *fuente, *salida; char caracter; fuente=fopen("./MPI_lista.orig","rb"); salida=fopen("./MPI_lista.c","wb"); //Archivo que contiene la estructura principal de las funciones. if(fuente!=NULL && salida!=NULL) { while(!feof(fuente)) { fread(&caracter, sizeof(char), 1, fuente); //El carácter @ indica que este será substituido por el campo que es un apuntador if(caracter!='@') fwrite(&caracter, sizeof(char), 1, salida); else fwrite(ptr_sig, sizeof(char),strlen(ptr_sig),salida); } fseek(salida,-1,SEEK_END); //Esta ultima escritura es para evitar que sea repetido el ultimo carácter, //es posible que no sea necesaria, si se tiene problemas se deberá comentar. caracter=' '; fwrite(&caracter, sizeof(char), 1, salida); fclose(fuente); fclose(salida); } else perror("No se puede crear el archivo de salida necesario para el manejo del TDA lista...."); } 161 9.4. Anexo E Manual de usuario “Rutinas de Balance” Para hacer uso de las rutinas de balance se deberán seguir los siguientes pasos. Como primer paso se debe correr el programa “scan” pasándole como argumentos el nombre del archivo en donde se encuentra declarada la estructura a buscar, y el nombre de dicha estructura, a continuación se presenta un ejemplo de este procedimiento. Supongamos que tenemos la siguiente estructura en el archivo “def_structura.h” typedef struct Registro Nodo; struct Registro { char nombre[40]; unsigned matricula; int edad; char sexo; Nodo *sig; }; Para que el programa “scan” genere los archivos correspondientes sólo se deberá dar alguna de las siguientes instrucciones desde la línea de comandos. $: ./scan def_estructura.c Nodo; $: ./scan def_estructura.c Registro; El resultado de estas instrucciones es la misma, para el archivo “def_TDA.h” es: typedef struct Nodo nodo typedef struct Registro Nodo; struct Registro{char nombre[40];unsigned matricula; int edad; char sexo; Nodo *sig;}; En el caso del archivo “MPI_lista.c” el resultado es: /***** Archivo obtenido de la búsqueda realizada para la obtención de la estructura necesaria para la creación del TDA lista *****/ #include "protos.h" /***** Inserción de nodo en la lista *****/ void inserta_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista; if(es_vacia(ap)) *lista=elem; else { while(ap->sig!=NULL) ap=ap->sig; ap->sig=elem; } } /***** Elimina un nodo de la lista *****/ void elimina_nodo_lista_MPI(nodo **lista, nodo *elem) { nodo *ap=*lista, *aux=NULL; 162 if(!es_vacia(*lista)) { if(ap=elem) { aux=*lista; *lista=aux->sig; free(aux); } else while(ap->sig!=NULL) { if(ap->sig==elem->sig) { aux=ap->sig; ap->sig=aux->sig; free(aux); break; } ap=ap->sig; } } } /***** Inicia el apuntador al siguiente elemento de la lista con NULL *****/ void Asigna_null_asig(nodo *ap) { ap->sig=NULL; } Una vez obtenidos ambos archivos para hacer uso de las rutinas en el código de nuestra aplicación se deberá declarar una variable de tipo MPI_Balance, al llamar a las rutinas de balance esta variable deberá ser pasada por referencia al igual que la lista que deberá ser declarada de la siguiente forma: MPI_Balance var; Nodo *lista; //Rutinas de iniciación de MPI. MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &id); MPI_Comm_size(MPI_COMM_WORLD, &proc); MPI_Get_processor_name(nombre, &long_nom); /* Aquí va el código de creación de la lista */ /* Aquí va la llamada al monitor, el cual se encargar de censar la carga del nodo del cluster //Se realiza las llamadas a la rutina que comienza el balance MPI_Init_balance(&var, &lista); */ /* Código que hace uso de la lista, hay que recordar que ha estas alturas ya no se debe manejar la variable lista para el manejo de la misma sino el campo de la estructura el cual es: *var.TDA */ //Se terminan los cálculos se llama a la terminación de del balance MPI_Finaliza_balanceo(&var); MPI_Finaleze(); El monitor deberá cumplir con las siguientes características: 1. Deberá recibir como argumento una variable de tipo MPI_Balance creada en este proyecto. 163 2. Deberá realizar la llamada a las rutinas Abre_candados() y Cierra_candados() para garantizar la integridad de los datos en caso de hacer uso de la lista, como lo sería ver la cantidad de elementos de la lista. 3. Deberá de hacer la llamada a la rutina Inicia_balance(), la cual da la señal para que rutina de envío comience la migración de datos. Hay que recordar que el usuario deberá de realizar las invocaciones a las rutinas Abre_candados() y Cierra_candados() para garantizar la integridad de los datos, él deberá determinar cual es la sección critica de su código para realizar tales invocaciones. Lo único que tiene que tomar en cuenta es que las rutinas de balance agregan y/o eliminan nodos del TDA lista. El paso siguiente es la compilación y el enlazado del ejecutable para compilar se deberá además de incluir los fuentes del proyecto que se este trabajando, los fuentes “balance.c” y “MPI_lista.c” y para poder enlazarlos se deberá de agregar la opción -lpthread además de la necesarias para la compilación de nuestro fuente, por ejemplo, vea la siguiente forma: $:mpicc –o ejecutable mi_codigo.c balance.c MPI_lista.c -lpthread Es importante resaltar que como el código del usuario ya cuenta con la definición de la estructura utilizada para la creación del TDA lista, al incluir los archivos “balance.c” y/o “MPI_lista.c” estos incluyen en su archivo de inclusión al archivo “def_TDA.h” el cual también contendrá la definición de la estructura antes mencionada resultado del escaneo del programa “scan” la definición de dicho archivo deberá ser comentada para poder compilar el proyecto con las rutinas de balance de carga. 164 9.5. Anexo F Manual de usuario programa “scan” El programa es muy sencillo de utilizar. Tan solo hay que realizar la llamada desde la línea de comandos de la siguiente forma: $: ./scan <archivo.c|archivo.h> <TDA> Si no es llamado de la esta forma el archivo desplegara un mensaje de error y una pequeña ayuda de cómo ejecutar el programa, por ejemplo: si tenemos el siguiente archivo “estructura.h”, con el siguiente contenido. struct Reg_Alumno { int clave; char nombre[30]; int edad; char sexo; char direc; struct Reg_Alumno *sig; }; Además el programa crea una línea extra, la cual es una redefinición del tipo de dato encontrado al término del escaneo utilizado en el programa de la primera parte, dicha redefinición es para el uso de las funciones necesarias para la migración de los datos. Para realizar el escaneo con el programa la forma de hacerlo es: $: ./scan estructura.h Reg_Alumno De tal forma que el resultado del ejemplo anterior es el archivo “def_TDA.h” con el siguiente contenido. typedef struct Reg_Alumno nodo; struct Reg_Alumno{int clave; char nombre[30];int edad; char sexo; char direc; struct Reg_Alumno *sig;}; 165