Creación/apertura de ficheros.
El equivalente en Windows a la llamada al sistema
open
de UNIX es:
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
Devuelve un HANDLE, pues el fichero abierto o creado es, a
todos los efectos, un objeto de Windows. Existen muchas
posibilidades para la apertura del fichero. Las que nos interesan
vienen en el resumen.
Se puede especificar el acceso deseado (lectura solo o lectura
y escritura). En dwShareMode
decimos si queremos
que mientras tengamos abierto el fichero puedan otros procesos
acceder o modificarlo o ninguna de las dos cosas.
dwCreationDisposition
sirve para especificar
qué queremos que ocurra si el fichero ya existe o,
por el contrario, si no existe.
En cuanto a dwFlagsAndAttributes, ahí se meterá
información variada acerca del nuevo fichero. Por ejemplo,
los atributos que queremos que posea. Si especificamos
FILE_FLAG_WRITE_THROUGH
indicamos que queremos
que las modificaciones hechas al fichero
se reflejen automáticamente
en el disco magnético.
Si activamos FILE_FLAG_OVERLAPPED
se indica que el fichero se manejará con
entrada/salida solapada.
Si se activa FILE_FLAG_DELETE_ON_CLOSE
significa que
queremos que el fichero se borre automáticamente cuando se
cierre (por ejemplo, para un fichero temporal).
También se puede indicar uno de los flags
FILE_FLAG_RANDOM_ACCESS
o
FILE_FLAG_SEQUENTIAL_SCAN
si sabemos de antemano
el modo en que vamos a acceder al fichero, al azar o
secuencialmente. Si especificamos uno de estos flags, no
significa que no podamos acceder al fichero de otra manera,
sino que el sistema operativo organizará la entrada/salida
para que el tiempo de acceso sea óptimo para uno u otro modo.
Lectura, escritura y posicionado en un fichero.
La lectura y escritura en un fichero es equivalente a lo ya
visto en read
y write
para UNIX.
Los prototipos de las funciones de Windows que realizan
esta función son:
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);
BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);
Tan sólo debéis poner a NulL
el último parámetro, a no ser
que queráis realizar
entrada/salida solapada.
También la función de la API encargada
de colocar el puntero
de fichero parece una copia exacta de la que vimos para UNIX,
lseek.
También podrá usarse para
conocer la longitud del fichero, aunque se disponga de una
función para esto también: GetFileSize
.
Observemos el prototipo de la función
de colocación del puntero de fichero:
DWORD SetFilePointer( HANDLE hFile, LONG lDistanceToMove, LPLONG lpDistanceToMoveHigh,
DWORD dwMoveMethod);
lDistanceToMove
tiene su sentido
así como el valor
devuelto por la función,
que será el desplazamiento del puntero
de fichero después de realizado el movimiento.
dwMoveMethod
indicará
mediante macros cuál será
el punto de origen respecto del cual
se efectuará el movimiento.
Pero, ¿qué significa
lpDistanceToMoveHigh
? Este valor
aparece porque con lDistanceToMove
, de tipo LONG
(32 bits) "solo" podemos expresar
hasta un tamaño máximo de
4294967296 bytes, o sea, algo más de cuatro Gb.
Si el tamaño
del fichero fuera mayor, no podríamos colocar el puntero en
cualquier lugar de un solo movimiento. La solución es que se
construye un entero de 64 bits formado por
lpDistanceToMoveHigh
como los 32 bits superiores
y lDistanceToMove
como los 32 bits inferiores.
El apaño es malo, pero ahí está
y casi nunca superaremos
los cuatro Gb por lo que el valor alto será cero.
La razón
por la que es un puntero es porque lpDistanceToMoveHigh
también es un valor de retorno de la función.
La parte baja
no lo es porque ya la devuelve la propia función.
Este esquema
lo encontraremos en muchas otras funciones que manejen ficheros.
Para truncar la longitud de un fichero,
se puede usar la función
SetEndOfFile
. Hay primero que poner el puntero
de fichero en la posición donde deseemos cortar el fichero.
Otras opciones de fichero.
Si queremos vaciar los búferes intermedios de un fichero, hay
que usar FlushFileBuffers
.
Para bloquear una parte de un fichero para acceso exclusivo,
es decir, para que ningún otro proceso pueda acceder a ella,
se usará:
BOOL LockFile( HANDLE hFile, DWORD dwFileOffsetLow, DWORD dwFileOffsetHigh,
DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesToLockHigh);
Esta función, salvo por el problema de los enteros de 64 bits
comentado más arriba, es muy fácil de manejar.
Memoria compartida y ficheros proyectados en memoria.
Para Windows son lo mismo los ficheros proyectados en memoria
que la memoria compartida entre procesos. En UNIX,
vimos lo primero en la
sesión
octava de Sistemas Operativos I y lo segundo, en la
tercera de esta asignatura.
Como consecuencia
de esta fusión,
los ficheros proyectados en memoria tendrán,
como
los semáforos de Windows
la posibilidad de tener o no un nombre para que los puedan usar
otros procesos. Dispondremos, pues, de dos funciones,
CreateFileMapping
y OpenFileMapping
,
como con los semáforos. Veamos, por ejemplo, la primera:
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect, DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow, LPCTSTR lpName);
hFile
tiene que ser:
- O bien el handle de un fichero abierto en un modo de
acceso compatible con el acceso que deseamos a la zona
proyectada.
- O bien NulL, si queremos que lo proyectado resida en
el archivo de paginación de Windows (el equivalente
a las zonas de memoria compartida de UNIX).
Como puede ser que el tamaño de un fichero
sea superior a lo que
cabe en 32 bits (variable de tipo DWORD
),
el tamaño
se especifica de dos veces. Es decir, se construyen dos valores
de 32 bits a partir de la cantidad de 64 bits y se pasan como
argumentos diferentes.
El último parámetro
nos va a dar el nombre de la zona de memoria
por si queremos compartirla con otro proceso.
LPVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap);
servirá para proyectar efectivamente el fichero en la zona
de memoria. El significado de los parámetros es evidente.
Solo considerar que, de nuevo, el desplazamiento dentro
del fichero es de 64 bits repartidos en dos pedazos de 32
bits. La función nos devolverá
la dirección de memoria donde
se ha realizado la proyección.
La función opuesta la realiza
UnmapViewOfFile
.
También se dispone de la función
FlushViewOfFile
para hacer efectivo el volcado
de la zona de memoria sobre el fichero.
Práctica.
El ecologismo está de moda,
así que ahí va una práctica reciclada.
Haremos un programa del estilo al del productor-consumidor
que vimos en clase, con memoria compartida.
Habrá dos procesos,
A y B, el A será el proceso productor. El B,
el proceso consumidor.
Para comunicarse, usarán una zona de
memoria compartida de un carácter.
El proceso A producirá los
caracteres: "En un lugar de la Mancha de cuyo nombre no quiero
acordarme, no ha mucho que vivía un hidalgo de los de lanza
en astillero, adarga antigua, rocín flaco y galgo corredor.".
Producirá los caracteres
a un ritmo de 10 caracteres por segundo
(usad la llamada al API Sleep
para controlar
esto).
El proceso B, por su parte,
consumirá los caracteres según le
vayan llegando, con un gasto de CPU mínimo. Para consumir un
carácter, lo que hace es leerlo.
A los caracteres que le llegan
en una posición par (segundo, cuarto, etc.) los imprime por
la pantalla tal cual. A los que le llegan en una posición
impar, los transforma en mayúsculas
y los imprime por pantalla.
Para trasformar en mayúsculas
usad la función de biblioteca
toupper
.
Haced un único programa fuente (.c
). Si es
necesario, podéis usar un argumento
de la línea de órdenes opcional.
Otra práctica.
Haced un programa que admita un nombre de fichero como
único parámetro.
Se llamará el programa mataoes
.
El programa, mediante proyección del fichero en memoria
eliminará todas las oes minúsculas
('o
') que
tenga el fichero, posiblemente quedando la longitud
de dicho fichero reducida.
Reserva de memoria virtual.
Las verdaderas llamadas al API para la reserva de memoria
dinámicamente en un proceso de Windows son aquellas que
comienzan por Virtual...
Para poder obtener memoria, primero hay que solicitar la reserva,
luego hay que confirmarla (COMMIT).
La reserva sólo se producirá
en múltiplos del tamaño
de página del sistema. Para reservar o
confirmar memoria virtual se usa la función:
LPVOID VirtualAlloc( LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType,
DWORD flProtect);
Reservaremos, si ponemos MEM_RESERVE
en los
flAllocationType
. Confirmaremos con
MEM_COMMIT
.
Respecto al primer parámetro, puede ser
NulL si, cuando hacemos la reserva (no cuando confirmamos)
dejamos al sistema que elija la dirección virtual
donde reservar la memoria.
Una zona reservada no está gastando memoria real,
solo tenemos
reservado el rango de direcciones.
Solo cuando confirmamos, es
cuando realmente se pone memoria real detrás.
En el resumen
vienen más indicadores y su significado.
La memoria, cuando se acabe de usar, debe ser liberada, es decir
pasar a no confirmada, y las direcciones de memoria liberadas.
Se hará con VirtualFree
.
También es posible
bloquear una zona de memoria
de modo que no se haga paginación
con ella y permanezca en memoria principal a ser posible.
Esto se consigue con VirtualLock
y
VirtualUnlock
. Debido a que las prestaciones
del sistema pueden verse muy mermadas si se bloquea mucha
memoria, el sistema operativo
sólo dejará bloquear pocas páginas.
Se dispone, además,
de la función GlobalMemoryStatus
que nos dará información
de la memoria disponible en el sistema.
Bibliotecas de enlace dinámico. Construcción.
Para concluir esta parte dedicada a Windows vamos a ver una parte
muy importante de él:
las bibliotecas de enlace dinámico,
o DLLs. En teoría vimos en qué consiste
el enlazado dinámico.
En Windows se usa profusamente. De hecho, las funciones
de las diferentes APIs se encuentran en bibliotecas DLL.
En resumen, se trata de que el código de ciertas funciones
venga en unos ficheros especiales llamados bibliotecas.
Cuando un programa necesite dicho código,
lo cargará de
la biblioteca. Así se logra que el código de funciones
muy generales no esté innecesariamente repetido en todos
los ejecutables. También, con las bibliotecas de enlace
dinámico resulta más fácil
actualizar a la vez muchos programas.
Para construir una biblioteca de enlace dinámico, nos podemos
ayudar de las facilidades que nos ofrece el compilador
Visual C++. Para ello, abriremos un nuevo proyecto de tipo
"Win32 Dynamic-Link Library":
Cuando se nos pida escoger, tomaremos "A DLL that exports
some symbols":
Observaremos que el entorno de desarrollo ha sustituido
nuestra vieja conocida función main
por:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Cada vez que un proceso quiera usar la DLL que vamos a crear,
se hará automáticamente una llamada
a DllMain
.
La diferencia estará en el parámetro
ul_reason_for_call
. Cuando hagamos
LoadLibrary
,
valdrá DLL_PROCESS_ATTACH
.
Cuando hagamos FreeLibrary
, valdrá
DLL_PROCESS_DETACH
. Si se crea o muere un hilo
entre ambos instantes, se llamará con los valores
DLL_THREAD_ATTACH
y DLL_THREAD_DETACH
respectivamente. Esta llamada nos servirá
para poder hacer tareas de reserva inicial o limpieza de la DLL.
Haremos que la función devuelva TRUE si nos parece bien
la petición o FALSE, si no.
En el código fuente también
vienen definidas una clase,
una función y una variable que exportará la DLL.
En cuanto
a la clase, la podéis borrar directamente (tanto del .cpp
como del .h) pues no vamos a usar C++. Respecto a la
función y la variable,
ahí podéis ver cómo se pueden definir
en la DLL para que luego la usen otros procesos. Compilad
la DLL y buscad, en el directorio "Debug" desde fuera del
compilador el fichero que se ha generado. Pulsando sobre el
fichero con el botón derecho del ratón
sobre el menú
"Vista rápida", se os ofrecerá
mucha información acerca
de la DLL. En particular, las funciones y variables que
exporta. Esto fue lo que nos dio a nosotros:
Si la función se llamaba en mi caso fnBibliot
,
¿por qué dice que la función exportada se llama
?fnBibliot@@YAHXZ
? La razón está en el
enlazador de C++. Las funciones de C++ cuando se guardan
en los módulos objeto cambian (o revisten) su nombre en
parte debido a que en C++ dos funciones diferentes pueden
tener el mismo nombre en el código fuente. Esto hace que
usar la función así exportada
sea un verdadero infierno.
Para volver a sentir las piernas, es necesario incluir
la opción de enlazado literal de C. Sustituid en el fichero
de cabecera el prototipo de la función exportada por:
extern "C" BIBLIOT_API int fnBibliot(void);
El nombre de la función
cambiará en vuestro caso. Ahora ya
podéis comprobar en la vista rápida de la DLL (una vez
recompilada) que el nombre que aparece es el correcto.
La macro BIBLIOT_API
está definida como
__declspec(dllexport) y lo podéis usar mejor.
Si queréis exportar variables o funciones de la DLL, con
estos ejemplos deberíais poder hacerlo.
DLLs. Utilización por un programa.
Llegó la hora de usar nuestra flamante DLL. Para poder
usarla, hay dos posibilidades cargarla al inicio del
programa o cargarla cuando se necesite. Nosotros
veremos la segunda opción.
Lo primero que tenemos que hacer es cargar la DLL en
el espacio de memoria del proceso. Lo haremos con:
HINSTANCE LoadLibrary( LPCTSTR lpLibFileName);
En lpLibFileName
indicaremos el
camino completo de la DLL. Se nos devolverá un
manejador de la DLL o NulL
,
si la llamada fracasó.
Además, la llamada hará
que se invoque la función
DllMain
de la DLL con el parámetro
ul_reason_for_call
igualado a la macro
DLL_PROCESS_ATTACH
.
Para poder usar las funciones hay que obtener un puntero a ellas.
Lo hacemos con:
FARPROC GetProcAddress( HMODulE hModule, LPCSTR lpProcName);
En hModule
va el handle de la DLL y, como
segundo parámetro, el nombre de la función.
En nuestro caso, hemos hecho:
FARPROC funciOn;
[...]
funciOn=GetProcAddress(hbiblio,"fnBibliot");
if (funciOn==NulL)
{
PERROR("GetProcAddress"); return 1;
}
printf("El valor de la función es %d.\n",funciOn());
También hay que acordarse de liberar la DLL cuando se haya
acabado de usar con FreeLibrary
.
Si queremos escribir en la pantalla dentro de una función
de una DLL puede ser que tengamos que recurrir a escribir
directamente con un manejador de la entrada, salida o
error estándar:
HANDLE GetStdHandle( DWORD nStdHandle);
Puede ser el parámetro: STD_INPUT_HANDLE, STD_OUTPUT_HANDLE o
STD_ERROR_HANDLE.
BOOL SetStdHandle( DWORD nStdHandle, HANDLE hHandle);
¿Sois capaces de manejarlas sin nada más y escribir
algo en la pantalla dentro de una DLL? Si es así,
estéis aprovechando el curso.
Práctica.
La práctica de esta sesión es como sigue:
Haced una DLL que exporte las funciones:
VOID Poner_a_cero(VOID);
VOID Incrementa(VOID); y
LONG Mostrar(VOID).
La DLL mantendrá un contador interno que será puesto
a cero, incrementado o devuelto su valor con las
funciones que exporta.
Para probar la DLL, haced un programa de nombre
carrera
. Este programa creará cuatro
hilos y pondrá sus identificadores por la pantalla.
Los hilos se quedarán esperando por un objeto de
tipo evento.
El hilo principal pondrá el contador de la DLL a cero.
A continuación,
señalará el evento para que todos comiencen. Cada
hilo incrementará sin pausas el contador de la DLL
un millón de veces. Mientras, el hilo primero esperará
a que vayan acabando los hilos corredores e imprimirá
sus identificadores según vayan llegando.
Cuando hayan llegado todos, imprimirá el valor del
contador de la DLL y acabará con limpieza.
Aplicaciones relacionadas.
-
-
Funciones de biblioteca relacionadas.
-
-
LPEs.
- Sigo los pasos para construir la .dll de ejemplo y
no me da ningún error pero sin embargo no me crea
el fichero .dll, sino uno acabado en .lib.
Pista: El "Gran Hermano" cuida de nosotros.
Solución.