Sistemas Operativos Practica 1: procesos y concurrencia.

Anuncio
Sistemas Operativos
Practica 1: procesos y concurrencia.
Objetivos:


Introducir al alumno a los conceptos de programa, concurrencia, paralelismo y proceso o
tarea.
Manejo del concepto de concurrencia haciendo uso de la función fork.
Introducción
La programación consiste en tomar un algoritmo y traducirlo a un conjunto de sentencias en un
determinado lenguaje de programación. Estas sentencias son convertidas a instrucciones por un
compilador, y esto se denomina programa. Posteriormente, al ejecutar el programa cada instrucción
es efectuada de manera secuencial en una computadora; a la ejecución del programa se le conoce
como proceso o tarea. Sin embargo es posible ejecutar más un proceso o tarea en una computadora,
lo cual se conoce como concurrencia. La concurrencia sea asocia a un conjunto de procesos los cuales
son ejecutados en paralelismo real (simultáneamente) o bien en paralelismo abstracto. De manera
sencilla, la concurrencia es el entrelazamiento de las instrucciones de dos o más programas en
ejecución.
En esta práctica estudiaremos el concepto de concurrencia haciendo uso de la función fork. Primero
veremos como trabaja dicha función, posteriormente mostraremos algunos ejemplos usando fork y
finalmente terminamos con unos ejercicios.
Creación de procesos con fork
Los procesos de un sistema Linux (y en general en los sistemas basados en UNIX), tienen una
estructura jerárquica en donde un proceso (padre) puede crear un nuevo proceso (hijo), y así
sucesivamente (un proceso padre puede tener muchos hijos, pero un hijo un solo padre), lo cual
forma una estructura de tipo árbol, en donde la raíz es el nodo padre principal (en un ambiente Linux,
el nodo raíz será el shell desde el cual se ejecuta el proceso).
En ocasiones es adecuado “partir” un proceso en dos o más procesos (o subprocesos o tareas), tal
vez con la finalidad de acelerar su ejecución. Para el desarrollo de sistemas con varios procesos, el
sistema operativo Linux proporciona la función fork1.
La ejecución de la instrucción fork crea procesos hijos que son una copia fiel del padre después del
punto de ejecución de fork; y a partir de ese punto, aun cuando ambos procesos posean el mismo
código, posiblemente no contendrán los mismos datos, ello depende de la manera en que se haya
codificado el programa. Por ejemplo, consideremos el siguiente código escrito en lenguaje C:
10: int a, b, c, Id;
. . .
30: a = 120;
31: b = 200;
1 Estrictamente hablando, no es como tal una función, el nombre correcto es llamada al sistema, solo que vista desde el
lenguaje C es una función. Es una llamada al sistema debido a que es código que está dentro del sistema operativo, y
hacemos uso de fork al invocarla con la correspondiente función en lenguaje C.
32: c = a;
33: Id = fork ();
34: a = Id * 100; /*Sin fork, tenemos un solo proceso y a = ?*/
. . .
Justo antes de la ejecución de la línea 33 y después de la ejecución de la línea 32, tenemos un solo
proceso en donde el valor de las variables a, b, y c es: 120, 200 y 120 respectivamente. Después de la
ejecución de la línea 33, pero antes de la ejecución de la línea 34, se tiene ahora dos procesos
exactamente iguales: un proceso padre y un proceso hijo. En ambos procesos las variables a, b y c
tendrán los valores de 120, 200 y 120 respectivamente. ¿Qué sucede al ejecutar la línea 34? ¿Cuánto
vale la variable Id?
Llamar a la instrucción fork crea un segundo proceso (hijo) con el mismo código, solo que el proceso
padre recibirá en la variable Id el identificador del proceso hijo, mientras que el hijo recibirá el valor
de cero en Id. Debido a lo anterior, después de la ejecución de la línea 34, la variable a será igual a
cero en el proceso hijo, mientras que en el padre la variable a tendrá un valor diferente a cero.
Llamar a la función fork requiere del archivo de cabecera unistd.h, el cual tiene la declarado la
función como sigue: int fork(void). Un ejemplo más completo es el siguiente:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main ()
{
int iId;
iId = fork ();
/*A partir de la
switch (iId)
{
case 0:
printf ("Soy
break;
case -1:
printf ("Hay
break;
default:
printf ("Soy
break;
}
return (0);
}
siguiente línea, padre e hijo inician su ejecución*/
el hijo.\n");
un error.\n");
el padre.\n");
Como se menciono, tanto padre como hijo son exactamente iguales, tienen el mismo contenido en
las variables pero no las comparten; es decir, una vez creados, siguen su ejecución de manera
independiente, y lo que los hace diferentes es su identificador. Esto último es muy importante, ya
que significa que aun cuando hay una relación de parentesco entre los procesos, ambos son
aparentemente independientes.
Debido al parentesco que adquieren el par de procesos cuando uno crea al otro, estos se pueden
comunicar haciendo uso de las funciones wait y exit, que se incluyen en el archivo de cabecera
stdlib.h, y que están definidas como2:
void exit(int status);
pid_t wait(int *status);
exit termina al proceso que la manda a invocar. El valor de estado se le regresa al proceso padre para
que este pueda conocer como termino su proceso hijo. wait del lado del padre, obtiene el estado con
el que termina cualquiera sus procesos hijo. Cuando un proceso ejecuta wait y ninguno de sus
procesos hijos ha terminado, este se queda bloqueado. El valor que regresa esta función es el pid del
proceso hijo que termino.
La función wait almacena la información de estado con el que termino un proceso hijo en la memoria
apuntada por status. Esta información puede ser evaluada usando las siguientes macros:


WIFEXITED (status) es distinto de cero si el hijo termino normalmente.
WEXITSTATUS (status) evalúa los ocho bits menos significativos del código de retorno
del hijo que termino, que podrían estar activados como el argumento de una llamada a exit o
como el argumento de un return en el programa principal.
Ejemplos con fork
Un padre crea varios hijos:
/*Creación de varios hijos por parte de un padre*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define N 4
int main()
{
int i;
pid_t pid;
for(i=0;i<N;i++)
{
switch(pid=fork())
{
case 0:
printf("Soy el proceso hijo: %d y mi padre es %d \n", getpid(),
getppid());
break;
case -1:
printf("Error en la creación del proceso \n");
exit(0);
default:
printf("Soy el proceso padre: %d \n",getpid());
}
if(pid==0) break; /*El hijo no hace nada*/
}
sleep(10);
2 Más adelante estudiaremos otras formas de comunicación; por lo pronto, en este caso solo se contempla la comunicación
del proceso hijo al padre, cuando el hijo finaliza su ejecución.
return (0);
}
Una estructura lineal de procesos:
/*Estructura lineal de procesos*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 4
int main()
{
}
int i,status=0;
pid_t pid,pid_raiz;
pid_raiz=getpid();
for (i=0;i<N;i++)
{
if((pid=fork())==0)
{
printf("Soy el proceso hijo: %d y mi padre es %d \n",
getpid(),getppid());
}
else
{
if(pid==-1)
{
printf("Error en la creación del proceso \n");
exit(1);
}
else
{
printf("Soy el proceso padre: %d \n",getpid());
wait(&status); /*Esperando al hijo*/
if(pid_raiz==getpid())
{ /*Si soy padre, le envío a mi padre el número de procesos*/
printf("El numero de procesos que somos es: %d \n",
WEXITSTATUS(status)+1);
exit(0);
}
else exit(WEXITSTATUS(status)+1);
}
}
}
sleep(3);
exit(1);
Ejercicios
Con ayuda de la llamada al sistema fork, desarrolle los siguientes programas:
1. Un proceso padre que cree dos hijos, y estos a su vez creen dos hijos, para que finalmente estos
creen a tres hijos; al finalizar, cada padre o hijo deberá enviar a su padre el número hijos creados, de
tal forma que el padre principal imprima el número de hijos creados. Por ejemplo, un estructura de
nivel 3, tendrá en el nivel 0 al padre raíz; en el nivel 1 habrá dos hijos; en el nivel 2 habrá 4 hijos (dos
por cada hijo del nivel 1); finalmente en el nivel 3, habrá 12 hijos (llamados hojas); en total hay 18
nodos o procesos hijos creados. Su programa debe solicitar el nivel.
2. Un proceso (raíz) que solicite un número entero N entre 3 y 21, y con dicho número genere un
arreglo de N elementos en donde el contenido del arreglo será el número que le corresponde en el
arreglo; por ejemplo, para N = 10, tendríamos el siguiente arreglo:
0
1
2
3
4
5
6
7
8
9
El proceso raíz deberá crear tres procesos hijos, cada uno de los cuales tendrá acceso a una parte del
arreglo (usted define qué sección del arreglo), deberá sumar los elementos del arreglo que le
corresponden (que desde luego son arreglos disjuntos), y enviar el resultado al proceso padre para
que este finalmente imprima el resultado.
3. Elabore un programa que genere un árbol no balanceado de procesos. El proceso padre raíz deberá
generar dos hijos, y estos a su vez deben generar más hijos de la siguiente manera: los hijos del lado
izquierdo van a generar un árbol binario en cambio los hijos del lado derecho van a generar un árbol
de tres hijos; ver fig. 1.
Figura 1. Árbol no balanceado de procesos de nivel 2.
El proceso padre principal, deberá pedir el nivel hasta el cual se van a generar hijos. Cada proceso le
regresar a su padre el número de hijos que ha engendrado, de tal forma que el proceso padre raíz
imprimirá el número de procesos creados; para la fig. 1, el número de procesos es de 21.
Para ver que efectivamente ha creado el árbol de procesos solicitado, investigue el uso del comando
ps de Linux.
Fecha de entrega: 15/05/2012.
Descargar