PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

PRIMERA PRÁCTICA OBLIGATORIA (2003-04)

The Lord of the Rings


  1. Enunciado.

    El programa que hay que presentar constará de un único fichero fuente de nombre Tolkien.c. La correcta compilación de dicho programa, producirá un fichero ejecutable, cuyo nombre será obligatoriamente Tolkien. Respetad las mayúsculas/minúsculas de los nombres.

    La ejecución del programa creará una serie de procesos cuya vida está inspirada en los personajes de "El señor de los anillos". Los personajes que aparecerán se describen a continuación:



    El mecanismo de procreación, tanto para elfos como para hobbits, consistirá en los siguientes pasos:
    1. La parte femenina de la pareja mostrará su disponibilidad para la procreación enviando la señal SIGUSR1 a su pareja.
    2. La parte masculina, una vez recibida la señal, responde a su pareja enviando la señal SIGUSR1.
    3. La parte femenina decide cuántas veces se ha de repetir los dos pasos anteriores al azar (entre 2 y 11, úsese la función rand()).
    4. Una vez satisfecha, y como no necesita más de la parte masculina, le envía la señal SIGTERM. Su pareja muere, por consiguiente.
    5. La parte femenina, finalmente, tiene un proceso hijo.

    La única diferencia en el proceso de procreación de hobbit y elfos está en que los elfos lo realizan sin consumir CPU mientras que los hobbits (al igual que los humanos, por cierto) sí que la consumen y realizan espera ocupada.

    Los procesos tienen que aparecer bajo la orden ps -f con su nombre. Esto significa que será necesario usar una llamada de la familia exec para reemplazar argv[0], aunque vayamos a ejecutar el mismo ejecutable. Esto os servirá también para que vuestro código diferencie qué personaje se está ejecutando en cada momento. Se deja a vuestra libertad el añadir más argvs, si lo consideráis necesario.

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

    Para generar números aleatorios comprendidos entre un valor bajo y otro alto, ambos inclusive, podéis usar la siguiente función:
    #include <stdlib.h>
    #include <sys/types.h>
    #include <time.h>
    [...]
    int azar(int bajo,int alto)
       {
        static int primeraVez=(0==0);
        if (primeraVez) {srand(time(NULL)); primeraVez=!primeraVez;}
        return rand()/(RAND_MAX+1.0)*(1+alto-bajo)+bajo;
        }
  2. Plazo de presentación.

    Hasta el miércoles 21 de abril, inclusive.

  3. Normas de presentación.

    Acá están.

  4. LPEs.

    1. Las tareas que tiene que realizar el programa son variadas. Os recomiendo que vayáis programándolas y comprobándolas una a una. No es muy productivo hacer todo el programa de seguido y corregir los errores al final. El esquema que os recomiendo seguir para facilitaros la labor es del tipo:
      1. Construid un primer programa que tenga un hijo. Dejad a padre e hijo en pause(). Compilad, ejecutad, depurad y matad a los procesos.
      2. Haced que el hijo ejecute un exec que vuelva a ejecutar el mismo programa, pero que en argv[0] ponga "Deagol". A la entrada del programa, comprobad argv[0]. Si es "Tolkien", hacéis lo que hacíais en el punto anterior. Si es "Deagol", lo dejáis en pause(). Compilad, ejecutad en segundo plano, depurad. Mirad con ps -f que hay dos procesos, padre e hijo, que el hijo se llama "Deagol" y el padre, "Tolkien". Matad a los procesos.
      3. Cread del mismo modo a Smeagol y dejadlo bloqueado, sin consumir CPU. Os recomiendo hacer una función para cada uno de los personajes.
      4. Haced que Smeagol registre SIGALRM para que, cuando reciba dicha señal, pueda matar a su hermano Deagol. Necesitará para ello el PID de Deagol. Pensad en cómo hacerlo. No debéis usar ficheros para ello. Para probarlo, enviad la señal SIGALRM a Smeagol desde la línea de órdenes y ved que Deagol desaparece.
      5. Una vez hemos matado a Deagol, haced que Semeagol pase a llamarse Gollum.
      6. Creamos a Drogo y Primula y pasamos el pid de Drogo a Primula. Los dejamos, en principio, bloqueados.
      7. Proceso de procreación de Drogo y Primula. Para ello, Primula tiene el PID de Drogo, pero Drogo necesita el PID de Primula. Se os permite usar un fichero, si no encontráis mejor solución. En el caso de usar un fichero, debéis garantizar que Drogo no lea del fichero antes de que se haya escrito en él el PID de Primula. Podéis usar para ello la llamada al sistema lockf.
      Podéis continuar la práctica siguiendo un esquema similar al expuesto. Puede ser necesario que alteréis el orden de creación de los personajes que os he indicado en estos puntos.
    2. Algunos tenéis problemas porque Primula mata a Drogo antes incluso de comenzar a procrear. Esto ocurre porque Primula en más rápida y envía la señal SIGUSR1 a Drogo antes de que Drogo pueda siquiera hacer su sigaction para registrarla. Como no está registrada, su comportamiento por defecto es matar al proceso. La solución está en bloquear la señal con sigprocmask para que Drogo no la reciba hasta que no haya registrado su manejadora. La solución, no obstante, no consiste en que Drogo bloquee la señal, pues estaríamos en las mismas, sino que sea Tolkien quien lo haga por él y él herede el bloqueo.
    3. Para matar los procesos después de cada ejecución, hay una manera muy cómoda. Si habéis usado "Tolkien &" para ejecutar la práctica, podéis usar "kill %" para matar a todos los procesos de golpe.
    4. Surge un problema con el PID de Frodo. Lo necesitan Gollum y Arwen. Podéis escribirlo en un fichero que se llame pidFrodo. El problema es el de siempre: quien quiera leer de ese fichero debe hacerlo después de que el PID haya sido escrito. La solución que os propongo (no tiene por qué ser la única) consiste en que Tolkien crea primero a Drogo y a Primula. Cuando Primula crea por fin a Frodo, escribe su PID en el fichero y envía la señal SIGALRM a Tolkien. Tolkien se ha quedado esperando, sin consumir CPU, a recibir la señal y, sólo después de recibirla, crea al resto de personajes. Para que Tolkien se quede esperando por la señal sin consumir CPU, podéis usar sigsuspend.
    5. Si hacemos la modificación del LPE anterior, es decir, si Smeagol/Gollum nace después de Primula, Frodo no puede recibir el PID de Gollum para devolverle el anillo. Podéis usar cualquier esquema de los vistos para pasar ese PID, incluyendo la generación de un fichero pidGollum para que Frodo lo pueda leer. La trampa se esconde en este caso en un posible interbloqueo que os sucederá si no sois especialmente cuidadosos.
    6. No se puede usar sleep() o similares para sincronizar los procesos. Hay que usar otros mecanismos.
    7. Sabéis que si usáis espera ocupada en lugares donde explícitamente no se haya dicho que se puede usar, la práctica está suspensa. No obstante, existe existe una variedad de espera ocupada que podríamos denominar espera semiocupada. Consiste en introducir una espera de algún segundo en cada iteración del bucle de espera ocupada. Con esto el proceso no consume tanta CPU como en la espera ocupada, pero persisten los demás problemas de la técnica del sondeo, en particular el relativo a la elección del periodo de espera. Aunque la práctica no estará suspensa si hacéis espera semiocupada, se penalizará en la nota bastante si la usáis. En conveniente que la evitéis.
    8. Evitad, en lo posible, el uso de variables globales. Tenéis la posibilidad de declarar variables estáticas.
    9. Algunos tenéis problemas al devolver el número de años de Arwen, aunque no al devolver el número de años de Frodo. La razón es que el rango de valores que un proceso puede devolver a su padre es de 0 a 255, esto es, un byte. Para Frodo, nos da. Para Arwen, que vive de 601 a 730, no. Pero si aguzáis un poco el ingenio podéis apañároslas.
    10. Cuando Drogo y Primula tienen que procrear, han de hacerlo consumiendo CPU. La manera más sencilla consiste en usar una variable para indicar si ha llegado la señal de procreación del compañero. Por ejemplo, en Drogo, el código puede quedar así:
      manejadora:
          he_recibido_SIGUSR1=Verdadero;
      fuera de la manejadora:
          he_recibido_SIGUSR1=Falso;
          [...]
          Para siempre_jamás
             {
              while (!he_recibido_SIGUSR1) /* No hago nada */;
              he_recibido_SIGUSR1=Falso;
              kill(pidPrimula,SIGUSR1);
              }
      
      Observad que el orden de la asignación de la variable y el kill tiene que ser el que os he puesto. Si no lo hacéis así, no funcionará. Si lo hacéis al revés, puede ocurrir que Drogo reciba la señal de Primula, vaya a la manejadora, ponga la variable a Verdadero, vuelva de la manejadora, salga del while y envíe la señal a Primula de vuelta. Si, en ese momento le quitan la CPU, puede que Primula le dé tiempo a responder antes de que Drogo haya puesto la variable he_recibido_SIGUSR1 a falso. En tal caso, Drogo salta a la manejadora, pone la variable a verdadero, vuelve, pone la variable a falso y cae en el while atrapado sin remedio, pues Primula ya le mandó SIGUSR1 y no lo va a volver a hacer.
    11. Algunos me comentáis que Primula y Celebrian se "sincronizan" para procrear, esto es, que aunque cada vez que ejecutáis deciden mandar un número diferente de señales, siempre las dos mandan las mismas. No hay problema por esto. Es simplemente que, al nacer tan cercanas en el tiempo, inician su generador de números aleatorios al mismo valor. Si queréis que no pase esto, buscad algún truco, como por ejemplo cambiar esta línea en la función azar:
      if (primeraVez) {srand(4*getpid()+time(NULL)); primeraVez=!primeraVez;}
                          
    12. ¿Qué hago cuando mi programa se desboca para no perjudicar el funcionamiento de la máquina?
      Solución.
    13. El programa que he hecho se para a veces. O en mi casa se para, pero en clase, no. O en clase sí, pero en casa, no.
      Solución.


  5. Prácticas propuestas en años anteriores.


© 2003 Guillermo González Talaván.