PRÁCTICAS DE SISTEMAS OPERATIVOS II

OCTAVA SESIÓN


  1. 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.

  2. 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.

  3. 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.

  4. 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:

    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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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.

  11. Aplicaciones relacionadas.



  12. Funciones de biblioteca relacionadas.



  13. LPEs.


© 2025 Guillermo González Talaván.