PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

DECIMOTERCERA SESIÓN


  1. Sistemas de ficheros en Windows NT.

    Los sistemas de ficheros montados en un ordenador que corra Windows NT están organizados en volúmenes. Para hacer referencia a un volumen, se usan las letras de unidad. Esta situación cambia en Windows 2000.

    Podemos saber cuántas unidades hay montadas en el sistema con GetLogicalDrives o con GetLogicalDriveStrings. La primera nos devuelve la información sobre un mapa de bits donde la unidad A viene representada por el bit menos significativo. La segunda nos da los nombres de las unidades en un búfer separados por el carácter nulo. Debido a la gran cantidad de llamadas que existen, os comentaré sólo las dificultades de manejo de cada una, debiéndoos remitir al resumen para encontrar la información completa.

    Una vez sabemos que una unidad existe, podemos obtener su tipo con GetDriveType. Hay que pasarle la ruta del directorio raíz como parámetro. Por ejemplo, GetDriveType("a:\\");. El hecho de doblar el carácter \ viene porque estamos programando en C y el C exige doblarlo para que no actúe como carácter de escape en las cadenas. Tened cuidado porque incluso en la ayuda del compilador ¡a veces se les olvida!

    Sabemos ya su tipo. ¿Qué más datos podemos obtener? Por ejemplo los que nos brinda:
    BOOL GetVolumeInformation( LPCTSTR lpRootPathName, LPTSTR lpVolumeNameBuffer, 
                               DWORD nVolumeNameSize, LPDWORD lpVolumeSerialNumber,
                               LPDWORD lpMaximumComponentLength, LPDWORD lpFileSystemFlags,
                               LPTSTR lpFileSystemNameBuffer, DWORD nFileSystemNameSize);
    La mayor parte de parámetros de la función son parámetros de retorno, donde la función nos devuelve información y nosotros sólo tenemos que proporcionarle la variable donde almacenarla. En el caso de que sean arrays o cadenas de caracteres le pasamos también la longitud para que la función no invada zonas de memoria que no le correspondan. ¿Pero qué os voy a contar a estas alturas? Esta función, en concreto, devuelve el nombre del volumen, su número de serie, el tamaño máximo que puede tener en caracteres el nombre de un fichero, información adicional del volumen y de qué tipo es el sistema de ficheros: FAT, NTFS, etc. Los valores almacenados en lpFileSystemFlags vienen en el resumen.

    Tenemos también una función para cambiar el nombre de un volumen, a saber, SetVolumeLabel y otra que nos dará el espacio libre que queda en la unidad: GetDiskFreeSpace. Esta información nos la dará en clústeres (sectores lógicos).

  2. Directorios y manejo de ficheros.

    En el tratamiento de los directorios, Windows NT no presenta ninguna novedad. Disponemos de las funciones de la API: GetCurrentDirectory, SetCurrentDirectory, CreateDirectory y RemoveDirectory, de significado evidente. Para borrar un directorio, tiene que estar vacío.

    Lo que es más novedoso es que se disponga de llamadas a la API para realizar operaciones sobre ficheros. Fáciles de manejar, tenemos CopyFile, DeleteFile y MoveFile.

  3. Creación/apertura de ficheros.

    El equivalente en Windows NT 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 NT. Existen muchas posibilidades para la apertura del fichero. Las que nos interesan vienen en el resumen. Se puede especificar el acceso deseado (lectura sólo 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.

  4. 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 NT 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) "sólo" 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.

  5. Obtener información de un fichero.

    De un fichero podemos conocer su tamaño con:
    DWORD GetFileSize( HANDLE hFile, LPDWORD lpFileSizeHigh); 
    La razón de tan extraños parámetros la acabamos de ver unas líneas más arriba.

    También podemos conocer o cambiar alguno de los tiempos propios del fichero:
    BOOL GetFileTime( HANDLE hFile, LPFILETIME lpCreationTime, 
                      LPFILETIME lpLastAccessTime, LPFILETIME lpLastWriteTime); 
    Estos tiempos son el tiempo en que se creó el fichero, el tiempo en que´por última vez se accedió a el y el tiempo más tardío en el que ha sido modificado. Si el sistema de ficheros no proporciona alguno de estos tiempos (p. ej. FAT), nos devolverá cero. Sin embargo, el problema es que la estructura donde nos da los datos no es "de consumo humano". Una vez obtenida la información debemos usar otras funciones para interpretarla. Si queremos comparar dos de estos tiempos, usaremos CopareFileTime y si lo que deseamos es que nos lo traduzcan, usaremos:
    BOOL FileTimeToSystemTime( CONST FILETIME *lpFileTime, LPSYSTEMTIME lpSystemTime);
    La estructura SYSTEMTIME ya sí tiene interpretación directa. Para cambiar los tiempos de un fichero (haciendo trampa) usaremos SetFileTime. Los más avispados os habréis dado ya cuenta de que hace falta la función opuesta de traducción. Pues sí, se llama SystemTimeToFileTime.

    Los muy exigentes disponen de la función que más información da de un fichero (la "fstat" de Windows NT). Su nombre es GetFileInformationByHandle y su especificación está en el resumen.

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

  7. Listado de directorios/búsqueda de ficheros.

    El procedimiento para encontrar un fichero o para hacer un listado de los ficheros contenidos en un directorio es el mismo. Consta de tres pasos:
    1. Apertura de un objeto de búsqueda y obtención del correspondiente handle con:
      HANDLE FindFirstFile( LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData); 
      En lpFileName irá el nombre del fichero que se busca (con posibilidad de uso de comodines para hacer listados).
    2. Llamar cuantas veces sea necesario hasta acabar con el directorio a la función FindNextFile.
    3. Destruir el objeto de búsqueda con la función FindClose.

    Estas funciones, según van encontrando ficheros devuelven información en una estructura del tipo WIN32_FIND_DATA. Sus campos tienen interpretación directa, excepción hecha del último:
    typedef struct _WIN32_FIND_DATA 
       {DWORD dwFileAttributes;
        FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime;
        DWORD nFileSizeHigh; DWORD nFileSizeLow;
        DWORD dwReserved0; DWORD dwReserved1;
        TCHAR cFileName[ MAX_PATH ];
        TCHAR cAlternateFileName[ 14 ]; } WIN32_FIND_DATA; 
    El último es el nombre alternativo que usan los sistemas FAT para nombrar los ficheros largos. Por ejemplo, "Escritorio" puede aparecer como "ESCRIT~1".

  8. Práctica.

    Hacer un programa que imprima el nombre y tamaño de los ficheros del directorio actual que han sido modificados por última vez un miércoles.

  9. Entrada/salida solapada. Versión simple.

    La entrada/salida solapada consiste en un método de poder solapar entrada/salida con trabajo intenso de CPU en una aplicación. Cuando efectuamos por ejemplo una petición de lectura de un fichero con ReadFile por ejemplo, sabemos que el hilo solicitante se queda bloqueado hasta que los datos estén disponibles. En la entrada/salida solapada, el hilo efectúa la petición de lectura y no se bloquea. Cuando los datos estén listos es notificado de modo asíncrono.

    Para poder realizar la entrada/salida solapada necesitamos especificar el flag FILE_FLAG_OVERLAPPED en CreateFile. Además, en las funciones de lectura y escritura usaremos una estructura del tipo:
    typedef struct _OVERLAPPED {
          DWORD Internal; DWORD InternalHigh; /* Reservado, no tocar. */
          DWORD Offset; DWORD OffsetHigh; /* Desplazamiento donde leer/escribir */
          HANDLE hEvent; /* NULL, o evento que queremos se active cuando acabe */
          } OVERLAPPED; 
    De esta estructura no tocaremos ninguno de los cuatro primeros campos. De hecho el tercero y el cuarto especificarán el desplazamiento del puntero de fichero. El último parámetro tiene que ver con el método como deseamos ser avisados de que la entrada/salida solapada ha acabado. Si hEvent es NULL, no seremos avisados. Si no, tiene que ser un handle a un objeto de tipo evento de los vistos en la sesión anterior. Este objeto pasará a estar señalado cuando haya acabado la entrada/salida.

    Si lo dejamos a NULL, ¿cómo nos enteraremos? Pues podemos usar la función GetOverlappedResult. Que dónde podéis encontrar cómo se maneja... efectivamente, en el resumen.

    Las funciones de lectura y escritura, cuando estamos trabajando con entrada/salida solapada, si la petición se va a posponer devuelven fallo y el error ERROR_IO_PENDING en GetLastError, aunque sabemos que esto no es un error en sí.

  10. Entrada/salida solapada. Versión avanzada.

    El colmo de la comodidad en esto de la entrada/salida solapada es que cuando se realice la operación de entrada/salida, automáticamente se invoque una función de rellamada. Pues es posible. Para ello, sólo hay que usar las funciones de lectura y escritura extendidas:
    BOOL ReadFileEx( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead,
                     LPOVERLAPPED lpOverlapped, 
                     LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
    BOOL WriteFileEx( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite,
                      LPOVERLAPPED lpOverlapped, 
                      LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
                
    En la estructura OVERLAPPED, que sigue siendo necesaria, hay que dejar el campo hEvent a NULL. El prototipo de la función de rellamada será:
    VOID CALLBACK FunciOn( DWORD dwErrorCode, 
                         DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped); 
    Como en todas las funciones de rellamada, los parámetros con los que se llamará variarán y serán puestos por el sistema operativo.

    Disponemos también de las versiones extendidas SleepEx, WaitForSingleObjectEx y WaitForMultipleObjectsEx de sus correspondientes versiones simples para poder detectar si ya se ha llamado a la función de rellamada de la entrada/salida solapada.

  11. Aplicaciones relacionadas.



  12. Funciones de biblioteca relacionadas.



  13. LPEs.


© 2000 Guillermo González Talaván.