14. Módulo de gestión de eventos.

Anuncio
14. Módulo de gestión de eventos.
Hasta este punto se han analizado el flujo principal de ejecución que sigue un
programa Java desde que es compilado hasta que es ejecutado por la maquina virtual
siguiendo distintas fases. De esta forma se ha visto como actúa el motor de ejecución de
la KVM, como implementa los objetos que componen los programas Java, la forma en
la que gestiona la memoria que ocupan dichos objetos y como se implementa la
concurrencia de tareas.
Cuando hablamos de eventos en cualquier lengua je de programación nos
referimos a operaciones ejecutadas por acción del usuario como puede ser pulsar un
botón del ratón. En este capítulo sin embargo, cuando hablamos de eventos nos
referimos a la forma en la que la KVM se comunica con el sistema operativo. Es decir la
forma en la que la maquina virtual es capaz de capturar los eventos que le lleguen del
sistema operativo y gestionarlos.
De esta forma la KVM es capaz de responder ante acciones que recibe del
sistema operativo como por ejemplo la apertur a de un fichero, la pulsación de una tecla
en el dispositivo de entrada estándar,…
14.1. Introducción.
Los eventos forman parte de un paradigma de la programación conocido como
programación guiada por eventos. En la programación tradicional existe un único flujo
de ejecución (incluso en sistemas multihilo) y solo se producen saltos en dicho flujo en
los puntos de salto (instrucciones if por ejemplo). En la programación guiada por
eventos el flujo de ejecución es conducido por los eventos externos que se van
sucediendo continuamente.
En lugar de esperar a que se ejecute un comando completo que es el que realiza
el procesado de la información, el sistema es preprogramado con un bucle de eventos
que examina de forma repetida buscando información a procesar (ya sea la pulsación de
un botón de un ratón, una apertura de un fichero,…) y entonces se emplea una función
de trigger para procesar dicha información.
El método por el cual la información en los eventos es recogida puede llegar a
ser muy variada y depende fuertemente de la plataforma hardware específico. Las
entradas pueden ser recogidas en un bucle de espera activa, o manejadores de
interrupciones pueden ser registrados para capturar eventos hardware. Los algoritmos de
reprogramación de eventos aseguran que los triggers adecuados son ejecutados cuando
se produce un evento incluso proveyendo una abstracción a nivel software que emula un
entorno de interrupciones.
Los programas guiados por eventos típicamente consisten en un numero
reducido de pequeños programas denominados manejadores de eventos, los cuales son
llamados como respuesta a los eventos externos y un dispatcher que es llamado por los
manteadores de eventos frecuentemente usan una cola de eventos para ir procesando
uno a uno. En muchos casos los manejadores de eventos pueden procesar la
información de dichos eventos por si solos.
La programación guiada por eventos tiene como principales ventajas flexibilidad
y asincronía e intenta ser lo mas modular posible. Interfaces gráficas de usuario (GUI)
es un ejemplo claro de programación orientada a eventos.
Figura 14.1: Modelo de gestión de eventos.
Los sistemas operativos son otro ejemplo clásico de programas guiados por
eventos en al menos dos niveles. En el nivel mas bajo, los manejadores de
interrupciones actúan directamente como eventos hardware para los niveles superiores y
el hardware de la CPU actúa como dispatcher. Los sistemas operativos también actúan
como dispatcher para los programas que corren en ellos pasando a estos tanto
información como interrupciones hardware provocadas por el usuario.
14.2. Los eventos en Java.
Java no es lenguaje de programación de eventos puro si bien si soporta un
mecanismo de gestión de eventos para atender las peticiones que el sistema operativo
envía al usuario. Esta gestión de eventos es totalmente transparente para el programador
Java y esta encapsulado dentro del propio lenguaje y de la maquina virtual Java. Por
ejemplo las comunicaciones por red tales como apertura de sockets están integradas
dentro de la gestión de eventos de la maquina virtual.
Figura 14.2: Manejo de eventos Java.
Si existe una programación de eventos cuando se crean interfaces de usuario
empleando bien AWT, Swing o applets si bien este gestión de eventos es a nivel
software y queda fuera del funcionamiento básico de la maquina virtual. Nosotros nos
centraremos en como la maquina virtual Java y en particular la KVM comunica los
eventos a nivel de sistema operativo con los programas que se ejecutan sobre la
maquina virtual.
14.3. Gestión de eventos hardware en la KVM.
La especificación oficial de la maquina virtual Java no define como la maquina
virtual interactúa con los eventos que llegan del sistema operativo o desde un
dispositivo externo. La implementación de la KVM, sin embargo, provee una variedad
de mecanismos que han sido diseñados para facilitar la integración de la KVM con el
sistema de eventos del sistema operativos o dispositivo destino.
Existen 4 formas diferentes para realizar la notificación y manejo de eventos en
la KVM:
•
•
•
•
Notificación sincrona (bloqueo).
Polling en código Java.
Polling en el intérprete de bytecodes.
Notificación asíncrona.
Diferentes soluciones pueden resultar apropiadas para diferentes
implementaciones de la KVM, dependiendo de las librerías GUI soportadas, que tipos
de librerías de comunicaciones en red se dispongan, etc.…
14.3.1.
Notificación sincronía (bloqueo).
Por notificación síncrona nos referimos a la situación en la cual la KVM realiza
la gestión de eventos mediante invocaciones a funciones I/O o eventos del sistema
directamente desde la propia maquina virtual. Como la KVM tiene un único hilo físico
de control de la maquina virtual, ningún otro hilo Java puede ser procesado mientras la
función nativa esta siendo ejecutada y ninguna otra función de la maquina virtual tal
como el recolector de basura puede ser ejecutada tampoco. Este es el método mas
sencillo de notificación de eventos y en muchos casos en suficientemente aceptable
siempre y cuando las funciones nativas estén correctamente diseñadas en cuanto a
errores y rendimiento se refiere.
Por ejemplo, enviar un datagrama a la red puede ser realizado de forma eficiente
usando esta aproximación ya que normalmente el datagrama es enviado a una pila con
un buffer y el tiempo que se espera hasta que el datagrama esta escrito en el buffer
suele ser pequeño. Sin embargo leer un datagrama es una historia diferente y es
preferible usar otro mecanismo de gestión de eventos. Usar una función nativa para
esperar la lectura completa del datagrama puede bloquear la KVM demasiado tiempo
mientras se completa la lectura de la red.
14.3.2.
Polling en código Java.
Frecuentemente la gestión de eventos puede ser implementada eficientemente
usando una combinación de funciones nativas y código Java. Esta es una manera muy
sencilla para permitir que otros hilos Java sean ejecutados mientras el hilo activo esta
esperando la respuesta de un evento. Cuando se emplea esta solución, un bucle de
espera activa en Java es ubicado normalmente en algunas de las librerías de ejecución
por lo cua l dicho bucle esta totalmente escondido de las aplicaciones que las emplean.
El procedimiento normal para la biblioteca Java indicada es iniciar una operación nativa
corta de entrada/salida y entonces repetir periódicamente la consulta sobre el estado de
dicha operación hasta que esta finalice. El bucle de espera activa contenido en la
biblioteca debe contener una llamada a Thread.yield para que de esta forma el resto de
hilos puedan ejecutarse eficientemente.
Este método de espera de notificación de eve ntos es muy sencillo de
implementar y no esta sujeto a ninguno de los tradicionales problemas de los hilos
puramente asíncronos (tales como necesidad de regiones críticas, semáforos o
monitores). Sin embargo existen dos claras desventajas. La primera de ellas es que son
necesarios algunos ciclos de CPU para realizar el polling que se podrían haber
empleado en ejecutar código de las aplicaciones (si bien el overhead es muy pequeño).
Por otro lado debido a la sobrecarga introducida en el intéprete se incremente la latencia
en el proceso de notificación de eventos (especialmente si la llamada a Thread.yield es
olvidada). Sin embargo esta sobrecarga es normalmente imperceptible salvo en sistemas
realmente críticos.
14.3.3.
Polling en el intérprete de bytecodes.
La tercera aproximación al problema de la implementación del manejador de
eventos hacer que el intérprete de bytecodes de forma periódica haga llamadas las
operaciones nativas de gestión de eventos. Esta solución es una variación de la
notificación síncrona anteriormente comentada. En solución fue originalmente muy
usada en la KVM, por ejemplo para implementar los eventos GUI de la Palm.
En esta solución, un manejador de eventos nativo es llamado periódicamente
desde el ciclo de ejecución del intérprete. Por razones de rendimiento esto no se hace
después de la ejecución de cada bytecode, pero cada 100 bytecodes ejecutados más o
menos. Se puede modificar dicho número de bytecodes a ejecutar antes de llamar al
manejador de eventos con lo cual el diseñador puede controlar la latencia de los eventos
frente a la carga adicional de CPU que conlleva la espera de nuevos eventos. Mientras
más pequeño sea el número mas pequeña es la latencia de eventos y mas grande la carga
en la CPU. Si aumentamos el numero de bytecodes entre invocación del manejador se
mejora la carga de la CPU pero se aumenta la latencia de los eventos.
La ventaja que ofrece esta solución respecto a las otras es que el coste en
rendimiento es mas pequeño que hacer polling en Java, y la latencia en la notificación
de eventos es más predecible y controlable. La forma en la que trabaja esta solución es
muy parecida a la notificación asíncrona que veremos en el siguiente apartado.
14.3.4.
Notificación asíncrona.
La implementación original de la KVM soporta únicame nte los tres mecanismos
de gestión de eventos antes comentados. En cambio, para poder soportar un mecanismo
auténtico de gestión asíncrona de eventos algunos nuevos mecanismos han sido
introducidos en versiones posteriores de la KVM.
Por notificación asíncrona se entiende que la gestión de eventos puede ocurrir en
paralelo mientras la maquina virtual continúa con su ejecución. Esta es generalmente la
solución más eficiente para la gestión de eventos y resulta en una latencia para dicha
notificación pequeña. Sin embargo, esta solución requiere que el sistema operativo
provea las facilidades operativas adecuadas para implementar el manejador asíncrono de
eventos. Tales facilidades pueden no estar disponibles en todos los sistemas operativos.
Por todo ello, esta solución es algo mas compleja de implementar, ya que el diseñador
de la maquina virtual tiene que tener especial cuidado con problemas de bloqueo. La
implementación de referencia que estamos estudiando ofrece algunos ejemplos que
pueden ser usados como punto de partida para implementar manejadores de eventos de
otros dispositivos o plataformas.
El procedimiento general en la notificación asíncrona es como sigue. Un hilo
llama a una función nativa para comenzar una operación de I/O. El código nativo
entonces suspende la ejecución del hilo e inmediatamente devuelve el control al
intérprete, permitiendo a esta que otro hilo continué ejecutándose. El intérprete entonces
selecciona un nuevo hilo a ser ejecutado. Algún tiempo después un evento asíncrono
ocurre y como resultado algún código nativo es ejecutado para continuar con la
ejecución del hilo que había suspendido. El intérprete entonces reinicia la ejecución del
hilo que se había quedado esperando a que ocurriera el evento.
En cuanto a implementación se refiere, hay dos formas se implementar la
notificación asíncrona. Se puede emplear hilos nativos del sistema o bien se puede usar
algún tipo de software de interrupciones, funciones callback o rutina de polling.
En el primer caso, antes de que la función nativa sea llamada y el hilo Java sea
suspendido, un nuevo hilo a nivel del sistema operativo es creado o reactivado y es
desde este hilo desde donde se realiza la invocación de la función nativa. Existe ahora
un hilo adicional nativo de control ejecutándose dentro de la maquina virtual. Después
de que el hilo nativo haya sido iniciado, el orden de ejecución entre ambos hilos en la
maquina virtual es indeterminado y depende de la ocurrencia de los eventos a los cuales
se esta esperando. Típicamente, el hilo original comienza a ejecutar otro hilo Java en el
intérprete y el nuevo hilo comienza a ejecutar la operación de I/O.
Es importante hacer notar que las funciones nativas de I/O serán ejecutadas fuera
del contexto de la maquina virtual pues se ejecuta en un hilo separado. Un conjunto de
macros especiales para acceder a este nuevo contexto han sido introducidas para que el
usuario no note la diferencia con la salvedad de que los punteros usados en estas macros
especiales son locales a dichas macros y contextos. Cuando la llamada bloqueante ha
finalizado el hilo nativo continúa ejecutándose y desbloquea a su vez el hilo Java al que
estaba asociado y a partir del cual se había creado. El hilo Java entonces es relanzado de
nuevo y el hilo nativo es o bien destruido o bien dejado en estado de hibernación hasta
que vuelva a ser necesario. Por ejemplo en la implementación de la KVM para el
sistema win32 se usa un pool de hilos nativos que son reusados cada vez que se produce
un evento.
La segunda implementación de la gestión de eventos asíncronos puede ser hecha
empleando funciones callback asociadas con peticiones de I/O. Aquí el código nativo es
introducido usando el hilo de ejecución normal del intérprete, la operación de I/O es
iniciada y cuando es completada una función de callback del sistema operativo es
llamada y el hilo Java se vuelve a reanudar. En este escenario el código nativo es
dividido en dos rutinas, la primera es la que comienza la ejecución de la operación de
I/O y la segunda invocada al finalizar la operación I/O. En este caso la primera rutina se
ejecuta en el contexto de hilo Java que la llama y la segunda no.
La última implementación es posiblemente la mas ineficiente y es que la rutina
de I/O sea testada periódicamente por el intérprete. Esta solución es muy similar a la
anterior (con la función callback), salvo que la segunda rutina es llamada repetidamente
desde el interprete para comprobar si la operación de I/O ha finalizado. En algunos
casos, cuando la operación de I/O ha sido completada la propia rutina de invocación se
encarga de desbloquear el hilo Java. Esta llamada al código nativo por el intérprete
siempre es hecha cuando no eventos pendientes y el código nativo ha de determinar por
si solo que hilo Java debe ser puesta a ejecutarse.
14.4. Paso de parámetros e incidencias en el recolector de
basura.
Cuando el código de gestión de eventos es invocado, los parámetros que se le
pasan están en la pila de ejecución del hilo Java. Estos son desapilados por el código
nativo, y si hay que devolver algún tipo de resultado este es apilado también por el
código nativo en la pila de ejecución del hilo Java justo antes de continuar con la
reactivación del hilo.
Dado que el código de gestión de eventos nativo puede acceder objetos ubicados
en memoria, puede haber conflictos con el recolector de basura sobre todo cuando la
KVM lleva mucho tiempo funcionando. En general, el recolector de basura es avisado
cuando hay algún código nativo ejecutándose. Los problemas suceden cuando el código
nativo involucra operaciones de I/O largas. El caso mas obvio es la espera a una
respuesta de la red. Para resolver este problema dos funciones llamadas
decrementAsynCount e incrementAsyncCount son empleadas. La primera de las
funciones permite al recolector de basura comenzar su ejecución y la segunda impide
que el recolector de basura se ejecute.
Se debe reseñar que si una referencia de un objeto es pasado a un método nativo,
pero no existe ninguna otra referencia a ella desde el código Java después de haberse
ejecutado incrementAsynCount el objeto puede ser reclamado accidentalmente desde el
recolector de basura si bien es un escenario que es muy difícil que se de.
14.5. Implementación gestor de eventos KVM.
El módulo de gestión de eventos de la KVM se encuentra contenido en los
ficheros events.h y events.c. Si bien ya hemos visto en otros capítulos como los distintos
módulos se comunican entre sí, el gestor de eventos esta fuertemente relacionado con
muchos de los módulos de la KVM tales como el intérprete, recolector de basura, el
gestor de hilos, el gestor de conexiones de red…
Por ejemplo precisamente dentro de events.h se definen las macros que el
intérprete emplea para invocar al módulo de gestión de hilos y forzar a realizar una
operación de thread-switching.
La implementación del gestor de eventos en la KVM se compone de dos capas.
La primera capa involucra directamente al intérprete de bytecodes. Se puede comprobar
como al inicio del intérprete hay una invocación a la operación de reschedule que se
emplea para realizar una operación de thread-switching. Dicha operación se ejecuta en
los siguientes pasos:
•
•
•
•
Se comprueba si existe algún hilo activo pendiente de ser ejecutado y si no hay
ninguno detiene la maquina virtual.
Comprueba si hay alguna alarma temporal establecida para volver a ejecutarse
para el hilo en cuestión.
Comprueba si ha ocurrido algún evento I/O.
Se ejecuta el intercambio de hilos.
Por razones estructurales, las operaciones anteriores son implementadas
mediante macros, que por defecto, están definidas en el fichero events.h. Aquí es donde
el código específico de gestión de eventos debe ser alojado. Por defecto, la macro
isTimeToReschedule decrementa un contador global y comprueba si esta ha llegado a
cero. Cuando es cero se ejecuta la segunda macro (resechedule). La idea es que esta
replanificación de hilos sea ejecutada solo una vez para muchos bytecodes.
La segunda capa en la implementación del gestor de eventos es la función:
void GetAndStoreNextKVMEvent(bool_t forever, ulong64 waitUntil)
Si un nuevo evento esta disponible desde el sistema operativo, esta función
llama internamente a otra función StoreKVMEvent que transforma el evento del sistema
operativo a lago que entiende la maquina virtual. Si no hay eventos disponibles, la
función no hace nada.
Los argumentos a esta segunda capa son los que siguen:
•
•
Si el argumento forever es TRUE, esta función debe esperar tanto tiempo como
sea necesario hasta que ocurra un evento.
Si el argumento forever es FALSE, esta función debe esperar al menos hasta el
tiempo indicado por waitUntil.
Algunas especificaciones de conservación de alimentación han sido incluidas en
la implementación de referencia de estas funciones, esa es la función de los dos
parámetros antes comentados. Si no hay eventos pendientes la función de esta segunda
capa puede poner la maquina a dormir hasta que ocurra el evento. Una discusión con
mas detalle acerca de la conservación de batería se encuentra reseñada en el último
apartado del capítulo.
14.5.1.
Macros de invocación desde el intérprete.
Una de estas macros es por ejemplo:
#define signalTimeToReschedule() (Timeslice = 0)
Que se emplea para forzar la ejecución del hilo actual y por tanto que haya una
operación de replanificación de hilos. O bien:
#define isTimeToReschedule() (Timeslice-- == 0)
Que se emplea para comprobar si hay que realizar la replanificación.
Sin embargo posiblemente la macro más importante es reschedule que es
invocada desde la macro interna al intérprete RESCHEDULE. Esta macro es la que hace
de puente entre el intérprete de bytecodes y el módulo de gestión de hilos. Básicamente
esta macro invoca la función switchThread del gestor de hilos una y otra vez bien hasta
que no quede ningún hilo que ejecutar o bien hasta que se han encontrado un hilo a
ejecutar:
#define reschedule()
do {
ulong64 wakeupTime;
if (!areAliveThreads()) {
return;
/* end of program */
}
checkTimerQueue(&wakeupTime);
InterpreterHandleEvent(wakeupTime);
__ProcessDebugCmds(0);
} while (!SwitchThread());
\
\
\
\
\
\
\
\
\
Se puede observar como antes de invocar la operación swicthThread se invoca al
método InterpreterHandleEvent que como veremos mas adelante deja al hilo en espera
de que se produzca un evento o hasta que cumpla el tiempo que el es pasado como
parámetro. Se puede también observar como este tiempo máximo de espera se consulta
de la cola de temporización del gestor de hilos y que se emplea para sincronización de
los hilos con funciones nativas.
14.5.2.
Inicialización módulo de gestión de eventos.
Veremos en este apartado las operaciones que conllevan inicializar el módulo
que estamos analizando en este capítulo, dicha operación de inicialización se realiza
como es lógico en el inicio de la maquina virtual junto con el resto de módulos que
integran el sistema.
La operación de inicialización de este módulo es:
void InitializeEvents()
Es una operación relativamente senc illa que básicamente se encarga de
inicializar las variables y estructuras compartidas a lo largo del módulo. Dicha
inicialización implica:
waitingThread = 0;
makeGlobalRoot((cell **)&waitingThread);
opened = FALSE;
eventInP = 0;
eventCount = 0;
Se pone a cero el puntero waitingThread que se empleara para referenciar los
hilos que estén a la espera de un evento, se inicializa opened a FALSE que se actualiza
al leer un evento del sistema y además se inicializan las variables enventInP y
eventCount que se emplean como veremos para gestionar el buffer donde la KVM va
almacenando los eventos que le lleguen.
Existe como vemos otra operación también muy importante y es hacer que el
hilo que espera el evento waitingThread sea un elemento raíz del sistema para de esta
forma evitar que el recolector de basura pueda borrarlo. Esto se debe a que este hilo es
solo accesible desde este módulo por lo cual si no fuera elemento raíz el recolector lo
consideraría basura y lo eliminaría.
14.5.3.
Captura de eventos del sistema.
Una vez que el módulo ha sido inicializado este queda a la espera de recibir los
eventos que le lleguen del sistema operativo. La forma en la que la KVM captura los
eventos que le lleguen del sistema en dos fases:
•
•
Lectura de los eventos del sistema.
Almacenaje de dichos eventos para que puedan ser accedidos posteriormente.
14.5.3.1.
Lectura de eventos del sistema.
Para la comunicación de la KVM con los eventos que genera el sistema
operativo es necesario un complejo protocolo de operaciones muy dependiente del
sistema operativo específico para el cual va destinado la versión de la KVM.
Básicamente se emplean una serie de operaciones que trabajan sobre los eventos como
si fuera un flujo de I/O cualquiera. De esta forma tenemos las operaciones que se
encargan de abrir y cerrar dicho flujo:
•
•
void
Java_com_sun_cldc_io_j2me_events_PrivateInputStream_open(void)
void
Java_com_sun_cldc_io_j2me_events_PrivateInputStream_close(void)
Ambas operaciones tienen la misma estructura basada en tomar la pila de
ejecución correspondiente al hilo y elevar la excepción correspondiente si opened es
cierto es decir si ya se había leído algún evento. También se actualiza opened a un valor
cierto si estamos abriendo el flujo o a false si lo estamos cerrando
INSTANCE instance = popStackAsType(INSTANCE);
(void)instance;
if (!opened) {
raiseException("java/lang/IllegalAccessException");
}
opened=TRUE/FALSE
Además tenemos las operaciones que se encargan de leer del flujo de eventos
antes mencionado un entero, un array de bytes o una cadena UTF-8 es decir los valores
o resultados obtenidos de ejecutarse el evento:
•
•
•
void
Java_com_sun_cldc_io_j2me_events_PrivateInputStream_readInt(void
)
void
Java_com_sun_cldc_io_j2me_events_PrivateInputStream_readByteArra
y(void)
void
Java_com_sun_cldc_io_j2me_events_PrivateInputStream_readUTF(void
)
Estas operaciones se basan en el empleo de un método común readOneEvent.
Este método opera de la siguiente forma:
•
Fija a cero el tiempo wakeUpTime local a este método:
ll_setZero(wakeupTime);
•
Invocando la operación ya comentada getKVMEvent que localiza el resultado del
evento al principio de la pila de ejecución (topStack). Se fija como hilo en espera
de capturar el evento al hilo actual y se suspende el hilo en espera de que la
maquina virtual lo capture en el segundo nivel tal y como hemos comentado al
principio de este apartado:
if (!getKVMEvent(FALSE, wakeupTime, &topStack)) {
waitingThread = CurrentThread;
topStack = 0;
suspendThread();
}
14.5.3.2.
Almacenaje de eventos y sus resultados.
Esta operación pública se implementa en el método storeKVMEvent, una función
de callback que se emplea para indicar que un evento ha ocurrido y para mantener una
copia temporal de los resultados del evento en un buffer.
Esta función recibe como parámetros:
•
•
Tipo de evento.
Lista de los argumentos que genera el evento.
Veamos a continuación como se realiza esta operación. Primero se preparan los
punteros que se van a emplear a lo largo de la función. Para ello si el contador de
eventos es cero (es decir no hay eventos almacenados en el buffer) se inicializa el
puntero eventInP que es el puntero al siguiente evento que debe ser accedido. Si el
contador de eventos es distinto de cero, se comprueba si hay espacio en el buffer para
dicho evento (marcado por un número máximo de parámetros MAXPARLENGTH)
para si no lo hubiera salir de la función y esperar a que se consuma algún evento del
buffer:
int inP;
int i;
va_list args;
if (eventCount == 0) {
eventInP = 0;
} else if (eventInP > (MAXPARMLENGTH - 1 - argCount)) {
return;
}
Una vez configurado los punteros se procede a usando dicho puntero almacenar
en el buffer el tipo del evento (type) seguido de los parámetros del mismo (args)
(teniendo en cuenta que la lista de parámetros pasada es variable):
inP = eventInP;
eventBuffer[inP++] = type;
va_start(args, argCount);
for (i = 0; i < argCount; i++) {
eventBuffer[inP++] = va_arg(args, cell);
}
va_end(args);
Se actualiza seguidamente el contador de eventos para que refleje que se ha
añadido un nuevo evento al buffer y el puntero al siguiente evento que ha sido
modificado justo antes:
eventCount += (inP - eventInP);
eventInP = inP;
14.5.4.
Conexión KVM con eventos capturados.
La conexión o unión entre los eventos que genera el sistema operativo y la
maquina virtual se realiza a través de la función InterpreterHandleEvent y cuya
declaración es como sigue:
void InterpreterHandleEvent(ulong64 wakeupTime);
Tener en cuenta que este método no esta completo debido a que gran parte de él
depende del sistema o plataforma destino a la que va destinada la maquina virtual.
Básicamente esta función implementa la espera de un hilo a algún evento y el parámetro
pasado es el tiempo límite de espera a dicho evento. Si dicho parámetro es cero se
indica que el sistema puede quedar en hibernación hasta que se produzca el evento. Este
método parte de que los eventos ya han sido registrados en la maquina virtual tal y
como hemos visto en el apartado anterior.
Lo primero que se hace es comprobar si existe algún hilo activo en cuyo caso se
fija el tiempo de espera wakeupTime a cero indicando de esta manera que no se desea
esperar al evento y se fija el parámetro forever a su valor por defecto que es FALSE
(dicho parámetro tendrá gran importancia un poco mas adelante):
if (areActiveThreads()) {
/* Indicate that we don't wait for an event */
ll_setZero(wakeupTime);
}
Si no hay hilos activos wakeUpTime tendrá un determinado valor que se debe
modificar en caso de que el depurador Java este activado o bien haya funciones nativas
asíncronas en uso:
else if (ll_zero_ne(wakeupTime)) {
#if (ENABLE_JAVA_DEBUGGER || ASYNCHRONOUS_NATIVE_FUNCTIONS)
ulong64 max = CurrentTime_md();
ll_inc(max, 20);
if (ll_compare_ge(wakeupTime, max)) {
wakeupTime = max;
}
#endif
}
Como vemos el valor del tiempo de espera nuevo se fija con un valor mínimo de
20 ms y un valor máximo rescatado a través de CurrentTime_md() propia de la
plataforma en cuestión y que devuelve en milisegundos el tiempo transcurrido desde el
inicio de la maquina virtual.
Si no existen hilos activos y además el parámetro wakeuptime pasado es cero
quiere decir que se va esperar hasta que llegue el evento a menos que el depurador Java
o bien alguna función nativa asíncrona este en ejecución. La forma de modificar el
tiempo de espera es la siguiente. Si el depurador java se encuentra activado
(ENABLE_JAVA_DEBUGGER):
•
Si el depurador esta funcionando (vmDebugReady) o bien las funciones
nativas asíncronas están activadas se fija como wakeUpTime el tiempo
actual mas 20 milisegundos:
if (vmDebugReady || ASYNCHRONOUS_NATIVE_FUNCTIONS) {
wakeupTime = CurrentTime_md();
ll_inc(wakeupTime, 20);
}
•
En otro caso se modifica la variable forever a TRUE (para que se quede
esperando al evento el tiempo que haga falta) y inicializa wakeUptime:
forever = TRUE;
ll_int_to_long(wakeupTime, -1);
En cambio si el depurador no esta activado tan solo se toma como wakeuptime el
tiempo actual más los 20 milisegundos de salvaguarda.
Una vez modificado convenientemente el límite temporal de acuerdo con las
opciones preestablecidas la segunda parte se basa en realizar la espera al evento. Nos
podemos encontrar con dos situaciones. La primera de ellas es que o bien no se ha leído
nada de la cola de eventos (cola donde se van almacenando los eventos que genera el
sistema operativo) indicada dicha condición por la variable oponed o bien no hay hilo
esperando evento. En esta primera situación se acometen las siguientes tareas:
§
Se comprueba si wakeUpTime vale cero en cuyo caso no hay que hacer nada
más, saliendo de flujo de captura del evento pues tenemos una espera infinita
hasta que llegue el evento:
if (ll_zero_eq(wakeupTime)) {
return;
}
§
Si wakeUptime es distinto de cero se han de realizar algunas tareas relacionadas
con el depurador, a saber:
o Si el depurador esta activo se le resta al wakeUpTime el tiempo que lleva
ejecutándose la maquina virtual. Si dicho valor resulta mayor que cero se
invoca a ProcessDebugCmds para parsear los comandos que se le han
pasado al depurador. Si el valor resulta menos que cero se manda a
dormir el hilo:
if (vmDebugReady) {
long64 delta64 = *(long64*)&wakeupTime;
ll_dec(delta64, CurrentTime_md());
if (ll_zero_gt(delta64)) {
unsigned long delta;
ll_long_to_uint(delta64, delta);
ProcessDebugCmds(delta);
} else {
SLEEP_UNTIL(wakeupTime);
}
o Si no esta ejecutándose el depurador mediante la macro SLEEP_UNTIL
(wakeUptime) ponemos el hilo a dormir el tiempo indicado:
La segunda situación que se puede dar es que se haya leído un evento de la cola
de eventos o que haya un hilo esperando un evento. Se toma de la cola de eventos el
evento mediante la función getKVMEvent que se encuentra detallado en el apartado de
funciones auxiliares. Mediante la macro popStackForThread se extrae la pila de
ejecución antigua del hilo (antes de invocar al evento) y posteriormente se inserta la
nueva pila de ejecución devuelta por getKVMEvent en la variable result. Así la nueva
pila de ejecución tiene los resultados que ha generado el evento:
if (getKVMEvent(forever, wakeupTime, &result)) {
(void)popStackForThread(waitingThread);
pushStackForThread(waitingThread, result);
resumeThread(waitingThread);
waitingThread = NULL;
}
Finalmente se continúa la ejecución del hilo invocando la operación del gestor
de hilos resumeThread.
14.5.5.
Funciones auxiliares.
En este apartado comentaremos en detalle cada una de las funciones auxiliares
que se han empleado o se han hecho referencia a lo largo del capítulo.
14.5.5.1.
Recupera un evento que se haya producido.
Esta función es local al módulo de gestión de eventos y se encarga de obtener el
siguiente evento o parámetro de la cola de eventos que van llegando desde el sistema
operativo. Como ya hemos visto es invocada por ejemplo desde el enlace entre los
eventos y el intérprete, en la función InterpreterHandleEvent.
Esta función mantiene el siguiente prototipo usando distintos parámetros que
iremos viendo poco a poco:
static bool_t getKVMEvent(bool_t forever, ulong64 waitUntil,
cell *result)
La primera operación que se realiza es comprobar si existe ya algún evento
registrado, si no es así se invoca otra función auxiliar GetAndStoreNextKvmEvent
propia de la implementación particular de la versión de la KVM:
if (eventCount == 0) {
/* Try and get the machine's system to give us an event */
GetAndStoreNextKVMEvent(forever, waitUntil);
}
Independientemente de la implementación de esta función auxiliar su
funcionamiento debe ser el mismo: esperar hasta que se produzca un evento bien de
forma ininterrumpida si el parámetro forever pasado es cierto o bien hasta que se ha
llegado al límite temporal wakeUpTime.
Si por el contrario ya existe un evento registrado, se toma dicho evento del
buffer donde se almacenan cuando estos son recuperados por la maquina virtual y se
decrementa el contador de eventos:
if (eventCount > 0) {
*result = eventBuffer[eventInP - eventCount];
eventCount--;
return TRUE;
} else {
return FALSE;
}
Para acceder al buffer se emplea tanto el puntero al siguiente evento dentro del
mismo eventInP que se actualiza al guardar el evento y el contador de eventos
eventCount.
Evidentemente si a pesar de de que el contador de eventos indique que hay
eventos en el buffer, al acceder a él no hay ninguno se devuelve un valor lógico falso
indicando que no se ha podido recuperar e evento.
14.6. Parámetros de configuración del gestor de eventos.
El módulo de gestión de eventos que estamos estudiando en este capítulo es
posiblemente uno de los módulos que requieren de una mayor configuración externa. El
motivo de esta aseveración es su alta dependencia con la plataforma o sistema operativo
sobre la que se instalará la maquina virtual.
Como hemos visto a lo largo del capítulo la gestión de eventos de la KVM se
basa en la forma en la cual dichos eventos son reportados desde el sistema operativo a la
propia maquina virtual, de ahí el que gran parte de la culpa del rendimiento de este
módulo la tengan las funciones nativas del sistema que compartes operación con la
KVM. Algunas de estas funciones nativas son GetAndStoreNextKVMEvent o
CurrentTime_md,…
Sin embargo existen algunos parámetros que si se pueden configurar como por
ejemplo el número máximo de parámetros que puede tener un evento que llegue del
sistema cuyo MAXPARAMLENGTH valor por defecto es 20. Otro ejemplo son los
parámetros que como ya hemos indicado en otros capítulos afectan a todos los módulos:
INCLUDEDEBUGCODE,
ENABLE_JAVA_DEBUGGER,
ASYNCHRONOUS_NATIVE_FUNCTIONS.
De entre estos tres últimos parámetros los dos primeros solo están activos en
entornos de depuración mientras que el tercero depende en fuerte medida de la
estrategia que se sigua a la hora de capturar eventos del sistema y es decisión del
usuario que esta adaptando la KVM el permitir o no funciones asíncronas.
14.7. Conservación batería.
La mayoría de los dispositivos a los cuales va destinado la KVM son
alimentados de forma autónoma mediante baterías y los constructores de dichos
dispositivos son muy conscientes del elevado consumo de batería. Para minimizar dicho
consumo, la KVM esta diseñada para detener el flujo de ejecución del intérprete cuando
no hay hilos activos en la maquina virtual y cuando la maquina virtual esta a la espera
de algún evento externo. Esto requiere sin embargo un soporte desde el sistema
operativo.
La función siguiente es crucial en esta tarea:
void GetAndStoreNextKVMEvent(bool_t forever, ulong64 waitUntil)
Esta función se encarga como hemos visto de llamar a las operaciones
específicas de hibernación del sistema operativo destino cuando esta función es llamada
con el parámetro forever a TRUE. La KVM ha sido diseñada para que llame a esta
función automáticamente con el parámetro forever a TRUE.
Esto permite a la implementación nativa de lectura de eventos llamar a las
operaciones especificas de hibernación del dispositivo en concreto hasta que el evento
ocurra.
Adicionalmente la macro SLEEP_UNTIL(wakeupTime) deber ser definida de
lamisca forma para llevar a hibernación el dispositivo durante el tiempo indicado por
wakeupTime.
14.8. Conclusiones.
La maquina virtual de la plataforma J2ME actúa de interfaz entre el usuario que ejecuta
un programa Java y el dispositivo o sistema operativo sobre el que se ejecuta. De esta
forma, la maquina virtual ha de ser capaz de comunicar los eventos que genera el
sistema operativo al usuario para que este a través de la aplicación Java pueda actuar en
consecuencia.
Básicamente existen 4 formas distintas de gestión de eventos en la KVM y son:
•
•
•
•
Notificación sincrona (bloqueo).
Polling en código Java.
Polling en el intérprete de bytecodes.
Notificación asíncrona.
Por notificación síncrona nos referimos a la situación en la cual la KVM realiza
la gestión de eventos mediante invocaciones a funciones I/O o eventos del sistema
directamente desde la propia maquina virtual. Como la KVM tiene un único hilo físico
de control de la maquina virtual, ningún otro hilo Java puede ser procesado mientras la
función nativa esta siendo ejecutada y ninguna otra función de la maquina virtual tal
como el recolector de basura puede ser ejecutada tampoco. Este es el método mas
sencillo de notificación de eventos y en muchos casos en suficientemente aceptable
siempre y cuando las funciones nativas estén correctamente diseñadas en cuanto a
errores y rendimiento se refiere.
Frecuentemente la gestión de eventos puede ser implementada eficientemente
usando una combinación de funciones nativas y código Java. Esta es una manera muy
sencilla para permitir que otros hilos Java sean ejecutados mientras el hilo activo esta
esperando la respuesta de un evento. Cuando se emplea esta solución, un bucle de
espera activa en Java es ubicado normalmente en algunas de las librerías de ejecución
por lo cual dicho bucle esta totalmente escondido de las aplicaciones que las emplean.
El procedimiento normal para la biblioteca Java indicada es iniciar una operación nativa
corta de entrada/salida y entonces repetir periódicamente la consulta sobre el estado de
dicha operación hasta que esta finalice. El bucle de espera activa contenido en la
biblioteca debe contener una llamada a Thread.yield para que de esta forma el resto de
hilos puedan ejecutarse eficientemente.
La tercera aproximación al problema de la implementación del manejador de
eventos hacer que el intérprete de bytecodes de forma periódica haga llamadas las
operaciones nativas de gestión de eventos. Esta solución es una variación de la
notificación síncrona anteriormente comentada. En solución fue originalmente muy
usada en la KVM, por ejemplo para implementar los eventos GUI de la Palm.
Por notificación asíncrona se entiende que la gestión de eventos puede ocurrir en
paralelo mientras la maquina virtual continúa con su ejecución. Esta es generalmente la
solución más eficiente para la gestión de eventos y resulta en una latencia para dicha
notificación pequeña. Sin embargo, esta solución requiere que el sistema operativo
provea las facilidades operativas adecuadas para implementar el manejador asíncrono de
eventos. Tales facilidades pueden no estar disponibles en todos los sistemas operativos.
Por todo ello, esta solución es algo mas compleja de implementar, ya que el diseñador
de la maquina virtual tiene que tener especial cuidado con problemas de bloqueo. La
implementación de referencia que estamos estudiando ofrece algunos ejemplos que
pueden ser usados como punto de partida para implementar manejadores de eventos de
otros dispositivos o plataformas.
La implementación del gestor de eventos en la KVM se compone de dos capas.
La primera capa involucra directamente al intérprete de bytecodes. Se puede comprobar
como al inicio del intérprete hay una invocación a la operación de reschedule que se
emplea para realizar una operación de thread-switching. Dicha operación se ejecuta en
los siguientes pasos:
•
•
•
•
Se comprueba si existe algun hilo activo pendiente de ser ejecutado y si no hay
ninguno detiene la maquina virtual.
Comprueba si hay alguna alarma temporal establecida para volver a ejecutarse
para el hilo en cuestión.
Comprueba si ha ocurrido algún evento I/O.
Se ejecuta el intercambio de hilos.
La segunda capa en la implementación del gestor de eventos es la función:
void GetAndStoreNextKVMEvent(bool_t forever, ulong64 waitUntil)
Si un nuevo evento esta disponible desde el sistema operativo, esta función
llama internamente a otra función StoreKVMEvent que transforma el evento del sistema
operativo a lago que entiende la maquina virtual. Si no hay eventos disponibles, la
función no hace nada.
Asimismo la KVM es capaz de capturar los eventos que produce el sistema
operativo en dos fases. En una primera fase, lee el evento que se produce desde el
sistema operativo empleando un protocolo de comunicaciones específico y dependiente
del sistema operativo sobre el cual se ejecuta la maquina virtual. En una segunda fase y
mediante la operación StoreKVMEvent se leen de un buffer temporal cumplimentado en
el paso anterior los valores que el evento hubiera generado.
Finalmente mediante el método InterpreterHandleEvent los eventos
almacenados son interpretados y manejados por la maquina virtual de la forma que sea
conveniente. Esta última gestión de eventos también es fuertemente dependiente de la
maquina destino de la implementación de la maquina virtual.
Como se ha podido observar a lo largo del capítulo este módulo esta
íntimamente relacionado con el sistema operativo. Es por ello que la implementación de
referencia de la KVM solo muestra en muchas partes un esqueleto u orientación de los
métodos y operaciones de este módulo. Para un mejor control y adaptación de la
maquina virtual se hace necesario detallar más ese esqueleto .
Documentos relacionados
Descargar