1. Practica 2. Creación de procesos e hilos
1. Objetivos
1).-Que el alumno aprenda a crear procesos y a controlar la estructura jerár-
quica resultante con las llamadas al sistema fork, getpid, getppid, wait y exit.
2).-Que el alumno aprenda a gestionar hilos con la biblioteca de funciones
pthread (POSIX Threads).
2. Introducción
2.1. Creación de proceso con fork.
Un proceso es cualquier programa o comando en ejecución con un identifica-
dor de proceso o pid asignado por el sistema y que está en alguno de los siguientes
estados: Running (R), Sleeping (S), Waiting (D), Stopped (T) o Zombie (Z). To-
dos los procesos, exepto el proceso init cuyo pid es 1, tienen un proceso creador
al cual llamamos su proceso padre. Una llamada al sistema para crear procesos en
Linux es fork() y su declaración es:
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);
Después de la invocación de fork() se crea un proceso hijo con el mismo
código que el proceso padre y una copia de sus datos (variables y valores). El
proceso padre recibe como valor de retorno un entero positivo que corresponde
al pid de su hijo y el proceso hijo recibe como valor de retorno 0, este valor
se pude utilizar para controlar las instrucciones que debe ejecutar cada proceso.
Por otro lado, fork() regresa -1 si el proceso hijo no se puede crear. Ambos
procesos continúa con la ejecución del programa a partir del fork() de manera
concurrente. Ambos procesos comparten los archivos abiertos pero cada uno tiene
1
2. su propio contador de programa. Adicionalmente, las funciones getpid() y
getppid() regresan el pid y el ppid (pid del proceso padre) de quien las invoca,
su declaración es:
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
Ejemplo 1.
/*
* $ gcc 1fork.c -o fork
* $ ./fork
*
* Después de invocar fork(): El proceso hijo incrementa en uno
*sus variables y escribe el resultado en el archivo salida.txt, el proceso
*padre incrementa en dos sus variables y escribe el resultado en el archivo
*salida.txt, ambos procesos imprimen el contenido del archivo.
*
*Cada proceso maneja sus variables de forma independiente aun siendo globales.
*Los dos procesos tienen acceso concurrente al archivo, cada proceso cierra el
archivo para sí mismo.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int x=1; //variable global
int main()
{
pid_t ret;
FILE *apArch;
int y=2; //variable local
if( (apArch=fopen("salida.txt","w")) == NULL ){
printf("Error al abrir archivon");
exit(0);
}
ret=fork();
switch(ret){
case -1:printf("No se pude crear un proceso hijo.n");
break;
case 0: x=x+1;
y=y+1;
fprintf(apArch,"soy el proceso hijo pid: %d, pidPadre: %d, x= %d,y
= %dn",getpid(),getppid(),x,y);
fclose(apArch);
break;
default:x=x+2;
y=y+2;
2
3. fprintf(apArch,"soy el proceso padre pid: %d, pidHijo: %d, x= %d,y
= %dn",getpid(),ret,x,y);
fclose(apArch);
}
system("cat ./salida.txt"); //Muestra el contenido del archivo
//lo hacen los dos procesos.
return 1;
}
Compilación y ejecución
$ gcc 1fork.c -o fork
$ ./fork
soy el proceso padre pid:17275, pidHijo: 17276, x=3,y=4
soy el proceso hijo pid:17276, pidPadre: 17275, x=2,y=3
soy el proceso padre pid:17275, pidHijo: 17276, x=3,y=4
soy el proceso hijo pid:17276, pidPadre: 17275, x=2,y=3
2.2. Comunicación mediante wait y exit.
Después de una invocación a fork() el proceso padre puede esperar la ter-
minación de su hijo con la función wait() y su hijo puede avisarle a su padre
que ya terminó con la función exit(), su declaración es:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
pid_t wait(int *status);
void exit(int st);
exit() termina el proceso que la invoca y le regresa el valor de st al sistema
quien lo almacena en status. wait() bloquea la ejecución del proceso padre
hasta que alguno de sus procesos hijos invoca exit(). wait() regresa el pid
del proceso que invocó exit() y continúa ejecutándose, dos de las macros que
se pueden utilizar para evaluar el contenido de status son:
WIFEXITED(status) Distinto de cero si el hijo terminó normalmente.
WEXITSTATUS(status) Evalúa los ocho bits menos significativos del
código de retorno del hijo, puede ser el argumento de exit() o de return(),
es decir st.
3
4. Ejemplo 2 y 3
Descargar el .zip de moodle.
Compilación y ejecución
$ gcc 2fork.c -o fork
$ ./fork
$ ps -eo pid,user,comm|grep fork|head -n 1|awk ’{print $1}’|xargs pstree -cp
$ gcc 3fork.c -o fork
$ ./fork
$ ps -eo pid,user,comm|grep fork|head -n 1|awk ’{print $1}’|xargs pstree -cp
2.3. Hilos POSIX
POSIX Threads (Portable Operating System Interface Threads) es una biblio-
teca de funciones proporcionada por los sistemas operativos tipo UNIX para la
gestión de hilos. Para trabajar con hilos POSIX es necesario incluir la biblioteca
de funciones pthread.h en el encabezado de los programas en lenguaje C y
compilar con la opción lpthread. Algunas de sus funciones son:
#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t
*attr, (void *)(*star_routine)(void *), void *arg);
void pthread_exit(void *value_ptr);
int pthread_join(pthread_t thread, void **value_ptr);
La función pthread_create sirve para crear hilos, regresa 0 si tuvo éxito
y un valor de error en caso contrario, sus argumentos deben ser:
tid Un apuntador a la variable donde se almacenará el identificador del
hilo, si es que la llamada tuvo éxito.
attr Es un apuntador a la variable donde se almacenan los atributos que
el sistema debe asignar al hilo. Si es igual a NULL, tendrá los atributos por
omisión.
(*star_routine) Es una referencia a la función que el hilo ejecuta
cuando es creado. La función devuelve un apuntador sin tipo (void *) y
su único argumento es un apuntador sin tipo (void *). Para poder utilizar
un argumento de otro tipo se debe hacer un cast al recuperar el valor.
4
5. arg Es el argumento que se le pasa a la función star_routine().
La función pthread_exit termina el hilo que la invoca. El valor del argu-
mento value_ptr queda disponible para pthread_join si la primera tuvo
éxito. value_ptr debe apuntar a datos que existan después de que el hilo ter-
mine.
La función pthread_join suspende la ejecución del hilo que la invoca has-
ta que el hilo identificado con thread termine, ya sea porque llamó a pthread_exit
o por que fue cancelado. Si value_ptr no es NULL, entonces el valor retornado
por thread es almacenado en la ubicación apuntado por value_ptr.
Ejemplo 4.
El hilo principal crea dos hilos y espera a que termine su ejecución, cada uno
ejecuta la función ejecutaHilo() con la cual imprime un mensaje y termina,
cada hilo se ejecuta a su propia velocidad. Las variables h1 y h2 son referencias
a los hilos, asignadas por posix.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void ejecutaHilo();
void main()
{
pthread_t h1,h2;
pthread_create(&h1,NULL,(void *)&ejecutaHilo,NULL);
pthread_create(&h2,NULL,(void *)&ejecutaHilo,NULL);
pthread_join(h1,NULL);
pthread_join(h2,NULL);
}
void ejecutaHilo()
{
printf("Hilo en ejecución...n");
sleep(5);
pthread_exit(NULL);
}
Compilación y ejecución
$ gcc ejemplo1.c -lpthread
$ ./a.out
Hilo en ejecución...
Hilo en ejecución...
5
6. Mientras se está ejecutando, hacer lo siguiente en otra terminal:
$ ps -eo pid,user,comm|grep a.out|head -n 1|awk ’{print $1}’|xargs pstree -cp
a.out(4411)-----{a.out}(4412)
|--{a.out}(4413)
Ejemplo 5.
La biblioteca de funciones pthread.h le asigna a cada hilo un identificador
que se guarda en tid, y se puede obtener con la función pthread_self(). Por
otro lado Linux también le asigna un identificador a cada hilo, el cual se puede
obtener con la llamada al sistema SYS_gettid que no pertenece a C. Además
los hilos pueden ser nombrados por los argumentos de la función que ejecutan.
Lo anterior se muestra en el siguiente código.El hilo principal crea dos hilos,
cada uno ejecuta la función ejecutaHilo() que recibe un id asignado por el
programa, el hilo imprime dicho identificador junto con los identificadores asig-
nados por posix y por Linux.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <syscall.h>
void ejecutaHilo(void *id);
void main()
{
pthread_t h1,h2;
int id1=10, id2=20;
pthread_create(&h1,NULL,(void *)&ejecutaHilo,(void *)&id1);
printf("hilo1( %u) creadon",(unsigned int)h1);
pthread_create(&h2,NULL,(void *)&ejecutaHilo,(void *)&id2);
printf("hilo2( %u) creadon",(unsigned int)h2);
pthread_join(h1,NULL);
pthread_join(h2,NULL);
}
void ejecutaHilo(void *id)
{
int *idh=(int *)(id);
int tid=syscall(SYS_gettid);//LLamada al sistema explícita, para obtener el
id de Linux
printf("Hilo en ejecución con idPosix= %u, idLinux= %d, idPrograma= %dn",(int)
pthread_self(),tid,*idh);
sleep(7);
pthread_exit(NULL);
}
6
7. Compilación y ejecución
$ gcc ejemplo2.c -o -lpthread
$ ./a.out
hilo1(2075494144) creado
Hilo en ejecución con idPosix=2075494144, idLinux=4262, idPrograma=10
hilo2(2067101440) creado
Hilo en ejecución con idPosix=2067101440, idLinux=4263, idPrograma=20
Mientras se está ejecutando, hacer lo siguiente en otra terminal:
$ ps -eo pid,user,comm|grep a.out|head -n 1|awk ’{print $1}’|xargs pstree -cp
a.out(4411)-----{a.out}(4412)
|--{a.out}(4413)
Ejemplo 6.
Notar el uso de pthread_join y pthread_exit.
Notar que se pude hacer referencia al id asignado por posix con h1 y h2,
sin necesidad de utilizar pthread_self()
Es decir, que los hilos se pueden gestionar con el id asignado por posix (h1
y h2), sin necesidad de conocer el id asignado por Linux.
Notar que comparten memoria: Pueden leer las variables globales y las re-
ferencias a memoria (apuntadores) son sobre un espacio de memoria com-
partido.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int varGlobal = 1000; //compartida por los hilos.
void ejecutaHilo(void *id); //ejecutada por los hilos.
void main()
{
pthread_t h1,h2; //id posix para los hilos
int v1=5, v2=6; //argumentos para la función que ejecutan los hilos
int *r1=NULL; //para guardar el valor de exit y leerlo con join
int i;
pthread_create(&h1,NULL,(void *)&ejecutaHilo,(void *)&v1);
pthread_create(&h2,NULL,(void *)&ejecutaHilo,(void *)&v2);
for(i=0;i<10;i++){
printf("Hilo principal, varGlobal= %dn",varGlobal+=1);
sleep(1);
}
7
8. pthread_join(h1,(void **)&r1);
printf("hilo1 termina con: %dn",*r1);
pthread_join(h2,(void **)&r1);
printf("hilo2 termina con: %dn",*r1);
}
void ejecutaHilo(void *v)
{
int *vh=(int *)(v);
sleep(*vh); //utiliza el argumento recibido.
printf("Hilo que recibe %d, espera %ds, imprime varGlobal= %d, modifca val
recibidon",*vh,*vh,varGlobal);
*vh=5*(*vh); //modifica el argumento recibido.
pthread_exit(vh); //termina y deja vh para el join.
}
Compilación y ejecución
$ gcc ejemplo3.c -lpthread
$ ./a.out
Hilo principal, varGlobal=1001
Hilo principal, varGlobal=1002
Hilo principal, varGlobal=1003
Hilo principal, varGlobal=1004
Hilo principal, varGlobal=1005
Hilo que recibe 5, espera 5s, imprime varGlobal=1005, modifca val recibido
Hilo principal, varGlobal=1006
Hilo que recibe 6, espera 6s, imprime varGlobal=1006, modifca val recibido
Hilo principal, varGlobal=1007
Hilo principal, varGlobal=1008
Hilo principal, varGlobal=1009
Hilo principal, varGlobal=1010
hilo1 termina con: 25
hilo2 termina con: 30
Mientras se está ejecutando, hacer lo siguiente en otra terminal:
$ ps -eo pid,user,comm|grep ej1hilos|head -n 1|awk ’{print $1}’|xargs pstree -cp
a.out(4736)-----{a.out}(4737)
|--{a.out}(4738)
8
9. 3. Actividades.
1. Construir un árbol binario de procesos de altura N. Hacer que N sea gene-
rado aleatoriamente y que el nodo raíz imprima el número total de hijos.
Utilizar sleep() para poder visualizar el árbol con el comando visto en los
ejercicios.
NOTA. No utilizar recursividad.
2. Modificar el ejemplo cuatro para que cada hilo genere dos hilos más y es-
pere su finalización. Explicar qué es lo que pasa ¿Quién es el padre de los
nuevos hilos? Mostrar el árbol de procesos (hilos).
3. Modificar el ejemplo uno para que un hilo clone al proceso con fork(). Ex-
plicar qué es lo que pasa ¿También se clonan los hilos? Mostrar el árbol de
procesos (hilos).
4. Después de haber ejecutado el fork() del punto anterior. Dado que todos los
hilos comparten los recursos del proceso al que pertenecen ¿Es posible que
un hilo sepa a qué proceso pertenece? En caso afirmativo ¿Cómo?
5. Modificar el ejemplo tres para que genere N hilos y los guarde en un arreglo.
Cada hilo debe ejecutar la función ejecutaHilo recibiendo como argu-
mento la localidad (i) donde se encuentra el hilo, el hilo debe escribir su va-
lor i en la variable global y salir con pthread_exit(). El hilo principal
debe esperar por cada uno de los hilos con la función pthread_join().
N debe ser definido en el programa o dado por el usuario. Asegurarse de que
los hilos se ejecuten concurrentemente, es decir, no serializar la ejecución
con sleep u otro código.
6. ¿Cuál es el valor que se espera que tenga la variable global? y ¿Por qué?
7. Hacer que cada hilo del programa anterior, además de su identificador (i),
imprima quien es su vecino posterior (i+1) y anterior (i-1), suponiendo que
el arreglo es circular. Notar que no es necesaria la comunicación entre hilos,
cada hilo debe calcular i-1 e i+1 a partir de i.
9