PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

UNDÉCIMA SESIÓN


  1. Compilación y ejecución en Windows NT.

    Para la realización de las prácticas de esta parte de la asignatura, usaremos Visual C++ de Microsoft. Como de lo que se trata es de que entendáis el modo de funcionamiento del sistema operativo, no usaremos las posibilidades gráficas de Windows NT, sino que haremos "aplicaciones de consola".

    La primera aplicación que haréis será el progrma "Hello, World!". Hacer, lo que se dice hacer, es un decir, porque Visual C++ ya nos lo dará hecho. Abrid el compilador y, dentro del menú File, seleccionad la opción New...

    Dentro de ella, pulsad en la pestaña "Projects", seleccionad "Win32 Console Application" y dad un nombre y una ubicación al proyecto.



    Seleccionad la opción que crea una aplicación "Hello, World!".



    Aunque Visual C++ trabaja con C++, como su propio nombre parece indicar, podéis ignorar las clases de C++ y trabajar en C sin apenas diferencias. El programa que "habéis creado" lo podéis encontrar en la pestaña "File View" dentro de "hola files", en la carpeta "Source Files". Se llama hola.cpp:



    Podéis ahora compilar y ejecutar el programa mediante la opción del menú Build, Execute hola.exe.

    Los programas que haremos usarán las llamadas al API Win32, el equivalente a las llamadas al sistema de UNIX. Para poder usar estas llamadas al API, hemos de incluir el fichero de cabecera <windows.h>. Por ejemplo, podemos hacer que el mensaje "Hello, World!" tarde en aparecer tres segundos usando la llamada al API Sleep. Para ello, hacemos:
    // hola.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    #include <windows.h>
    
    int main(int argc, char* argv[])
    {
        Sleep(3000);
        printf("Hello World!\n");
        return 0;
    }           
    Como habréis observado, el propio entorno os recuerda el prototipo de las funciones que uséis y también os da formato automáticamente al programa (¡viva la uniformidad!). De todos modos, esto es configurable. En el menú Tools, apartado Options, se puede hacer que que el entorno no formatee automáticamente el programa. Lo que sí debéis hacer es seleccionar la opción de incluir espacios en lugar de tabuladores para que las prácticas se os impriman bien siempre y pasen bien por el sistema de correo electrónico:



    El entorno también os ofrece la posibilidad de depurar mediante su interfaz gráfica los programas. Podéis establecer puntos de ruptura (breakpoints), ejecutar paso a paso, etc. Si mientras estáis depurando, necesitáis que el programa se ejecute con algún argumento de la línea de órdenes, lo podéis establecer en el menú Project, Settings, donde aparece en la figura siguiente:



    Notas relativas a Visual Studio .NET

    1. Para abrir un proyecto nuevo, hay que ir al menú Archivo -> Nuevo -> Proyecto... Os aparecerá una ventana con un árbol. Desplegad donde pone "Proyectos Visual C++" y seleccionad "Win32". Introducid el nombre del proyecto y pulsad dos veces en "Proyecto de consola Win32". Pulsad Finalizar en el diálogo que aparece. Tendréis un nuevo proyecto con una función main un tanto extraña. Es así para compatibilizar con UNICODE. No os preocupéis.
    2. Para compilar el programa, debéis ir al menú "Generar" -> Generar . Para ejecutarlo, en el mismo menú generar, pulsad sobre "Iniciar".
    3. Si queréis pasar argumentos de la línea de órdenes, el método es es siguiente. Pulsáis en el menú Proyecto -> Propiedades de . Os aparece un diálogo. Pulsáis en "Depuración". Introducid los argumentos en el campo que pone "Argumentos de comando" (sic).
    4. Para obtener ayuda, debéis usar una aplicación externa. Se encuetra en Inicio -> Programas -> Microsoft Developer Network. Allí pulsáis sobre la pestaña de "Índice" y tecleáis el nombre de la función que buscáis.


  2. Notaciones.

    En el mundo Microsoft, es habitual seguir una notación para los tipos y los nombres de las variables (notación húngara). Observemos el prototipo de una de las funciones que usaremos:
    BOOL GetExitCodeProcess( HANDLE hProcess, LPDWORD lpExitCode); 
    En primer lugar, notaréis que no se escatima en el nombre de las funciones y variables. Suelen ser nombres largos y se suele poner en mayúsculas la primera letra de cada palabra que forma el nombre.

    En cuanto a los tipos de datos derivados, es costumbre que vayan en mayúsculas. Así, tenemos tipos de datos enteros como: UINT, WORD, DWORD, etc.

    El tipo BOOL, que representa un valor lógico, es un entero como es costumbre en C. Hay definidas las constantes true y false para este tipo.

    En cuanto al tipo HANDLE, es lo que se conoce en Windows como un manejador, o incluso asa. Cualquier objeto de Windows NT tiene su manejador. El manejador es la manera de hacer referencia a un objeto de Windows NT en una función que maneje el objeto. Es algo parecido al descriptor de fichero que veíamos en UNIX para los ficheros. En el caso de esta función, es un manejador del proceso del cual queremos conocer su código de retorno.

    Cuando un tipo derivado comienza por LP, significa que tenemos un puntero. Así, el tipo LPDWORD será un puntero a una variable del tipo DWORD. LP viene del inglés "long pointer" y hace referencia a los punteros de largo alcance que se usaban con el esquema de memoria segmentada de los primeros procesadores de la familia del 8086. En el parámetro de la función, como se puede intuir, es donde se nos devolverá el código de retorno del proceso que estemos investigando. Las funciones de cadena suelen usar el tipo LPSTR.

    Las variables no se libran de estas notaciones. De hecho, la notación húngara se refiere al modo de nombrarlas. Normalmente, delante del nombre de la variable se pone un par de letras que indican el tipo al que pertenece de modo que, sólo viendo el nombre, veamos de qué tipo es la variable. Sin embargo, y aunque parezca incongruente, no se sigue un convenio similar con el nombre de las funciones.

    Evidentemente, a no ser que lleguéis a trabajar en el gigante informático, no es necesario que uséis estos convenios en vuestros programas.

    Aquí tenéis los tipos de datos que más usaremos de Win32:
    Tipo Descripción
    BOOL, BOOLEAN Variable booleana (TRUE/FALSE)
    BYTE Byte (8 bits)
    CALLBACK Convenio de llamada para funciones de rellamada
    CHAR Carácter ANSI de 8 bits
    DWORD Entero sin signo de 32 bits
    FLOAT Variable de punto flotante
    HANDLE Handle, manejador o asa de un objeto
    HFILE Handle de un fichero
    INT Entero con signo
    LONG Entero de 32 bits con signo
    LONGLONG Entero de 64 bits con signo
    LPcosa Puntero a cosa
    LPCSTR Cadena de caracteres ANSI constante acabada en \0
    LPCWSTR Cadena UNICODE constante acabada en \0
    LPCTSTR Cadena UNICODE o ANSI constante según se defina
    LPCVOID Puntero a una constante de cualquier tipo
    LPSTR Cadena de caracteres ANSI acabada en \0
    LPWSTR Cadena UNICODE acabada en \0
    LPTSTR Cadena UNICODE o ANSI según se defina
    Pcosa Puntero a cosa
    SHORT Entero corto
    TBYTE, TCHAR WCHAR ó CHAR según proceda
    UCHAR, UINT, ULONG,
    ULONGLONG, USHORT
    Valores sin signo de CHAR, INT, ...
    WCHAR Caráter UNICODE
    WINAPI Convenio de llamada para llamadas al API
    WORD Entero sin signo de 16 bits

    Debido a que 256 caracteres que proporciona un único byte se quedan muy cortos para representar todos los caracteres de los distintos idiomas del mundo (recordemos que los 128 primeros son comunes en ASCII y el resto variables, correspondiendo al español los del ISO-Latin1), surgen los caracteres UNICODE de dos bytes de longitud. Con ellos se puede representar hasta 65536 caracteres. En Windows NT se les llama caracteres anchos (wide) y se tiende a su uso porque, aunque las cadenas ocupen el doble, cada número representa un único carácter.

  3. Procesos en Windows NT.

    La creación de procesos en Windows NT sigue un esquema diferente al de UNIX. Se usa la función del API CreateProcess, cuyo prototipo es:
    BOOL CreateProcess( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, 
                        LPSECURITY_ATTRIBUTES lpProcessAttributes, 
                        LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bHeredarHandles, 
                        DWORD dwCreationFlags, LPVOID lpEnvironment, 
                        LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, 
                        LPPROCESS_INFORMATION lpProcessInformation); 
    Como primera diferencia con las llamadas al sistema del UNIX, observad cómo la función devuelve un valor de tipo BOOL, es decir, verdadero o falso. Devolverá falso (0) cuando falle y habrá que usar la llamada del API GetLastError para ver cuál fue el error en concreto que se produjo. Si lo que deseamos saber es el identificador o el HANDLE del proceso recién creado, hay que acudir a los campos hProcess y dwProcessId de la estructura de tipo PROCESS_INFORMATION que nos devuelve la función. Observaréis también que las llamadas del API de Win32 suelen tener muchos más parámetros que sus equivalentes de UNIX. Afortunadamente, casi todos los parámetros pueden llevar un valor por defecto. En el resumen podéis ver una breve descripción de los parámetros que podéis usar.

    Por ejemplo, si queremos arrancar una sesión del "Block de Notas" desde dentro de una aplicación, haríamos:
        PROCESS_INFORMATION informaciOn;
        STARTUPINFO         suinfo;
    [...]
        /* O bien rellenamos la estructura "a mano" */
        suinfo.cb=sizeof(suinfo);
        suinfo.lpReserved=NULL;
        suinfo.lpDesktop=NULL;
        suinfo.lpTitle=NULL;
        suinfo.dwFlags=0;
        suinfo.cbReserved2=0;
        suinfo.lpReserved2=NULL;
        /* O usamos la llamada a la API siguiente... */
        GetStartupInfo(&suinfo);
    [...]
        if (!CreateProcess(NULL,"notepad",NULL,NULL,false,
                           CREATE_NEW_PROCESS_GROUP,NULL,
                           NULL,&suinfo,&informaciOn))
        {
            fprintf(stderr,"CreateProcess: error %d.\n",GetLastError());
            return 1;
        }
    [...]       
    Los errores que produce este código son numéricos. Si queremos algo parecido al perror de UNIX, hay que esforzarse un poco:
        PROCESS_INFORMATION informaciOn;
        STARTUPINFO        suinfo;
    [...]
        GetStartupInfo(&suinfo);
        if (!CreateProcess(NULL,"notepada",NULL,NULL,false,
                           CREATE_NEW_PROCESS_GROUP,NULL,
                           NULL,&suinfo,&informaciOn))
        {
            LPVOID lpMsgBuf;
            FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | 
                       FORMAT_MESSAGE_FROM_SYSTEM |
                       FORMAT_MESSAGE_IGNORE_INSERTS, NULL,
                       GetLastError(),
                       MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
                       (LPTSTR) &lpMsgBuf,0,NULL );
            fprintf(stderr,"CreateProcess: %s\n",lpMsgBuf);
            LocalFree( lpMsgBuf );
            return 1;
        }       
    No estaría mal que usárais una macro que hiciera este trabajo sucio. En esencia, se trata de que el sistema reserve memoria dinámicamente y escriba el código de error allí en el idioma por defecto del ordenador. La macro podría llamarse así en honor al UNIX, que en paz esté:
    #define PERROR(a) \
        {             \
            LPVOID lpMsgBuf;                                      \
            FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |        \
                       FORMAT_MESSAGE_FROM_SYSTEM |               \
                       FORMAT_MESSAGE_IGNORE_INSERTS, NULL,       \
                       GetLastError(),                            \
                       MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), \
                       (LPTSTR) &lpMsgBuf,0,NULL );               \
            fprintf(stderr,"%s:%s\n",a,lpMsgBuf);                 \
            LocalFree( lpMsgBuf );                                \
        }       
    Y al usarse quedaría:
    if (!CreateProcess(NULL,"notepada",NULL,NULL,false,
                       CREATE_NEW_PROCESS_GROUP,NULL,
                       NULL,&suinfo,&informaciOn))
    {
        PERROR("CreateProcess");
        return 1;
    }           
    El error que nos da ahora la aplicación es bastante más descriptivo:
    CreateProcess:El sistema no ha encontrado el archivo especificado.
                
    En el resumen, podéis encontrar cómo obtener un manejador o el identificador del proceso actual, acabar con procesos u obtener el código de retorno de un proceso. Además, se habla algo acerca de la prioridad de los hilos de un proceso, que veremos posteriormente.

  4. Hilos en Windows NT.

    En la parte de teoría veíamos qué era un hilo. Ahora, lo vamos a ver en funcionamiento. Cada proceso de Windows NT puede tener uno o más hilos de ejecución. Estos hilos se estarán ejecutando en paralelo. Para crear un nuevo hilo de ejecución en un proceso, primero hay que tener una función para que la ejecute el hilo. Esta función no puede tener un prototipo cualquiera, sino que tiene que ser:
    DWORD WINAPI funciOn(LPVOID parAmetro); 
    El código de retorno de la función será el código de retorno del hilo y, a la función, se le podrá pasar un parámetro (un puntero). Se dejó que fuera un puntero para que así se pudiera pasar cualquier cosa a la función. Si se necesita pasar más de un parámetro, se define una estructura que los contenga y se pasa un puntero a esa estructura.

    Una vez tenemos la función, podemos ver el prototipo de la función del API que crea un hilo:
    HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, 
                        LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, 
                        DWORD dwCreationFlags, LPDWORD lpThreadId);
    Un par de parámetros los dejaremos a su valor por defecto: lpThreadAttributes a NULL, dwStackSize a 0. El tercer parámetro es un puntero a la función que ejecutará el thread, el cuarto es parámetro que pasaremos a la función cuando se ejecute el thread. dwCreationFlags es un campo de bits donde se especifican opciones de la función (en estos momentos bien es verdad que sólo hay una opción, pero la intención es lo que cuenta). En el último parámetro se nos devuelve el identificador del thread.

    En el resumen podéis ver más posibilidades para actuar sobre los hilos de un proceso.

  5. Prioridades en Windows NT.

    8 EJEMPLOS DE PLANIFICACIÓN.

    8.2 Windows NT.

    Tiene en cuenta que el sistema es multitarea, pero monousuario. Se trata de un planificador apropiativo por prioridades.

    Se planifica según hilos, aunque los procesos poseen una prioridad base.

    Clases de prioridad (mayor número=mayor prioridad):

      • Tiempo real (16-31): para tareas que requieran atención inmediata.
      • Prioridad variable (0-15): aplicaciones de usuario.

    Cada proceso tiene una prioridad base.

    Reglas de prioridad para los hilos:

      • Un hilo de tiempo real tiene una prioridad fija.
      • Un hilo de prioridad variable puede variar su prioridad siempre que no exceda el valor 15 ni sea inferior al valor inicial de la prioridad del hilo.
      • Cada hilo de prioridad variable tiene una prioridad base que puede estará entre dos números más o menos que la prioridad base de su proceso y respetando la regla anterior.

    Dentro de cada prioridad, se sigue un turno rotatorio.

    El ejecutor de Windows NT premia a los hilos de prioridad variable que tienen E/S y castiga a los que agontan su cuanto.

    En sistemas multiprocesadores, se reserva siempre un procesador para los hilos de menor prioridad y hay un atributo del hilo de afinidad a un procesador en concreto.

     

    La prioridad base del proceso vista en teoría depende de la clase de prioridad a la que pertenezca:
    Clase Prioridad base del proceso
    IDLE 4
    NORMAL 7
    HIGH 13
    REALTIME 24

    La clase de prioridad se establece en la función CreateProcess. Por su parte, la prioridad base del hilo depende de la prioridad base del proceso y se establece con la función SetThreadPriority. Se usan unas macros para ello:
    Macro Prioridad base del hilo
    THREAD_PRIORITY_IDLE 1 ó 16
    THREAD_PRIORITY_LOWEST base_proceso - 2
    THREAD_PRIORITY_BELOW_NORMAL base_proceso - 1
    THREAD_PRIORITY_NORMAL base_proceso
    THREAD_PRIORITY_ABOVE_NORMAL base_proceso + 1
    THREAD_PRIORITY_HIGHEST base_proceso + 2
    THREAD_PRIORITY_TIME_CRITICAL 15 ó 31

    Hay que tener cuidado cuando se establecen las prioridades de los hilos porque si se establecen prioridades demasiado altas (especialmente de la zona de tiempo real), puede ser que las acciones vitales del sistema se vean afectadas (actualización del puntero del ratón, volcado de los datos temporales al disco duro, etc.)

  6. Duplicación de manejadores.

    Debido a que Windows NT utiliza memoria virtual, las direcciones que maneja cada proceso son direcciones lógicas. Esto quiere decir que la dirección 0x1234 de un proceso no tiene por qué ser la misma que la dirección 0x1234 de otro. Esto hace que los manejadores (handles) de un mismo objeto, que no son más que un puntero, pueden estar en direcciones de memoria diferentes para cada proceso. Dicho de otro modo, si queremos que varios procesos tengan acceso a un mismo objeto (y hay posibilidad de ello, esto es, permisos) hay que duplicar el manejador en el espacio de direcciones del otro proceso. Esto lo podemos hacer con la función de la API:
    BOOL DuplicateHandle( HANDLE hSourceProcessHandle, HANDLE hSourceHandle, 
                          HANDLE hTargetProcessHandle, LPHANDLE lpTargetHandle, 
                          DWORD dwDesiredAccess, BOOL bHeredaHandle, DWORD dwOptions); 
    En el resumen, podéis ver cómo usar la función.

    Sólo hay una pequeña pega, ¿cómo podemos obtener un handle del otro proceso? Evidentemente, nos hará falta algún mecanismo de comunicación entre procesos para lograrlo. Estos mecanismos existen en Windows NT y los veremos en sucesivas sesiones. También será de utilidad la función que nos permite obtener un manejador de un proceso a partir de su número de identificación:
    OpenProcess(PROCESS_ALL_ACCESS,FALSE,identificador); 


  7. Práctica.

    Hay que crear un programa que admita un único argumento de línea de órdenes. Su nombre será hijos. El argumento es un número entero>100. El programa comprobará que los argumentos pasados son correctos y, si no es así, emitirá el correspondiente aviso por el canal de error estándar. El proceso creará dos hilos de ejecución que ejecutarán una misma función fnHilo. La función fnHilo desreferenciará el puntero que se le ha pasado, para conseguir un carácter. A continuación, repetirá un bucle tantas veces como el argumento pasado al programa y, en cada iteración, imprimirá, con printf el carácter que se le ha pasado. Usad fflush para que el carácter se imprima inmediatamente. El primero de los hilos creado por el padre, recibirá como parámetro '-'. El segundo, '+'. El propio padre llamará (sin crear un hilo) a la función fnhilo, pasándole un cero ('0'). Ejecutad la práctica y ved cómo Windows da la CPU a los diferentes hilos, variando en cada ejecución el valor pasado al programa. Si se añade un Sleep(0) después del fflush, ¿cómo afecta a la salida? Podéis ver la salida con más comodidad si la redirigís a un fichero desde la línea de órdenes.

  8. Aplicaciones relacionadas.



  9. Funciones de biblioteca relacionadas.



  10. LPEs.


© 2006 Guillermo González Talaván.