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.