Capítulo 38 La memoria

Windows tiene su manera particular de manejar la memoria del sistema. Esto es lógico, y era de esperar, ya que como recurso que es, la memoria también debe ser controlada y administrada por el sistema operativo.

Memoria virtual

Para administrar la memoria, en el API32, Windows mantiene un espacio virtual de memoria con direcciones de 32 bits para cada proceso. Esto permite que cada uno de los procesos disponga de hasta cuatro gigabytes de memoria. Dos de esos gigas están disponibles para el usuario, los correspondientes a las primeras direcciones de memoria; los otros dos están reservados para el núcleo del sistema.

Se trata de un espacio virtual, esto quiere decir que las aplicaciones no acceden directamente a memoria, y que las direcciones que manejamos no corresponden a direcciones de memoria física. El sistema se encarga de mapear esas direcciones virtuales a direcciones físicas. En esto, como en todos los recursos, el sistema operativo trabaja como intermediario entre el usuario y el hardware.

Este modo de trabajar proporciona a cada aplicación gran cantidad de memoria, de hecho, en la mayoría de los casos proporciona más memoria de la disponible físicamente. Para mantener toda esa memoria se trabaja con un fichero en disco como almacén de memoria complementaria: el fichero de paginación o fichero de intercambio.

El sistema de memoria virtual trabaja con unidades de memoria llamadas páginas. El tamaño de cada página varía dependiendo del tipo de ordenador, (en microprocesadores de la familia x86 suele ser de 4 KB). Mientras es posible, las páginas de memoria se asignan a cada proceso desde la memoria física, pero cuando es necesaria más memoria, los procesos inactivos pueden copiar algunas de sus páginas en el fichero de intercambio y liberar de ese modo parte de la memoria física que se puede asignar al proceso activo.

Todo esto es transparente para el usuario, que sólo notará que cuando es sistema está muy cargado aumenta la actividad del disco y disminuye la velocidad del sistema.

Nos interesa saber, de todos modos, que cada página de memoria virtual asociada a un proceso puede estar en uno de tres posibles estados:

  • Libre: no es accesible, pero puede ser reservada o asignada.
  • Reservada: no se usa por el proceso, ni tampoco está asociada a memoria física ni ocupa espacio en el fichero de intercambio. El proceso tampoco podrá obtener memoria de páginas reservadas.
  • Asignada: página que tiene memoria física o espacio en el fichero de intercamnio asociado. Estas páginas pueden ser protegidas, para evitar su acceso o limitarlo a sólo lectura, o pueden ser accedidas en lectura y escritura.
Nota:

Puedes ver más detalles sobre la memoria virtual en el artículo de José Manuel: Memoria virtual.

Un poco de historia

Vamos a ver algunos conceptos que tal vez te suenen, pero que en el API de Win32 han quedado obsoletos. En cualquier caso, puede ser conveniente saber algo sobre ellos, recordar sus aplicaciones y comprender por qué ya no son necesarios.

Memoria local y global

En las primeras versiones de Windows, de 16 bits, existen dos tipos de memoria que se pueden usar para crear objetos de memoria para un proceso: la memoria local y la memoria global.

Esto tiene su fundamento en el modo de trabajar de los procesadores de Intel. En el modo real la memoria se divide en segmentos, en cada uno de los cuales se usan punteros de 16 bits para acceder a la memoria, debido a esto, cada segmento tiene un tamaño de 64KB. Estos punteros de 16 bits son llamados punteros near o cercanos. Para acceder a la memoria fuera del segmento se usan punteros far o lejanos, que son punteros de 32 bits.

Hasta Windows 3.1 se trabajaba en modo real, y a cada proceso se le asignaba un segmento. La memoria que se puede obtener por el proceso dentro de su propio segmento se denomina memoria local, y la que se obtiene fuera de él, se denomina memoria global. Los bloques de memoria local tienen que ser, por definición, pequeños, ya que su tamaño máximo estaba limitado al espacio libre en el segmento.

A partir de Windows 95 y en Windows NT, es decir, con el API de 32 bits, el procesador trabaja en modo protegido. El concepto de segmento desaparece, y la memoria es lineal, con direcciones de 32 bits, lo que proporciona acceso a 4GB de memoria. Además se introduce el modelo de memoria virtual, lo cual elimina cualquier diferencia entre memoria local y global, y en el API32 las funciones para reservar y liberar memoria local o global son equivalentes, y siempre retornan punteros de 32 bits, o sea, punteros far.

Otros atributos de la memoria en Windows

Además de lo comentado hasta ahora, Windows mantiene otros dos atributos para cada objeto de memoria obtenido: la movilidad y la descartabilidad.

Objetos móviles y fijos

El primer atributo se refiere a la movilidad del objeto. De nuevo estamos ante un concepto que ha perdido sentido en el API32. En versiones de Windows de 16 bits cada objeto podría tener el atributo de movilidad activado o desactivado.

¿Por qué crear objetos móviles? Debido al modo en que se organiza la memoria en Windows de 16 bits, después de un tiempo de funcionamiento del sistema, la memoria podía estar muy fragmentada, con pequeños bloques de memoria reservados para distintos objetos, y pequeños huecos resultantes de la destrucción de objetos innecesarios. Esto puede provocar que, a pesar de existir suficiente memoria libre en el sistema, sea imposible conseguir un bloque del tamaño necesario para satisfacer una nueva petición.

La solución es que el sistema pueda trasladar algunos de los objetos existentes a otras posiciones, de modo que se desfragmente la memoria y las partes libres queden contiguas.

Pero esto crea un conflicto, ya que Windows es un sistema multitarea, habrá aplicaciones que estén usando ciertos objetos, de modo que tales objetos no pueden ser movidos. O bien, aunque esos objetos no se estén usando, los punteros que los manejan tienen valores constantes, o deben conservar sus valores mientras el proceso que los ha creado siga funcionando. De otro modo sería imposible manejarlos y liberarlos.

Cuando se crea un objeto de memoria fijo el sistema proporciona un puntero de 32 bits, y se pueden manejar como punteros corrientes:

  1. Creación: se crea un objeto fijo de memoria, y se obtiene un puntero.
  2. Uso: podemos trabajar con él ya que el valor del puntero es fijo.
  3. Destrucción: cuando ya no sea necesario destruimos el objeto, liberando la memoria asociada.

Sin embargo, los objetos de memoria móviles creados usando el API de Windows no proporcionan una dirección fija, sino un manipulador de memoria. El proceso es el siguiente:

  1. Creación: se crea un objeto movible de memoria, y se obtiene un manipulador.
  2. Bloqueo: cuando se va a usar el objeto se bloquea el objeto, y se obtiene una dirección para la memoria correspondiente.
  3. Uso: mientras permanece bloqueado, el objeto no será movido por el sistema, y podemos trabajar con él como si el valor del puntero fuese fijo.
  4. Desbloqueo: cuando no necesitamos manipular el objeto, lo desbloqueamos, y el sistema podrá moverlo si lo considera necesario.
  5. Destrucción: cuando ya no sea necesario, y si no está bloqueado, destruimos el objeto, liberando la memoria asociada y el manipulador.

Por supuesto, crear objetos móviles tiene la ventaja de que el sistema puede gestionar la memoria de un modo mucho más eficaz, pero siempre es posible, si nuestro programa lo requiere, crear objetos fijos. La diferencia es que en este caso, el sistema no podrá mover tales objetos para desfragmentar la memoria.

Objetos descartables y no descartables

El otro atributo está relacionado con el mismo problema.

Cuando se debe desfragmenta la memoria para conseguir bloques libres lo suficientemente largos, es necesario mover el contenido de cada objeto, de modo que los valores que contienen no se pierdan. El concepto de memoria descartable va un paso más allá. Si el contenido de cierto objeto móvil es fácilmente recuperable o se puede reconstruir, sin necesidad de almacenarlo de forma permanente, el sistema de gestión de memoria no necesita almacenarlo permanentemente, y puede mantener sólo los manipuladores, sin necesidad de mantener el contenido de tales objetos.

Por ejemplo, tenemos un mapa de bits en memoria creado a partir de un recurso. El contenido de la memoria correspondiente a ese mapa de bits puede ser recuperado del recurso tantas veces como sea necesario, pero, siempre que sea posible, nos conviene mantenerlo en memoria, ya que recuperar ese recurso requiere cierto tiempo. Si creamos ese objeto como descartable, el sistema puede borrar la memoria asociada cuando lo considere necesario, haciendo innecesario tanto mantener esa memoria como copiarla.

El proceso, cuando se trabaja con objetos descartables es algo más complicado:

  1. Creación: se crea un objeto descartable de memoria, y se obtiene un manipulador.
  2. Bloqueo: cuando se va a usar el objeto se bloquea el objeto, y se obtiene una dirección para la memoria correspondiente.
  3. Restitución: se averigua si la memoria del objeto ha sido descartado por el sistema, y en ese caso se restituye su valor, ya sea por cálculo o por carga desde un fichero.
  4. Uso: mientras permanece bloqueado, el objeto no será movido por el sistema, y podemos trabajar con él como si el valor del puntero fuese fijo.
  5. Desbloqueo: cuando no necesitamos manipular el objeto, lo desbloqueamos, y el sistema podrá moverlo o descartarlo si lo considera necesario.
  6. Destrucción: cuando ya no sea necesario destruimos el objeto, liberando la memoria asociada y el manipulador.

Cada vez que bloqueemos el objeto hay que verificar si su memoria ha sido descartada o no. Un objeto cuya memoria haya sido descartada contendrá basura.

Tampoco es posible crear objetos fijos descartables. Los objetos descartables siempre deben tener el atributo de movilidad.

Funciones clásicas para manejo de memoria

Disponemos de nueve funciones para manejar memoria local y global. Los nombres son los mismos, cambiando el prefijo "Local" y "Global".

A modo de resumen, ya que no será frecuente que usemos estas funciones, podemos considerar esta tabla:

Local Global
Reservar memoria LocalAlloc GlobalAlloc
Descartar objeto de memoria LocalDiscard GlobalDiscard
Información sobre objeto de memoria LocalFlags GlobalFlags
Liberar objeto de memoria LocalFree GlobalFree
Obtener un manipulador de objeto LocalHandle GlobalHandle
Bloquear objeto LocalLock GlobalLock
Reubicar objeto de memoria LocalRealloc GlobalRealloc
Tamaño de un objeto LocalSize GlobalSize
Desbloquear un objeto LocalUnlock GlobalUnlock

Como complemento disponemos de la función GlobalMemoryStatus para obtener información sobre la memoria disponible en el sistema. Esta función usa una estructura MEMORYSTATUS para almacenar la información.

Ejemplo

Existen además, otras funciones útiles para manejar memoria:

Desventajas de este modelo de memoria

Afortunadamente, esto pertenece al pasado, salvo que tengas que crear aplicaciones para versiones de Windows de 16 bits, claro.

El modelo de memoria virtual hace que todo este tema de memoria móvil carezca de sentido, ya que nuestras aplicaciones no trabajan con memoria real, el sistema siempre puede mover cualquier página de memoria para liberar recursos, sin que eso afecte en modo alguno a las direcciones de memoria virtuales.

La memoria descartable aún puede resultar útil, ya que permite liberar recursos que se usan poco o que pueden regenerarse fácilmente.

Otra desventaja es que las funciones estándar para manejar memoria: malloc y free no funcionan de forma segura en el modelo de memoria del API de 16 bits, algo que sí sucede en el modelo virtual. En el API de 16 bits, la función malloc no puede obtener objetos de memoria móviles o descartables. Lo mismo sucede con los operadores new y delete.

Sólo en el caso en que queramos crear objetos descartables tendremos que hacer uso de las funciones del API para manejar memoria.

Funciones para manejo de memoria virtual

Ya hemos comentado que gracias al uso del modelo de memoria virtual, salvo para crear objetos de memoria descartables, con el API32 podremos usar las funciones estándar C, o los operadores de C++ para manejar memoria dinámicamente. Sin embargo, el modelo de memoria virtual nos permite un control sobre la memoria que no está disponible si sólo usamos funciones y operadores estándar.

El modelo virtual nos permite hacer cosas como:

Reservar direcciones de memoria virtual

Las funciones VirtualAlloc y VirtualAllocEx permiten reservar un rango de direcciones, sin asignarles memoria física, o asignándosela.

  • Podemos reservar direcciones de memoria, sin asignarles memoria física, de modo que se deje espacio para que estructuras dinámicas de datos crezcan durante la ejecución.
  • Asignar memoria a direcciones previamente reservadas.
  • Hacer ambas cosas a la vez.
   int *puntero;
   ...
   /* Reservar memoria sin asignar almacenamiento físico */
   puntero = VirtualAlloc(NULL, 100 * sizeof(int), MEM_RESERVE, PAGE_NOACCESS);
   /* Acomodar físicamente memoria previamente reservada */
   VirtualAlloc(puntero, 100*sizeof(int), MEM_COMMIT, PAGE_READWRITE);
   /* Reservar y acomodar espacio para memoria de una vez */
   puntero = VirtualAlloc(NULL, 100 * sizeof(int),
      MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

Liberar direcciones de memoria virtual

Las funciones VirtualFree y VirtualFreeEx permite realizar las operaciones inversas a las anteriores.

  • Liberar direcciones de memoria reservadas.
  • Liberar memoria asignada sin liberar las direcciones.
  • Liberar direcciones reservadas y memoria asignada.
   int *puntero;

   /* Libera memoria */
   VirtualFree(puntero, 0, MEM_RELEASE);
   /* Desasigna memoria, la memoria sigue reservada */
   VirtualFree(puntero, 100*sizeof(int), MEM_DECOMMIT);

Bloquear páginas de memoria asignada

La función VirtualLock permite bloquear determinadas páginas para que siempre permanezcan en memoria física, y no puedan ser transferidas al fichero de paginación.

Para desbloquear estas páginas se usa la función VirtualUnlock.

   int *puntero;

   ...
   VirtualLock(puntero, 100*sizeof(int));
   ...
   VirtualUnlock(puntero, 100*sizeof(int));

Establecer atributos de protección de acceso

Las funciones VirtualProtect y VirtualProtectEx permiten asignar atributos de lectura/escritura, sólo lectura o sin acceso.

   int *puntero;
   DWORD protant; /* Valor anterior de la protección */

   VirtualProtect(puntero, 100*sizeof(int), PAGE_READONLY, &protant);

Obtener información sobre páginas de memoria

Las funciones VirtualQuery y VirtualQueryEx se usan para obtener información sobre rangos de páginas de memoria virtual.

Estas funciones obtienen datos sobre rangos de páginas de memoria virtual a través de una estructura MEMORY_BASIC_INFORMATION. De este modo podemos saber el tamaño de un bloque de memoria, su estado, su tipo de protección, etc. Esta estructura también nos informa de esos mismos valores en la primera llamada a VirtualAlloc, independientemente del estado en que esté actualmente.

   int *puntero;
   MEMORY_BASIC_INFORMATION mbi;

   VirtualQuery(puntero, &mbi, sizeof(MEMORY_BASIC_INFORMATION));

Generalmente, en nuestras aplicaciones, no necesitaremos recurrir a funciones del API para manejar memoria, ya que el sistema se encarga de gestionar las llamadas a las funciones estándar de memoria: malloc, free, etc, y el uso de los operadores new y delete, de modo que en realidad siempre usaremos memoria virtual. El sistema se encarga también de proporcionar el almacenamiento físico para esa memoria, y nosotros no tendremos que preocuparnos por esos temas.

Pero a veces puede ser necesario gestionar la memoria de nuestra aplicación, asegurarse de que va a existir memoria disponible en fases siguientes del programa, aunque no la vayamos a necesitar de forma inmediata, proteger ciertas zonas, etc. En esos casos será útil saber que existen estos mecanismos, y saber aplicarlos de forma adecuada a cada caso.

Ejemplo 48

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 48 win048.zip 2005-08-14 4250 bytes 224