PRÁCTICAS DE SISTEMAS OPERATIVOS II

SÉPTIMA SESIÓN


  1. Funciones de sincronización.

    7 SISTEMAS DE EJEMPLO.

    7.2 Windows.

    Windows trata la sincronización apoyado en el concepto de objeto.

    Algunos objetos pueden estar en el estado de señalado o de no señalado. Un hilo puede esperar a que un objeto pase de no señalado a señalado, bloqueando su ejecución.

    Los objetos implicados y la razón por la que pasan a estar señalados pueden ser:

      • Procesos: cuando acaba su último hilo.
      • Hilos: cuando terminan su ejecución.
      • Ficheros: cuando se completa una operación de entrada/salida.
      • Sucesos: cuando ocurre el suceso.
      • Semáforos: cuando la variable asociada al semáforo es estrictamente mayor que cero.
      • Temporizadores: cuando expira el intervalo de tiempo prescrito.
      • Exclusiones mutuas (mutexes): cuando un hilo sale de la zona de exclusión mutua.

    En general, cuando un objeto pasa a estar señalado, se desbloquean todos los hilos que estuvieran esperando por él. Esto es cierto salvo en el caso de las exclusiones mutuas, en las que, como es lógico, sólo se desbloquea un hilo.

     

    Para que un hilo de un proceso se bloquee esperando a que un objeto de Windows pase al estado de señalado, hay que usar la función del API WaitForSingleObject. El funcionamiento de esta función no es difícil por lo que os remitimos al resumen .

    Si lo que se desea es esperar por varios objetos a la vez, usaremos la función WaitForMultipleObjects. En este caso, podemos esperar hasta que todos pasen al estado de señalado o hasta que al menos uno de ellos pase al estado de señalado. Esto lo controlará el parámetro de la función fWaitAll:
    DWORD WaitForMultipleObjects( DWORD nCount, CONST HANDLE *lpHandles, 
                                  BOOL fWaitAll, DWORD dwMilliseconds); 
    El resto de los parámetros no presenta ninguna dificultad. Nos encontramos de nuevo un array que, como ya sabemos, necesita que pasemos en otro parámetro su longitud pues, en C, para la función la longitud del array sería desconocida si no lo hacemos.

    Estas funciones de espera van a suplir a muchas otras de UNIX. Con ellas, podremos decrementar semáforos, esperar por la muerte de un proceso o un hilo, hacer multiplexión de entrada/salida síncrona, etc.

    En cuanto a la sincronización en sí, dispondremos de más mecanismos que en UNIX. Tendremos semáforos, pero además secciones críticas, mutexes (exclusiones mutuas) y eventos. Empezaremos por ver los semáforos, pues ya los conocemos de UNIX y veremos en qué se diferencian de los otros mecanismos.

  2. Semáforos .

    Para recordar el funcionamiento de un semáforo, nada mejor que ir a la sesión primera . Algunas diferencias de los semáforos de Windows frente a los semáforos de UNIX son:
    1. Se deberá especificar un valor máximo para los semáforos de Windows.
    2. No se podrá incrementar varios semáforos a la vez.
    3. No se podrá decrementar un semáforo en más de una unidad cada vez.
    Para poder usar un semáforo, primero hay que crearlo. Al crearlo, obtendremos un manejador (handle) con el que acceder a él. Se usará la función del API:
    HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
                            LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName); 
    Como tenemos por costumbre, dejaremos los atributos de seguridad a su valor por defecto (NULL). Debemos especificar en la llamada de creación el valor inicial del semáforo y su valor máximo (esto es diferente con respecto a UNIX).

    Cuando veíamos los mecanismos IPC en UNIX comentábamos cómo debe existir alguna manera en que diferentes procesos se pusieran de acuerdo en usar una misma instancia de un determinado mecanismo. Esto se hacía mediante una clave. En Windows, se hace mediante un nombre que deben conocer todos los procesos que quieran manejar el mecanismo. Este nombre es el último parámetro de CreateSemaphore.

    También veíamos en UNIX que se podía crear un mecanismo IPC sin nombre con la macro IPC_PRIVATE. Esto se consigue en Windows poniendo NULL como último parámetro. Si queremos compartir el semáforo sin nombre, debemos usar la función del API que vimos en la sesión anterior DuplicateHandle.

    Cuando un proceso quiere usar un semáforo que ya está creado, debe usar la función OpenSemaphore:
    HANDLE OpenSemaphore( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName); 
    En el primer parámetro especificaremos el tipo de acceso deseado. Normalmente será SEMAPHORE_ALL_ACCESS, aunque también es posible obtener accesos parciales. El segundo parámetro nos servirá para especificar si queremos que el handle sea heredable por los procesos que creemos. El último parámetro es el nombre del semáforo.

    Una vez tenemos un handle del semáforo, podemos realizar operaciones sobre él. La más sencilla es la operación de incremento. Se realiza con la función:
    BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
    Especificaremos que queremos incrementar el semáforo en lReleaseCount unidades. La función nos devolverá en la dirección apuntada por lpPreviousCount el valor anterior del semáforo.

    Llegada la hora de decrementar el semáforo y, en el caso de que proceda, quedarnos bloqueados, usaremos cualquiera de las funciones de espera de Windows, por ejemplo:
    DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds); 
    El proceso que la invoque con el handle del semáforo efectuará una operación wait sobre él. No es posible decrementar atómicamente el semáforo en más de una unidad.

  3. Mutexes.

    Los mutexes, abreviatura de exclusiones mutuas son otros objetos de sincronización de Windows. Son muy parecidos a los semáforos. Se diferencian en tres aspectos:

    1. Son binarios. Es decir, solo pueden tomar dos valores (asimilables a ocupado(0) y libre(1)).
    2. Existe el concepto de posesión del mutex. La analogía que usamos para los semáforos de un cenicero con pajitas presenta el inconveniente de que inconscientemente se tiende a pensar que un proceso sólo puede dejar una pajita si antes logró cogerla, y eso es falso para los semáforos en general, tanto de UNIX como de Windows. Cualquier proceso puede incrementar el semáforo.

      Sin embargo, los mutexes funcionan de modo diferente. El hilo del proceso que haya logrado coger la única pajita del mutex será el único que pueda dejarla de nuevo y así permitir a otros hilos que estén bloqueados poder optar a ella.
    3. El hilo que posee el mutex puede tomar posesión de él varias veces más sin quedarse bloqueado. Si es este el caso, para liberarlo debe soltarlo tantas veces como lo cogió.


    En cuanto a las funciones que manejan los mutexes, son muy parecidas a las de los semáforos y están en el resumen . Tan solo mencionar:
    HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner,
                        LPCTSTR lpName); 
    El valor bInitialOwner indicará si el hilo que crea el mutex tendrá inmediata posesión de él (y el mutex valor nulo) o si, por el contrario, el mutex estará libre (con valor unidad).

  4. Eventos.

    Mejor traducido por suceso, respetaremos el nombre más parecido al inglés. En su funcionamiento más elemental es un objeto con el cual podemos controlar de un modo manual su estado (señalado o no). Lo creamos con la función CreateEvent:
    HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, 
                        BOOL bInitialState, LPCTSTR lpName); 
    Con esta función podemos indicar si queremos que el objeto nazca señalado o no (bInitialState). Como todo buen objeto de Windows, los hilos de los procesos se quedarán bloqueados esperando sobre él hasta que pase al estado de señalado. Lo podemos poner en estado de señalado con SetEvent. Lo podemos pasar a estado de no señalado con ResetEvent. Podemos, incluso, invocar PulseEvent y lograremos que esté temporalmente en estado de señalado para que se desbloqueen todos los hilos que estén esperando sobre él y pasar a continuación al estado de no señalado.

    Finalmente, si en CreateEvent hacemos que bManualReset sea falso, cada vez que hagamos SetEvent no hará falta que hagamos el correspondiente ResetEvent, pues se hará de forma automática.

  5. Secciones críticas.

    Ya conocemos de la parte de teoría de los sistemas operativos lo que es una sección crítica. Sabemos que podemos delimitar secciones críticas de nuestros programas mediante, por ejemplo, un semáforo. Sin embargo, Windows nos proporciona un mecanismo más sencillo para marcar las zonas de un programa que forman una sección crítica. Se puede hacer con ayuda de un objeto de tipo CRITICAL_SECTION. Normalmente se usará para delimitar secciones críticas dentro de un mismo proceso en las cuales sólo puede haber un hilo ejecutando de por vez.

    Para usar estos objetos, hay que primero crear una variable de tipo CRITICAL_SECTION y darle valor inicial así:
    CRITICAL_SECTION sc1;
    [...]
    InitializeCriticalSection(&sc1); 
    Cada una de las partes del código que formen la sección crítica hay que rodearla por las instruciones:
    EnterCriticalSection(&sc1);
    [[...Sección crítica...]]
    LeaveCriticalSection(&sc1);
    No hay que olvidarse de liberar la sección crítica cuando ya no se vaya a usar más con DeleteCriticalSection. También existe la posibilidad de probar a entrar en la sección crítica pero no quedarse bloqueado si no se puede (algo así como la versión no bloqueante o IPC_NOWAIT de UNIX) con TryEnterCriticalSection.

  6. Operaciones atómicas sobre variables de tipo LONG.

    Windows nos permite realizar operaciones atómicas sobre variables de tipo LONG. Las operaciones incluyen el incremento, decremento e intercambio atómico:
    LONG InterlockedIncrement( LPLONG lpAddend);
    LONG InterlockedDecrement( LPLONG lpAddend);
    LONG InterlockedExchange( LPLONG Target, LONG Value); 
    Estas operaciones son importantes como vemos en teoría para poder dar soluciones de tipo hardware a la concurrencia. En este caso, además, tenemos mayor problema que con los procesos de UNIX pues la memoria es compartida por todos los hilos de ejecución de un proceso y el acceso compartido a las variables puede dar problemas. Estos problemas a veces permanecen ocultos hasta que usamos el programa en un ordenador multiprocesador, pues entonces aumenta la probabilidad de fallo.

  7. Activación y espera atómica.

    En ciertas ocasiones resulta conveniente poder soltar una sección crítica a la vez que quedarnos esperando sobre otro objeto de sincronización, todo de un modo atómico. Para ello, viene que ni pintada la siguiente llamada al sistema:
    DWORD SignalObjectAndWait( HANDLE hObjectToSignal, HANDLE hObjectToWaitOn,
                               DWORD dwMilliseconds, BOOL bAlertable);
    El significado de sus dos primeros parámetros es evidente; el tercero funciona igual que el correspondiente de WaitForSingleObject y el último, lo dejaremos a FALSE. Su significado excede el ámbito de lo razonable para este curso. En cuanto a su valor de retorno también es similar al de WaitForSingleObject.

    Un uso estándar típico podría ser el siguiente:
         HILOsTRABAJADORes                                           HILOGESTOR
         =================                                           ==========
         W(Mutex);                                                   idElegido=RealizarTareaYElecciOn();
           while (elegido!=miId)                                     W(Mutex);
            {SignalObjectAndWait(Mutex,Evento,INFINITE,FALSE);         elegido=idElegido;
             W(Mutex);}                                                PulseEvent(Evento);
         S(Mutex);                                                   S(Mutex);
                                                                     EsperaAHiloElegido();
    Los hilos que concurren a realizar la tarea, toman el mutex para poder observar el sistema tranquilamente. Viendo que no son el elegido, atómicamente, sueltan el mutex y se quedan esperando sobre un evento a que el hilo gestor realice otra elección. En ese momento, vuelve a tomar el mutex y lo reintenta hasta que tiene éxito;. El consumo de CPU que produce este esquema no es óptimo frente a usar un objeto de sincronización por cada hilo trabajador, pero es mínimo.
  8. Práctica.

    Windows no tiene la posibilidad de decrementar en más de una unidad un semáforo atómicamente. UNIX sí. Con esto es casi trivial hacer la práctica. Sin ello, hay que pensar un poco :).

    Cuando construyeron el edificio de usos múltiples de Volrejas no lo hicieron con ascensor. Ahora ya lo instalaron. En el cartel pone "Para tres personas", pero eso es porque no conocen a los gemelos Robustines. Con sus 163.5 kg por barba, cada uno ocupa por dos. Eso por no comentar su afición a subir por el ascensor continuamente. Toda la mañana se pasan subiendo por el ascensor y bajando por la escalera, para volver a subir. Esta compulsión la comparte con Muzzala y Mocosette.

    Haced un programa que, durante un minuto muestre cómo suben y bajan los personajes descritos. Considerad que el viaje en ascensor tarda 3 s y que en bajar por las escaleras tardan 1 s.

    La salida por pantalla ha de consistir en líneas como esta:
    Esperando: Mu R2          Ascensor: Mo             Escalera: R1        
    Esperando: Mu R1 R2       Ascensor: Mo             Escalera:         
    Esperando: Mu R1 R2       Ascensor:                Escalera: Mo        
                etc. 
    Soluciones y comentarios .

  9. Mensajes en Windows.

    El sistema de mensajes de Windows está muy asociado a las ventanas. El sistema está continuamente mandando mensajes a las ventanas de las aplicaciones: cuando se mueve el ratón, cuando pasan a primer plano, cuando se pulsa sobre ellas, cuando se selecciona un menú, etc. En la aplicación dueña de la ventana existe una función especial que se encargará de atender y procesar estos mensajes.

    Las aplicaciones que estamos realizando son aplicaciones de consola. Difieren de las aplicaciones normales de Windows en que no tienen una entrada/salida asociada a una ventana. Es por eso que no veremos los mensajes de Windows en su conjunto. Solo veremos un par de funciones que permitirán mandar mensajes entre hilos que no posean ventanas.

    Los mensajes de Windows tienen un tipo de mensaje que identificará cuál es la razón del mensaje y dos argumentos, un argumento de 16 bits y otro de 32 bits. Podemos mandar un mensaje a un hilo con la función:
    BOOL PostThreadMessage( DWORD idThread, UINT Msg, WPARAM wParam, LPARAM lParam);
    En los parámetros se especificará el identificador del hilo receptor del mensaje, el tipo de mensaje, el parámetro de 16 bits y el parámetro de 32 bits. En el tipo de mensaje, podemos usar el WM_USER y siguientes pues están reservados para aplicaciones de usuario. El envío que realiza esta función es un envío no bloqueante en el sentido visto en teoría. Existe otra función SendMessage que no veremos que realiza envíos bloqueantes (la función no retorna hasta que el destinatario haya recibido el mensaje).

    Para que un hilo pueda recibir un mensaje es necesario que el hilo tenga una cola de recepción de mensajes. Los hilos con ventanas a su cargo automáticamente la tienen. Como nuestros hilos pueden no tenerlas, es necesario que el hilo haya realizado antes una llamada:
    PeekMessage(&mensaje, NULL, WM_USER, WM_USER, PM_NOREMOVE);
    antes de poder recibir ningún mensaje.

    Esta última función sirve para poder espiar para ver si hay algún mensaje en la cola. Su prototipo es:
    BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax,
                      UINT wRemoveMsg); 
    En el primer parámetro se depositará la información del mensaje espiado, el segundo lo dejaremos a NULL. En el tercero y el cuarto especificaremos qué rango de tipo de mensajes queremos comprobar. El último parámetro servirá para indicar si queremos que el mensaje desaparezca de la cola o no, después de espiado.

    Para conseguir el mensaje, se usa la función:
    BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);
    igual que PeekMessage con la diferencia de que el mensaje siempre desaparece y de que la función se queda bloqueada si no hay mensajes de los tipos solicitados en la cola. Podemos usar WaitMessage para ver si hay mensajes de cualquier tipo en la cola.

    Los campos de la estructura MSG que devuelven estas funciones se pueden consultar en el resumen.

  10. Aplicaciones relacionadas.



  11. Funciones de biblioteca relacionadas.



  12. LPEs.


© 2023 Guillermo González Talaván.