Proyecto de Investigación Depurador para Aplicaciones
Transcripción
Proyecto de Investigación Depurador para Aplicaciones
UNIVERSIDAD AUTÓNOMA METROPOLITANA UNIDAD IZTAPALAPA C. B. I. LICENCIATURA Proyecto de Investigación Depurador para Aplicaciones Distribuidas Asesora: Dra. Elizabeth Pérez Cortés Alejandra Carreón Castany Iván Rodríguez Serrano México D.F. a 26 de Junio del 2003 Índice Introducción 1. Marco teórico 1.1 Sistemas Distribuidos 1.1.1 Características 1.1.2 Aplicaciones y Ejemplos 1.1.3 Ventajas y Desventajas 1.2 Memoria Virtual Distribuida 1.2.1 Funcionamiento 1.2.2 Coherencia de Memoria 1.3 Programación multihilo 1.4 Depuradores 2 3 3 5 6 7 7 11 15 18 2. Descripción del Problema: Depuración de Aplicaciones en Paralelo 2.1 Determinismo y No Determinismo 2.2 Depuradores y Aplicaciones Distribuidas 2.3 Requisitos Funcionales 2.4 Requisitos de Interfaz 20 21 21 22 23 3. Soluciones existentes 3.1 Proyecto Athapascan 3.2 Sistema TreadMarks 24 25 26 4. Solución Propuesta 4.1 Antecedentes 4.2 Propuesta 4.2.1 Características Funcionales 4.2.2 Características de Interfaz 28 29 31 31 32 5. Implementación 5.1 Back-End. Manejo de Hilos 5.1.1 Puntos de Ruptura 5.1.2 Ejecución Paso a Paso 5.1.3 Abrazos mortales 5.2 Front-End. Interfaz de Usuario 5.2.1 Principales Pantallas 5.2.1 Diagrama de Secuencia de Pantallas 5.3 Acoplamiento 5.3.1 Sockets 5.3.2 Java Native Interface (JNI) 5.4 Pendientes 34 35 37 37 37 38 38 43 44 44 45 45 6. Conclusiones 47 2 Depuración de Aplicaciones Distribuidas CAPÍTULO 1 Marco Teórico Desde que comenzó la era de las computadoras, algunos avances tecnológicos han sido muy significativos e importantes. Uno de estos avances fue la invención de las redes de área local (LAN -Local Area Networks) y las redes de área amplia (WAN -World Area Networks), con las cuales es posible conectar y comunicar cientos o miles de computadoras para transferir información entre ellas en tiempos muy cortos. Así, actualmente se tienen sistemas de cómputo compuestos por un gran número de CPU s conectadas por una red de alta velocidad, a los que se les conoce como sistemas distribuidos. En este capítulo se definen algunos de los aspectos más importantes de los sistemas distribuidos, así como algunas de sus aplicaciones más importantes. Posteriormente se hablará de la Memoria Virtual Distribuida, describiendo su funcionamiento, la manera en la que se logra la coherencia de la memoria y la forma en que se programa. Finalmente se presenta el tema de los depuradores con una breve descripción de sus funciones básicas. 3 Depuración de Aplicaciones Distribuidas 1.1 Sistemas Distribuidos Un sistema distribuido es un conjunto de computadoras independientes con sistemas de comunicación. Una aplicación distribuida es un conjunto de procesos que se comunican y se sincronizan para realizar una tarea común. Entre estas aplicaciones podemos encuentran, las comparten datos (Bases de Datos), las que comparten hardware (como periféricos y getionadores de dispositivos de E/S) y las que comparten aplicaciones (Tarantella) y las aplicaciones que requieren una disminución de latencia en las tareas. Además de las aplicaciones en las que se comparten recursos, algunas aplicaciones son distribuidas por naturaleza, como las aplicaciones de bancos, la reservación de boletos en aerolíneas, las videoconferencias, etc. Para realizar todo esto se necesita de software especialmente diseñado que permita que las computadoras coordinen sus actividades, compartan los recursos del sistema (hardware, software y datos) y que además realice la asignación de dichos recursos. A diferencia de las máquinas multiprocesador, las cuales pueden contener varias CPU's y cada una de ellas tiene acceso físico a la memoria, los sistemas distribuidos carecen de esta memoria física compartida para lo cual se hace necesario, a fin de ejecutar las aplicaciones que usan esta memoria, la creación de una Memoria Virtual Distribuida(MVD). Un aspecto importante de los sistemas distribuidos es que los usuarios deben percibirlos como una única computadora, y no deben notar que el sistema está compuesto por un conjunto en el que los componentes pueden encontrarse en localidades diferentes. Se ha realizado mucho trabajo de investigación y desarrollo de los sistemas distribuidos y de sus principios fundamentales, y aunque este trabajo todavía no ha terminado, ya se tienen muchas implementaciones prácticas y un mayor conocimiento de su diseño. 1.1.1 Características Algunas de las características más importantes de los sistemas distribuidos no son consecuencia directa de las distribución, sino que se logran mediante software que debe ser cuidadosamente diseñado. Compartición de Recursos La compartición de recursos (hardware, software y datos) es la característica más importante de los sistemas distribuidos, y está fuertemente ligada al tipo de software disponible en el sistema. 4 Depuración de Aplicaciones Distribuidas Generalmente se utiliza el término manejador de recursos para denotar a un módulo de software que se encarga de manejar al conjunto de recursos de un determinado tipo. Mediante estos manejadores se puede accesar al conjunto de recursos compartidos. Bajo esta perspectiva, se tienen dos modelos de sistemas distribuidos: el modelo cliente-servidor y el modelo basado en objetos. En el primero se tiene un conjunto de procesos servidores que actúan como manejadores de recursos de un tipo, y un conjunto de procesos clientes que desarrollan tareas que requieren acceso a los recursos compartidos. Los procesos servidores atienden peticiones de los procesos clientes cuando estos necesitan acceder a los recursos. En el modelo basado en objetos, los recursos compartidos se ven como objetos con una interfaz de manejo de mensajes que provee el acceso a sus operaciones. Los objetos son distinguibles y se pueden mover por toda la red. Cuando un programa requiere el acceso a un recurso compartido, envía un mensaje al objeto correspondiente, éste efectúa el procedimiento adecuado y concede el acceso al recurso. Los dos modelos pueden ser implementados en diferentes ambientes de hardware y software. Concurrencia y Paralelismo La concurrencia y la ejecución en paralelo surgen automáticamente en los sistemas distribuidos debido a tres factores: la independencia de los recursos, la separación de las actividades de los usuarios y la localización de los servidores en computadoras diferentes. La separación de las actividades permite que éstas se ejecuten en paralelo, mientras que el acceso y la actualización de los recursos compartidos generan concurrencia, que debe ser sincronizada. Tolerancia a Fallas La tolerancia a fallas se logra más eficientemente en los sistemas distribuidos que en los sistemas centralizados. Se basa en dos aspectos: redundancia de hardware y recuperación de software. La redundancia de hardware se utiliza para asegurar que las tareas esenciales sean reasignadas a otra computadora cuando una falla. En tanto que la recuperación de software involucra el diseño del mismo para permitir que el estado de los datos se recupere (roll back) cuando ocurre una falla. En los sistemas distribuidos, generalmente, los servidores que son esenciales para poder efectuar operaciones críticas se duplican, así, si un servidor tiene una falla, se asegura que dichas operaciones puedan seguir efectuándose. 5 Depuración de Aplicaciones Distribuidas Transparencia La transparencia se define como "el ocultamiento" de la separación de componentes al usuario y al programador de aplicaciones en un sistema distribuido, así éste es percibido como un todo, y no como una colección de componentes independientes. Según la ANSA (Advanted Network Systems Architecture), existen ocho formas de transparencia: de acceso, localización, concurrencia, duplicación, fallas, migración, desempeño y escala. 1.1.2 Aplicaciones y Ejemplos Desde su aparición, los sistemas distribuidos han sido utilizados en diferentes aplicaciones. A continuación se presentan algunas de las más conocidas. Aplicaciones comerciales Estas aplicaciones requieren un alto nivel de seguridad, privacidad de la información y fiabilidad. Se caracterizan por el acceso concurrente a bases de datos, la existencia de puntos de servicio distribuidos en diferentes puntos geográficos, tiempos de respuesta garantizados y la posibilidad de que el sistema crezca tanto como la empresa lo requiera. Algunos de los ejemplos más comunes se presentan en los sistemas utilizados en aerolíneas para la reservación y venta de boletos, y las redes de operación de los bancos. Redes (LAN y WAN) El incremento en el número de computadoras que se conectan a través de este tipo de redes y el alcance del software que ayuda a su uso, hacen de ésta una de las aplicaciones más importantes de los sistemas distribuidos. Así, este tipo de redes de estaciones de trabajo es usado en un gran número de compañías y universidades en las que se comparten recursos de hardware y software, como impresoras, programas de aplicación, etc. Otro ejemplo importante de este tipo de aplicaciones es Internet, un conjunto de redes de área amplia y local que soporta un gran número de aplicaciones como el correo electrónico, el servicio de noticias, etc. La gran velocidad de las LAN ha sido muy importante para el desarrollo de sistemas distribuidos más transparentes, ya que permiten "esconder" mejor el efecto de la distribución de hardware y software. 6 Depuración de Aplicaciones Distribuidas Sistemas operativos distribuidos Un importante logro de la investigación y desarrollo de sistemas distribuidos ha sido el diseño de los sistemas operativos distribuidos, los cuales, al igual que los sistemas operativos tradicionales, son un conjunto de componentes de software que simplifican las tareas de programación y soportan el mayor número de aplicaciones posibles. La diferencia entre estos tipos de sistemas operativos es que los distribuidos son modulares y extensibles, por lo que pueden ser agregados nuevos componentes en respuesta a nuevas necesidades. El ejemplo más importante de estos sistemas es BSD Unix (Unix distribuido), desarrollado en la Universidad de California en Berkeley a finales de los 70's. En éste se incluía soporte para la comunicación entre procesos. Posteriormente, Sun Microsystems desarrolló NFS (Network File System) tomando como punto de partida el BSD Unix y le asoció RPC (Remote Procedure Call) y NIS (Network Information Services), que actualmente son usados en la mayoría de las implementaciones del Unix distribuido. Algunos otros sistemas operativos distribuidos como Amoeba, Mach y Chorus han surgido tratando de corregir las limitaciones y dificultades de los sistemas Unix multi-usuarios. 1.1.3 Ventajas y Desventajas En esta sección describiremos algunas de las ventajas que hacen de un sistema distribuido una opción viable, así como algunas de las desventajas que presentan. a) Ventajas Dos de las principales razones por las que se usan los sistemas distribuidos es el rendimiento y la economía: un conjunto de microprocesadores proporciona un mayor número de instrucciones por segundo (MIPS) que un sistema centralizado o un mainframe, ya que éstas se pueden ejecutar en forma paralela. Además, al añadir más procesadores al sistema se puede obtener un mejor precio/rendimiento. Además, los sistemas distribuidos permiten la compartición de recursos y de información, así como la transferencia inmediata de esta última en pequeñas y grandes distancias. Otro punto importante es la confiabilidad, ya que al distribuir el trabajo en varias máquinas permiten que el sistema pueda seguir funcionando si alguna de ellas falla, y así sólo se pierde un pequeño porcentaje del desempeño de todo el sistema. Cuando un mainframe resulta insuficiente para realizar el trabajo necesario, lo más común es reemplazarlo por uno más grande (si es que existe) o añadir uno nuevo, lo cual representaría un costo excesivo. En cambio, si se tiene un sistema distribuido, sólo es necesario añadir más procesadores al sistema, obteniendo un desarrollo gradual que resulta ser más fácil de solventar. 7 Depuración de Aplicaciones Distribuidas b) Desventajas La principal desventaja en la utilización de los sistemas distribuidos es el software, ya que aún no se cuenta con la experiencia suficiente en el diseño, implantación y uso de software distribuido. Por el momento no existe un ambiente de desarrollo integral, que provea al programador de las herramientas necesarias para el desarrollo de sus aplicaciones. Dichas herramientas se encuentran aún en una etapa poco madura, en virtud de ciertas complicaciones que un sistema distribuido añade. Por ejemplo, en la depuración de los sistemas distribuidos debe tenerse en cuenta un factor extra: el no-determinismo. Además, al contar con múltiples máquinas los puntos de falla se incrementan y se hace indispensable un software capaz de detectarlos y realizar las operaciones necesarias para evitar la perdida total del sistema. La seguridad y coherencia de los datos es un problema que no es necesariamente fácil de resolver en los sistemas distribuidos. Además existen ciertos datos que no deben ser accesibles por todos los usuarios del sistema, el cual no deberá permitir el acceso a esos datos. En este sentido, es más fácil prohibir el acceso en un sistema centralizado que en uno distribuido. Por último, puede darse una saturación de la red en la que se basa el sistema debido a que se sobrecarga de mensajes entre las computadoras o con el software que impide la pérdida de mensajes. 1.2 Memoria Virtual Distribuida (MVD) Los sistemas con varias CPU caen en alguna de las dos siguientes categorías: los que tiene memoria compartida y los que no. Las máquinas de memoria compartida (multiprocesadores) son más fáciles de programar pero más difíciles de construir, mientras que las máquinas sin memoria compartida (multicomputadoras) son más difíciles de programar pero más fáciles de construir. La Memoria Virtual Distribuida (MVD) es una técnica para facilitar la programación de las multicomputadoras, simulando la memoria compartida en ellas. En las siguientes secciones, para entender mejor la MVD, examinaremos un par de multiprocesadores de memoria compartida, ya que gran parte del funcionamiento de ésta se basa en tales máquinas. 1.2.1 Funcionamiento a) Multiprocesadores basados en bus Un bus es una colección de cables paralelos que conectan la CPU con la memoria, algunos de ellos con la dirección a la que desea leer o escribir en la CPU, otros más se utilizan para enviar o recibir datos y el resto para el control de las transferencias. Un bus esta integrado en un circuito, pero en muchos sistemas, los 8 Depuración de Aplicaciones Distribuidas buses son externos y se utilizan para conectar tarjetas, memoria y controladores de E/S a la CPU. Una forma práctica de construir un multiprocesador es basarlo en un bus al que se conectan dos o más CPU's. Cuando cualquiera de las CPU's desea realizar la lectura de una palabra, coloca la dirección de ésta en el bus y una señal en la línea de control, indicando que desea realizar una lectura. Cuando la memoria encuentra la palabra requerida, la coloca en el bus y coloca una señal en la línea de control anunciando que está lista, entonces la CPU realiza la lectura. La escritura se realiza de manera análoga. Para evitar que más de una CPU intente accesar a la memoria al mismo tiempo, existen varios esquemas de arbitraje. Por ejemplo, para utilizar el bus, la CPU primero coloca una señal especial de solicitud y espera el permiso para usarlo. Este permiso se puede generar de manera centralizada, utilizando un dispositivo de arbitraje, o de manera descentralizada, donde la primera CPU que realice una solicitud en el bus ganará cualquier conflicto. Se hace evidente que al colocar varias CPU's en un solo bus, éste de seguro se sobrecargará. Un método para reducir la carga del bus es equipando a cada CPU con un caché “husmeador”, llamado así porque husmea en el bus. Un protocolo común es el de escritura a través del caché. Cuando una CPU realiza la lectura de una palabra de memoria por primera vez, esa palabra es llevada por el bus y guardada en el caché de la CPU solicitante. Si más tarde la CPU la necesita, la toma directamente del caché sin necesidad de una solicitud a memoria, reduciendo así el tráfico del bus. La ocultación se realiza de forma independiente por cada CPU. En consecuencia, una misma palabra puede estar en dos o más CPU's. Por lo que cuando se realiza una escritura se presentan varios casos: si ninguna CPU tiene la palabra en su caché, la memoria sólo se actualiza. Si la CPU que realiza la escritura tiene la palabra en su caché, tanto éste como la memoria se actualizan, ésta última mediante el bus. Un problema surge cuando se desea escribir una palabra que se encuentra en dos o más cachés. Una posible solución es que si la palabra se encuentra en el caché de la CPU que desea realizar la escritura, la entrada de caché se actualiza. Esté o no, también se escribe en el bus para actualizar la memoria. Los demás cachés al ver la lectura, verifican si tienen la palabra y si es así, invalidan sus entradas de caché. El protocolo de escritura a través del caché es fácil de entender e implementar, pero tiene la seria desventaja de que todas las escrituras utilizan el bus, por lo que el número de CPU's que se conectan a un bus es aún muy pequeño como para permitir la construcción de multiprocesadores a gran escala. 9 Depuración de Aplicaciones Distribuidas b) Multiprocesadores basados en un anillo El siguiente paso hacia los sistemas de MVD son los multiprocesadores basados en un anillo, cuyo ejemplo es Memnet. En éste, un espacio de direcciones se divide en regiones, de modo que cada máquina tenga un pedazo para su pila y otros datos, y códigos no compartidos. La parte compartida es común para todas las máquinas (y distribuida entre ellas) y se guarda de manera consistente mediante un protocolo de hardware parecido a los utilizados en los multiprocesadores basados en bus. La memoria compartida se divide en bloques de 32 bytes, que es la unidad mediante la cual se realizan las transferencias entre las máquinas. Todas las máquinas en Memnet están conectadas mediante un anillo de fichas modificado. El anillo consta de 20 cables paralelos, que juntos permiten enviar 16 bits de datos y 4 bits de control cada 100 nanosegundos, para una velocidad de datos de 160 Mb/segundo. El dispositivo Memnet en cada máquina contiene una tabla, la cual consta de un espacio por cada bloque contenido en el espacio compartido de direcciones, y está indizada por el número de bloque. Cada entrada contiene un bit Válido que indica si el bloque está presente en el caché y actualizado; un bit exclusivo, especificando si la copia local, si existe, es la única; un bit Origen que se activa sólo si ésta es la máquina de origen del bloque; un bit interrupción, utilizado para forzar la interrupciones; y un campo de Posición, que indica la localización del bloque en el caché si está presente y es válido. Cuando la CPU desea leer una palabra de la memoria compartida, su dirección se transfiere al dispositivo Memnet, el cual verifica la tabla del bloque para ver si está presente. De ser así, la solicitud es satisfecha de inmediato. En caso contrario, el dispositivo Memnet espera hasta capturar la ficha que circula; después, coloca un paquete de solicitud en el anillo y suspende la CPU. El paquete de solicitud contiene la dirección deseada y un campo vacío de 32 bytes. Mientras el paquete pasa por el anillo, cada dispositivo en el camino verifica si tiene el bloque necesario. De ser así, coloca el bloque en el espacio vacío y modifica el encabezado del paquete para inhibir la acción de las demás máquinas. Si el bit exclusivo del bloque está activo, se limpia. Como el bloque debe de estar en algún lugar, cuando el paquete regresa al emisor, se garantiza que contiene el bloque solicitado. La CPU que envía la solicitud guarda entonces el bloque, satisface la solicitud y libera la CPU. Si la máquina solicitante no tiene espacio suficiente en su caché para contener el bloque recibido, se toma al azar un bloque oculto y lo envía a su lugar de origen, con lo que libera un espacio de caché. El trabajo de escritura es diferente al de lectura. Hay que definir tres casos. Si el bloque que contiene la palabra por escribir está presente y el bit Exclusivo está activo, la palabra sólo se escribe de manera local. 10 Depuración de Aplicaciones Distribuidas Si el bloque necesario está presente pero no es la única copia, se envía un paquete de invalidación por el anillo para que las otras máquinas desechen sus copias del bloque por escribir. Cuando el paquete de invalidación se envía de regreso al solicitante, el bit Exclusivo se activa para ese bloque y se procede a la escritura local. Si el bloque no está presente, se envía un paquete que combina una solicitud de lectura y una de invalidación. La primera máquina que tenga el bloque lo copia en el paquete y desecha su copia. Todas las demás máquinas sólo desechan el bloque de sus cachés. Cuando el paquete regresa al emisor, éste lo guarda y escribe en él. La mayor diferencia entre los multiprocesadores basados en bus y los basados en anillos como Memnet es que los primeros están fuertemente acoplados; con regularidad, las CPU, están en un gabinete. En contraste, en un multiprocesador basado en anillo pueden estar menos acopladas, e incluso potencialmente en escritorios dispersos en un edificio, como máquinas en una LAN, aunque este acoplamiento débil puede afectar el desempeño. Además a diferencia de un multiprocesador basado en bus, uno basado en anillo no tiene memoria física compartida. Los cachés son lo único que existe. En ambos aspectos, los multiprocesadores basados en un anillo son casi una implementación en hardware de la MVD. Una vez que hemos visto los conceptos necesarios de los multiprocesadores modernos, los cuales tienen mucho en común con los sistemas de MVD, podremos continuar con la definición de una MVD. En los primeros días de la computación distribuida, todos suponían de manera implícita que los programas en las máquinas sin memoria compartida físicamente (multicomputadoras) se ejecutaban, obviamente, en diferentes espacios de direcciones. Con este punto de vista se pensaba de manera natural en términos de la transferencia de mensajes entre espacios de direcciones ajenos. En 1986, Li propuso un esquema diferente, conocido como memoria virtual distribuida (Li, 1986, y Li y Hudak, 1989). En resumen Li y Hudak propusieron tener una colección de estaciones de trabajo conectadas por una LAN compartiendo un solo espacio de direcciones virtuales con páginas. En la variante más simple, cada página esta presente en una máquina. En el hardware se hace una referencia a las páginas locales, con toda la velocidad de la memoria. Un intento por referenciar una página en una máquina diferente causa un fallo de página en hardware. El cual realiza un señalamiento al sistema operativo (fallo de página). Entonces, el sistema operativo envía un mensaje a la máquina remota, quien encuentra la página necesaria y la envía al procesador solicitante, así se reinicia la instrucción detenida y se puede concluir. En esencia, este diseño es similar a los sistemas de memoria virtual tradicionales: cuando un proceso hace referencia a una página no residente, ocurre fallo de página y el sistema operativo busca la página y la asocia con el proceso. La diferencia aquí es que en lugar de obtener la página del disco, el sistema operativo la obtiene de otro procesador en la red. Por desgracia, aunque de hecho este sistema es fácil de programar y de construir, para muchas aplicaciones exhibe un desempeño pobre ya que las páginas andan de un lado a otro de la red. En los últimos años, un área de investigación 11 Depuración de Aplicaciones Distribuidas intensa ha sido la de hacer que estos sistemas de MVD sean más eficientes, de modo que todavía hay numerosas técnicas por descubrir. Todas éstas tienen el objetivo de minimizar el tráfico de la red y reducir la latencia entre el momento de una solicitud de memoria y el momento en que se satisface ésta. Un método consiste en no compartir todo el espacio de direcciones, si no sólo una porción seleccionada de éste, a saber, aquellas variables o estructuras de datos que se necesita utilizar en más de un proceso. En este modelo, las máquinas no tienen acceso a una memoria ordinaria, si no a una colección de variables compartidas, lo cual produce un alto nivel de abstracción. Esta estrategia no sólo reduce en gran medida la cantidad de datos a compartir, sino que, en la mayoría de los casos, se dispone de información considerable acerca de los datos compartidos disponibles, como su tipo, lo que puede ayudar a optimizar la implantación. Una posible optimización consiste en repetir las variables compartidas en varias máquinas. Al compartir las variables duplicadas en vez de páginas completas, el problema de simular un multiprocesador se reduce al de conservar de manera consistente varias copias de un conjunto de estructuras de datos tipificados. En potencia, las lecturas pueden ser de manera local, sin ningún tráfico en la red, y las escrituras mediante un protocolo de actualización con varias copias. Estos protocolos tienen amplio uso en los sistemas distribuidos de bases de datos, por lo que algunas ideas en ese campo podrán ser de utilidad. La diferencia real entre los multiprocesadores y los sistemas de MVD es la contestación a la siguiente pregunta: ¿se puede tener acceso a los datos remotos sólo mediante la referencia a sus direcciones? En todos los multiprocesadores, la respuesta es si. En los sistemas MVD la respuesta es no: se necesita siempre la intervención del software. De manera análoga, la existencia de memoria global sin enlaces, es decir, una memoria asociada con una CPU en particular, es posible en los multiprocesadores pero no en los sistemas de MDV. En los multiprocesadores, cuando se detecta un acceso remoto, se envía un mensaje a la memoria remota mediante un controlador de caché o MMU. En los sistemas MVD, se envía por medio del sistema operativo o el sistema de tiempo de ejecución. 1.2.2 Coherencia de Memoria Al permitirse varias copias de las páginas para escritura se mejora el desempeño, pues basta con actualizar cualquier copia. Pero un nuevo problema aparece: ¿cómo mantener consistentes todas las copias? El mantenimiento de una consistencia perfecta se vuelve casi imposible cuando las diversas copias se encuentran en maquinas diferentes que sólo pueden comunicarse al enviar mensajes a través de una red lenta (comparada con las velocidades de la memoria). Por lo tanto en algunos sistemas DMS se suelen implantar métodos de consistencia menos perfectos como precio de un mejor desempeño. 12 Depuración de Aplicaciones Distribuidas Un modelo de consistencia es en esencia un contrato entre el software y la memoria (Adve y Hill, 1990). Esto significa que si el software acuerda obedecer ciertas reglas, la memoria se compromete a trabajar de forma correcta. Existe un gran número de diferentes contratos, desde los que imponen sólo pequeñas restricciones hasta los que hacen de la programación algo casi imposible. En esta sección describiremos algunos de estos modelos de consistencia (contratos) utilizados en los sistemas MVD. a) Consistencia Estricta La consistencia más restrictiva es la llamada consistencia estricta. Es definida por la siguiente condición: Cualquier lectura a la locación de memoria X regresa el valor guardado por la operación de escritura más reciente a X. Esta definición implica la existencia de un tiempo global absoluto que haga que la determinación de "el más reciente" no sea ambigua. Los uniprocesadores han observado tradicionalmente esta consistencia. En resumen, cuando una memoria es estrictamente consistente, todas las escrituras son instantáneamente visibles a todos los procesos y un orden de tiempo global es mantenido. Si una localidad es cambiada, todas las subsecuentes lecturas a esta locación ven el nuevo valor, sin importar cuán poco después de los cambios las lecturas sean hechas, cuáles procesos hagan las lecturas o dónde estén localizados. Similarmente, si se hace una lectura, ésta toma el valor actual, no importa cuan rápido la siguiente escritura sea hecha. b) Consistencia Secuencial Aunque la consistencia estricta es el modelo de programación ideal, es casi imposible implantarla en un sistema distribuido. Además, la experiencia muestra que los programadores suelen controlar bien los modelos más débiles. Por ejemplo, en todos los libros de texto sobre sistemas operativos se analizan las secciones críticas y el problema de exclusión mutua. Este análisis siempre incluye la advertencia de que los programas escritos en paralelo (como el problema de los productores y consumidores) no debe establecer hipótesis acerca de las velocidades relativas de los procesos ni del intercalado de sus instrucciones en el tiempo. Si se cuenta con el hecho de que dos eventos dentro de un proceso suceden tan rápido que el otro proceso será incapaz de hacer algo, se están buscando problemas. En vez de esto, al lector se le enseña a programar de forma tal que no importe el orden exacto de ejecución de las proposiciones (de hecho, de las referencias a memoria). Cuando sea esencial el orden de los eventos, deben utilizarse semáforos u otras operaciones de sincronización. De hecho, aceptar este argumento significa aprender a vivir con modelos de memoria más débiles. Con algo de práctica, muchos programadores en paralelo podrán adaptarse. 13 Depuración de Aplicaciones Distribuidas La consistencia secuencial es un modelo de memoria un poco más débil que la consistencia estricta. Fue definida por primera vez por Lamport (1979), quien dijo que una memoria con consistencia secuencial es la que satisface la siguiente condición: El resultado de cualquier ejecución es el mismo que si las operaciones de todos los procesos fueran ejecutadas en algún orden secuencial, y las operaciones de cada proceso individual aparecen en esta secuencia en el orden especificado por su programa La definición anterior significa que cuando los procesos se ejecutan en paralelo en diferentes máquinas (o aún en seudoparalelo en un sistema de tiempo compartido), cualquier intercalado válido es un comportamiento aceptable, pero todos los procesos deben ver la misma serie de llamadas a memoria. Una memoria donde un proceso (o procesos) ve un intercalado y otro proceso ve otro distinto no es una memoria con consistencia secuencial. Es importante notar que no se habla del tiempo, es decir, no hay referencia alguna al almacenamiento “más reciente”. Observe que en este contexto, un proceso ve las escrituras de todos los procesos pero sólo sus propias lecturas. La memoria con consistencia secuencial no garantiza que una lectura regrese el valor escrito por otro proceso un nanosegundo antes, un microsegundo antes, o incluso un minuto antes. Sólo garantiza que todos los procesos vean todas las referencias a memoria en el mismo orden. Una memoria con consistencia secuencial se puede implantar en un sistema de MVD o multiprocesador que duplique las páginas que se pueden escribir, garantizando que ninguna operación de memoria comienza hasta que las anteriores hayan concluido. Por ejemplo, en un sistema con un mecanismo de transmisión eficiente, confiable y por completo ordenado, todas las variables compartidas se podrían agrupar en una o más páginas y se podrían transmitir las operaciones a las páginas compartidas. No importa el orden exacto en que se intercalen las operaciones mientras todos los procesos estén de acuerdo en el orden de todas las operaciones en la memoria compartida. c) Consistencia débil No todas las aplicaciones requieren estar viendo todas las escrituras, mucho menos verlas en orden. Considere el caso de un proceso dentro de una región crítica leyendo y escribiendo algunas variables en un ciclo. Aunque se supone que los demás procesos no tocan las variables hasta que el primer proceso salga de su sección crítica, la memoria no tiene forma de saber cuándo un proceso está ella y cuándo no, de modo que debe propagar todas las escrituras a todas las memorias de la manera usual. La mejor solución consiste en dejar que el proceso termine su sección crítica y garantizar entonces que los resultados finales se envíen a todas partes, sin preocuparse demasiado porque todos los resultados intermedios han sido propagados a todas las memorias en orden, o incluso si no fueron propagados. Esto se lleva a cabo mediante 14 Depuración de Aplicaciones Distribuidas un nuevo tipo de variable, una variable de sincronización, que se utiliza con fines de sincronización. Las operaciones en ella se utilizan para sincronizar la memoria. Cuando termina una sincronización, todas las escrituras realizadas en esa máquina se propagan hacia fuera y todas las escrituras realizadas en otras máquinas son traídas hacia la máquina en cuestión. En otras palabras, toda la memoria (compartida) está sincronizada. Dubois (1986) define este modelo, llamado consistencia débil, diciendo que tiene tres propiedades: 1. Los accesos a las variables de sincronización son secuencialmente consistentes. 2. No se permite realizar un acceso a una variable de sincronización hasta que las escrituras anteriores hayan terminado en todas partes. 3. No se permite realizar un acceso a los datos (lectura o escritura) hasta realizar todos los accesos anteriores a las variables de sincronización. La consistencia débil tiene el problema de que, cuando se tiene acceso a una variable de sincronización, la memoria no sabe si esto se realiza debido a que el proceso ha terminado de escribir en las variables compartidas o está a punto de iniciar su lectura. En consecuencia, debe realizar las acciones necesarias en ambos casos, a saber, garantizar que todas las escrituras iniciadas localmente han sido terminadas (es decir, propagadas a las demás máquinas), así como recoger todas las escrituras de éstas. Si la memoria establece la diferencia entre la entrada a una región crítica y salir de ella, será posible una implantación más eficiente. Para proporcionar esta información, se necesitan dos tipos de variables u operaciones de sincronización. La consistencia de liberación (Gharachorloo et. al., 1990) proporciona estos dos tipos. Los accesos de adquisición indican a la memoria del sistema que está a punto de entrar a una región crítica. Los accesos de liberación dicen que acaba de salir de una región crítica. Estos accesos se implantan como operaciones ordinarias sobre variables o como operaciones especiales. En cualquier caso, el programador es responsable por colocar un código explícito en el programa para indicar el momento de realizarlos; por ejemplo, llamando a procedimientos de biblioteca como acquire y release o procedimientos como enter_critical_region o leave_critical_region. También es posible utilizar barreras en vez de las regiones críticas con la consistencia de liberación. Una barrera es un mecanismo de sincronización que evita que cualquier proceso inicie la fase n + 1 de un programa hasta que todos los procesos terminen la fase n. Cuando un proceso llega a una barrera, debe esperar hasta que todos los demás procesos lleguen ahí también. Cuando llega el último, todas las variables compartidas se sincronizan y continúan entonces todos los procesos. La salida de la barrera es la adquisición y la llegada es la liberación. Además de estos accesos de sincronización, también se lee y escribe en las variables compartidas. La adquisición y liberación no tienen que aplicarse a toda la memoria, sino que protegen sólo algunas variables compartidas específicas, en cuyo caso sólo éstas se mantienen consistentes. Las variables compartidas que se mantienen consistentes son protegidas. 15 Depuración de Aplicaciones Distribuidas El contrato entre la memoria y el software dice que cuando el software realiza una adquisición, la memoria se asegurará que todas las copias locales de las variables protegidas sean actualizadas de manera consistente con las remotas, en caso necesario. Al realizar una liberación, las variables protegidas que hayan sido modificadas se propagan hacia las demás máquinas. La realización de una adquisición no garantiza que los cambios realizados de manera local sean enviados a las demás máquinas de inmediato. De manera análoga, la realización de una liberación no necesariamente importa las modificaciones de las demás máquinas. Para aclarar el concepto de consistencia de liberación, describiremos de forma breve una implantación simple, en el contexto de la memoria distribuida compartida (la consistencia de liberación fue ideada en realidad para el multiprocesador Dash, pero la idea es la misma, aunque la implantación no lo es). Para realizar una adquisición, un proceso envía un mensaje a un controlador de sincronización solicitando una adquisición sobre una cerradura particular. Si no hay competencia, se aprueba la solicitud y termina la adquisición. Después, se pueden realizar de manera local una serie arbitraria de lecturas y escrituras a los datos compartidos. Ninguno de estos se propaga a las demás máquinas. Al realizar la liberación, los datos modificados se envían a las demás máquinas que los utilizan. Después de que cada máquina ha reconocido la recepción de los datos, el controlador de sincronización es informado de la liberación. De esta manera, se pueden realizar una cantidad arbitraria de lecturas y escrituras sobre las variables compartidas con un costo fijo. Las adquisiciones y liberaciones de las diversas cerraduras ocurren de manera independiente entre sí. Aunque el algoritmo centralizado descrito anteriormente puede realizar el trabajo, de ninguna manera es el único método. En general, una memoria distribuida compartida tiene consistencia de liberación si cumple las siguientes reglas: • Antes de realizar un acceso ordinario a una variable compartida, deben terminar con éxito todas las adquisiciones anteriores del proceso en cuestión. • Antes de permitir la realización de una liberación, deben terminar las lecturas y escrituras anteriores del proceso. • Los accesos de adquisición y liberación deben ser consistentes con el procesador (no se pide la consistencia secuencial). 1.3 Programación Multihilo Como hemos visto en la sección anterior, los conceptos básicos para programar una MVD con modelos de consistencia débil (consistencia de liberación) son los mismos que los programadores han usado para la programación en paralelo. En esta sección daremos un repaso de estos conceptos para entender mejor las ideas de las secciones siguientes. Todos estamos familiarizados con los programas secuenciales: un programa secuencial tiene un principio, una secuencia de ejecución y un final, y en cualquier 16 Depuración de Aplicaciones Distribuidas momento durante el tiempo de ejecución del programa hay sólo un punto de ejecución. Sin embargo existen aplicaciones en las que una programación secuencial simplemente sería inadecuada o repercutiría en un bajo desempeño del sistema, tal es el caso de los sistemas distribuidos en los que el servidor atiende a múltiples clientes proporcionándoles ficheros, spoolers de impresión, terminales, discos, etc. Si un usuario tuviera que esperar que se atienda su solicitud de impresora hasta que otro termine su acceso a disco el sistema resultaría completamente ineficiente, pues son tareas que pueden realizares de manera independiente. En máquinas multiprocesador en donde pueden existir varios puntos de ejecución al mismo tiempo, seria un terrible desperdicio de recursos ejecutar programas secuenciales. Todos estos problemas suelen resolverse ejecutando en el sistema multiples procesos separados, sin embargo el inconveniente de una solución como esta es que, cada proceso tiene, al menos: su espacio de direcciones propio, código del programa, contador de progama, memoria de montón, pila de memoria, "stack pointer", conjunto de archivos descriptores y tabla de direcciones, lo que hace que el compartir datos entre procesos sea muy complicado, lo cual generalmente se hace a través de memoria compartida, generando otro inconveninte, poca portabilidad de los programas. Un inconveniente más lo produce la sincronización de la memoria, la cual tambien se torna complicada. Además de esto al producir multiples procesos, los cuales contienen, cada uno, todo lo ya mencionado, produce que los recurso se agoten rápidamente. La programación multihilo proporciona una manera más eficiente y fácil de enfrentar los problemas que requieren múltiples puntos de ejecución. Algunos textos usan el nombre de procesos ligeros al referirse a los hilos. Un hilo es similar a un proceso real, pues ambos tienen un solo flujo secuencial de control. Sin embargo un hilo es considerado un proceso ligero pues corre dentro del contexto de un programa y toma ventaja tanto de los recursos asignados a ese programa como de su ambiente. Al igual que un programa secuencial, un hilo tiene un principio, una secuencia y un final, y en cualquier momento durante la ejecución del hilo sólo hay un punto de ejecución. Sin embargo, un hilo en sí mismo no es un programa, esto es, no puede ejecutarse propiamente. Un hilo consta de su propio contador de programa, pila de memoria, “stack pointer” y tabla de señales; todo lo demás, en especial la memoria virtual, se comparte con todos los hilos del mismo proceso. Todo esto hace que los hilos sean económicos en cuanto a la memoria, sin dejar de mencionar que los datos se comparten obviamente de manera más sencilla. Existen diferentes paquetes de hilos que proporcionan primitivas específicas para la gestión y programación de los hilos. El ejecutar tareas completamente independientes en paralelo, usando hilos, es una muy buena idea. Sin embargo, en la mayoría de las aplicaciones existen tareas que se relacionan entre sí a través de datos compartidos. En estos casos en donde se debe tener especial cuidado de planificar los accesos a estos datos para evitar errores. La manera más sencilla de realizar esta planificación es a través de variables denominadas candados. Un candado puede ser visto como una variable binaría que 17 Depuración de Aplicaciones Distribuidas sirve para impedir que más de un hilo se encuentre en una zona específica (región crítica) al mismo tiempo. Imaginemos que nuestro programa contiene 2 hilos que comparten dos variables: x, y. El hilo1 debe colocar a las variables el valor de 0 (fig. 1a) y el hilo2 les debe colocar el valor de 1 (fig. 1b). El cuidado que se debe tener para sincronizar el acceso a estas variables es evidente, pues resultaría catastrófico para el programa si ambos hilos intentaran accesar a las variables para escribir sus respectivos valores. ----------------- ------------------- --------- ---------- x = 0; y = 0; ----------------- x = 1; y = 1; ------------------- 1a 1b Figura 1: a) El hilo1 coloca a las variables el valor 0 b) El hilo2 coloca a las variables el valor 1. Para evitar este problema, podemos añadir a este código una variable candado, candado1, que se encargue de evitar que ambos hilos accesen a las variables al mismo tiempo (Fig. 2). Cuando un hilo intente tomar el candado, primero verifica que este esté libre. Si es así, lo toma y entra a la región crítica, de no estarlo el hilo se duerme en espera de que el candado sea liberado. Debemos Observar que el hilo al salir de la zona crítica deberá liberar el candado para que el otro hilo pueda tener acceso a esta zona. ------------------------toma_candado(candado1); x = 0; y = 0; libera_candado(candado1); ------------------------ ------------------------------toma_candado(candado1); x = 1; y = 1; libera_candado(candado1); ------------------------------- Figura 2: Se colocó un candado para sincronizar el acceso a las variables compartidas. 18 Depuración de Aplicaciones Distribuidas El olvidar liberar un candado es uno de los errores más comunes en la programación en paralelo y como podemos darnos cuenta provocaría que ambos hilos se bloqueen en espera de la liberación del candado, cayendo en lo que se llama un abrazo mortal. Bloqueos Mutuos (abrazos mortales) Un bloqueo mutuo puede definirse formalmente de la siguiente manera: Un conjunto de procesos está en bloqueo mutuo si cada proceso del conjunto está esperando un evento que sólo otro proceso del conjunto puede causar. Puesto que todos los procesos están esperando, ninguno de ellos puede causar alguno de los eventos que podrían despertar a cualquiera de los demás miembros del conjunto y todos los procesos continúan esperando indefinidamente. Condiciones para el bloqueo mutuo Coffman et. al. (1971) demostraron que se deben cumplir cuatro condiciones para que haya un bloqueo mutuo: 1.- Condición de exclusión mutua. Cada recurso está asignado únicamente a un sólo proceso, o está disponible. 2.- Condición de retener y esperar. Los procesos que actualmente tienen recursos que les fueron otorgados previamente pueden solicitar nuevos recursos. 3.- Condición de no expropiación. No es posible quitarle por la fuerza a un proceso los recursos que le fueron otorgados previamente. El proceso que los tiene debe liberarlos explícitamente. 4. Condición de espera circular. Debe haber una cadena circular de dos o más procesos, cada uno de los cuales está esperando un recurso retenido por el siguiente miembro de la cadena. 1.4 Depuradores La depuración es el proceso de encontrar los errores de un programa con el propósito de corregirlos o eliminarlos. Los depuradores permiten seguir el programa paso a paso y ver cómo cambian los valores y expresiones a medida que el programa se ejecuta. Un depurador funciona permitiéndole al programador detener su programa en cualquier punto a medida que se ejecuta, de modo que puede verificar o incluso alterar el valor de las variables o de otros elementos de los datos. 19 Depuración de Aplicaciones Distribuidas El método tradicional de depuración, la depuración cíclica, involucra ejecutar el código muchas veces con la misma entrada. El programador inserta puntos de ruptura (breakpoints) dentro del código y examina el estado del programa. En iteraciones sucesivas, los puntos de ruptura se acercan a la localización del error, y en algún punto el programador lo encuentra y lo elimina. Así, la depuración cíclica es una estrategia común cuando un programa se ejecuta una y otra vez para localizar errores. La depuración cíclica asume que la ejecución del programa puede ser confiablemente reejecutada cualquier cantidad de veces, esto es que siempre para una misma entrada se tendrá la misma salida. En un sistema distribuido esto no se puede asumir debido a que se produce un no determinismo, el que como veremos en el siguiente capítulo, dificulta la depuración en una MVD. 20 Depuración de Aplicaciones Distribuidas CAPÍTULO 2 Planteamiento del Problema: Depuración de Aplicaciones en Paralelo Si la depuración de programas seriales puede ser una tarea tediosa, depurar programas paralelos puede ser una tarea además de tediosa, frustrante. El paralelismo introduce una nueva dimensión de errores y la ocurrencia de un comportamiento inesperado del programa. Las secciones de un programa que son individualmente correctas y libres de errores pueden crear resultados imprevistos cuando se ejecutan concurrentemente. Además, la ejecución puede no ser consistente. Algunas veces, el programa correrá hasta su terminación, como se esperaba. En otros casos, puede fracasar y no completarse, aún con entradas que han sido probadas exitosamente. Esto usualmente se conoce como el problema del no determinismo. 21 Depuración de Aplicaciones Distribuidas 2.1 Determinismo y No Determinismo Una de las consecuencias más importantes de las aplicaciones distribuidas es el no determinismo. Un proceso es determinista cuando produce siempre el mismo resultado cuando se ejecuta con los mismos datos, de lo contrario, el proceso se llama no determinista. El comportamiento de un programa no determinista no solo depende de la entrada. Para ejemplificar lo anterior, recordemos la definición de un sistema distribuido y consideremos las siguientes instrucciones que se ejecutan simultáneamente en dos procesadores diferentes: Procesador A Procesador B j = 10 j = 100 impresión j impresión j En este ejemplo se esperaría que el proceso A imprima 10 y el proceso B imprima 100, pero la falta de un tiempo global absoluto que haga posible determinar qué proceso realizó la "última" escritura, produce el no determinismo, por lo que las lecturas de la variable j pueden ver las escrituras en orden distinto, imprimiendo 10 ó 100. Un programa es externamente determinístico si existe una única salida para cada entrada, y es internamente determinístico sí y sólo sí es externamente determinístico y la secuencia de instrucciones que ejecuta cada hilo junto con los valores de los operandos usados por cada instrucción son determinísticos. La depuración cíclica asume que la ejecución de un programa puede ser confiablemente reejecuta, es decir, podemos asegurar que a una misma entrada recibiremos siempre la misma salida. Sin embargo, en una aplicación distribuida debido al no determismo, no podemos asegurar lo mismo, lo cual hace más difícil la depuración de estos programas. 2.2 Depuradores y Aplicaciones Paralelas Los depuradores de aplicaciones centralizadas o de hilo único permiten al usuario la utilización de puntos de ruptura, la ejecución paso a paso y examinar los registros, la pila y valores de datos globales. En cambio, la depuración de programas multihilos tiene un aspecto a considerar: la concurrencia. Sin embargo, en los ambientes de desarrollo de programas multihilos sería útil una variedad de herramientas que ayuden a la depuración y la puesta a punto. Estas herramientas 22 Depuración de Aplicaciones Distribuidas pueden ayudar a disminuir mucha de la dificultad de encontrar errores de sincronización en las aplicaciones distribuidas. Depuradores para MVD Los programas que utilizan el paradigma de Memoria Virtual Distribuida pueden ser los más difíciles de depurar. El hecho de tener variables globales compartidas significa que si el valor de la variable no es el que se esperaba al terminar la ejecución, es difícil decidir cual de los procesos es el "culpable", ya que todos tienen acceso a ella. Más problemáticos que las variables globales son los errores de sincronización. Aún los programadores expertos algunas veces olvidan ejecutar el acceso secuencial a una variable compartida usando candados o semáforos. Se puede dar el caso, por ejemplo, de que en 99 ejecuciones seguidas de un determinado programa, el código trabaje bien porque el patrón de acceso a la memoria es igual a la suposición implícita del programador. Entonces, en la ejecución número 100, el programa falla. Así, es muy difícil depurarlo, ya que en la siguiente ejecución muy probablemente el programa funcionará bien nuevamente. En situaciones no determinísticas como ésta, es muy complicado recrear sistemáticamente el error. 2.3 Requisitos Funcionales Los depuradores de aplicaciones multi-hilos pueden proveer al usuario de mecanismos para examinar el número y estado de los diferentes hilos, las llamadas a las pilas y variables de sincronización que se utilizan en el programa. En algunos casos, es deseable que cuando la ejecución de un hilo se detiene en un punto de ruptura, todos los otros hilos que se encuentran en ejecución, también se detengan. Esto permite al programador ver todo el estado del proceso sin tratar con otros hilos que puedan alterarlo mientras se depura. Existen también situaciones en las cuales se preferiría dejar que los otros hilos sigan corriendo. Un ejemplo de esto es la depuración de aplicaciones en las que un hilo produce un dato mientras otro hilo lo consume. Además, deben contener herramientas que le permitan interactuar con el programa durante la reejecución generada con las trazas. Una de ellas consiste en colocar puntos de ruptura que detengan la ejecución en algún punto especifico para inspeccionar el estado actual del programa, y después continuarla. También se debe contar con la ejecución paso a paso con la cual el programador puede revisar el patrón de acceso a la memoria revisando una a una las instrucciones en el mismo nivel de abstracción en el cual programó. 23 Depuración de Aplicaciones Distribuidas Un depurador post-mortem es usado cuando el programa ha fracasado y se tiene suficiente información para reconstruir el fallo (la escena del crimen). Típicamente, el fallo se produce porque algún proceso ejecuta código erróneo que provoca un acceso no sincronizado a la memoria o porque se produce un abrazo mortal (deadlock). 2.4 Requisitos de Interfaz Un depurador distribuido tiene que manejar concurrentemente un gran número de procesos distribuidos, pero la pantalla de una computadora no tiene el suficiente espacio para desplegar la información detallada de todos los procesos o hilos. Por lo tanto, se debe organizar y presentar al programador la información de lo que se conoce como una sesión de depuración de una manera coherente y consistente, además de fácil de manejar. Para lo anterior se pueden tomar cuatro estrategias: • • • • Hacer el espacio de trabajo inmediato al usuario más grande Permitir la interacción con múltiples agentes Incrementar la velocidad de interacción en tiempo real entre el usuario y el sistema Utilizar abstracciones visuales para mover la información al sistema perceptual y apresurar la asimilación y recuperación de la información Estas cuatro estrategias nos permiten cubrir la demanda de recuperación, almacenamiento, manipulación y entendimiento de grandes cantidades de información, utilizando de una mejor manera la tecnología de gráficas para disminuir el costo de encontrar la información y después accesarla. 24 Depuración de Aplicaciones Distribuidas CAPÍTULO 3 Algunas Soluciones Existentes En este capítulo se presentan dos trabajos de investigación sobre la depuración de aplicaciones distribuidas: el proyecto ATHAPASCAN y el sistema TreadMarks. Ambos trabajos utilizan la técnica de reejecución de la aplicación utilizando las trazas recolectadas durante la ejecución original. 25 Depuración de Aplicaciones Distribuidas 3.1 Proyecto ATHAPASCAN ATHAPASCAN es un sistema de tiempo de ejecución multi-hilos para la programación paralela. Se ejecuta dinámicamente con el desarrollo de un conjunto de hilos locales o remotos interconectados. La comunicación entre los hilos locales se logra con la memoria compartida y primitivas de sincronización, mientras que para los hilos remotos se utiliza el paso de mensajes. En ATHAPASCAN existen dos fuentes principales de no determinismo: las condiciones de competencia que surgen del acceso a la memoria compartida y el paso de mensajes. Cuando los hilos quieren entrar a la región crítica o quieren obtener un semáforo, se genera competencia por la sincronización. Eliminarla da como resultado un programa completamente determinístico. Por las características del sistema es posible recibir mensajes de cualquier fuente, produciendo competencia si un hilo puede escoger entre los diferentes mensajes. La reejecución necesita asegurar que el orden de los mensajes recibidos sea el mismo que durante la ejecución original. Si dos ejecuciones de un programa paralelo tienen el mismo estado inicial y todos los eventos de ambas ejecuciones son elementales y dependen solamente del estado previo del proceso donde se desarrollan, se tiene el siguiente resultado: dos ejecuciones de un programa paralelo son equivalentes si el orden de recepción de mensajes por cada uno de los procesos y el orden parcial de acceso a la memoria compartida es el mismo en ambas ejecuciones. El resultado anterior se utiliza para la implementación del mecanismo de reejecución de ATHAPASCAN. En la ejecución original se guarda el orden de recepción de mensajes y de acceso a los objetos de sincronización. Los accesos no sincronizados no se guardan, ya que de hacerlo, se incrementaría demasiado el tiempo de ejecución y el tamaño del registro. Las trazas se utilizan para forzar a que las siguientes reejecuciones sean equivalentes a la original, ya que cumplen las hipótesis de equivalencia descrita anteriormente y no realizan accesos no sincronizados a la memoria compartida. Como la reejecución del programa será determinística al menos hasta la primera competencia por los datos, se podrán realizar ejecuciones de programas incorrectos hasta el primer error y será más fácil identificarlo. El no determinismo provocado por las condiciones de competencia frecuentemente evita la depuración cíclica de programas. Por lo tanto, se debe al menos grabar el orden de ejecución de las operaciones de sincronización y utilizar esta información durante la reejecución y depuración. El registro de las trazas debe introducir el menor gasto posible de tiempo y espacio. Por esta razón, sólo se guarda en ellas el orden parcial de las operaciones de sincronización. Para esto se utiliza una técnica basada en el método ROLT (Reconstruction Of Lamport Timestamps), la cual es una optimización de los relojes de Lamport clásicos. 26 Depuración de Aplicaciones Distribuidas El método de Lamport consiste en vincular a un reloj lógico con cada operación de sincronización asignando a cada hilo un reloj, el cual va implícito en cada mensaje que intercambia. Cada operación de sincronización es vista como un emisor, o como un emisor seguido de un receptor, ya que se considera al mensaje como un consumible que es producido por un emisor y luego consumido por un receptor. La optimización de ROLT consiste en tomar esta última combinación como un único evento, además de incorporar un reloj a cada variable compartida de sincronización. Con esto, lo que se consigue es reducir el tamaño de las trazas y reducir la velocidad de los incrementos de los relojes. Cuando ocurre una sincronización se incrementan los relojes del hilo que ejecuta el evento y de la variable compartida de sincronización, y se le asigna a ambos relojes el valor del mayor. Las trazas consisten de una secuencia de intervalos, y se tiene una traza por cada hilo. Para reducir la cantidad de información, sólo se almacenan los incrementos no determinísticos de los intervalos permitiendo una correcta reejecución. 3.2 Sistema TreadMarks Esta solución presenta un algoritmo de seguimiento y recuperación para sistemas de MVD basados en la Consistencia de Liberación Perezosa (CLP) utilizando TreadMarks. El algoritmo tolera fallas en un nodo particular manteniendo un registro de dependencias de datos distribuido en la memoria volátil de los procesos. Así, cada vez que un proceso envía información relevante a otro, mantiene una bitácora de ésta en su memoria. Ya que el protocolo de CLP mantiene la información mas importante en sus estructuras de datos, el algoritmo sólo se encarga de guardar el instante en cuál se transfiere. Cada vez que ocurre una operación de sincronización, se guarda una pareja de vectores de tiempo del emisor y el receptor en la memoria de cada uno de éstos. Cada procesador hace un checkpoint periódicamente para guardar todo su estado actual. Cuando un proceso falla, el sistema lo reinicia en un procesador disponible, usando el último checkpoint que se tiene guardado. El proceso que se está recuperando se reejecuta con información reunida de los registros de los otros procesos, los cuales, al no tener fallas, no son forzados a hacer un roll-back. Así, el sistema recuperará su consistencia sí las lecturas de los procesos que fallaron pueden ser repetidas para regresar los mismos valores que devolvieron antes de fracasar. Los TreadMarks aseguran que todos los programas sin condiciones de competencia se comportan como si se ejecutaran en una memoria secuencial convencional. 27 Depuración de Aplicaciones Distribuidas La CLP divide la ejecución de cada proceso en intervalos lógicos que comienzan en cada acceso de sincronización y se clasifican en release y acquire. Por ejemplo, adquirir un candado es una operación acquire y liberarlo es una operación release, mientras que una barrera se ve como un release seguido de un acquire. LockAcquire se ejecuta cuando un proceso trata de adquirir un candado. Se envía un mensaje con la petición al lock manager en el que se incluye el vector de tiempo del proceso que hace la solicitud. Si no se ha creado un nuevo intervalo desde la última liberación local de ese candado, el proceso que lo va a liberar crea un nuevo registro para el intervalo actual y crea registros de notificaciones de escritura para todas las páginas modificadas durante el intervalo. Las diferencias en las modificaciones de esas páginas se crean cuando el proceso recibe una nueva petición o una notificación de escritura para una página. Cuando un proceso espera en una barrera, ejecuta una rutina llamada Barrera. Le manda una petición al manejador de la barrera, en la que coloca su vector de tiempo y todos los intervalos entre su tiempo lógico actual y el tiempo lógico del ultimo intervalo local conocido por el manejador. Después, el manejador envía a los otros procesadores todos los intervalos entre su vector de tiempo y el vector actual del proceso. El almacenamiento de las diferencias, los registros de las notificaciones de escritura y los registros internos, no se libera hasta que se ejecuta la recolección de basura, es decir, un proceso mantiene efectivamente un registro de todos los accesos a la memoria compartida desde la última recolección. Durante ésta, todos los procesos se sincronizan en una barrera y cada uno actualiza sus copias locales de páginas compartidas y envía un mensaje al manejador de la recolección para informarle que ha terminado. Entonces cada proceso libera todas las estructuras de datos usadas por el protocolo de CLP y continua su ejecución. 28 Depuración de Aplicaciones Distribuidas CAPÍTULO 4 Solución Propuesta En este capítulo se describe la solución al problema de la construcción de un depurador para aplicaciones distribuidas basada en los requisitos funcionales y de interfaz descritos en el capítulo dos. Para llegar a la propuesta, se estudiaron los sistemas ATHAPASCAN y TreadMarks, con el fin de conocer distintas formas en que se han planteado soluciones y para obtener información adicional que podría ayudar a refinar la solución que presentamos. 29 Depuración de Aplicaciones Distribuidas Con el estudio de el proyecto ATHAPASCAN y del sistema de TreadMarks, nos dimos cuenta de la importancia de saber qué es lo que necesitan contener las trazas para lograr la reejecución de un programa que falla. Debido a que ATHAPASCAN es un sistema de tiempo de ejecución, necesita que las trazas le proporcionen información más detallada del tiempo global en el que se están ejecutando los procesos y los tiempos en los que se están haciendo los accesos a memoria. En cambio, TreadMarks sólo registra los tiempos en los que la información es transferida y no registra los cambios en la memoria hasta que algún proceso los solicite. 4.1 Antecedentes En la primera etapa del proyecto se desarrolló un simulador de ambiente concurrente en el cuál se basará nuestra propuesta, descrita en las siguientes secciones. Este simulador ejecuta los programas para la MVD tomando las trazas que ésta generó cuando la aplicación se ejecutó originalmente. Con la información que actualmente presentan las trazas se puede reproducir la misma secuencia de ejecución en una memoria central, por lo que no es necesario agregarles información acerca del tiempo en que se realizan los accesos a memoria. A continuación daremos un breve resumen del funcionamiento del simulador, así como de las estructuras de datos de las cuales nos apoyaremos para implementar nuestra solución. Para poder asegurar que dos ejecuciones son equivalentes, se utiliza el método de reproducción conducida por control el cual dice: dos programas son equivalentes si el orden parcial de los accesos a la memoria compartida son idénticos en ambas ejecuciones. Para reejecutar el sistema bajo un orden parcial, sólo es necesario grabar el orden en que fueron adquiridos los candados que protegen las secciones críticas, o bien cuando los procesos se detuvieron en una barrera. La información de sincronización es almacenada para cada uno de los candados que son creados por las aplicaciones que usan la MVD, es decir, la información se agrupa por candado, de esa manera se evita que el simulador emplee tiempo en clasificar la información. La manera de colectar la información por las trazas es la siguiente: cada candado está vinculado a una estructura con la información necesaria para su administración (inf_candado), como se muestra en la figura 3a. A ella se adicionó un campo llamado historial, fig. 3b, que consiste de una cola que, aprovechando el hecho de que cada candado tiene sólo un hilo propietario a la vez, se usa para que el hilo se encole él mismo. De esta manera se va construyendo el historial del candado con el orden en que los hilos lo accesaron. 30 Depuración de Aplicaciones Distribuidas Estructura inf_candado: IdCandado Creador Propietario probable Adquirido Cola de peticiones Mutex de administración Var de condición despertar 3a IdCandado Creador Propietario probable Adquirido Cola de peticiones Cola de Historial Mutex de administración Var de condición despertar 3b Figura 3. a) Información del candado y b) Información del candado con el historial Los hilos son nombrados lógicamente de igual manera que en la MVD, es decir, se cuenta con una tabla de hilos. El identificador del hilo proporcionado por el sistema es almacenado en la tabla, al igual que el número de nodo en el cual le corresponde ejecutarse, así que su identificador es concatenado con el identificador del nodo. En esta tabla, los tres primeros lugares están dedicados a los hilos servidores y los 5 restantes son proporcionados para los hilos de aplicación. El simulador no cuenta con los hilos servidores, por lo que los tres primeros lugares se mantienen vacíos. Este sistema soporta un máximo de 5 hilos de aplicación por cada nodo definido. Para cada candado, el simulador cuenta con la información de quién lo tomó y cuándo lo liberó. A continuación se describe la estructura de las tablas con las que cuenta el Simulador: a) Tabla de HilosCreados: Pid: Identificador de hilo asignado por el sistema operativo. IdLogico: Contiene el índice que le corresponde al hilo en la tabla de hilos. Nodo: Identificador del nodo en el que al hilo le corresponde ejecutarse. b) Tabla de NodosDefinidos: Esta tabla contiene un arreglo de tamaño ocho que es el número máximo de hilos que se pueden ejecutar en un nodo. En el arreglo se almacenan los identificadores de los hilos que se están ejecutando en el nodo. NumeroHilos: Almacena el total de hilos que se están ejecutando en el nodo. El número de nodo esta dado por el índice de la tabla. 31 Depuración de Aplicaciones Distribuidas c) Tabla de RegionesCreadas: IdRegion: Identificador de la región asignada por el usuario. Tam: Tamaño de la región asignada por el sistema. NumPag: Número de páginas que le fueron asignadas a la región. Destruir: Es una bandera que permite verificar si la región está marcada para posteriormente destruirla o liberarla. Dirección: Es un apuntador a la dirección inicial de región que fue creada. d) Tabla de CandadosCreados IdCandado: Identificador del candado, es un número consecutivo iniciado en cero. Estado: Indica si el candado está ocupado o libre. 4.2 Propuesta A partir de la información generada por el simulador antes descrito, implementaremos un ambiente de depuración que le permita al programador de manera interactiva detectar los posibles errores de su aplicación. 4.2.1 Características Funcionales a) Puntos de Ruptura Como se mencionó anteriormente, una de las principales herramientas en un depurador son los puntos de ruptura, ya que con éstos se puede intentar aislar la fuente del error. En este aspecto nuestro depurador permitirá al programador colocar puntos de ruptura en las llamadas a las primitivas del manejo de la MVD (obtener_candado_MCD, liberar_candado_MCD, etc.). Además, el programador podrá relacionar dichos puntos con un candado y/o un hilo en particular. b) Ejecución Paso a Paso Una herramienta que no podía faltar es la ejecución paso a paso, la propuesta del depurador es permitir dicha ejecución definiendo la longitud del paso en cada intento de acceso a la memoria compartida y en la salida de ésta. Cada paso puede ser visto a través de una pantalla en donde se desplegará el Código de la aplicación resaltando la instrucción en donde se ha detenido. c) Visualización de Variables El depurador permitirá conocer el valor de las variables que los hilos estén utilizando. Es importante aclarar que el programador sólo podrá visualizar el valor de las variables, pero de ningún modo será posible modificarlo. 32 Depuración de Aplicaciones Distribuidas d) Detección de Abrazos Mortales Basados en las trazas generadas en la ejecución original del programa, proponemos como una herramienta extra, la detección de abrazos mortales. 4.2.2 Características de Interfaz a) Manejo de Archivos El desarrollo de la Interfaz permitirá al programador administrar y modificar los archivos que compongan el proyecto que esté depurando. También le permitirá compilarlo estableciendo la ruta y el nombre de su archivo Makefile . b) Desarrollo de Hilos a través del tiempo Se implementará una interfaz gráfica que permita ver el desarrollo de los hilos de aplicación, representados por rectángulos dispuestos horizontalmente y cuyo tamaño aumentará en correspondencia directa con el desarrollo del hilo. Se identificarán los estados de espera, bloqueado y listo a través de los colores amarillo, rojo y verde respectivamente. Esta interfaz contendrá una lista con los identificadores de los hilos en la que el programador podrá seleccionar un hilo en específico y ver su información de manera independiente. La información del hilo seleccionado por el programador, será desplegada en una nueva pantalla en la que aparecerá su estado, su memoria y un historial en el que se indica qué candados ha tomado y liberado. Dado que el simulador nos provee de un historial para cada candado, la información necesaria para esta operación será recolectada durante la reejecución. c) Visualización Abrazos Mortales Holt (1972) mostró cómo pueden modelarse las cuatro condiciones para el bloqueo mutuo usando grafos dirigidos. Los grafos tienen dos clases de nodos: proceso, que se indican con círculos, y recursos, que se indican con cuadros. Un arco que va de un nodo recurso a un nodo proceso indica que el recurso fue solicitado previamente por el proceso, le fue concedido, y actualmente está en su poder. En la Fig. 4a. el recurso R esta asignado actualmente al proceso A. Un arco de un proceso a un recurso indica que el proceso está bloqueado esperando ese recurso. En la figura 4b el proceso B está esperando el recurso S. En la 4c se observa un bloqueo mutuo: el proceso C está esperando el recurso T, que actualmente está en poder del proceso D. El proceso D no va a liberar el recurso T porque está esperando el recurso U, que esta en poder de C. Ambos proceso esperarán eternamente. Un ciclo en el grafo implica que hay un bloqueo mutuo en el que intervienen los proceso y recursos del ciclo. 33 Depuración de Aplicaciones Distribuidas A B C T R S U D (a) (a) (b) (c) Figura. 4. Representación gráfica de la asignación de recursos. Basados en lo anterior, el programador podrá visualizar, a través de una gráfica dirigida, cuándo y en cuál proceso, se ha realizado un abrazo mortal. Si el depurador detecta un abrazo, mostrará dicha gráfica automáticamente y de manera modal (para que el usuario no tenga acceso al resto de la aplicación hasta que cierre la ventana que contiene la gráfica). 34 Depuración de Aplicaciones Distribuidas CAPÍTULO 5 Implementación Hasta este momento se ha estudiado cómo algunos sistemas abordan el tema de la depuración distribuida y se han planteado los requisitos funcionales y de interfaz para la construcción del depurador. En este capítulo se presenta la descripción de la implementación de tales requisitos. 35 Depuración de Aplicaciones Distribuidas La implementación del depurador es conceptualmente sencilla, se trata de mantener un servicio de peticiones del donde el usuario podrá poner y quitar puntos de ruptura y ejecutar la aplicación paso a paso. Además, el depurador deberá tener la característica de poder detectar los abrazos mortales. El depurador se dividió en tres partes: el manejo de hilos (Back-End), la interfaz de usuario (Front-End) y el acoplamiento entre ambos. Las siguientes secciones del capítulo especifican la implementación de cada una de las partes. A lo largo del proyecto se presentaron diferentes contratiempos como fueron: definir la interfaz con el usuario y la comunicación entre ésta y el resto del desarrollo; encontrar cuáles eran los algoritmos más sencillos y más eficientes de implementar, entre otras. Una vez que se definió una posible implementación tanto de la parte funcional del depurador como de la interfaz, nos encontramos con que muchos de los conceptos y funciones necesarias para dicha implementación nos eran desconocidos. Por esta razón, gran parte del tiempo dedicado al proyecto se fue en investigar y estudiar, por lo que quedaron algunas tareas pendientes para que el proyecto quede por completo terminado. La última sección del capítulo, llamada Pendientes, presenta una pequeña descripción de las tareas que quedan por implementar, así como una posible forma de realizar dichas tareas que ayuden a tener un punto de partida cuando el proyecto sea retomado. 5.1 Back-End. Manejo de Hilos. Como se mencionó en el capítulo 4, la depuración se realiza en un ambiente centralizado, para lo cual se necesita un archivo de trazas que nos permite una reejecución idéntica a la realizada en la MVD. Además es necesario realizar un mapeo de las primitivas utilizadas en la MVD. En esta sección analizaremos cómo se implemento el depurador aprovechando que tenemos el control sobre las primitivas de la MVD. Se mencionarán también los problemas que surgieron para poder implementar estas ideas. Recordemos que debido a que nuestro depurador hace la simulación de un ambiente distribuido en uno centralizado, tenemos varios hilos corriendo en nuestra máquina, por lo que debemos tener en cuenta que uno de los hilos que se están ejecutando pertenece a nuestro depurador. Existen primitivas, claramente definidas, en las cuales se pueden colocar puntos de ruptura y sobre las cuales se puede dar un paso. Al ejecutar el programa, cada vez que un hilo llegue a una primitiva se verifica si se ha puesto un punto de ruptura para él en esa primitiva, de ser así, todos los hilos excepto el hilo del depurador serán detenidos. Para implementar lo anterior utilizamos el calendarizador del sistema operativo con la finalidad de poder pasar el control de los hilos de la aplicación al hilo del depurador. 36 Depuración de Aplicaciones Distribuidas La forma en que funciona el simulador es por medio de primitivas, esto es, mapeamos cada una de las funciones de manejo de memoria y recursos, de manera que al ejecutarse el programa éste llame a las funciones que hemos creado, en las cuales se hace la simulación del sistema distribuido de manera centralizada siguiendo los conceptos descritos en capítulos anteriores. Para implementar un depurador no basta con simplemente tener funciones mapeadas pues de esta manera no existe una interacción entre el usuario y su programa, para ello debemos lograr que nuestro depurador tome, de alguna forma, el control de la aplicación. Como mencionamos anteriormente el depurador se basa en el uso del calendarizador del sistema operativo, esto es, la idea propuesta para implementarlo consiste en tener hilos de la aplicación del usuario y un hilo en el que se estará ejecutando nuestro depurador de manera que podamos estar pasando de un hilo de la aplicación al hilo del depurador. La tarea crítica del hilo del depurador será dormir a todos lo hilos de la aplicación para evitar que sean calendarizados nuevamente, antes de que el usuario haga una petición. La pregunta ahora es ¿cuándo creamos el hilo del depurador si lo primero que se ejecuta es la función main de la aplicación del usuario? La idea propuesta es valernos de la primitiva inicializa_MCD (también mapeada). En esta función se lee la traza para que a partir de ésta se inicialicen los arreglos de recursos y se defina la secuencia en que éstos serán tomados. Es aquí donde crearemos el hilo del depurador y le daremos el control una vez que se inicialice todos los arreglos. Debido a que en cada una de las primitivas se podrá ceder el control al depurador, debemos almacenar en una variable global glbIdDebug el ID del hilo del depurador al momento de crearlo. A continuación describiremos dos de las funciones más importantes utilizadas para implementar el depurador. • int pth_yield(pth_t tid); La función regresa el control explícitamente al calendarizador de hilos del sistema operativo. Usualmente, durante la ejecución, se regresa el control al calendarizador cuando un hilo espera por un evento. Pero cuando un hilo hace uso de tiempos largos de CPU, es razonable interrumpirlo explícitamente, haciendo algunas llamadas a pth_yield( pth_id ) para dar oportunidad a otros hilos de ejecutarse también. Esto es obviamente parte de la cooperación de Pth. Por supuesto que un hilo no tiene que ceder el control de su ejecución, pero cuando se quiere programar una aplicación servidor con un buen tiempo de respuesta los hilos deben ser cooperativos y esto se puede lograr llamando a ésta función para dividir el tiempo de CPU en pequeñas unidades. Usualmente el parámetro tid es NULL, indicando que el calendarizador puede elegir libremente el hilo que será despachado a continuación. Pero si queremos indicar al calendarizador cuál hilo debe ser el siguiente, se especifica utilizando su identificador como parámetro. Así, si tid es diferente de NULL y el hilo especificado está listo, se garantiza que éste recibirá el control de la ejecución. Si tid está en un estado diferente (no esta listo) la especificación del hilo no tiene efecto y es igual a llamar la función con tid igual a NULL. 37 Depuración de Aplicaciones Distribuidas Ésta función generalmente regresa TRUE cuando la llamada se realizó con éxito y sólo regresa FALSE (con errno igual a EINVAL) si el tid especificado es invalido o no está listo. • int pth_mutex_acquire(pth_mutex_t *mutex, int try, pth_event_t ev); Adquiere un candado mutex. Si el candado está previamente bloqueado por otro hilo, la ejecución del hilo actual es suspendida hasta que el candado sea liberado o el evento ev ocurra (cuando ev es diferente de NULL). Los bloqueos recursivos están explícitamente soportados, por lo que un hilo puede tomar un candado más de una vez antes de liberarlo, pero entonces tiene que liberar el candado igual número de veces para que otro hilo pueda tomarlo. Cuando try es TRUE la función nunca suspende la ejecución, regresa inmediatamente FALSE y errno vale EBUSY. 5.1.1 Puntos de Ruptura Para implementar l_break, que es una lista defina. Está formada por primitiva, de manera que hilo. el punto de ruptura se cuenta con una estructura llamada que contendrá todos los puntos de ruptura que el usuario nodos que contienen una estructura con los campos hilo – se pueda identificar en cuál primitiva se debe detener cuál Cada vez que un hilo ingrese en una primitiva, se recorrerá la lista en busca de un punto de ruptura. Si éste existe, se dará el control al hilo del depurador para que suspenda a todos los hilos de la aplicación y entre a un ciclo infinito en espera de una nueva petición por parte del usuario. 5.1.2 Ejecución paso a paso El paso se implementa de manera similar al punto de ruptura, con la diferencia de que éste aplica para cualquier hilo que llegue a la primitiva. Cuando un hilo llega a una primitiva verifica la variable step, si su valor es verdadero (true) le da el control al hilo del depurador el cual duerme a todos los hilos de la aplicación y entra a un ciclo infinito en espera de otra petición del usuario. 5.1.3 Abrazos Mortales Para que el depurador pueda detectar un abrazo mortal la idea es aplicar el algoritmo de detección de Shoshani y Coffman, el cual requiere de tres estructuras: una en la que mantengamos todos los recursos disponibles, otra que contenga los recursos tomados por los procesos y una tercera que contenga los recursos que esperan los procesos. La creación y mantenimiento de estas estructuras se dará de la siguiente manera: 38 Depuración de Aplicaciones Distribuidas • Recursos disponibles Contienen todos los candados disponibles para la aplicación, información que se obtiene previamente de la traza. • Recursos que esperan los procesos Si cuando un hilo intenta tomar un candado este no esta disponible, se almacena en un arreglo un nodo con el ID del candado que se intentó tomar y el ID del hilo que intentó hacerlo. Una vez que el candado logra ser tomado se debe eliminar el nodo correspondiente del arreglo. • Recursos tomados Cada vez que un candado sea tomado se revisa el arreglo de los recursos que esperan los procesos para ver si estaba en espera, si es así, el nodo es eliminado. Una vez hecho esto, se almacena en un arreglo un nodo que contiene el ID del candado tomado y el ID del hilo que lo tomo. 5.2 Front-End. Interfaz de Usuario Como se mencionó en el capítulo 2 (Descripción del problema), durante la depuración de aplicaciones distribuidas el programador tiene que manejar simultáneamente mucha información, por lo que es necesario que a través de la Interfaz de Usuario tenga el acceso a tal información de manera clara y fácil de manejar. Para lograrlo, la aplicación consta de un gran número de ventanas (más de 10 entre ventanas y diálogos ) que ayudan al programador a manejar la información. 5.2.1 Principales Pantallas En esta sección se presentará una breve descripción de las características y funcionalidades de las pantallas que conforman la IU para posteriormente mostrar la forma en la que el usuario “navegará” a través de ellas. 1) Principal A partir de esta pantalla pueden abrirse las cuatro ventanas que dan la funcionalidad al depurador: • • • • Depurador (Debugger - Frame) Make (Make - Frame) Administrador de Archivos (AdminArchivos - Frame) Bloqueos (Bloqueos - Frame) Además contiene un botón (Directorio) que al oprimirlo abre un FileDialog con el que se obtiene el directorio en el cual se ubica el Proyecto. Ya que se tiene el directorio se abre el Administrador de Archivos. 39 Depuración de Aplicaciones Distribuidas Figura 5. Pantalla Principal del depurador. A través de ella se pueden abrir las ventanas que dan la funcionalidad al depurador. 2) Administrador de Archivos A través de esta pantalla se tiene el control de los archivos del proyecto. Tiene dos botones: Añadir y Eliminar con los cuales el usuario elige los archivos que desea tener a la mano para revisar su código. Cuando se oprime cualquiera de los botones, se abre el dialogo BusquedaArchivoDlg, y al regresar de éste aparecerán en un ListBox los archivos que el usuario ha seleccionado. Al dar un doble click sobre algún archivo, se abre la ventana Archivos que contiene el listado (no editable) del código. El dialogo BusquedaArchivoDlg contiene los cuatro botones tradicionales para seleccionar archivos: añadir, añadir todos, eliminar y eliminar todos. Además contiene dos opciones para añadir y eliminar con comodín (por ejemplo añadir todos los archivos de c con *.c ). Figura 6. Pantalla del Administrador de Archivos. 40 Depuración de Aplicaciones Distribuidas Así, el administrador de archivos consta de 2 ventanas (AdminArchivos y Archivos) y 2 diálogos (BusquedaArchivosDlg y ComodinDlg), los cuales se presentan en forma modal. Figura 7. Pantallas auxiliares del Administrador de Archivos: a) Búsqueda de Archivos y b) Visualización de Archivos 41 Depuración de Aplicaciones Distribuidas 3) Make En esta ventana se compila la aplicación del usuario con nuestras librerías del depurador. En el espacio que encuentra debajo del botón se escribirá la salida que devuelve el depurador (gcc). Figura 8. Pantalla Make. Muestra el resultado de la compilación del código del usuario con las librerías del compilador. 4) Depurador Desde esta ventana se puede acceder a toda la información relacionada con la depuración en sí. En la barra de herramientas se tienen cuatro botones: • Hilos (toggle button): muestra o esconde la información de todos los hilos y cómo han ido cambiando su estado a lo largo de la ejecución. Al dar un doble clic sobre algún hilo, se abre una nueva ventana que contiene más información del hilo. • Breakpoints: muestra una ventana de dialogo que contiene tres componentes ListBox: uno con las primitivas, otro con los candados y el último con los hilos. Para colocar un breakpoint, el usuario elige una opción de cada lista y oprime el botón aceptar. El botón cancelar cierra el dialogo. • Paso: Permite que el hilo seleccionado de el siguiente paso. • Continuar: Permite que se continúe la ejecución del programa. 42 Depuración de Aplicaciones Distribuidas Figura 9. Pantalla de Depuración. A través de ella se puede mostrar la información de los hilos, poner o quitar breakpoints, dar un paso sobre un hilo específico y continuar la ejecución de la aplicación. En la parte izquierda de la pantalla se encuentra una tabla que contiene la información de los breakpoints que ha colocado el usuario mediante la relación primitiva-hilo-candado. Tiene además un botón para eliminar el o los breakpoints que se encuentren seleccionados. En la parte derecha se encuentra una una lista con los hilos disponibles para que seleccione sobre cuál quiere dar el siguiente paso. Para poner un breakpoint, el usuario debe presionar el botón correspondiente y se mostrará la siguiente pantalla: Figura 10. Pantalla de Breakpoints. 43 Depuración de Aplicaciones Distribuidas En la pantalla se muestran tres listas: una con las funciones (primitivas) disponibles, otra con los candados y una más con los hilos. Para poner un nuevo breakpoint, el usuario debe elegir la primitiva en la cual desea colocarlo así como el candado y el hilo. 5.2.1 Diagrama de Secuencia de Pantallas 44 Depuración de Aplicaciones Distribuidas 5.3 Acoplamiento Debido a la necesidad de una interfaz amigable, al inicio del proyecto se decidió desarrollar la interfaz con el lenguaje de programación Java para tratar de sacar el máximo provecho de algunas de sus principales características: es simple y familiar, por lo que se puede aprender rápidamente; es orientado a objetos con lo cuál se puede tomar ventaja de las más modernas metodologías de desarrollo y es multihilo con lo cuál se pretendía manejar más fácilmente el Back-End. Sin embargo, uno de los mayores problemas al que nos enfrentamos fue decidir como iba a interactuar la interfaz con el simulador-depurador. 5.3.1 Sockets La primera solución pensada se basó en el uso de sockets: tener un servidor en el Back-End que espere peticiones de un cliente (Front-End) tales como poner un candado, quitar un candado, dar un paso sobre algún hilo, etc. La idea estaba basada en el manejo de un conjunto de mensajes definidos con una estructura específica: al comienzo del mensaje se utilizaba un identificador (ID) específico para cada petición o respuesta, después venía un separador ( | ), el tamaño en bytes del mensaje, un separador, el cuerpo del mensaje (separado o no por uno o más separadores), un separador y al final un identificador del fin del mensaje. Sin embargo esta idea resultó poco óptima debido a que tanto el Back-End como el Front-End radican en la misma máquina. Además, la decisión de cuál sería el Cliente y cuál el Servidor es complicada, ya que se tienen casos en los cuales el BackEnd requiere hacer peticiones a la Interfaz, lo que lo convertiría en un cliente. 45 Depuración de Aplicaciones Distribuidas 5.3.2. Java Native Interface (JNI) Fue entonces cuando se pensó en conectar el manejo de los hilos y la interfaz de usuario a través de la utilización de Java Native Interface (JNI) que es la interfaz que java proporciona para comunicarse con el código de C. JNI define una convención de nombres y llamadas para que la Máquina Virtual de Java pueda localizar e invocar a los métodos nativos. De hecho, JNI está construido dentro de la máquina virtual Java, por lo que ésta puede llamar a sistemas locales para realizar entrada/salida, gráficos, trabajos de red y operaciones de threads sobre el host del sistema operativo. JNI tiene una interfaz bidireccional que permite a las aplicaciones JAVA llamar a código nativo y viceversa. El problema de utilizar el JNI radicó fundamentalmente en incorporar las librerías de manejo de hilos (pthread) en el ambiente del código nativo. Además, la utilización del JNI como medio de enlace entre la interfaz y el manejo de hilos requiere que éste último sea reescrito en su totalidad, trasladando los métodos originales escritos en C a las implementaciones de los métodos nativos en java. 5.4 Pendientes Una idea que quedo pendiente de implementar es la de mostrar al usuario el contenido de la memoria de sus variables, esto se deberá hacer, en una primera aproximación, de manera “cruda”, es decir mostrando sólo la zona de memoria, pues hasta ahora no es posible saber el tipo de dato de la variable (entero, real, carácter, etc.) , ya que no existe un analizador sintáctico que nos permita saberlo. Para mostrar la zona de memoria una interacción entre la Interfaz y “C” por medio de sockets no es de gran ayuda. Otra característica importante del depurador que quedó sin implementar es la detección de abrazos mortales. La idea que propuesta para implementar dicha característica se basa en implementar el algoritmo de Shoshani y Coffman. Para lograr esto, falta por crear y mantener dos estructuras: Recursos tomados y Recursos que esperan los procesos, la primera es muy sencilla, basta con crear una lista con nodos que contengan el ID del candado tomado y el ID del hilo que lo tomó e implementar un método de inserción a la lista y un método de eliminación dado el ID del candado. La inserción de nuevos nodos a la lista si hará en la primitiva ObtenerCandado una vez que el candado fue tomado, la eliminación de un nodo se hará en la primitiva LiberarCandado una vez que el candado se libero. Ambas primitivas se encuentran en el listado Candado.c. Mantener la segunda estructura es un poco más complicado, pues debemos saber en que momento un hilo se queda suspendido en espera de un candado. Para esto, lo que sugerimos es usar la función pth_mutex_acquire(pth_mutex_t *mutex, int try, pth_event_t ev) con try igual a TRUE para evitar que el hilo sea suspendido de manera inmediata, almacenar el valor de la función y llamar la función pth_mutex_acquire nuevamente con try igual a FALSE. Si el valor de retorno de la primera llamada de la función es igual a FALSE insertamos un nodo, con el ID del 46 Depuración de Aplicaciones Distribuidas candado y el ID del hilo, a la lista. En la misma primitiva ObtenerCandado, una vez tomado el candado debemos eliminar el nodo de la lista, si existe. Además, falta desplegar el estado de los hilos, donde la interfaz debe mostrar un histórico de cada hilo, para lo cuál puede valerse de la estructura HilosCreados la cual contiene en cada nodo un hilo y su estado actual ya que éste va cambiando en el tiempo. Por último, será necesario agregar las opciones y funciones para que el usuario pueda eliminar los puntos de ruptura. Pendientes: • Abrazos mortales o Estructura de recursos tomados o Gráfica • Gráficas de progreso de los hilos o Registro del tiempo o Gráfica • Interfaz gráfica o Acoplamiento • Información de las variables o Vaciado de la memoria o Acceso a la zona de memoria 47 Depuración de Aplicaciones Distribuidas CAPÍTULO 6 Conclusiones Un depurador de sistemas distribuidos conlleva una serie de características que permitan al usuario seguir su programa, de manera que pueda detectar los errores que éste pueda tener. En este capitulo haremos un recuento de lo que hasta ahora se tiende del depurador así como en análisis de los motivos por los cuales no se pudieron concluir todas las características que se tenían planeadas al inicio del proyecto. 48 Depuración de Aplicaciones Distribuidas La realización de este proyecto tiene su origen en la necesidad de contar con una herramienta que permita al programador de aplicaciones distribuidas seguir paso a paso la ejecución de su programa. La primera etapa del proyecto consistió en la creación de un simulador de la aplicación, esto es, reproducir el programa distribuido en un ambiente centralizado a partir de un archivo de trazas. La siguiente etapa fue la creación un depurador como tal. Las características que se definieron para dicho depurador fueron las siguientes. • • • • • • • Interfaz gráfica amigable Ejecución paso a paso Puntos de ruptura Detección de abrazos mortales Graficación de abrazos mortales Gráfica del desarrollo de los hilos (status vs. tiempo) Accesos a las variables del programa Estas características se fueron definiendo con base en las soluciones ya existentes y en los requerimientos de los usuarios de estas herramientas. Un error que cometimos al definir éstas, y que después repercutió en la terminación del proyecto, al menos así lo creemos, fue el hecho de no tomar en cuenta la complejidad de cada una de ellas. Como ejemplo están la graficación de los abrazos mortales y el acceso a las variables del programa. El analizar e intentar implementar estas dos tareas nos tomó más tiempo del que disponíamos y desdichadamente no tuvimos éxito pues cuando creíamos encontrar una solución siempre encontrábamos casos en los que no aplicaba dicha solución. Creemos que el encontrar la forma correcta de desplegar el gráfico así como la forma de reasignar tamaños a fin de que sea visible por completo en la pantalla es tema incluso de un proyecto aparte. El reto principal de este proyecto fue el hecho de que muchos de los conceptos necesarios para implementar las características deseadas en el depurador, nos eran en su momento desconocidos. Este reto lo fuimos superando conforme avanzaba el proyecto, documentándonos y probando diferentes soluciones. Sin embargo el desconocimiento de muchas herramientas ya existentes nos hizo seguir direcciones que no siempre eran las correctas o las mejores. Un claro ejemplo de esto fue el desarrollo de la interfaz gráfica (Front-End) . Conforme los tiempos que teníamos se iban acortando decidimos desarrollar por separado el Front-End y el Back-End. Al hacer esto se presentó un nuevo problema: cómo unir ambas partes. El Back-End se estaba desarrollando en lenguaje C, mientras que para el Front-End se utilizó Java, una decisión que nuevamente nos trajo el problema de cubrir la curva de aprendizaje de este lenguaje. Tomamos la decisión de unir el Back-End con el Front-End usando una comunicación por sockets, por lo que el Back-End funcionaria como un servidor y el Front-End como un cliente. Esta solución es a todas luces poco óptima y complica la implementación de otras características. Incluso ahora, surgen dudas con respecto a las decisiones tomadas al inicio del proyecto, como por ejemplo: ¿porqué no usar una herramienta gráfica cuyo lenguaje nativo sea C (KDevelop para el ambiente KDE o el SDK del Gnome) para poder utilizar de una manera más sencilla las librerías de pth? 49 Depuración de Aplicaciones Distribuidas En cuanto al Back-End, quedaron pendientes de implementar y mantener estructuras necesarias para terminar la totalidad de las características que inicialmente se concibieron para el depurador. Los errores y problemas con los que nos encontramos, creemos, son parte de la formación que un proyecto terminal pretende dar a un alumno, pues nos enseñan a enfrentarnos a este tipo de situaciones y a investigar lo más posible con el fin de encontrar una solución al problema. Esperamos que estas conclusiones sirvan a las siguientes generaciones que quieran continuar con nuestro trabajo pues el concepto de un depurador de aplicaciones distribuidas es un tema que sigue siendo actual, es un problema al cual se le pueden seguir aportando nuevas y mejores soluciones.