PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

SEXTA SESIÓN


  1. Semáforos y exclusión mutua.

    Recordemos lo visto en la asignatura teórica:

    4 SEMÁFOROS.

    Aparecen por vez primera en el tratado de Dijkstra de 1965 ya visto.

    Visión a alto nivel:

    cenicero con pajitas.

    Visión a nivel medio:

      • Un semáforo se inicializa con el número máximo de procesos que pueden usarlo sin bloquearse.
      • Un proceso que accede al semáforo, ejecuta la función wait sobre él:

    semáforo_t s1;

    [...]

    wait(&s1);

    signal(&s1);

    Visión a bajo nivel:

    A la operación wait a veces se la llama operación P (del holandés proberen, comprobar) y a la operación signal, operación V (del holandés verhogen, incrementar).

    Significado de la variable asociada a un semáforo:

      • Valor positivo=x: aún pueden acceder "x" procesos sin bloquearse. No hay procesos esperando.
      • Cero: no hay procesos esperando y si accede alguno, se bloqueará.
      • Negativo =-y: hay "y" procesos esperando.

     

    Cada S.O. puede proporcionar llamadas al sistema diferentes sobre las que construir wait y signal.

    Ejemplo de realización de exclusión mutua con semáforos:

      • Tenemos dos impresoras indistinguibles.
      • No puede haber dos procesos imprimiendo a la vez en una impresora.

    Varios procesos podrían ejecutar a la vez este mismo código sin interferir en la impresión de los demás.



  2. Mecanismos IPC.

    Los semáforos están incluidos en UNIX dentro de lo que se conoce como mecanismos IPC (de Inter Process Communication). Como sabemos, cualquier buen sistema operativo moderno debe aislar en lo posible los diferentes procesos que se están ejecutando en la máquina de modo que no interfieran entre sí accidentalmente. Sin embargo, hay veces en que deseamos que los procesos puedan interactuar y comunicarse. Para eso se crean los mecanismos IPC. Veremos tres de esos mecanismos en este curso: semáforos, memoria compartida y paso de mensajes (buzones).

    Todos los mecanismos IPC requieren que los procesos que vayan a usar una instancia de ellos se pongan de acuerdo para usar la misma. Es decir, en el ordenador hay muchos semáforos, por ejemplo. De todos los que hay, los procesos que vayan a colaborar han de ponerse de acuerdo en usar el mismo semáforo. Esto se hace mediante una clave. Existe una función de biblioteca, ftok que nos genera una de estas claves. Su prototipo es:
          key_t ftok(const char *path, int id); 
    La clave será una variable de un tipo especial: key_t. La clave se genera a partir de una ruta de acceso a un fichero que tiene que ser accesible en tiempo de ejecución por el proceso y un código de identificación, que suele ser un carácter. Por ejemplo, si un semáforo lo van a usar hijos de un mismo proceso, podemos generar una clave así:
    key_t clave;
    [...]
    clave=ftok(argv[0],'G'); 
    Sabemos que tendremos acceso al propio programa ejecutable pues lo estamos ejecutando...

  3. Observando los semáforos y borrándolos desde la shell.

    Para ver qué semáforos hay en nuestro sistema en estos momentos, se puede usar la orden de la shell ipcs -s:
    <Tejo>/home/so$ ipcs -s
    IPC status from /dev/kmem as of Thu Mar 16 13:07:04 2000
    T      ID     KEY        MODE        OWNER     GROUP
    Semaphores:
    s       0 0x2f141f40 --ra-ra-ra-      root       sys
    s       1 0x41145e23 --ra-ra-ra-      root      root
    s       2 0x4e140002 --ra-ra-ra-      root      root
    s       3 0x41155197 --ra-ra-ra-      root      root
    s       4 0x01090522 --ra-r--r--      root      root
    s       5 0x850b32f6 --ra-------    ingres       sys 
    En la columna ID vemos el identificador de semáforo. De él hablamos en el siguiente apartado. La columna KEY nos da en hexadecimal la clave asociada a ese semáforo. MODE indica los permisos de acceso del semáforo. OWNER y GROUP son el propietario el grupo al que pertenece el semáforo.

    Con ipcrm podemos eliminar un semáforo. Por ejemplo:
    <Tejo>/home/so$ ipcrm -s 5
    ipcrm: semid(5): permission denied 
    Evidentemente, hay que tener permisos para hacerlo :) .

  4. Declarando/creando un semáforo.

    Declarar un semáforo en UNIX es sencillo. La única dificultad consiste en que el Sistema Operativo nos reserva los semáforos por lotes (conjuntos). Es decir, se reserva un conjunto de cuatro semáforos, por ejemplo. Evidentemente, si deseamos usar sólo un semáforo, pediremos un conjunto de un semáforo. La función que se usa es:
          int semget(key_t clave, int nsems, int semflg); 
    Como primer parámetro se le va a pasar una clave como las que hemos visto en el apartado anterior. El segundo es el número de semáforos que queremos que tenga el conjunto que queremos usar y el último parámetro es un campo de bits que nos permite especificar opciones. De estas opciones, interesa IPC_CREAT. Si varios procesos hacen un semget para acceder a un semáforo, uno de ellos ha de especificar la macro IPC_CREAT con un OR (|) de los permisos con los que quiere crear el semáforo. El resto especificará 0 como último parámetro.

    La llamada al sistema, si no hay error, devolverá un identificador para el conjunto de semáforos declarado. Este identificador es similar al descriptor de fichero de los ficheros, es decir, se usará en todas las llamadas al sistema con las que queramos operar sobre el semáforo recién creado.

    Si el semáforo va a ser usado sólo por un proceso y sus hijos, también se puede dejar al sistema operativo que genere una clave única para el proceso. Esto se hace usando como primer parámetro de semget la macro IPC_PRIVATE. Todos los hijos usarán entonces el mismo valor devuelto por semget para acceder al semáforo. Por ejemplo:
    int semAforo;
    [...]
    semaforo=semget(IPC_PRIVATE,3,IPC_CREAT | 0600);
    [...]
    switch (fork())
       {case -1: /* Errores */
           [...]
        case 0:  /* Hijo */
           /* Puede usar el semáforo pues conoce la variable semAforo. */
           [...]
        default: /* Padre */
           /* También puede usar el semáforo pues también conoce semAforo. */
        }       


  5. Manejando un semáforo.

    Todas las operaciones que podemos hacer con un semáforo salvo las más importantes (incrementarlo y decrementarlo) se hacen con la llamada al sistema semctl. El prototipo es:
          int semctl(int semid, int semnum, int cmd, ...  /* arg */); 
    El primer parámetro es el identificador del conjunto de semáforos sobre el que queremos trabajar, que lo habremos obtenido con semget. El segundo parámetro es el índice del semáforo sobre el que queremos trabajar (al primer semáforo le corresponde un cero). El tercer parámetro es la operación que queremos realizar. Puede ser una de estas:

    En Solaris, todo funciona muy parecido a como lo hace en HPUX. Sin embargo, el cuarto parámetro, caso de ser requerido por la función de semctl ha de ser del tipo union semun. Además, la definición de esta unión tiene que incluirse explícitamente en nuestro programa. La definición es:
         union semun 
            {int             val;
             struct semid_ds *buf;
             ushort_t        *array;
             };
    Es un caso extraño, porque lo normal sería que la definición viniera en algún fichero de cabecera. Es más, si no se hace así y se hace como en HPUX, se puede obtener un fallo de acceso a memoria en tiempo de ejecución.

  6. Incrementando/decrementando un semáforo.

    Como todos debéis saber, para incrementar o decrementar un semáforo es necesario utilizar operaciones especiales, conocidas como wait y signal. Esto es así porque si no, no permitiríamos que el proceso se bloquee. Además, las operaciones han de ser atómicas para no dar lugar a inconsistencias.

    Para realizar las operaciones wait y signal, disponemos de la llamada al sistema semop. El prototipo es:
          int semop(int semid, struct sembuf *sops, unsigned int nsops);
    semop se vuelve algo complicada porque permite realizar más de una operación wait y signal a la vez. Veámoslo con un ejemplo: tenemos 7 semáforos cuyo identificador es semAforo y queremos hacer un wait sobre el primero, un signal sobre el tercero y 4 waits a la vez sobre el séptimo. Se haría así:
    struct sembuf sops[3];
    [...]
    sops[0].sem_num=0;     /* Sobre el primero, ... */
    sops[0].sem_op=-1;     /* ... un wait (resto 1) */
    sops[0].sem_flg=0;
    
    sops[1].sem_num=2;     /* Sobre el tercero, ...  */
    sops[1].sem_op= 1;     /* ... un signal (sumo 1) */
    sops[1].sem_flg=0;
    
    sops[2].sem_num=6;     /* Sobre el séptimo, ... */
    sops[2].sem_op=-4;     /* ... 4 waits (resto 4) */
    sops[2].sem_flg=0;
    
    semop(semAforo,sops,3); 
    Observad cómo sops no lleva ampersand (&), pues el nombre de un array sin nada detrás ya es un puntero al primer elemento del array. También observad cómo la llamada al sistema necesita que le especifiquemos el número de operaciones que incluye el array porque en C un puntero en tiempo de ejecución no es más que una dirección de memoria y no tiene información adicional acerca del objeto al que hace referencia. Esto es, la llamada semop no puede saber cuántos elementos tiene el array que le pasamos. La llamada semop se queda bloqueada hasta que se puedan realizar todas las operaciones que se especifiquen en el array sops.

    En alguna operación del array sops se puede especificar en el campo sem_flg la opción IPC_NOWAIT y, en ese caso, la operación sería no bloqueante. Esto es, si no se puede decrementar el semáforo no nos quedaríamos bloqueados (aunque no se decremente). Utilizar este flag es una práctica nefasta por cuanto la esencia de un semáforo es precisamente quedarse bloqueado en el caso de no poder realizar la operación wait.

  7. Operación "esperar a que sea cero"

    En UNIX es normal que se disponga de una operación adicional a las tradicionales wait y signal. Es la operación "esperar a que sea cero", que podemos denotar como W0. Si un proceso realiza esta operación y el valor del semáforo es cero, el proceso continúa. Si es distinto de cero, el proceso espera, sin consumo de CPU, a que el valor del semáforo sea cero. Para indicar la operación W0, se debe poner a cero el campo sem_op de struct sembuf.

  8. Práctica.

    La práctica consiste en generar cuatro procesos que pretenden acceder a una zona de exclusión mutua en la que sólo pueden estar dos de ellos a la vez. El acceso a la zona de exclusión mutua se regulará por un semáforo. Cada proceso calculará un número igual a la parte entera de la última cifra de su PID dividida por dos. A ese número le sumará uno y le llamará somnolencia. El proceso dormirá tanto como indique su somnolencia, esperará hasta entrar en la sección crítica, dormirá dentro tanto como indique su somnolencia y, vuelta a empezar. Se puede usar sleep. Cuando se pulse CTRL+C, todos los procesos acabarán y el semáforo será borrado del Sistema Operativo.

    Salida por pantalla: para comprobar el correcto comportamiento del programa, la salida por pantalla será como sigue. Al principio, el primer proceso limpiará la pantalla con la función:
    system("clear"); 
    Cuando un proceso entre en la sección crítica escribirá la última cifra de su PID en la línea de la pantalla cuyo número es igual a ella. Cuando vaya a salir de la sección crítica borrará (escribirá un espacio encima) en dicha línea. Así se podrá observar a simple vista que no hay más de dos procesos a la vez en la sección crítica. Para poner el cursor en la línea 7, columna 3, por ejemplo, se puede usar la función:
    system("tput cup 7 3"); 
    Observación: si usáis printf para escribir en la pantalla debéis saber que esta función tiene un búfer intermedio que hace que lo que se escriba no aparezca inmediatamente en pantalla. El búfer se vacía en tres circunstancias:
    1. Se recibe un retorno del carro (\n).
    2. Se llena y no queda más remedio que volcarlo.
    3. Se ejecuta la función fflush(stdout);.

    Tenedlo en consideración. Podéis encontrar en /usuarios/profes/gyermo/SESION6/critica el ejecutable de esta práctica.

    Soluciones y comentarios (para HPUX. Para Solaris, hay que hacer las pequeñas modificaciones que se han comentado en el apartado de semctl).

  9. Órdenes de la shell relacionadas.

    tput
    órdenes dirigidas al terminal
    ipcs
    muestra los mecanismos ipc en uso
    ipcrm
    borra un mecanismo ipc en uso


  10. Funciones de biblioteca relacionadas.

    system
    permite ejecutar órdenes de la shell desde dentro de un programa escrito en C
    fflush
    vacía el búfer intermedio de un flujo (stream)


  11. LPEs.


© 2000 Guillermo González Talaván.