Jerarquía de procesos.
Como ya se indicó con anterioridad un proceso puede crear otros procesos. De igual forma los nuevos pueden crear otros y así sucesivamente.
Para representar gráficamente este proceso de creación sucesiva de procesos se utiliza el llamado grafo de procesos que no es más que un árbol dirigido con raíz . Un arco del nodo Pi al nodo Pj. En este caso se dice que Pi es el padre de Pj o que Pj es hijo de Pi. Cada proceso tiene un solo padre, pero tantos hijos como sean necesarios.
Existen diversas formas en que un proceso puede crear uno al hacer uso de una operación con este objetivo (como la fork). Para ello se debe tener en cuenta como continúa la ejecución y como se compartirán los recursos.
Desde el punto de vista de como continúa la ejecución se pueden instrumentar dos casos:
- ? El padre continúa la ejecución concurrentemente con el hijo (construcción fork/join).
- ? El padre espera hasta que el hijo termina (construcción parbegin/parend).
Desde el punto de vista de cómo se compartirán los recursos se pueden instrumentar también dos casos:
- ? El padre y el hijo comparten todas las variables (construcción forK/join).
- ? El hijo solo tiene acceso a un subconjunto de las variables del padre (esquema del Unix).
Un proceso termina cuando ejecuta su última instrucción, sin embargo existen circunstancias en que se requiere terminarlo en forma forzada. Un proceso termina a otro mediante una instrucción del tipo Kill Id.
La operación kill es usualmente invocada solamente por un proceso padre para culminar la ejecución de un hijo. Como la instrucción requiere la identificación del proceso a ser terminado, la instrucción fork da como retorno esta información (Id = fork L). Existen numerosas razones por las cuales un padre puede detener la ejecución de un hijo.
En muchos sistemas operativos se establece la condición de que los procesos hijos no pueden continuar la ejecución si el padre ha sido finalizado.
Un proceso que no termina su ejecución durante todo el tiempo en que el sistema operativo está funcionando se dice que es estático. Un proceso que finaliza se dice que es dinámico.
Si un sistema operativo consiste de solo un número limitado de procesos estáticos entonces su correspondiente grafo de procesos será también estático (es decir nunca cambia). En caso diferente será dinámico. La estructura estática solo está presente en sistemas operativos muy simples.
La jerarquía de procesos antes discutida está presente en la mayoría de los sistemas operativos que soportan el modelo de procesos.
Para terminar veamos un ejemplo simple consistente de dos procesos secuenciales que se ejecutan concurrentemente y que comparten algunos datos en común. Este es un ejemplo representativo de los sistemas de operación.
El ejemplo consiste en el clásico problema del productor/consumidor. Un proceso productor genera información que es tomada por el proceso consumidor. Por ejemplo, un manejador de una impresora produce caracteres que son consumidos por la impresoras.
Para permitir a los procesos productor y consumidor ejecutarse concurrentemente se creará una piscina de buffers. El productor generará en un buffer, mientas que el consumidor tomará de otro buffer (si lo hicieran en el mismo tendrían que correr secuencialmente).
El productor y el consumidor deberán ser coordinados de forma tal que el consumidor no trate de consumir elementos que aún no han sido producidos. En este caso, el consumidor deberá esperar hasta que el elemento es producido.
El problema que estamos tratando tiene dos formas de presentarse: el primer caso consiste en fijar un límite (diga menos n) en la cantidad de buffers disponibles en la piscina, en segundo no se establece esta restricción y por ello el productor siempre podrá generar nuevos elementos (siempre habrá buffers vacíos).
La solución que se planteará estará referida al primer caso y por ello el productor deberá esperar cuando todos los buffers estén llenos.
La piscina de buffers compartida por ambos procesos se instrumentará como un arreglo circular con dos punteros lógicos: in y out. La variable in apunta al próximo buffer libre, mientras out apunta al primer buffer lleno.
Para controlar las condiciones extremas de todos los buffers vacíos o llenos se podrían utilizar estas dos posibilidades (in = out y in + 1 % n = out, respectivamente), pero en este caso solo se permite que se puedan llenar hasta n - 1 buffers (se obliga a que exista uno vacío).
Para remediar esta dificultad se incorpora en la solución un contador que originalmente recibe valor 0. Esta variable se incrementará cada vez que se llene un buffer de la piscina y decrementando cada vez que se consume uno lleno de información.
/* Ejemplo de productor consumidor */
#define n 100
#define TRUE 1
typedef …item;
item buffer[n];
short int counter = 0, in = 0, out = 0;
void producir_elemento(punt_nextp)
item *punt_nextp;
{
.
.
.
}
void concumir_elemento(nextc)
item nextc;
{
.
.
.
}
void productor()
{
item nextp;
while (TRUE){
producir_elemento(&nextp);
while (counter= =n) skip;
buffer [in] = nextp;
in = (in+1) % n;
counter++;
}
}
void consumidor()
{
item nextc;
while (TRUE) {
while (counter = = 0) skip;
nextc = buffer[out];
out = (out+1) % n;
counter--;
consumir_elemento(nextc);
}
}
main()
{
parbegin
productor();
condumidor();
parend;
}
La instrucción skip no hace nada y por ello el while se repite. Nótese que esta espera implica que se continúe ejecutando aún cuando no se está haciendo nada útil (espera ocupada).
La incorporación de las instrucciones parbegin y parend se hacen con el objetivo de brindar la opción de concurrencia.
No hay comentarios.:
Publicar un comentario