Página principal  Página principalProgramación  ProgramaciónC/C++  C/C++Volver  Volver

  Los rincones del API Win32
Archivos proyectados en memoria

Vamos a profundizar en el último aspecto que nos falta sobre la gestión de memoria en Win32: los archivos proyectados en memoria.



Durante los últimos artículos hemos ido explicando con todo detalle cómo se gestionan los recursos de memoria en la arquitectura Win32: memoria virtual, pila y montones. En las explicaciones (más o menos claras), hemos abordado tanto los aspectos generales, aplicables a cualquier entorno y arquitectura, como las características propias de Win32.

En esta ocasión vamos a tratar los archivos proyectados en memoria, un aspecto, que aunque específico de la plataforma Win32, también se puede aplicar a otras arquitecturas (de hecho UNIX y Linux los utilizan).



Los archivos proyectados en memoria (del inglés "memory-mapped files"), son un tipo de archivo especial que se basan en la capacidad de la memoria virtual para utilizar espacio físico en disco como si fueran páginas de memoria RAM.

Básicamente, se trata de almacenar datos en memoria, como variables, registros, buffers, o cualquier otra estructura de datos, pero obligar al sistema a que proyecte esas páginas de memoria en un archivo concreto, en vez de utilizar el archivo de paginación de sistema. Después, al acceder a esas páginas de memoria, en realidad estaremos accediendo a un espacio en el disco duro, correspondiente al archivo que hemos proyectado.



Cuando hablamos de la memoria virtual, dijimos que el espacio de direcciones virtuales se estructuraba en un árbol formada por "Page Directory" - "Page Tables" - "Page Frames". Toda esta estructura lo representábamos en una tabla lineal para no complicar más el asunto. En esta tabla (nuestro "Page Directory") se indicaba tanto el número de cada una de las páginas (en total 1.048.576 páginas de 4 KB cada una), como la dirección virtual que le corresponde a cada una de ellas, y la dirección física donde está proyectada (que se calculaba a partir de la dirección virtual) . Lo más habitual era que una página de memoria virtual, se proyectase sobre el archivo de intercambio del sistema (normalmente "Win386.swp" en sistemas Windows 95 y "pagefile.sys" en sistemas Windows NT/2000).

Como ya vimos, cuando un programa necesita leer datos de una página que no estaba en RAM, se produce un fallo de página (page fault o page interrupt), lo cual desencadena un proceso en el que se accede a los datos dentro del archivo de intercambio, y se vuelcan a memoria RAM física. Después de esto, se puede continuar la ejecución de modo transparente al usuario.

Ahora supongamos que le decimos al sistema que ciertas páginas de memoria, sean almacenadas en un archivo específico (por ejemplo "c:\datos.dat") en vez de utilizar el archivo de intercambio de sistema. El proceso sería el mismo, con la única diferencia del lugar donde se almacenan las páginas.

Así podríamos manipular cualquier estructura de memoria (un array, una pila, una cola, registros, etc.) a través de punteros, pero en realidad estaríamos leyendo y grabando datos en un archivo de disco.

Con este sistema conseguiríamos olvidarnos del antiguo método de lectura de ficheros (apertura, lectura secuencial o aleatoria y cierre), más propio de la década de los 80 y la programación en COBOL que los nuevos lenguajes de programación del siglo XXI.

Pues, gracias a los archivos proyectados en memoria, esto es posible. Con ellos, podemos manipular un archivo pensando que está cargado en memoria al completo, y simplemente con un puntero accederemos a donde queramos: al principio, al final, desde atrás, hacia adelante... el sistema será el responsable de cargar las páginas que sean necesarias cuando accedemos a ellas.



Básicamente, los archivos proyectados en memoria se utilizan en 3 tareas:

  1. Leer los archivos contenidos en los archivos .EXE y .DLL cuando se ejecuta un proceso.

  2. Realizar operaciones de E/S a disco sin buffers de memoria intermedios.

  3. Crear zonas de memoria compartida, para intercambiar datos entre procesos.

Vamos a explicar cada uno de estos usos:


Supongo que alguna vez os habréis preocupado viendo que vuestro ejecutable ocupa demasiado espacio en disco, pensando que cuanto mayor sea el ejecutable, más lenta será la carga, incluso quizá alguien ha utilizado uno de esos compresores de ejecutables, que hacen que el programa "parezca" más pequeño.

En realidad, esto no es así, un ejecutable no carga más lento por ser más grande, ya que los archivos EXE (y librerías DLL) se cargan a través de archivos proyectados en memoria.

Vamos a explicarlo detalladamente con un ejemplo. Supongamos que hemos creado un ejecutable que ocupa en disco 400 KB. Cuando hagamos doble clic sobre él (es decir, cuando lo ejecutemos), ocurrirá lo siguiente:

  1. Se abre el fichero a través de la función del API CreateFile.

  2. Se leen los datos de la cabecera del ejecutable, como referencias a funciones en DLLs externas, posición en el espacio de memoria donde debe comenzar la imagen, creación de variables estáticas (las que son globales, que no están definidas dentro de ninguna función o clase), etc. Todas estas tareas se hacen a través de la funciones de la librería "imagehlp" (dbghelp.dll), tales como ImageDirectoryEntryToData, ImageLoad, ImageNtHeader, MapAndLoad, etc.

  3. Se reserva un bloque de memoria virtual para almacenar todo el ejecutable, pero se compromete en el mismo espacio físico donde reside el archivo EXE. Es decir, el "Page Directory" indicará que el rango de páginas reservado cuenta con almacenamiento físico comprometido en el archivo "Programa.exe". De este modo no se lee todo el ejecutable para volcarlo a memoria, sino que se deja en disco, y cada vez que se lea una nueva instrucción para ejecutarla, se accederá a la página en disco (si no está todavía en RAM).

  4. Cada vez que se vaya a ejecutar una instrucción de nuestro programa, se irá a leer a la dirección de memoria virtual correspondiente. Si la página de esa dirección virtual no se encuentra en ese momento en RAM, se producirá un fallo de página y el sistema entrará en el proceso de lectura de la página y volcado a RAM. Una vez que ya se cuenta con la nueva instrucción en memoria física, se continúa la ejecución.

Como hemos visto, el tamaño del ejecutable no influye (hasta cierto punto) en el tiempo de carga, ya que realmente no se realiza toda la carga del código ejecutable al arrancar, sino que se va haciendo conforme se necesita. Si el contenido de una página nunca llega a ejecutarse, esa página nunca se cargará en RAM, sino que permanecerá en el propio archivo ejecutable.


Además, si creamos una nueva instancia de nuestro programa (ejecutándolo de nuevo), se reservará otro bloque de memoria proyectándolo en el mismo espacio físico (el archivo EXE de nuevo), por lo que se reserva memoria virtual para cada una de las instancias, pero sólo se utiliza la memoria física que ocupa el archivo ejecutable en el disco.

En la figura de la derecha podemos ver un esquema de cómo se proyecta un mismo ejecutable sobre el espacio de direcciones virtuales de dos procesos.


Esta característica nos permite manipular cualquier estructura en memoria, cuando en realidad lo que estamos haciendo es manipular datos en disco. El propio sistema es el encargado de llevar a RAM las páginas que leemos, y volcar a disco las páginas que escribimos.

Supongamos que tenemos una aplicación que almacena datos en disco (por ejemplo los favoritos, o un historial de acciones o cualquier otro elemento persistente). El algoritmo típico en estos casos es:

  1. Al arrancar la aplicación, leer los datos del archivo en disco y volcarlo en estructuras en memoria, como un array de registros, un vector, etc.

  2. Manipular las estructuras de memoria, según las acciones del usuario (añadir, borrar, cambiar nombre, etc.).

  3. Al cerrar la aplicación, grabar de nuevo los datos a los archivos en disco

Este sistema, aunque muy utilizado, tiene bastantes inconvenientes:


Quizá la utilidad estrella para los archivos proyectados en memoria sea la compartición de datos entre procesos.

Como ya hablamos en su día, la plataforma Win32 pone muy difícil que los datos puedan ser compartidos entre distintos procesos, dado el carácter privado de su espacio de memoria y el nivel de seguridad que se busca a la hora de desarrollar un sistema operativo robusto. Para solucionar esto, la gente de Microsoft recomienda el uso de archivos proyectados, utilizando dos métodos:

Más adelante profundizaremos en estos métodos.

Cuando cualquiera de los dos procesos modifique algún dato, debe notificar al otro proceso (por ejemplo, con un mensaje de ventana) y éste dispondrá de los datos inmediatamente.

Internamente, la compartición de datos se consigue a través de una estructura llamada "prototype page table entry". A esta "cosa", que no sé ni cómo traducirla, le llamaremos PPTE.

Esta estructura, es parecida a nuestro amigo el "Page Table", sin embargo, todos los procesos tienen acceso a un único PPTE global, el cual gestiona qué páginas están compartidas entre procesos y cuales son las direcciones virtuales y físicas de cada una de estas páginas. Creo que con esto nos basta.

Además de compartir datos entre procesos, Windows permite desligar estos datos cuando alguno de los procesos hagan modificaciones. Es decir, los datos se comparten inicialmente, y continúan en este estado mientras no hayan sido modificados, cuando alguno de los procesos cambie el contenido de una página, se hará una copia de esta página y las modificaciones se harán sobre la nueva copia. El proceso que modificó el contenido ahora proyectará su memoria sobre la copia de la página, en vez de la página compartida. Este comportamiento se denomina "Copy on Write" o "Copia con escritura" y es uno de los métodos más utilizados en distintos sistemas operativos.

Windows proporciona otros métodos para compartir datos entre procesos, como el obsoleto DDE o el mensaje WM_COPYDATA, aunque internamente utiliza una sistema basado en archivos proyectados en memoria.



Ahora que sabemos cual es la base teórica, vamos a ver cómo se hace a través del API Win32.

Como su propio nombre indica, los archivos proyectados en memoria constan de tres componentes:

Archivos (objeto File), proyectados (objeto FileMapping) y memoria (objeto View)


Se tratan de un objeto del núcleo que se representa por un descriptor. Este descriptor se obtiene a través de la función del API CreateFile, que aunque no lo parezca, se usa tanto para abrir un archivo existente como para crearlo.

Para los programadores de C++Builder, otra opción puede ser utilizar un objeto de cualquier descendiente de la clase THandleStream que nos proporciona la VCL, aunque no vamos a entrar en este caso, ya que nuestro objetivo es centrarnos en el API Win32.

La función CreateFile en realidad se utiliza para abrir cualquier dispositivo de E/S: desde un fichero de disco o una partición física, hasta un puerto de comunicaciones, aunque nosotros nos vamos a centrar en los ficheros de disco, que es el caso que nos ocupa.

El prototipo de la función es el siguiente:

    HANDLE CreateFile(
                 LPCTSTR    lpArchivo, 
                 DWORD      dwModoAcceso,
                 DWORD      dwCompartir,
                 LPSECURITY_ATTRIBUTES lpSecuridad, 
                 DWORD      dwCreation,
                 DWORD      dwAtributos,
                 HANDLE     hFicheroPlantilla
           );

Como podéis ver la función es bastante compleja. Vamos a intentar aclarar un poco el asunto:

Esta función nos retorna el descriptor del objeto del núcleo de tipo "File". Si se indicó la bandera OPEN_ALWAYS o CREATE_ALLWAYS y el archivo existe, se retorna ERROR_ALREADY_EXISTS, y si se ha producido algún error, se retornará INVALID_HANDLE_VALUE.

Una vez que esté abierto el fichero, y ya no lo necesitemos más, debemos cerrarlo a través de la función propia para cerrar objetos del núcleo: CloseHandle.


Una vez que tenemos el objeto archivo disponible, podemos crear la proyección, o bien abrir una proyeción ya creada.

Los objetos proyección son otro tipo de objetos del núcleo, que están disponibles para el proceso que los creó y los sus procesos-hijo (si son heredables). En cualquier momento podremos crear un nuevo objeto proyección (a través de CreateFileMapping), abrir una proyección existente (con OpenFileMapping) o bien duplicar el descriptor (con DuplicateHandle).

La creación de un nuevo objeto proyección de archivo se hace a través de la función del API Win32 CreateFileMapping:

    HANDLE CreateFileMapping(
               HANDLE                hArchivo,        // archivo abierto
               LPSECURITY_ATTRIBUTES lpSeguridad,     // estructura de seguridad
               DWORD                 flProtección,    // protección de la proyección
               DWORD                 dwTamañoMaxHigh, // tamaño máximo (32 bits más altos)
               DWORD                 dwTamañoMaxLow,  // tamaño máximo (32 bits más bajos)
               LPCTSTR               lpNombre         // nombre de la proyección
           );

Como vemos, esta función tampoco es sencilla, pero tranquilos, que allá vamos con la explicación:

La función nos retorna un descriptor de objeto proyección si todo va bien. Si el nombre indicado ya está siendo utilizado, se retornará ERROR_ALREADY_EXISTS, y nos retornará el descriptor del objeto con ese nombre (como si hubieramos utilizado OpenFileMapping).
Si la función falla, se retorna NULL.
Como cualquier otro objeto del núcleo, una vez terminenos de utilizarlo, debemos cerrarlo a través de la función CloseHandle.

Otra opción que se nos ofrece es acceder a un objeto proyección que haya sido previamente creado, ya sea en nuestro mismo proceso o en otros.
Esto se realiza a través de la función OpenFileMapping:

    HANDLE OpenFileMapping(
               DWORD   dwTipoAcceso,       // el tipo de acceso
               BOOL    bHeredarDescriptor, // si se hereda a subprocesos
               LPCTSTR lpNombre            // nombre de la proyección a abrir
           );

Ahora la cosa es algo más sencilla:

Retorna el descriptor del objeto proyección, o NULL si no encuentra ningún descriptor con ese nombre o si las banderas indicadas en dwTipoAcceso no son compatibles.

Al igual que con CreateFileMapping, debemos cerrar el descriptor del objeto del núcleo, ya que al abrir un objeto ya existente en el núcleo, se incrementa un contador de referencias interno. Cerrando el descriptor (con CloseHandle), decrementaremos el contador de referencias, o destruimos definitivamente el objeto (si el contador de referencias llega a cero).


El último paso es crear una vista concreta sobre la proyección de archivos. La vista representa una porción del objeto proyección al que tendremos acceso a través de un puntero. Podemos crear una vista completa del archivo (un puntero al inicio), desde la mitad, una vista de 100 KB, etc.

Windows permite crear múltiples vistas sobre una misma proyección, incluso desde distintos procesos. La dirección de memoria retornada por la vista puede apuntar al mismo bloque de memoria virtual (en sistemas Windows 95/98/Me siempre es así), o a distintas zonas de memoria virtual (aunque realmente se proyectan sobre la misma memoria física).

La función que debemos utilizar para crear vistas es la siguiente:

    LPVOID MapViewOfFile(
               HANDLE hObjetoProyección,    // objeto proyección
               DWORD  dwTipoAcceso,         // tipo de acceso a la vista
               DWORD  dwDesplazamientoHigh, // desplazamiento (32 bits altos)
               DWORD  dwDesplazamientoLow,  // desplazamiento (32 bits bajos)
               DWORD  dwTamaño              // nº de bytes de la vista
           );

Y ahora la explicación:

La función retorna un puntero al inicio de la zona de memoria que representa la vista, o NULL si se produce algún tipo de error.

Si proyectamos varias vistas sobre la misma proyección, no se nos garantiza que la dirección retornada por MapViewOfFile sea la misma, ya que el sistema buscará el lugar más adecuado para crear la vista.

Además de esta función, existe una versión extendida (MapViewOfFileEx), en la que se añade un nuevo parámetro: lpDirecciónBase. Este parámetro (un puntero genérico), nos permite indicar la dirección de inicio para la vista. Esta dirección de inicio debe ser múltiplo del valor dwAllocationGranularity, del que ya hablamos anteriormente. La función retornará la dirección de inicio de la vista, que será el valor que hemos pasado en ldDirecciónBase, o el múltiplo de 64 KB inmediatamente anterior. Si en la dirección indicada no es posible crear la vista (por falta de espacio o cualquier otro motivo), se retornará NULL.

Esta función es útil para crear una vista en una posición fija, y que cualquier proceso acceda directamente a esta dirección (abriendo la proyección previamente), para leer los datos del archivo proyectado.

Una vez que hemos terminado con los datos de la vista, es necesario cerrarla para liberar los recursos asociados. Esto se hace a través de la función UnmapViewOfFile:

    BOOL UnmapViewOfFile(
             LPCVOID lpDirecciónBase // dirección de la vista
         );

En este caso la función es muy sencilla, símplemente debemos pasar la dirección de una vista previamente creada (el valor retornado por MapViewOfFile).

Windows garantiza que todas las páginas que sean modificadas y no hayan sido todavía guardadas en el archivo, se volcarán al archivo proyectado, aunque esto sólo es así cuando se ha creado una proyección sobre un archivo físico, y no sobre el archivo de paginación del sistema.

De todas formas, en cualquier momento podemos forzar a que Windows almacene el contenido de las páginas modificadas en el archivo físico, a través de la la siguiente función:

    BOOL FlushViewOfFile(
             LPCVOID lpDirecciónBase,
             DWORD   dwNumeroBytes
    );

Esta función también es sencilla, símplemente se nos pide la dirección a partir de la que queremos grabar, y el número de bytes totales.
Nos retornará TRUE si las páginas han sido grabadas con éxito, o FALSE si ha ocurrido algún error.

Bueno, estas son las funciones disponibles para el manejo de archivos proyectados en memoria. Ahora vamos a profundizar en uno de los usos más importante de los archivos proyectados: la comunicación entre procesos.



Uno de los usos más importantes que le podemos dar a los archivos proyectados es para comunicación de datos entre dos o más procesos.

La arquitectura de Windows pone muy difícil esta tarea, ya que la gente de Microsoft, en un intento de hacer más robusto el sistema operativo, se preocupó mucho de hacer inaccesible el espacio de memoria de un proceso.

La solución recomendada para este problema pasa por el uso de los archivos proyectados en memoria, utilizando las características de los denominados "objetos del núcleo", en los que a continuación vamos a profundizar.


¿Y por qué vamos a hablar de objetos del núcleo en un artículo sobre archivos proyectados? Pues porque los archivos proyectados son un tipo de objetos de núcleo, y es muy importante comprender el funcionamiento de los objetos del núcleo para entender a los archivos proyectados.

Estos objetos son un tipo especial dentro de todos los que podemos crear dentro del sistema Win32. Básicamente, Win32 tiene tres tipos de objetos:

  1. Objetos de usuario (como ventanas, menús, ganchos, etc.): son objetos identificados por un descriptor (que no suele ser más que la dirección de memoria donde comienza el objeto). Cualquier proceso que conozca el descriptor puede acceder al objeto en cuestión.

  2. Objetos gráficos o GDI (como el objeto Bitmap, Font, Pen, Brush, etc.): estos objetos son privados al proceso que los crea, aunque éste tiene disponibilidad total del objeto.

  3. Objetos del núcleo (como la proyecciones de archivo, hilos, etc.): son objetos gestionados por el núcleo del sistema operativo. Estos objetos pueden ser accedidos desde varios procesos, siempre y cuando se conozca el descriptor que los identifica.

Todos ellos se identifican por un descriptor (el famoso handle), aunque el significado de este descriptor es muy distinto dependiendo del tipo de objeto (puede ser un puntero, un índice de un tabla, etc.).

Los objetos del núcleo son un grupo de objetos con ciertas características especiales. La principal de ellas es que se almacenan en una tabla interna al proceso. Cada vez que se crea un objeto del núcleo, se busca una entrada libre en esta tabla y se retorna la posición que ocupa. Esa posición (el índice) será el descriptor del objeto. Esta es la razón por la que los descriptores del objetos del núcleo suelen ser números bajos, mientras que los descriptores de objetos de usuario o GDI son número altos (porque son direcciones de memoria).

Un proceso tendrá acceso a los objetos del núcleo que aparezcan en su tabla de objetos, aunque él no los haya creado.

Dentro de estos objetos del núcleo tenemos: archivos, variables de entorno, procesos, hilos, mutex, eventos, semáforos, tuberías y proyecciones de archivo.

Cada objeto del núcleo tiene una función específica para su creación, normalmente CreateXXX (donde XXX es el nombre del objeto): CreateFile, CreateFileMapping, CreateSemaphore, CreateMutex, etc.

Básicamente, lo que se hace durante la creación de un objeto del núcleo es crear una nueva entrada en la tabla de objetos, y retornar el índice asociado. Además, en la misma tabla se marcan las propiedades de cada objeto, como sus atributos de seguridad, si es heredable etc.

En cada una de estas funciones, tenemos un parámetro que es un puntero a una estructura de tipo SECURITY_ATTRIBUTES. Este es, precisamente, el atributo que controla si el descriptor a crear va a ser heredado o no por los subprocesos. Esta estructura se define del siguiente modo:

    typedef struct _SECURITY_ATTRIBUTES {
        DWORD   nLength;
        LPVOID  lpSecurityDescriptor;
        BOOL    bInheritHandle;
    } SECURITY_ATTRIBUTES;

Además de la creación de objetos, en algunos casos es posible la apertura de objetos previamente creados, a través de funciones OpenXXX (como nuestra amiga OpenFileMapping). Estas funciones, lo que hacen es copiar la entrada correspondiente de la tabla de objetos del proceso creador (algo así como forzar la herencia).

Cada objeto del núcleo cuenta con un contador de referencias, que no es más que un valor que indica la cantidad de procesos que tienen acceso al objeto. Cuando se crea un objeto del núcleo, se añade la entrada a la tabla de objetos y se marca el contador con 1. Cada vez que el objeto se hereda o abre desde otro proceso, el contador de referencias se incrementa en uno. Cuando ya no sea necesario utilizar un objeto del núcleo, debe cerrarse a través de una función genérica: CloseHandle. Esta función, lo que hace es eliminar la entrada en la tabla de objetos (aunque no el objeto en sí), y restar uno al contador de referencias. Cuando el contador de referencias de un objeto llega a 0, todos los recursos asociados al objeto se liberan y éste se considera destruido.

Esta es la razón por la que hay que llamar a CloseHandle siempre, hayamos creado o abierto el objeto, ya que debemos decrementar el contador de referencias, y será el sistema el encargado de liberar el objeto.

Este es el comportamiento que tienen los objetos de núcleo, y de él nos podemos aprovechar para la comunicación entre procesos.

Básicamente nos podemos comunicar con otros procesos con la siguientes métodos:


Este sistema se utiliza para comunicar dos procesos independientes, sin ningún "parentesco" entre ellos. Se basa en la apertura del objeto del núcleo a través de los denominados named-filemapping, es decir: proyecciones de archivo nombradas.

Ya hemos hablado algo de esto, y básicamente, se trata de asignar un nombre a una proyección de archivo y abrir la proyección utilizando el nombre que hemos asignado.

Cuando se abre la proyección, en realidad lo que está ocurriendo es que se incrementa el contador de referencias del objeto, se copia la entrada de la tabla de objetos del núcleo y devuelve el mismo descriptor (porque la copia se hace en el mismo índice de la tabla).

Para abrir la proyección tenemos dos opciones: utilizar la función para tal efecto (OpenFileMapping), o intentar crear una proyección con el mismo nombre. Si ya existe una proyección con ese nombre, la función CreateFileMapping incrementará el contador de referencias y nos retornará ERROR_ALREADY_EXISTS. Este código de error puede despistarnos, ya que en muchas ocasiones no se estará produciendo ningún error, sino que será la situación esperada. Sin embargo, en otras ocasiones sí que será un error, ya que no podemos garantizar que se nos reserve un nombre de proyección. Si, por ejemplo, queremos crear una proyección llamada "Mi proyección", nadie nos garantiza que otro proceso (incluso programado por otra persona o empresa), haya utilizado este nombre. Para solucionar esto, es recomendable utilizar nombres largos y descriptivos (de hasta 256 caracteres), por ejemplo indicando nuestro nombre, la fecha de programación o incluso un GUI, como puede ser:

JM.7C7B001B-2831-4078-A5DD-2B89C0FD91F7.Nombre de la proyección

El "modus operandi" a seguir para compartir datos es el siguiente:

  1. Desde el primer proceso: crear una proyección de archivo con un nombre dado.

  2. Desde el segundo proceso: abrir la proyección con el mismo nombre, o bien intentar crearla y comprobar si retorna ERROR_ALREADY_EXISTS.

  3. Utilizar el objeto indistintamente desde uno o otro proceso. Cada vez que se produzcan cambios, puede ser recomendable notificar a los demás procesos (más adelante hablaremos de esto).

  4. Al finalizar los procesos que hayan abierto o creado el objeto, se debe cerrar el descriptor con la función CloseHandle.

Este es el sistema más sencillo y práctico, así que he seguido este método en el ejemplo de Visual C++ que acompaña al artículo (encapsulado en la clase CProyeccion).


Este caso se basa en la característica que nos ofrece Windows de que un proceso pueda crear procesos hijos (o subprocesos). En la documentación de Win32 se define proceso-hijo como "un proceso que se crea por otro proceso, llamado proceso-padre". La definición no es muy buena (yo diría que es bastante mala), pero para ir tirando nos puede servir. Para aclarar, podemos decir que cada vez que desde nuestros programas llamamos la función CreateProcess, estamos creando un proceso-hijo.

Ya sabemos que al aplicar herencia de descriptores, en realidad lo que se hace es copiar la entrada correspondiente de la tabla de objetos del núcleo e incrementar en uno el contador de referencias. Lo único que debemos hacer, es informar al nuevo proceso del descriptor del objeto, para que pueda acceder a él.

El modo de comunicar estos procesos "cosanguíneos" es a través de los descriptores de objetos del núcleo.

El esquema es el siguiente:

  1. Crear una proyección de archivo (como ya sabemos hacer), indicando TRUE en el campo bHeredarDescriptor de la estructura SECURITY_ATTRIBUTES.

  2. Crear un proceso hijo llamando a CreateProcess desde el proceso padre.

  3. Notificar al proceso recién creado (el hijo). Para esto se puede utilizar cualquier sistema de notificación, como mensajes de ventana, variables de entorno, etc.

  4. Cuando hayamos terminado de utilizar el objeto, debemos llamar a CloseHandle, tanto en los procesos hijos como en el padre.


¿Alguien pensaba que esto era todo? Pues no, en la plataforma Win32 siempre hay alguna sorpresa. El tercer y último método para compartir información se basa en la duplicación de descriptores.

Básicamente es lo mismo que la herencia de descriptores, pero la copia de descriptores se hace en el momento que queramos, y no durante la creación del proceso.

La duplicación se hace a través de la siguiente función:

    BOOL DuplicateHandle(
             HANDLE   hProcesoOrigen,
             HANDLE   hDescriptorOrigen,
             HANDLE   hProcesoDestino,
             LPHANDLE lpDescriptorDestino,
             DWORD    dwAcceso,
             BOOL     bHeredarDescriptor,
             DWORD    dwOpciones
         );

Que nadie se asuste que la función es sencilla:

La función retorna TRUE si todo ha ido bien, o FALSE en caso contrario.

Para compartir datos entre procesos utilizando esta técnica podemos optar por dos enfoques: duplicando el descriptor desde el proceso origen, o desde el proceso hijo.

En el primer caso, los pasos a seguir son los siguientes:

  1. Obtener el descriptor del proceso destino. Normalmente esto lo obtendremos a través de la función CreateProcess, a la hora de crear el proceso-hijo.

  2. Duplicar el descriptor de la proyección desde el proceso origen.

  3. Notificar al proceso destino de su nuevo descriptor.

  4. Ambos procesos (el origen y el destino), deben cerrar los objetos a través de la función CloseHandle.

En el segundo caso (duplicar desde el destino), los pasos a seguir son:

  1. Obtener el descriptor del proceso origen. Para ello necesitamos el identificador de proceso (Process ID) y obtener el descriptor a través de la función OpenProcess. Una llamada típica suele ser algo así como:
    hProcess = ::OpenProcess( PROCESS_DUP_HANDLE, FALSE, ProcessID );

  2. Duplicar el descriptor de la proyección desde el proceso destino.

  3. Ambos procesos deben cerrar los descriptores.

Este es el sistema seguido en el ejemplo de C++Builder.


En todos los sistemas de comunicación entre procesos hemos hablado de "notificar al proceso destino".

Vamos a tratar esta tema, aunque en realidad no tenga mucho que ver con los archivos proyectados, pero sí con su uso más importante. Además, la aplicación de ejemplo en Visual C++, utiliza la notificación junto con los archivos proyectados, para comunicar varios procesos entre sí.

La notificación de procesos consiste en informar a un proceso de cierto dato, normalmente un descriptor.

Existen varios métodos para notificar a un proceso de que ha ocurrido "algo", aunque ahora sólo voy a exponer el más típico: los mensajes de ventana.

La explicación completa de qué son los mensajes de ventana o el bucle de mensajes, cómo funcionan, y cómo los implementa internamente Win32 sería muy extensa, así que vamos a suponer que todo el mundo sabe lo que es un mensaje, aunque no comprendamos muy bien qué ocurre internamente.

El sistema de mensajes se basa en registrar un mensaje especial, y notificarlo a todas las ventanas del sistema.

A este mensaje sólo responderán aquellas aplicaciones que hayan registrado previamente este mensaje, es decir, nuestras aplicaciones.

El paso de registrar el mensajes es sencillo, nos bastan con la siguiente función del API:

    UINT RegisterWindowMessage(
             LPCTSTR lpNombre // nombre del mensaje
         );

Símplemente indicando una cadena con el nombre del mensaje, el sistema nos retorna un identificador de mensaje único, dentro del rango 0xC000 hasta 0xFFFF. Cuando se intenta registrar varias veces el mismo nombre de mensaje, el sistema retornará el mismo identificador.

En nuestro ejemplo para Visual C++, puede verse el uso que he dado a esta función dentro del método Registrar de la clase CNotificacion.

Una vez registrado, debemos lanzarlo a todas las aplicaciones del sistema, a través de la función PostMensage o SendMessage. No vamos a entrar en la diferencia entre ambas funciones, y lo dejaremos para otra ocasión, en que tratemos en profundidad los mensajes de ventana.

La sintaxis de ambas funciones es la misma, así que sólo explicaré una de ellas:

    LRESULT SendMessage(
                HWND   hWnd,    // descriptor de ventana destino
                UINT   Msg,     // identificador del mensaje a enviar
                WPARAM wParam,  // primer parámetro
                LPARAM lParam   // segundo parámetro
            );

Los parámetros son los siguientes:

En el ejemplo para Visual C++, se usa la función SendMessage junto con el parámetro especial HWND_BROADCAST dentro del método Enviar de la clase CNotificacion.

El último paso que debemos dar, es interceptar el mensaje que recibe la aplicación. Para ello he utilizado una técnica llamada "Subclasificación del bucle de mensajes", aunque no voy a entrar a detallarla, ya que para ello, lo primero que tendría que hacer es explicar qué es un bucle de mensajes. Símplemente diré que me he apoyado en la VCL de C++Builder para simplificar el código, concretamente en la propiedad WindowProc de la clase TControl.

En el ejemplo para C++Builder puede verse cómo hacer la "subclasificación" dentro de la clase TMainForm, a través del método SubClassWndProc.


En ciertos casos, además de la notificación es necesaria una sincronización al acceso de la memoria entre procesos. Esto es necesario para asegurarnos la coherencia de los datos en memoria, para que un proceso lea datos cuando realmente están disponibles o completos, o un proceso grabe cuando ningún otro lo está haciendo.

Para asegurarnos la sincronización entre procesos, se utilizan las mismas técnicas y objetos que para la sincronización entre hilos (semáforos, eventos, etc.).



Esto es todo lo que han dado de sí los archivos proyectados en memoria. Creo que con lo que aquí he expuesto, todo el mundo está en condiciones de empezar a usar este sistema, ya sea para compartir datos entre aplicaciones, o utilizarlo como método de entrada/salida a disco.

Bueno, pues esto se acaba, tanto el artículo como la primera parte de la serie "Los rincones del API Win32", que he dedicado a las estructuras de memoria más importantes.

Espero que en estos cuatro artículos hayamos comprendido un poco mejor cómo se maneja la memoria en la plataforma Win32. Hemos abarcado casi todos los aspectos, desde los más sencillos y documentados, hasta los más oscuros.

Nos veremos en la próxima serie de artículos de "Los rincones del API Win32" que tratará sobre... ¿alguna sugerencia?



Todo lo que hemos ido explicando, se utiliza de modo práctico en los siguiente ejemplos:

Visual C++ 6
Un pequeño navegador web que implementa un sistema de favoritos basado en archivos proyectados en memoria. De este modo se consigue que la lista de favoritos permanezca siempre actualizada desde cualquier instancia de la aplicación.
Proximamente.

C++Builder 6
Una aplicación padre que genera procesos hijo y se comunica con ellos a través de archivos proyectados en memoria.
Ejemplo en zip



Autor: JM



Página principal  Página principalProgramación  ProgramaciónC/C++  C/C++Volver  VolverCreative Commons License 2003 by JM