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 .