PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS
PRIMERA PRÁCTICA EVALUABLE (2006-07)
Bolos
Enunciado.
El programa que hay que presentar constará de un único
fichero fuente de nombre bolos.c
. La
correcta compilación de dicho programa, producirá un
fichero ejecutable, cuyo nombre será obligatoriamente
bolos
. Respetad las mayúsculas/minúsculas
de los nombres, si las hubiere.
La ejecución del programa creará una serie de procesos
que se transmitirán señales entre ellos.
En concreto, el árbol de procesos que hay que
crear es el siguiente:
Para comprender mejor el funcionamiento de la práctica,
cambiemos la posición de los procesos en el dibujo de manera
que formen una figura semejante a la posición de partida
de los bolos en una bolera
Las
relaciones paterno-filiales son las indicadas por las líneas
sólidas.
Una vez construido el arbol, el proceso padre muere y los
demás permanecen vivos, bloqueados y sin consumir CPU.
En este momento, habremos recuperado el control desde nuestra
shell y enviaremos al proceso A una señal
SIGTERM
.
La señal se va a ir transmitiendo a otros procesos
según el esquema que se indicará más abajo, volcando a su paso
otros bolos.
Transcurridos tres segundos, el proceso A comprobará cuántos
procesos siguen vivos, imprimirá un dibujo con la situación
por la pantalla, ejecutará una orden
ps -fu usuario
que sirva de comprobación y
acabará, no dejando ningún proceso vivo.
En el siguiente dibujo, se observa cómo se propagan las
señales:
Si un proceso recibe la señal SIGTERM
y no es
un proceso del final (no es ni G, ni H, ni
I, ni J) puede hacer una de estas cuatro cosas:
- No enviar la señal a ningún proceso.
- Enviar la señal al proceso situado debajo a
la izquierda.
- Enviar la señal al proceso situado debajo a la derecha.
- Enviar la señal a ambos procesos.
El que realice una u otra acción va a depender del reloj
del sistema. Consultará los microsegundos marcados por el
reloj del sistema mediante una llamada gettimeofday
.
Dividirá el valor entre cuatro y obtendrá el resto. Dependiendo
del valor del resto de la división (0, 1, 2 ó 3) se realizará la
acción correspondiente de la lista anterior.
Para que el proceso A sepa si siguen vivos los bolos
E, H o I, podéis usar la llamada al
sistema waitpid
en su versión no bloqueante.
Para saber qué ha ocurrido con
los procesos de la ristra B-D-G, se
procederá así:
- Si A no mandó la señal a B, sabe que los
tres están en pie.
- Si A mandó la señal a B, hará un
wait
bloqueante y B le dirá en su código de retorno
cuántos bolos de la ristra B-D-G
siguen en pie.
Para que B sepa cuántos bolos quedan en pie se hace
lo mismo, pero ahora el papel de A lo ocupa B
y el de B lo ocupa D.
En el caso de la ristra C-F-J, se procede
de modo equivalente.
Ejemplo: en el caso de la ristra
B-D-G, supongamos que A mató a B
y B mató a D, pero D no mató a G.
En ese caso, D devuelve a B
un uno, indicando que hay un bolo en pie y B devuelve a
A
otro uno. A sabe que queda un bolo en pie de la ristra,
por lo que tiene que ser G a la fuerza.
Los procesos, una vez creados, deben permanecer bloqueados,
sin consumir CPU mientras esperan a recibir la señal y enviarla
a otros procesos, si procede.
Si se requiere sincronizar alguna acción, se pueden usar
señales, pero la espera se debe realizar sin
consumo de CPU. Para simplificar el problema, podéis
suponer que al ser mandada la señal inicial manualmente,
ya ha habido tiempo suficiente para que todos los procesos
hayan sido creados para cuando el primer proceso la reciba.
Sin embargo, esta concesión en aras de simplificación, no
implica suponer ninguna condición, incluida ésta, en ninguna
otra parte de la práctica.
Para ver si habéis conseguido crear el árbol de procesos
correctamente, al menos en la primera fase, podéis descargaros
el siguiente ejecutable:
Arbol_dominO. No os va a
funcionar en Linux, pues es un ejecutable binario y los
procesadores son diferentes.
Desde el servidor, para
ver el árbol de procesos que habéis generado, usad la
orden: ps -f | grep "bolos$" | grep -v grep | sort |
Arbol_dominO
. Si os aparece el mensaje
"Árbol con múltiples raíces. Se aborta.
",
significa que habéis corrido varias veces vuestra práctica
sin matar los procesos de la anterior.
En el caso de Linux, podéis usar la opción -f
de la
orden ps
.
Restricciones
- Se deberán usar llamadas al sistema siempre que sea
posible, a no ser que se especifique lo contrario.
- No está permitido usar la función de biblioteca
system
, salvo indicación explícita en
el enunciado de la práctica.
- Se ha de lograr que al hacer un
ps
aparezca por la pantalla en lugar de bolos
,
el nombre de cada proceso: A, B, C, etc.
- No se puede suponer que los PIDs de los procesos
de una ristra van a aparecer consecutivos. Puestos
en plan exquisito, ni siquiera podemos suponer que
estarán ordenados de menor a mayor (puede ocurrir
que se agoten los PIDs y retome la cuenta partiendo
de cero).
- No está permitido el uso de ficheros, tuberías u
otro mecanismo externo para transmitir información
entre los procesos.
Plazo de presentación.
Hasta el viernes, 23 de marzo de 2007, inclusive.
Normas de presentación.
Acá están.
LPEs.
- 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 se os muestra a
continuación:
- Haced un pequeño programa que cree los procesos
P y A. Compiladlo, ejecutadlo,
comprobadlo con
Arbol_dominO
y
depuradlo, si fuera necesario.
- Lograd que P muera y A se quede solo.
- Cread ahora los procesos
B, H, E, I y C.
Alternativamente, podéis intentar generar
los procesos mediante un bucle, sin hacerlo
en el código explícitamente.
- Acabad generando las dos ristras B-D-G y
C-F-J, por el mismo procecimiento.
- Una vez el árbol esté bien generado, comprobad
que los procesos no consumen CPU con
top
.
- Se debe lograr que los procesos recién creados
cambien el nombre que aparece al hacer
ps
. Para ello, un buen truco
consiste en que ejecuten un exec
que conduzca a su propio programa otra vez
pero en el que hacemos que
argv[0]
pase a ser
el nombre deseado. Al entrar por el
main
, miramos el valor de
argv[0]
y sabemos que si vale
bolos
es la entrada inicial
y si vale A
es la segunda entrada
del proceso A y lo podemos dirigir a una
función propia.
- Ahora debéis conseguir que A no se muera cuando
le mandéis la señan
SIGTERM
sino
que imprima algo por la pantalla, por ejemplo.
- Llegamos a la parte más difícil. Para empezar,
debemos hacer que los procesos propaguen la
señal
SIGTERM
. Es mejor que al
principio la propagen a los dos que tienen
debajo siempre. Es probable que os encontréis
con problemas para que un proceso consiga los
PIDs de los procesos que tiene que matar.
Os recomiendo que antes de matar, impriman lo
que van a hacer y el PID de quien van a matar
por la pantalla. Es probable que llegados a
este punto tengáis que cambiar el orden en que
se crean los procesos...
- Una vez todos matan a los que tienen que matar,
introducid la condición del
gettimeofday
.
- Haced que A espere tres segundos. Podéis usar
la función de biblioteca
sleep
.
A continuación que mire si E, H e I siguen
vivos y lo ponga por la pantalla.
- Nos ocupamos ahora de la ristra B-D-G.
Si A mató a B, que haga un wait y pida el
código de retorno.
Si B mató a D que
coja su código de retorno y se lo trasmita
a A. Si no, devuelve a A un 2. Si D mató a
G, hace un wait y devuelve a B un cero. Si no
es así, devuelve un uno. A imprime con esta
información cuántos están vivos.
- Hacer lo mismo con la ristra C-F-J.
- Borrad la información de depuración y que A
imprima de forma bonita qué bolos quedan vivos.
- A finaliza ejecutando el ps para que podamos
comprobar que todo está bien.
- No se puede usar
sleep()
o
similares para sincronizar los procesos. Hay que
usar otros mecanismos.
- 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.
- Evitad, en lo posible, el uso de variables globales.
Tenéis la posibilidad de declarar variables
estáticas.
- Tened cuidado con el uso de
pause()
.
Salvo en bucles infinitos de pause
s,
su uso puede estar mal. Mirad la solución a la práctica
propuesta en la sesión quinta acerca de él o el
siguiente LPE.
- Es conveniente que, antes de entregar la práctica,
comprobéis que los
kill
s están enviando la
señal a los procesos correctos. Si al imprimirlo resulta
que a veces mandan la señal al proceso "0", es que está
mal. Lo que ocurre es que el proceso que realiza el
kill
recibe la señal y
salta a la manejadora antes de actualizar la
variable que contiene el pid de su hijo. Por eso la
variable tiene 0. La solución consiste en que el
patriarca bloquee la señal y cada proceso la
desbloquee justo antes de quedarse parado y después de
haber hecho todas sus tareas.
- 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.
- ¿Qué hago cuando mi programa se desboca
para no perjudicar el funcionamiento de la
máquina?
Solución.
- Habréis observado, como no puede ser de otro modo,
que al ejecutar un
exec
las variables
de un proceso se borran. Es natural pues el
proceso vuelve a cargar el programa y comienza de
cero con la función main
. Así es muy
difícil pasar un pid a otro proceso que lo necesite.
Una solución es usar argumentos adicionales en la
llamada a exec
de modo que se reciba
la información en argv[1]
,
argv[2]
, ...
- Para resolver el problema de que el proceso A tiene,
al final, que ejecutar un
ps
y matar
a todos los procesos, podéis hacer que A tenga un
hijo que ejecute el ps
al hacer un
exec
. El proceso A esperará
a que el hijo acabe con un wait
y
matará a todos los procesos que queden vivos.
- Para que A al final pueda matar a todos los procesos
que queden vivos, podéis intentarlo mandando una
señal
SIGINT
en un kill desde A, indicando
como PID un 0. Comentadme si es efectivo.
- Es evidente que si A tiene que hacer un
ps
o mandar hacerlo, va a aparecer en la lista como
vivo, aunque represente a un bolo muerto. No os
preocupéis por este detalle. Solucionarlo complicaría
la práctica innecesariamente.
Prácticas propuestas en años anteriores.
© 2007 Guillermo González Talaván y
Ana Belén Gil González.