cerrar-sesion editar-perfil marker video calendario monitor periodico fax rss twitter facebook google-plus linkedin alarma circulo-derecha abajo derecha izquierda mover-vertical candado usuario email lupa exito mapa email2 telefono etiqueta

400430407. Componentes conscientes de la memoria

Escrito por Redacción en Tema de portada
no hay comentarios Haz tu comentario
Imagen de logotipo de facebook Imagen de logotipo de Twitter Imagen de Logotipo de Google+ Imagen de logotipo de Linkedin

La mayor parte del software que se queda sin memoria simplemente se colapsa. En los sistemas operativos modernos, esto ocurre cuando los programas necesitan más memoria virtual de la que hay disponible. Un programa que reserva o requiere demasiada memoria virtual puede quedarse sin espacio libre. Cuando eso ocurre, puede comportarse mal: Las asignaciones de heap pueden fallar, nuevos hilos pueden no iniciarse, las pilas pueden no lograr crecer, etc. Podría salir educadamente o colapsarse en ese punto, a menudo sin tan siquiera una queja sobre lo que realmente ha salido mal.

Idealmente, los programas gestionan con gracia las condiciones de “sin memoria” y siguen funcionando. Al menos, podrían proporcionar algún resultado de diagnóstico detallado, o buscar formas de enfrentarse a la situación y sobrevivir hasta que los recursos vuelvan a estar disponibles. Dichos resultados positivos son posibles si el programa puede identificar la memoria virtual que no se necesita en realidad en el momento en el que se produce el problema de memoria baja.

((Los programas gestionan con gracia las condiciones de “sin memoria” y siguen funcionando. Al menos, podrían proporcionar algún resultado de diagnóstico detallado, o buscar formas de enfrentarse a la situación))

Dadas las técnicas modernas de programación basadas en componentes, los componentes del programa normalmente no tienen suficiente información sobre los demás para entender y cumplir con los requisitos de memoria virtual de los otros, según va aumentando la “presión de la memoria”. Si bien el aspecto de ocultación de información del buen diseño basado en componentes es útil de muchas formas, puede obstaculizar la capacidad de los componentes de compartir recursos limitados de memoria virtual.

Hay dos tipos de memoria virtual no utilizada que se hacen interesantes cuando tenemos el programa escaso de memoria:
– Memoria virtual reservada.
– Memoria virtual comprometida y no usada.

La memoria virtual reservada ocupa parte del espacio de dirección virtual del programa, lo más probablemente porque un componente la ha apartado de manera activa, aunque evidentemente no está siendo utilizada. Memoria virtual comprometida, pero no usada se produce cuando los componentes no están haciendo una elección prudente en lo que respecta a sus marcas de memoria. No hay nada que evite que un componente se apropie bien de memoria virtual reservada o comprometida para evitar un colapso, si los componentes tienen la suficiente inteligencia como para reconocer radios de acción adecuados que podrían ser apropiados. Los radios de acción apropiados pueden usarse para proporcionar espacio para la preparación y presentación de resultados de diagnóstico, o con cualquier otro fin requerido para mantener el programa funcionando cuando, de otra forma, no podría continuar.

Algunos componentes reservan de manera activa memoria virtual y no la utilizan. Al reservar memoria virtual, un componente de forma característica la hace inutilizable para otros componentes del mismo programa que están ejecutándose en el mismo proceso- a no ser que los componentes sean lo suficientemente “listos” como para darse cuenta de que podrían comprometer esa memoria para su propio uso, en lugar de causar un colapso al quedarse sin memoria.

Podemos compilar este tipo de inteligencia en nuestro programa, haciendo que los componentes sean lo suficientemente inteligentes como para no morir por quedarse sin memoria cuando hay disponible memoria reservada y no comprometida. Para ello, necesitamos un pequeño componente de control interno/analizador de memoria virtual que registra información sobre cómo nuestro programa usa la memoria virtual. Esta información puede incluir un conjunto de dataciones registradas según se ejecuta el programa, cada vez que se reserva memoria virtual.

Cuando la memoria virtual está baja, es posible que los otros componentes puedan necesitar el componente de control interno/analizador para hacerse con la zona de la memoria reservada que tiene la datación más antigua. La zona está así disponible para ser utilizada para cualquier fin que necesite el programa para mantenerse vivo.

Algunos componentes reservan de manera activa memoria virtual y no la utilizan. Puede que se deje en un estado no inicializado, o llena de un patrón simple – normalmente todos NULL. En la mayoría de las plataformas modernas, la memoria virtual se compromete a al menos una página cada vez (por ejemplo, una página ocupa cuatro kilobites de memoria virtual en la mayoría de las versiones de Windows).

((En la mayoría de las plataformas modernas, la memoria virtual se compromete a al menos una página cada vez))

Las páginas de memoria que se quedan sin inicializar pueden ser des-comprometidas por cualquier componente en un proceso, y no se produce ningún daño a no ser que la memoria comprometida la necesite realmente el componente que la comprometió inicialmente. El mismo componente de control interno/analizador de memoria virtual que controla la memoria virtual reservada puede ser ampliado para hacer que los componentes de software sean lo suficientemente inteligentes como para no morir por una situación de quedarse sin memoria cuando hay disponible memoria comprometida, no inicializada.

El análisis puede implicar rastrear las páginas de memoria como “en uso” o “no en uso”. Una forma de decir si una página está en uso es ejecutar una rutina de compresión, como el algoritmo LZW, en los contenidos de la página. Esto puede hacerse con cada página rastreada por el componente de control interno/analizador cuando se produce una situación de quedarse sin memoria y cuando hay disponibles páginas reservadas pero no comprometidas. Cuando se encuentra que toda o parte de la página está inicializada, se puede considerar que la página está en uso.

El análisis puede también implicar el registro de una datación, durante la ejecución, cada vez que se comprometen páginas de memoria virtual. Cuando la memoria virtual está baja, es posible que los otros componentes puedan necesitar el componente de control interno/analizador para des-comprometer la página no inicializada con la datación más antigua, para que esa pagina pueda volver a ser utilizada cuando sea necesario para mantener funcionando el programa.

El diseño de un control interno

Las figuras 1-5 muestran cómo podríamos diseñar un sencillo componente de control interno/analizador de memoria virtual que ayuda al programa a mantenerse vivo, al menos el tiempo suficiente para poder extraer algún tipo de información de diagnóstico, si no más tiempo. La idea es saber dónde está la memoria virtual según la van reservando o comprometiendo los distintos componentes de nuestro programa.

Para que ello funcione, necesitamos código que intercepte las invocaciones al sistema que se encargan de reservar, comprometer y liberar la memoria virtual. En algunos sistemas operativos, como Linux, la elección de qué invocaciones interceptar es sencilla: En Linux, las zonas de memoria virtual se crean mediante invocaciones a mmap() y se liberan mediante invocaciones a munmap().

En otros sistemas operativos, como Windows, no hay una función API documentada que tenga la responsabilidad de crear todas las zonas de memoria virtual para el uso de nuestro proceso. Sin embargo, hay una función API, VirtualAlloc(), que podemos utilizar para crear algunas zonas. Si depuramos hasta llegar a VirtualAlloc(), alcanzaremos una función exportada que es invocada por la mayoría, si no todas, las zonas que crea nuestro programa. En versiones actuales de Windows, incluidas Vista y XP, esta función se denomina NtAllocateVirtualMemory(). Esta función va emparejada con NtFreeVirtualMemory(), que se invoca para liberar las zonas.

Hay numerosas formas de organizar la intercepción de la función. Un método sencillo es reemplazar los primeros bites de la función a interceptar con una instrucción, como por ejemplo un salto, que pasa el control a una rutina que querríamos invocar cada vez que se invoca esa función. La rutina puede después restaurar los primeros bites de la función interceptada, invocarlo con sus parámetros originales, interceptarla de nuevo, y después hacer cualquier procesado que tengamos en mente.

Este sencillo método puede dar respuesta a las necesidades de intercepción del concepto de control interno/analizador de memoria virtual de las figuras 1-5. El listado número 1 (disponible online en www.ddj.com/code/ ) hace todo esto en sistemas de arquitectura x86 que se ejecutan en Windows. Observemos que el código alcanzado mediante las instrucciones de salto puede mejorarse para los programas de multihilado si añadimos algún mecanismo de serialización de manera que las invocaciones objetivo del sistema siempre serán interceptadas cuando llega un nuevo hilo.

En el listado número 1 se proporcionan algunas sugerencias relativas a la colocación de las invocaciones de sincronización, que crea un control para excepciones de «sin memoria”. Las rutinas invocadas por este control harán uso de la información localizada mediante las funciones interceptadas que crean y liberan las zonas de memoria virtual.

Podemos organizar la intercepción de las invocaciones al sistema para que se produzcan automáticamente cuando se carga el módulo de control interno/analizador de memoria virtual. Eso se logra (en Windows) en el listado número 1 al hacer la intercepción dentro de la invocación DllMain() del módulo. De esa forma, sólo se necesita una línea del código que cargue el módulo y desencadene su mecanismo de rastreo de memoria virtual en tiempo real.

En Windows, la línea de código pertinente es una invocación LoadLibrary(); véase el listado número 2 (también disponible online en www.ddj.com/code/). Tras esta invocación, se interceptan las invocaciones de asignación/des-asignación de memoria virtual del listado número 2. Como alternativa, podemos omitir totalmente la invocación LoadLibrary() y enlazar estáticamente el módulo de control interno/analizador de memoria virtual. Si enlazamos de forma estática el control interno a un componente que se cargue al comienzo de cada ejecución, eso hace que el rastreo de la memoria virtual se inicie al comienzo de la ejecución, lo que le proporciona a dicho control interno más zonas entre las que elegir si la memoria virtual va baja.

Las figuras 1-3 describen las rutinas en el rastreo de memoria virtual que pueden ser invocadas desde las funciones interceptadas. La efectividad de dichas rutinas depende del porcentaje de zonas o páginas de memoria virtual realmente no utilizada que se han encontrado, para cuando la presión por “excesiva memoria” hace detener nuestro programa. Por ese motivo, la intercepción debería hacerse en el nivel más bajo posible para así llegar a las zonas “más posibles”.

((Podemos organizar la intercepción de las invocaciones al sistema para que se produzcan automáticamente cuando se carga el módulo de control))

También debería establecerse lo antes posible durante la ejecución. Las zonas pueden rastrearse en una lista que está ordenada por las direcciones base de las zonas. En las figuras 1-3, algunos elementos de datos están asociados con cada zona rastreada. Estos elementos de datos incluyen la cadena de invocaciones que lleva a la creación de la región, y una datación. La datación se utiliza cuando el programa se queda sin memoria, para seleccionar una zona que no ha sido utilizada durante mucho tiempo como objetivo para ser reutilizada.

La cadena de invocación puede servir para identificar el componente responsable de la creación de la zona. El listado número 3 (disponible online en www.ddj.com/code/) nos da una rutina simple para coleccionar una cadena de invocaciones en la plataforma Intel.

Figura 1

Figura 2

Figura 3

Robo de Zonas

El control de excepción vectorial que se usa en el listado número uno es una solución perfecta para solventar excepciones de “no hay memoria” generadas por cualquier componente del proceso. Con este tipo de control de excepción, incluso los componentes que no hemos desarrollado nosotros, como los módulos de terceros que carga nuestro programa, se benefician de la protección del control interno. La rutina DllMain() del listado número 1 establece el control de excepción vectorial, al principio, antes de interceptar las invocaciones al sistema que son nuestro objetivo.

Como este control invoca rutinas que dependen del rastreo de memoria virtual, tal y como se organizó al interceptar estas invocaciones al sistema, la intercepción no se realiza a no ser que el control se ponga primero a punto, con éxito. El control de excepción vectorial está disponible en las versiones actuales de Windows, incluidas Vista y XP. Si nuestro sistema operativo no soporta el control de excepción vectorial, necesitamos proporcionar algún medio de gestionar las excepciones de “sin memoria” en cada hilo que se inicia.

((Mejor aún, podemos invocar una función API, como GetModuleHandle() en Windows, para buscar la dirección base de cada módulo que aparece en la cadena de invocaciones))

Como el control vectorial del listado número 1, nuestro control puede invocar una rutina que usa los datos rastreados sobre las zonas de memoria virtual de nuestro programa para mantener vivo el programa, como en la figura 4, que introduce el concepto de robo de zonas.

Figura 4

Figura 5

En las figuras 4 y 5, una zona de memoria virtual reservada se considera robada cuando está comprometido su uso por un componente que no la reservó. De igual forma, una página comprometida no usada se considera robada cuando está no reservada y vuelta a comprometer para su uso por un componente distinto al que originalmente lo comprometió. ¿Cómo sabemos que el componente original no vendrá, e intentará usar su espacio comprometido o reservado? No lo sabemos, pero nuestro control interno puede intentar desviar esa posibilidad robando memoria que ha estado sin utilizarse la mayor cantidad de tiempo, de entre toda la memoria que está rastreando. La auténtica protección implica eso, protección.

En Windows, se puede aplicar la función API VirtualProtect() a cada página o zona robada para que genere una excepción cuando se accede a la zona. El empleo de un control de excepción para que se encargue de todos los accesos a la zona implica, desde luego, que nuestro programa podría funcionar lento cuando los componentes empiezan a robar zonas. Por otro lado, es normalmente más tolerable un funcionamiento bajo que un colapso.

Si queremos que nuestro programa siga vivo mucho después de robar una zona que originalmente se reservó o comprometió para algún otro fin, entonces necesitaremos una forma de distinguir los componentes. Una manera sencilla de hacerlo está basada en las cadenas de invocación. Cada componente ocupa él mismo una zona, o un conjunto de zonas, en donde se carga en la memoria virtual.

Nuestro control interno puede rastrear esas regiones junto con todas las demás. Si es así, entonces comparar las direcciones base de las zonas asociadas con los componentes es cuestión de una búsqueda en nuestra lista de zonas. Mejor aún, podemos invocar una función API, como GetModuleHandle() en Windows, para buscar la dirección base de cada módulo que aparece en la cadena de invocaciones.

Nuestro control interno necesita poder reconocer invocaciones dentro de módulos que representan código API o código asignador, de manera que no comparará los módulos que contienen ese código. Esto lo confunde, lo que hace que no pueda reconocer cadenas de invocación que vienen de componentes distintos. Como muchos componentes comparten un asignador común, las cadenas de invocación asociadas con la creación de zonas, de manera característica terminan siempre en las mismas funciones.

Eso es por lo que no se pueden distinguir unos componentes de otros por las últimas entradas de una cadena de invocaciones asociada a la creación de zonas. Pero si dedicamos algún tiempo a depurar nuestro código de rastreo, especialmente en la rutina que implementa la figura número 1, en ese caso vamos conociendo dónde está el código de creación de zonas comunes en la memoria virtual. Al descartar esos radios de acción durante la comparación de cadenas de invocación y en su lugar buscar el invocador siguiente fuera de una de estas rutinas comunes para cualquier cadena dada, podemos obtener una idea de si el componente responsable de robar una zona es el mismo que está ahora accediendo a la misma. Si es así, nuestro control interno puede con seguridad desproteger la región y proseguir con el acceso. De lo contrario, evidentemente ha llegado el momento de robar otra zona.

((No necesitamos implementar un sistema de reconocimiento de componentes para hacer realidad este beneficio))

Una ventaja obvia de este sistema de rastreo/robo de zonas es la capacidad de construir un informe detallado cuando surge una condición de sin memoria. A menudo, cuando un programa se queda sin memoria, no puede ni tan siquiera quejarse antes de que se produzca el inevitable colapso. La técnica de hacer que esté disponible cualquier memoria virtual no utilizada para usarla en la construcción y visualización del resultado de diagnósticos puede ser muy útil en y por sí misma.

La capacidad añadida de proporcionar información sobre las zonas de memoria virtual que están siendo utilizadas, incluida información sobre qué componentes las crearon y cuándo fueron creadas, puede proporcionar las pistas que necesitamos para evitar otras condiciones previsibles similares de “sin memoria”. No necesitamos implementar un sistema de reconocimiento de componentes para hacer realidad este beneficio, si nos conformamos con obtener nuestro resultado de diagnóstico y con dejar que el programa se colapse. Pero si queremos mantener el programa vivo más tiempo, con toda probabilidad necesitaremos limpiar cualquier memoria virtual comprometida para fines de diagnóstico en cuanto podamos, una vez hayamos hecho que la información de diagnóstico esté disponible.

Desde luego, el componente de control interno en sí añade a la marca de memoria de nuestro programa, pero sólo de forma modesta. Todo el módulo de control interno puede contener quizá entre 3 y 5 veces la cantidad de código que hay del listado número 1 al 3. Los datos de rastreo para las zonas de memoria virtual son mínimos, porque normalmente no hay más de varios cientos de zonas a rastrear, incluso en un programa grande y complejo.

Bastantes componentes, incluso los comerciales como algunas JVM, dejan reservadas grandes cantidades de memoria virtual. Si esa memoria reservada puede ser reutilizada para mantener el programa vivo ante una presión de memoria, por otro lado abrumadora, entonces es que nuestro control interno se ha ganado el sustento.

Etiquetas

Noticias relacionadas

Comentarios

No hay comentarios.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Debes haber iniciado sesión para comentar una noticia.