UNAN-Leon Sistemas Operativos Gestión de Procesos GESTIÓN DE PROCESOS Introducción Información sobre procesos Identificadores del Proceso Identificadores de usuario y de grupo Ejecución de programas mediante exec Creación de procesos fork() Terminación de Procesos exit() y wait() Introducción Un proceso es un programa en ejecución, cargado en memoria. Un programa es una colección de instrucciones y datos almacenados en un archivo que se usa para iniciar los segmentos de instrucciones y datos de usuario. En Linux todo proceso es creado por el núcleo (o kernel) del sistema operativo previa petición de otro proceso, estableciéndose una relación jerárquica entre el proceso que realiza la petición de creación (conocido como proceso padre o "creador") y el nuevo proceso (denominado proceso hijo). Un proceso padre puede tener varios hijos y todo hijo tiene únicamente un padre. Los procesos se organizan de forma jerárquica. Al arrancar el sistema se crea el proceso init, que es la raíz del árbol de procesos y su PID es 1. Al igual que otros procesos de sistema su PPID es 0. Información sobre procesos Identificadores del Proceso Para leer los valores del PID y del PPID de un proceso utilizaremos las llamadas: 1 Msc. Rina Arauz UNAN-Leon Sistemas Operativos #include <sys/types.h> pid_t getpid(); pid_t getppid(); Identificadores de usuario y de grupo El núcleo le asocia a cada proceso 2 identificadores de usuario y dos de grupo. Los identificadores de usuario son el identificador del usuario real (UID) y el identificador del usuario efectivo (EUID). Para el grupo están el GID y el EGID. El UID identifica al usuario que es responsable de la ejecución del proceso y el GID al grupo al cual pertenece el usuario en cuestión. El EUID se usa para determinar el propietario. Normalmente el UID y EUID coinciden, pero si un proceso ejecuta un programa que pertenece a otro usuario, el UID y EUID serán diferentes. Se aplica la misma norma para el identificador de grupo. Para leer los valores de los identificadores de usuario y grupo de un proceso utilizaremos las llamadas: #include <sys/types.h> uid_t getuid(); uid_t geteuid(); gid_t get gid(); gid_t get egid(); Ejemplo: Proceso que imprime sus identificadores. #include <sys/types.h> #include <stdio.h> #include <unistd.h> void main(void) { int id_proceso; int id_padre; id_proceso = getpid(); id_padre = getppid(); printf("Mi dentificador de proceso es : %d\n", id_proceso); printf("El identificador de mi proceso padre es: %d\n", id_padre); printf("Identificador de usuario: %d\n", getuid()); printf("Identificador de usuario efectivo: %d\n", geteuid()); printf("Identificador de grupo: %d\n", getgid()); printf("Identificador de grupo efectivo: %d\n", getegid()); } Ejecución de programas mediante exec El servicio POSIX exec() permite cambiar el programa que se está ejecutando, reemplazando el código y datos del proceso que invoca esta función por otro código y otros datos procedentes de un archivo ejecutable. El contenido del contexto de usuario del proceso que invoca a exec() deja de ser accesible si la función se ejecuta correctamente y es reemplazado por el del nuevo programa. Por lo tanto en estas condiciones el programa antiguo es sustituido por el nuevo y nunca se retornará a él para proseguir su ejecución, ya que es el programa nuevo el que pasa a ejecutarse, el cual se ejecutará desde el principio. Existe toda una familia de funciones exec que podemos usar para ejecutar programas. Dentro de esta familia cada función tiene su interfaz propia, pero todas tienen aspectos comunes y obedecen al mismo tipo de funcionamiento. La declaración de la familia de las funciones exec es: 2 Msc. Rina Arauz UNAN-Leon Sistemas Operativos #include <unistd.h> Int execl (char * path, char *arg0…char argn, (char *) 0); Int execv (char * path, char *argv[ ] ); Int execle (char * path, char *arg0…char argn, (char *) 0), char *envp[ ] ); Int execve (char * path, char *argv [ ], char *envp[ ]); Int execlp (char *file, char *arg0…char argn, (char *) 0) Int execvp (char * file, char *arg v[ ]) Path apunta la ruta de un fichero ordinario ejecutable y file apunta al nombre de un fichero ejecutable. La ruta del fichero se construye buscándolo en los directorios que se indican en la variable de retorno PATH. Tanto path como file se refieren a ficheros ejecutables o a ficheros de datos (shell scripts). Los parámetros arg0...argn son punteros a cadenas de caracteres y constituyen la lista de argumentos que se le pasa al nuevo programa. Por convenio, al menos arg0 esta presente siempre y apunta a una cadena idéntica a path o a el ultimo componente de path. A continuación de argn, pasamos un puntero NULL (char *) 0 - , para indicar el final de los argumentos. (igual para argv). Si exec no se ejecuto correctamente devuelve –1. Resumiendo: Las llamadas execl*() reciben los argumentos como una "lista" de C, tal como la usamos en el código fuente: execlp("ls", "ls", "-al", NULL);. Las llamadas execv*() reciben los argumentos en un arreglo, similar a argv[]. Las llamadas exec*p() buscan en los directorios especificados por la variable de entorno PATH el binario a cargar. El primer argumento de exec() es el EJECUTABLE a cargar y luego vienen los argumentos a pasarle, recordando que el primero es argv[0]: el "nombre" del programa. Ejemplo de execlp #include<unistd.h> main() { /* Shell es un archivo shell script con permiso de ejecución*/ execlp(“/home/usuario/shell”,”shell”,0); } Ejemplo de execv #include<unistd.h> main() { char *av[]={“ls”,”-l”,0}; execv (“/bin/ls”,av); } Ejemplo de execvp #include <sys/types.h> #include <stdio.h> #include <unistd.h> void main(int argc, char **argv) { 3 Msc. Rina Arauz UNAN-Leon Sistemas Operativos char *argumentos[3]; argumentos[0] = "ls"; argumentos[1] = "-l"; argumentos[2] = NULL; execvp(argumentos[0], argumentos); } Creación de procesos fork() El servicio POSIX fork(), crea un nuevo proceso. El SO trata este servicio llevando a cabo una clonación del proceso que lo invoca, conocido como proceso padre del nuevo proceso creado, denominado proceso hijo. Todos los procesos se crean a partir de un único proceso padre lanzado en el arranque del sistema, el proceso init, cuyo PID es 1 y que por lo tanto está situado en lo más alto en la jerarquía de procesos de UNIX. El proceso que hace la llamada a fork se convierte en el proceso padre del proceso creado. Una vez realizada la copia, tanto padre e hijo continúan de forma independiente la ejecución en el mismo punto del programa, es decir, en la siguiente instrucción al fork. Es un error pensar que el hijo comienza la ejecución por el principio del programa. Esto es así porque el proceso hijo hereda del padre los datos y la pila que tuviera en el momento de la ejecución del fork, así como el valor de los registros. fork() crea un nuevo proceso; pero no inicia un nuevo programa. Todos los procesos del sistema, se crean mediante esta llamada al sistema. Cuando se duplica un programa, los nombres de las variables, constantes, etc., siguen llamándose igual pero, en realidad, son distintas ya que se encuentran en zonas distintas de memoria; cada una correspondiente al área de datos de sus respectivos procesos. fork() devuelve dos valores: 0 al proceso creado (hijo) y El PID del proceso creado (hijo) al padre o -1 en caso de error La ejecución del hijo es independiente de la del padre y concurrente con ella. El padre debe realizar una llamada a wait, para esperar la finalización del hijo. La declaración de fork() es: #include <sys/types.h> pid_t fork(); La forma de invocarla es pid = fork(). La llamada a fork() hace que el proceso actual se duplique. A la salida de fork() los dos procesos tienen una copia idéntica del contexto del nivel de usuario excepto el valor de PID, que para el proceso padre toma el valor del proceso hijo y para el proceso hijo toma el valor 0, si la llamada a fork() falla devolverá el valor –1. El proceso hijo hereda la mayoría de los atributos del proceso padre, ya que se copian del segmento de datos del sistema. 4 Msc. Rina Arauz UNAN-Leon Sistemas Operativos Una secuencia de código típica para manejar la llamada a fork() es : int pid ; ... if (( pid = fork() ) == -1 ) perror (“ error en la llamada a fork”); #include <stdio.h> else if (pid == 0) #include <unistd.h> //Codigo del Hijo #include <stdlib.h> else main () //codigo del padre Proceso Padre pid≠0 { int pid; pid= fork(); if (pid == 0) printf("%d: Soy el hijo!\n", getpid()); else printf("%d: Soy el padre de %d\n", getpid(), pid); } Ejemplo: #include <stdio.h> #include <unistd.h> #include <stdlib.h> main () { int pid; pid= fork(); if (pid == 0) printf("%d: Soy el hijo!\n", getpid()); else printf("%d: Soy el padre de %d\n", getpid(), pid); } Proceso Hijo #include <stdio.h> #include <unistd.h> #include <stdlib.h> main () pid=0 { int pid; pid= fork(); if (pid == 0) printf("%d: Soy el hijo!\n", getpid()); else printf("%d: Soy el padre de %d\n", getpid(), pid); } Padre Pid 571 Hijo Pid 574 5 Msc. Rina Arauz UNAN-Leon Sistemas Operativos Es típico necesitar invocar otro programa y seguir ejecutando código nuestro, Como lo hacemos? La única manera de hacerlo es creando OTRO proceso con fork() y reemplazar su código con exec(). main () { int x; x=fork(); if ( x== 0 ) { printf(“Soy el proceso hijo y hare una llamada a exec”); execl (“/bin/date”, “date”, 0); printf (“ ABC”); } No se ejecuta else { printf(“soy el proceso padre”); sleep(1); printf(“Adios”); } } Terminación de Procesos exit() y wait() Una situación muy típica en programación concurrente es que el proceso padre espera a la terminación del proceso hijo antes de continuar su ejecución. Para sincronizar los procesos padre e hijo se emplean las llamadas exit y wait. Su declaración es la siguiente: #include <stdlib.h> void exit (int status); Exit finaliza el proceso que le llama. Si hay procesos hijos cuando el padre ejecuta un exit, el PPID de los hijos se cambia a 1 (proceso init). #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); pid_t wait(int *status): El valor del parámetro status se utiliza para comunicar al proceso padre la forma en la que el proceso hijo termina. Suele ser 0 si el proceso termina correctamente y cualquier otro valor en caso de terminación anormal. Si queremos ignorar este valor, podemos pasarle a wait un puntero NULL. La función wait suspende la ejecución del proceso actual hasta que un proceso hijo ha terminado, si se ejecuta correctamente, retorna el PID del hijo cuya ejecución ha finalizado. Por el contrario devuelve -1 sino se crearon procesos hijos o ya no existen procesos por los que esperar. pid_t waitpid(pid_t pid, int *status, int options): La función waitpid suspende la ejecución del proceso en curso hasta que un hijo especificado por el argumento pid ha terminado. 6 Msc. Rina Arauz UNAN-Leon Sistemas Operativos El valor de options es un OR de cero o más de las siguientes constantes: WHOHANG, WUNTRACED, WCONTINUED, WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG, WCOREDUMP, WIFSTOPPED, WSTOPSIG y WIFCONFIRMED. waitpid() en caso de éxito devuelve el PID del hijo cuya ejecución ha finalizado y en caso de error -1. Ejemplo1: #include<unistd.h> #include<stdio.h> main() { int x; x=fork(); if (x==0) { printf(“Soy el hijo”); execlp(“ls”,”-l”,NULL); } else { wait(NULL); //Sincroniza de manera que el padre espera la terminación de un hijo printf(“saliendo”); } } Ejemplo2: #include <sys/types.h> #include <unistd.h> #include <wait.h> #include <stdio.h> main() { int p, s, e; p=fork(); if ( p== 0 ) { /* hijo */ sleep(3); printf("Hijo mi id=%d\n",getpid()); } else { /* padre …. Esperamos por el proceso hijo */ printf(“ la variable p=%d\n”,p); s=waitpid(p, &e, 0); printf("Padre p=%d , s=%d, e=%d\n",p,s,e); } } 7 Msc. Rina Arauz UNAN-Leon Sistemas Operativos Para utilizar FORK() dentro de un ciclo y evitar la propagación de procesos en 2^n procesos, puede usarse la llamada a EXIT() en los procesos hijos. De la siguiente forma: for (i=0; i<10; i++) { if (fork() == 0) { /* código de los hijos */ exit(0); } } Ejercicios 1) Crear un proceso que leerá por el terminal el nombre de un programa (orden de Unix) y seguidamente lo ejecutará. El programa a ejecutar debe residir en el directorio /bin y no debe necesitar ningún parámetro (p. ej., ls, date, time, cd, pwd, ...). No se debe de ejecutar un programa hasta que el anterior no haya acabado. El proceso terminará cuando se introduzca la cadena “salir”. 2) Escriba un programa donde se muestre un proceso principal que crea 4 procesos hijos. El proceso tiene una variable global “var” inicializada a uno. El proceso1 suma var=var+1, el proceso2 suma var=var+2, el proceso3 suma var=var+3, el proceso4 suma var=var+4. Cada proceso imprime su pid, su ppid y el valor de VAR. El proceso padre deberá esperar por la finalización de los procesos hijos e imprimir el pid de cada hijo que vaya finalizando y el valor de la variable var. Explique el resultado de var. 3) Considerando el siguiente fragmento de código: for (num= 1; num<= n; num++) { nuevo= fork(); if ((num== n) && (nuevo== 0)) execlp ("ls", "ls", "-1", NULL); } Dibuja la jerarquía de procesos generada cuando se ejecuta y n es 3. Indica en qué procesos se ha cambiado la imagen del proceso usando la función execlp. 4) Observa el siguiente código y escribe la jerarquía de procesos resultante. #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> 8 Msc. Rina Arauz UNAN-Leon Sistemas Operativos main () { int num; pid_t pid; srandom(getpid()); for (num= 0; num< 3; num++) { pid= fork(); printf (" Soy el proceso de PID %d y mi padre tiene %d de PID.\n", getpid(), getppid()); if (pid== 0) break; } if (pid== 0) sleep(random() %5); else for (num= 0; num< 3; num++) printf ("Fin del proceso de PID %d.\n", wait (NULL)); } Ahora compila y ejecuta el código para comprobarlo. Presta atención al orden de terminación de los procesos, ¿qué observas? ¿por qué? 9 Msc. Rina Arauz