PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

SEGUNDA PRÁCTICA EVALUABLE (2010-11)

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:
    gcc 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 jueves 28 de abril de 2011, inclusive e improrrogable.

  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. 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 otros procesos. 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.
    9. El compilador de encina tiene un bug. El error típicamente os va a ocurrir cuando defináis una variable entera en memoria compartida. Os va a dar Bus Error. Core dumped si no definís el puntero a esa variable apuntando a una dirección que sea múltiplo de cuatro. El puntero que os devuelve shmat, no obstante, siempre será una dirección múltiplo de cuatro, por lo que solo os tenéis que preocupar con que la dirección 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.
    10. Se os recuerda que, si ponéis señales para sincronizar esta práctica, la nota bajará. Usad semáforos, que son mejores para este cometido.
    11. Todos vosotros, tarde o temprano, os encontráis con un error que no tiene explicación: un proceso que desaparece, un semáforo que parece no funcionar, etc. La actitud en este caso no es tratar de justificar la imposibilidad del error. Así no lo encontraréis. Tenéis que ser muy sistemáticos. Hay un árbol entero de posibilidades de error y no tenéis que descartar ninguna de antemano, sino ir podando ese árbol. Tenéis que encontrar a los procesos responsables y tratar de localizar la línea donde se produce el error. Si el error es "Segmentation fault. Core dumped", la línea os la dará si aplicáis lo que aparece en la sección Manejo del depurador. En cualquier otro caso, no os quedará más remedio que depurar mediante órdenes de impresión dentro del código.

      Para ello, insertad líneas del tipo:
                           fprintf(stderr,"...",...);
      donde sospechéis que hay problemas. En esas líneas identificad siempre al proceso que imprime el mensaje. Comprobad todas las hipótesis, hasta las más evidentes. Cuando ejecutéis la práctica, redirigid el canal de errores a un fichero con 2>salida.

      Si cada proceso pone un identificador de tipo "P1", "P2", etc. en sus mensajes, podéis quedaros con las líneas que contienen esos caracteres con:
                           grep "P1" salida > salida2
    12. Os puede dar un error que diga Resource temporarily unavailable en el fork del padre. Esto ocurre cuando no exorcizáis adecuadamente a los procesos hijos zombies del padre. Hay dos posibilidades para solucionarlo:
      1. La más sencilla es hacer que el padre ignore la señal SIGCLD con un sigaction y SIG_IGN. El S.O. tradicionalmente interpreta esto como que no queréis que los hijos se queden zombies, por lo que no tenéis que hacer waits sobre ninguno de ellos para que acaben de morir
      2. Interceptar SIGCLD con una manejadora en el padre y, dentro de ella, hacer los waits que sean necesarios para que los hijos mueran. Pero esto trae un segundo problema algo sutil: al recibirse la señal, todos los procesos bloqueados en cualquier llamada al sistema bloqueante (en particular, los WAITs de los semáforos) van a fallar. Si no habéis puesto comprobación de errores, los semáforos os fallarán sin motivo aparente. Si la habéis puesto, os pondrá Interrupted system call en el perror. Como podéis suponer, eso no es un error y debéis interceptarlo para que no ponga el perror y reintente el WAIT. La clave está en la variable errno que valdrá EINTR en esos casos.
    13. No se debe dormir (es decir, ejecutar sleeps o pausas) dentro de una sección crítica. El efecto que se nota es que, aunque la práctica no falla, parece como si solamente un proceso se moviera o apareciera en la pantalla a la vez. Siendo más precisos, si dormís dentro de la sección crítica, y soltáis el semáforo para, acto seguido, volverlo a coger, dais muy pocas posibilidades al resto de procesos de que puedan acceder.
    14. La biblioteca no reintenta los WAITs de su semáforo, por lo que, de recibirse una señal, podría fallar. Si os da problemas, simplemente ignorad la señal SIGCLD en el padre como se explica más arriba.
    15. 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.
    16. 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.
    17. 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!

  7. Prácticas propuestas años anteriores.


© 2005 Guillermo González Talaván.