PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

SEGUNDA PRÁCTICA OBLIGATORIA (2004-05)

Un día en las carreras (de coches)


  1. Enunciado.

    En esta práctica vamos a simular, mediante procesos de UNIX, una carrera automovilística.

    El programa constará de un único fichero fuente, falonso.c, cuya adecuada compilación producirá el ejecutable falonso. Respetad las mayúsculas/minúsculas de los nombres.

    Para simplificar la realización de la práctica, se os proporciona una biblioteca estática de funciones (libfalonso.a) que debéis enlazar con vuestro módulo objeto para generar el ejecutable. Gracias a ella, muchas de las funciones no las tendréis que programar sino que bastará nada más con incluir la biblioteca cuando compiléis el programa. La línea de compilación del programa podría ser:
    c89 falonso.c libfalonso.a -o falonso
    Disponéis, además, de un fichero de cabeceras, falonso.h, donde se encuentran definidas las macros que usa la biblioteca.

    El proceso inicial se encargará de preparar todas las variables y recursos IPC de la aplicación. También se encargará de crear todos los procesos que gobernarán los coches. Manejará así mismo, los semáforos del circuito. El circuito tiene forma de lemniscata y posee dos carriles por los que se circula en el mismo sentido. Los denominaremos carril derecho (CD) y carril izquierdo (CI). La posición de un coche en el circuito viene completamente determinada dando su carril y el desplazamiento dentro de él. El desplazamiento es un número entero comprendido entre 0 y 136, ambos incluídos. Sendos planos del circuito, uno para cada carril, con los desplazamientos indicados se pueden observar a continuación:
             CARRIL_DERECHO
       XXXX###################XXXX
      XXXX#                   #XXXX
     XXXX#   0 - - - - 1 - -   #XXXX
    XXXX#  /60123456789012345\  #XXXX
    XXX#  |5 ############### 6\  #XXXX
    XXX#  |4 #~~~&~~~&~~~&~~# 7\  #XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    XXX#  |3 #~&~~~&~~~&~~~&~# 8\  #XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    XXX#  |2 #~~&~~~&~~~&~~~&~# 9\2 #XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    XXX#  |1 ################### 0\  ########################################XXXX
    XXX#   \09876543210987654321  1  54321098765432109876543210987654321098  #XXXX
    XXXX#   3 - - - - 2 - - - -1-  2   - -0- - - - -9- - - - -8- - - - -7- 7  #XXXX
     XXXX#  1         1        1    3     1                                \6 #XXXX
      XXXX########################## 4\  ###############################   |5 #XXXX
       XXXXXXXXXXXXXXXXXXXXXXXXXXXXX# 5\  #~~~&~~~&~~~&~~~&~~~&~~~&~~~&~#  |4 #XXXX
        XXXXXXXXXXXXXXXXXXXXXXXXXXXXX# 6\  #############################   |3 #XXXX
         XXXXXXXXXXXXXXXXXXXXXXXXXXXXX# 7\                                /2  #XXXX
                                   XXXX# 8-3- - - - -4- - - - -5- - - - -61  #XXXX
                                    XXXX# 90123456789012345678901234567890 #XXXX
                                     XXXX###################################XXXX
             CARRIL_IZQUIERDO
       XXXX###################XXXX
      XXXX# 60123456789012345 #XXXX
     XXXX# 5 0 - - - - 1 - - 6 #XXXX
    XXXX# 4/                 \7 #XXXX
    XXX# 3|  ###############  \8 #XXXX
    XXX# 2|  #~~~&~~~&~~~&~~#  \9 #XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    XXX# 1|  #~&~~~&~~~&~~~&~#  \0 #XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    XXX# 031 #~~&~~~&~~~&~~~&~# 2\1 #XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    XXX# 9|  ###################  \2 ########################################XXXX
    XXX# 8 \     1         1    1   3                                        #XXXX
    XXXX# 7 - - -2- - - - -1- - 0    4 - - - - 9 - - - - 8 - - - - 7 - - -    #XXXX
     XXXX# 6543210987654321098765432  5  654321098765432109876543210987654 \  #XXXX
      XXXX##########################  \6 ###############################  3|  #XXXX
       XXXXXXXXXXXXXXXXXXXXXXXXXXXXX#  \7 #~~~&~~~&~~~&~~~&~~~&~~~&~~~&~# 2|  #XXXX
        XXXXXXXXXXXXXXXXXXXXXXXXXXXXX#  \8 #############################  1|  #XXXX
         XXXXXXXXXXXXXXXXXXXXXXXXXXXXX#  \90123456789012345678901234567890/   #XXXX
                                   XXXX#  -3- - - - -4- - - - -5- - - - -6   #XXXX
                                    XXXX#                                   #XXXX
                                     XXXX###################################XXXX
    
    La práctica se invocará especificando dos parámetros exactamente desde la línea de órdenes. El primero es el número de coches que van a participar en la carrera. El segundo puede ser 0 ó 1. Si es 1, la práctica funcionará a velocidad normal. Si es 0, irá a la máxima velocidad.

    Los coches, al principio, se pueden colocar como se desee, siempre que dos coches no compartan la misma posición. Una vez en movimiento, ningún coche podrá chocar con otro de la pista. La velocidad y los cambios de carril se dejan a vuestra discreción.

    La función de cambio de carril hace que un coche cambie su carril y el desplazamiento según la siguiente tabla:

    Der -> Izq          Izq -> Der
    0..13 0..13 0..15 0..15
    14..28 15..29 16..28 15..27
    29..60 29..60 29..58 29..58
    61..62 60..61 59..60 60..61
    63..65 61..63 61..62 63..64
    66..67 63..64 63..64 67..68
    68 64 65..125 70..130
    69..129 64..124 126 130
    130 127 127..128130..131
    131..134129..132 129..133131..135
    135..136134..135 134..136136


    Así, por ejemplo, un coche situado en (CD,80) pasa a (CI,75) al cambiar de carril. Uno que esté en (CI,126) pasa a (CD,130), por su parte.

    Existe un cruce cerca del centro del circuito que está regulado mediante un par de semáforos. El funcionamiento de los semáforos lo realiza el proceso primero y deberá ser razonable (no se puede mantenerlos apagados o siempre en verde, o siempre en rojo, por ejemplo). Si un semáforo está en rojo, ningún coche deberá sobrepasarlo.

    El circuito también cuenta con un contador automático situado en las posiciones (CD,133) y (CI,131). La aplicación desarrollada llevará cuenta, por su parte, en una variable compartida del número de pasos por el contador. Esto es, cada coche, al pasar por el contador, incrementará la variable compartida. Al finalizar el programa, se deberá pasar la dirección de memoria a la biblioteca para que esta compruebe que vuestra cuenta y la de ella coinciden. El programa estará en continua ejecución hasta que el usuario pulse las teclas CTRL+C desde el terminal. En ese momento, todos los coches deben parar (en un estado consistente) y morirse. El proceso primero esperará por su muerte. Informará a la biblioteca de que ha acabado, proporcionándole vuestra cuenta de vueltas y destruirá los mecanismos IPC utilizados.

    Para que cualquier proceso pueda conocer en todo momento el estado del sistema, se va a usar una zona de memoria compartida. Los procesos no escribirán nunca en las partes controladas por la biblioteca. Son sólo informaciones. El mapa de la zona, expresado en bytes, es:
    Siguiendo la misma filosofía, deberéis crear un array de semáforos, el primero de los cuales se reservará para el funcionamiento interno de la biblioteca. El resto, podéis usarlos libremente.

    Para simplificar la realización de la práctica, se os proporciona una biblioteca estática de funciones (libfalonso.a) que debéis enlazar con vuestro módulo objeto para generar el ejecutable. Disponéis, además, de un fichero de cabeceras, falonso.h, donde se encuentran definidas las macros que usa la biblioteca. Las funciones proporcionadas por la biblioteca son las que a continuación aparecen. De no indicarse nada, las funciones devuelven -1 en caso de error:

    Respecto a la sincronización interna de la biblioteca, se usa el semáforo reservado bien para poder actualizar la pantalla, bien para el manejo de la memoria compartida. Seguro que necesitaréis hacer sincronizaciones adicionales para que la práctica funcione. Para que esas sincronizaciones estén en sintonía con la biblioteca, os ofrezco ahora un seudocódigo de las funciones que realiza la biblioteca. S es es semáforo interno que utiliza.
        * inicio_falonso:
             - limpia la pantalla
             - S=1
             - mensaje de bienvenida
             - dibuja el circuito
    
        * inicio_coche:
             - comprobación de parámetros
             - W(S)
             - si posición ocupada, S(S), poner error, volver.
             - pintar el coche y actualizar la memoria compartida
             - S(S)
    
        * avance_coche:
             - comprobación de parámetros
             - W(S)
             - borrar el coche y actualizar la memoria compartida
             - avanzar una posición
             - si hay choque, S(S), poner error, volver.
             - pintar el coche y actualizar la memoria compartida
             - si pasamos por el contador, incrementarlo y pintarlo
             - S(S)
             - si nos hemos saltado un semáforo en rojo, poner error, volver.
    
        * cambio_carril:
             - comprobación de parámetros
             - W(S)
             - borrar el coche y actualizar la memoria compartida
             - cambiar de carril
             - si hay choque, S(S), poner error, volver.
             - pintar el coche y actualizar la memoria compartida
             - S(S)
             - si nos hemos saltado un semáforo en rojo, poner error, volver.
    
        * luz_semAforo:
             - comprobación de parámetros
             - W(S)
             - dibujar semáforo y actualizar memoria compartida
             - S(S)
    
        * fin_falonso:
             - si no coinciden las vueltas, poner error, volver.
    
        Cada vez que se pone un error, se pone W(S) y S(S) rodeando la impresión
        en pantalla. Las funciones pausa() y velocidad() no usan el semáforo.
    En cuanto al consumo de CPU, no debe consumirse CPU cuando un coche espere a que el semáforo de su carril se ponga en verde. Tampoco en los instantes iniciales cuando, después de haber colocado todos, deis el pistoletazo de salida. Podéis, aunque evidentemente se premiará al que logre hacerlo bien siendo como es difícil, consumir CPU para evitar choques o alcances o los coches que estén esperando a la cola de un semáforo en rojo. En estos casos, y para ser respetuosos con el ordenador, se realizará en todo caso "semiespera ocupada", intercalando en el sondeo una pausa de una décima de segundo.

    En esta práctica no se podrán usar ficheros para nada, salvo que se indique expresamente. Las comunicaciones de PIDs o similares entre procesos se harán mediante mecanismos IPC.

    Siempre que en el enunciado o LPEs se diga que se puede usar sleep(), se refiere a la llamada al sistema, no a la orden de la línea de órdenes.

    Los mecanismos IPC (semáforos, memoria compartida y paso de mensajes) son recursos muy limitados. Es por ello, que vuestra práctica sólo podrá usar un conjunto de semáforos, un buzón de paso de mensajes y una zona de memoria compartida como máximo. Además, si se produce cualquier error o se finaliza normalmente, los recursos creados han de ser eliminados. Una manera fácil de lograrlo es registrar la señal SIGINT para que lo haga y mandársela uno mismo si se produce un error.

    Biblioteca de funciones libfalonso

    Con esta práctica se trata de que aprendáis a sincronizar y comunicar procesos en UNIX. Su objetivo no es la programación. Es por ello que se os suministra una biblioteca estática de funciones ya programadas para tratar de que no tengáis que preocuparos por la presentación por pantalla, la gestión de estructuras de datos (colas, pilas, ...) , etc. También servirá para que se detecten de un modo automático errores que se produzcan en vuestro código. Para que vuestro programa funcione, necesitáis la propia biblioteca libfalonso.a y el fichero de cabecera falonso.h. La biblioteca funciona con los códigos de VT100/xterm, por lo que debéis adecuar vuestros simuladores a este terminal.
    Ficheros necesarios:


  2. Pasos recomendados para la realización de la práctica

    Aunque ya deberíais ser capaces de abordar la práctica sin ayuda, aquí van unas guías generales:
    1. Crear los semáforos, la memoria comparida y el buzón, y comprobad que se crean bien, con ipcs. Es preferible, para que no haya interferencias, que los defináis privados.
    2. Registrar SIGINT para que cuando se pulse ^C se eliminen los recursos IPC. Lograr que si el programa acaba normalmente o se produce cualquier error, también se eliminen los recursos (mandáos una señal SIGINT en esos casos).
    3. Llamar a la función inicio_falonso en main. Debe aparecer la pantalla de bienvenida y, pasados dos segundos, dibujarse el circuito.
    4. Probad a poner los semáforos a distintos colores.
    5. Llega el momento de probar el circuito. Cread un hijo que maneje un coche que no cambie de carril y corra a velocidad constante.
    6. Añadid otro coche y plantead una rutina que evite los choques por alcance.
    7. Introducid los cambios de carril, si no lo habéis hecho ya para evitar los choques y cuidad de que no se produzcan choques al cambiar de carril.
    8. Haced que el primer proceso maneje de modo razonable los semáforos.
    9. Que los coches respeten el semáforo en rojo.
    10. Tened en cuenta el argumento que indica el número de coches y cread tantos como indique dicho número.
    11. Programar la cuenta, corregir la pulsación de CTRL+C si en alguna ocasión no funciona y completar la llamada a fin_falonso().
    12. Pulid los últimos detalles.


  3. Plazo de presentación.

    Hasta el lunes 16 de mayo de 2005, inclusive.

  4. Normas de presentación.

    Acá están. Además de estas normas, en esta práctica se debe entregar un esquema donde aparezcan los semáforos usados, sus valores iniciales y un seudocódigo sencillo para cada proceso con las operaciones wait y signal realizadas sobre ellos. Por ejemplo, si se tratara de sincronizar dos procesos C y V para que produjeran alternativamente consonantes y vocales, comenzando por una consonante, deberíais entregar algo parecido a esto:
         SEMÁFOROS Y VALOR INICIAL: SC=1, SV=0.
    
         SEUDOCÓDIGO:
    
                 C                                V
                ===                              ===
           Por_siempre_jamás               Por _siempre_jamás
              {                               {
               W(SC)                           W(SV)
               escribir_consonante             escribir_vocal
               S(SV)                           S(SC)
               }                               }
    


  5. Evaluación de la práctica.

    Dada la dificultad para la corrección de programación en paralelo, el criterio que se seguirá para la evaluación de la práctica será: si
    1. la práctica cumple las especificaciones de este enunciado y,
    2. la práctica no falla en ninguna de las ejecuciones a las que se somete y,
    3. no se descubre en la práctica ningún fallo de construcción que pudiera hacerla fallar, por muy remota que sea esa posibilidad...
    se aplicará el principio de "presunción de inocencia" y la práctica estará aprobada. La nota, a partir de ahí, dependerá de la simplicidad de las técnicas de sincronización usadas, la corrección en el tratamiento de errores, la cantidad y calidad del trabajo realizado, etc.

  6. LPEs.

    1. ¿Dónde poner un semáforo? Dondequiera que uséis la frase, "el proceso debe esperar hasta que..." es un buen candidato a que aparezca una operación wait sobre un semáforo. Tenéis que plantearos a continuación qué proceso hará signal sobre ese presunto semáforo y cuál será su valor inicial.
    2. Si ejecutáis la práctica en segundo plano (con ampersand (&)) es normal que al pulsar CTRL+C el programa no reaccione. El terminal sólo manda SIGINT a los procesos que estén en primer plano. Para probarlo, mandad el proceso a primer plano con fg % y pulsad entonces CTRL+C.
    3. Un "truco" para que sea menos penoso el tratamiento de errores consiste en dar valor inicial a los identificadores de los recursos IPC igual a -1. Por ejemplo, int semAforo=-1. En la manejadora de SIGINT, sólo si semAforo vale distinto de -1, elimináis el recurso con semctl. Esto es lógico: si vale -1 es porque no se ha creado todavía o porque al intentar crearlo la llamada al sistema devolvió error. En ambos casos, no hay que eliminar el recurso.
    4. Para evitar que todos los identificadores de recursos tengan que ser variables globales para que los vea la manejadora de SIGINT, podéis declarar una estructura que los contenga a todos y así sólo gastáis un identificador del espacio de nombres globales.
    5. A muchos os da el error "Interrupted System Call". Mirad la sesión quinta, apartado quinto. Allí se explica lo que pasa con wait. A vosotros os pasa con semop, pero es lo mismo. De las dos soluciones que propone el apartado, debéis usar la segunda.
    6. A muchos, la práctica os funciona exasperantemente lenta en encina. Debéis considerar que la máquina cuando la probáis está cargada, por lo que debe ir más lento que en casa.
    7. A aquellos que os dé "Bus error (Core dumped)" al dar valor inicial al semáforo, considerad que hay que usar la versión de semctl de Solaris (con union semun), como se explica en la sesión de semáforos y no la de HPUX.
    8. El número de coches máximo no está fijado. Sin embargo, ninguna de las pruebas que se hagan para evaluar la práctica pasará de veinte coches.
    9. Preguntáis acerca de dónde se hacen las comprobaciones de semáforos y el incremento de vueltas. Aquí lo tenéis:
      • Para el semáforo vertical, los puntos de comprobación son (CD,21) y (CI,23). Es decir, los coches se deben parar en el (CD,20) y (CI,22).
      • Para el semáforo horizontal, los puntos de comprobación son (CD,106) y (CI,99).
      • Los incrementos de vueltas se efectúan cuando un coche avanza hasta (CD,133) ó (CI,131). Cuidado con los cambios de carril, no contéis vueltas de más.
    10. Al acabar la práctica, con CTRL+C, al ir a borrar los recursos IPC, puede ser que os ponga "Invalid argument", pero, sin embargo, se borren bien. La razón de esto es que habéis registrado la manejadora de SIGINT para todos los procesos. Al pulsar CTRL+C, la señal la reciben todos, el padre y los coches. El primero que obtiene la CPU salta a su manejadora y borra los recursos. Cuando saltan los demás, intentan borrarlos, pero como ya están borrados, os da el error.
    11. Respecto al "pistoletazo" de salida, considerad que se trata de una sincronización de rendezvous o cita. Ningún coche se puede adelantar al pistoletazo y todos los coches tienen que estar iniciados cuando el pistoletazo se produzca. ¡A discurrir!
    12. El compilador de encina tiene un bug. El error típicamente os va a ocurrir cuando defináis una variable entera en memoria compartida para el contador. Os va a dar Bus Error. Core dumped si no definís el puntero con un desplazamiento que sea múltiplo de cuatro respecto al origen de la memoria compartida. La razón se escapa un poco al nivel de este curso y tiene que ver con el alineamiento de direcciones de memoria en las instrucciones de acceso de palabras en el procesador RISC de encina.


  7. Prácticas propuestas años anteriores.


© 2005 Guillermo González Talaván.