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.

Documentos relacionados