PRÁCTICAS DE SISTEMAS OPERATIVOS II

PRIMERA PRÁCTICA EVALUABLE

Los filósofos maquineros


  1. Enunciado.

    En un lejano país oriental la vida transcurre tranquila desde que terminó la guerra que dos potencias mundiales decidieron que se luchara allí para dilucidar sus diferencias y afán de dominio. Hay un templo perdido entre las montañas que sobrevivió milagrosamente al conflicto y un comedor donde se sirve esquisitos espagueti acompañados de una guarnición que varía con la época del año. Cruza nuestro escenario un río de aguas cristalinas, único obstáculo natural entre comedor y templo. El templo lo habitan varios filósofos, cuya vida transcurre plácida del modo siguiente1:
    Fruto de la guerra, quedaron esparcidas por el terreno algunas bombas fallidas, por lo que los filósofos deben seguir siempre el mismo camino si no quieren verse expuestos al peligro de que una de ellas les explote. La única excepción al caso es una explanada situada en el camino entre el comedor y el puente, cerca de este último. Allí tienen más libertad de movimiento. La naturaleza es exuberante en el paraje, como consecuencia quizá de la fortuna de que no existan en las cercanías recursos naturales de interés económico. Probablemente por esta misma causa, las infraestructuras son escasas y mal conservadas. En concreto, el puente:

    Captura de pantalla


    En esta práctica vamos a simular, mediante procesos de UNIX, el acontecer diario de los filósofos.

    El programa constará de un único fichero fuente, filosofar.c, cuya adecuada compilación producirá el ejecutable filosofar. 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 (libfilosofar.a) que debéis enlazar con vuestro módulo objeto para generar el ejecutable. Gracias a ella, algunas de las funciones necesarias para realizar la práctica 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 filosofar.c libfilosofar.a -o filosofar -lm
    Disponéis, además, de un fichero de cabeceras, filosofar.h, donde se encuentran definidas, entre otras cosas, las macros que usa la biblioteca.

    Cada filósofo vendrá representado por un proceso, siendo todos ellos hijos del mismo proceso padre resultado de arrancar el programa. El programa recibe tres argumentos: el número de filósofos que se debe crear (comprendido entre 1 y el valor de la macro MAXFILOSOFOS de filosofar.h), el número de vueltas que debe dar cada filósofo antes de morir y la lentitud de ejecución, un número mayor o igual que cero. Este último valor se ha de pasar a la biblioteca para que ella gestione la velocidad de la simulación.

    Los parámetros concretos de la simulación, como pueden ser el número de sitios de comedor y templo, capacidad máxima del puente, etc., son informados por la propia biblioteca al arrancar. Para la resolución de la práctica se pueden usar un único conjunto de semáforos, una única zona de memoria compartida y un único buzón. Mientras los dos primeros se necesitan crear aunque no se usen, pues los necesita la biblioteca, el tercero es opcional. El proceso padre debe realizar las siguientes acciones:
    Por su parte, los procesos filósofos deben:
    Una parte complicada del programa es que los filósofos anden, pues deben evitar chocarse con otro filósofo. El proceso consta de tres partes:
    1. Llamar a la función FI_pausaAndar. Esta llamada se debe hacer fuera de cualquier sección crítica u observaremos un enlentecimiento artificial de la práctica a baja velocidad que aumenta con el número de procesos, llegando incluso a bloquearse intermitentemente.
    2. Llamar a la función FI_puedoAndar. Esta función devuelve el valor 100 si la vía está libre. Si no lo está, devuelve el identificador del filósofo que está bloqueando nuestro camino.
    3. Si hay vía libre, llamar a la función FI_andar.
    Aparte de la incuestionable sincronización que debe existir en este proceso, se ofrece al programador dos posibilidades en el caso de que la función nos indique que el camino está bloqueado:

    Características adicionales que programar



    El programa debe estar preparado para que, si el usuario pulsa las teclas CTRL-C desde el terminal, la ejecución del programa termine en ese momento y adecuadamente. Ni en una terminación como esta, ni en una normal, deben quedar procesos en ejecución ni mecanismos IPC sin haber sido borrados del sistema. Este es un aspecto muy importante y se penalizará bastante si la práctica no lo cumple.

    Las funciones proporcionadas por la biblioteca libfilosofar.a son las que a continuación aparecen. De no indicarse nada, las funciones devuelven -1 en caso de error:

    Estad atentos pues pueden ir saliendo versiones nuevas de la biblioteca para corregir errores o dotarla de nuevas funciones.

    Respecto a la sincronización interna de la biblioteca, se usa un semáforo reservado para conseguir atomicidad en la actualización de la pantalla (semPantalla, iniciado a 1) y otro semáforo para hacer lo propio con los estados de la simulación (semTotal, iniciado también a 1).

    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, si hicieran falta, 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 solo 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 libfilosofar.a

    Con esta práctica se trata de que aprendáis a sincronizar y comunicar procesos en UNIX entre sí. Su objetivo no es la programación, aunque es inevitable que tengáis que programar. Es por ello que se os suministra una biblioteca estática de funciones ya programadas para tratar de que no debáis 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 libfilosofar.a y el fichero de cabecera filosofar.h. La biblioteca funciona con los códigos de VT100/xterm, por lo que debéis adecuar la emulación de vuestro 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. Hacer el código necesario para gestionar los argumentos que se le pasan al programa
    2. Crear los semáforos, la memoria comparida y el buzón (si es que vais a usar uno), y comprobad que se crean bien con ipcs. Es preferible, para que no haya interferencias, que los defináis privados.
    3. Registrar SIGINT para que cuando se pulse CTRL-C se eliminen los recursos IPC. Lograr que si el programa acaba normalmente o se produce cualquier error, también se eliminen los recursos (mandad una señal SIGINT en esos casos al proceso padre).
    4. Llamar a la función FI_inicio en main. Debe aparecer la pantalla de bienvenida y, pasados dos segundos, dibujarse la pantalla. Para la clave, usad el número proporcionado más arriba. Recoged los datos de simulación y mostradlos por el canal de errores. Incluid la plantilla del array de depuración y pasádselo a la función. Ejecutad la práctica, a partir de ahora, redirigiendo la salida de errores a un fichero para que no se os emborrone la pantalla.
    5. Los siguientes pasos conllevan resolver la práctica, según los parámetros que habéis recibido en los datos de la simulación.
    6. Que el padre cree ahora un proceso hijo y se quede esperando por su muerte. Este proceso no debe hacer, de momento, nada.
    7. Podéis ya dar vida a este proceso. Probad a que dé varios pasos. No será necesario que toméis precauciones si no puede avanzar, porque al estar solo, siempre podrá.
    8. Meted las órdenes para dar un paso en un bucle y considerad lo que devuelve FI_andar para ir resolviendo el ciclo de vida del filósofo. Como el filósofo está solo, no hace falta que os preocupéis por los condicionantes del puente, podéis elegir siempre el puesto 0 en el comedor y el templo, etc.
    9. Controlad que el filósofo dé las vueltas que pide en el enunciado y acabe sin dar problemas.
    10. Llega el momento crucial: cread dos filósofos. Si el segundo corre más que el primero, se tropezará con él, dando error. Resolved el problema según lo que hayáis decidido (semiespera ocupada o bloqueo del que espera).
    11. Id añadiendo sucesivamente más filósofos y resolviendo, uno a uno, los condicionantes del problema: que no entren en una antesala llena, que esperen a que haya un sitio en comedor o templo, que no pasen al puente si viene alguien en la otra dirección, etc.
    12. Una vez penséis que están resueltos todos los problemas, probad la simulación a lentitud 0. Si aparecen nuevos problemas, solucionadlos. Si no, haced vuestro esquema de sincronización y analizadlo desde el punto de vista teórico.
    13. Pulid los últimos detalles.
    14. Llevar la práctica al servidor y, en caso de detectarse errores, corregidlos.


  3. Plazo de presentación.

    Consultad la página de entrada de la asignatura.

  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, los buzones, y mesajes pasados y un seudocódigo sencillo para cada proceso con las operaciones wait y signal, send y receive 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)
               }                               }
    
    Daos cuenta de que lo que importa en el pseudocódigo es la sincronización. El resto puede ir muy esquemático. Un buen esquema os facilitará muchísimo la defensa.

  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, si se ha optado por semiespera ocupada, etc.

  6. LPEs.

    1. ¿Se puede usar la biblioteca en un Linux de 32 bits? No. Esta práctica está compilada para 64 bits.
    2. ¿Se puede proporcionar la biblioteca para el Sistema Operativo X, procesador Y? Por problemas de eficiencia en la gestión y mantenimiento del código no se proporcionará la biblioteca más que para Solaris-SPARC y Linux de 64 bits (con procesador de Intel o AMD). A veces podéis lograr encontrar una solución mediante el uso de máquinas virtuales.
    3. ¿Dónde poner un semáforo? Dondequiera que uséis la frase, "el proceso puede llegar a 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, dónde lo hará y cuál será su valor inicial.
    4. 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.
    5. 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.
    6. 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.
    7. A muchos os da el error "Interrupted System Call". Mirad la sesión dedicada a las señales, 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.
    8. 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 o en el linux de clase.
    9. 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.
    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 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.
    11. 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.
    12. 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,"%d:...",getpid(),...);
      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

1 Esta descripción debería leerse con música oriental de fondo y un par de gongs al principio y al final, pero en la universidad pública no nos alcanza el presupuesto.
2 Aunque corre el rumor dentro de la congregación de que puede que no sea la humedad sino un monje un punto anarcotecnologicista que, para satisfacer sus deseos, hace con un destornillador lo que la humedad no logra. El presunto autor tampoco conoce muy bien lo que es ser anarcotecnologicista y ni siquiera si la palabra existe, pero ese nombre le mola. Muchos coinciden en que muy bien de la cabeza no anda pues le han visto en las noches de luna llena hablar con su gato, lo que podría tolerarse si no fuera porque lleva el animal largo tiempo fallecido.
© 2024 Guillermo González Talaván. Esta página está libre de tecnología de Google o Apple.