Sistemas Operativos Practica 2: procesos ligeros y

Anuncio
Sistemas Operativos
Practica 2: procesos ligeros y comunicación entre procesos.
Objetivos:





Continuar con el estudio de los procesos mediante la llamada al sistema fork.
Comprender el beneficio de crear aplicaciones con hilos, y comparar los hilos con los
procesos hijos creados con fork.
Diseñar estructuras de concurrencia de acuerdo a las necesidades de las aplicaciones.
Comprender el concepto de la sincronización en hilos.
Implementar comunicación entre procesos, sobre un problema de sincronización.
Introducción
Recordemos que en el instante en que un proceso ejecuta la instrucción fork, se crea un proceso hijo
exactamente igual al padre (en cuanto a código y datos); ambos procesos solo serán iguales justo en
el instante en que se ejecuta fork, después de ello podrán seguir caminos diferentes como procesos
independientes.
En ocasiones es necesario que los procesos se comuniquen entre ellos, y una forma de hacerlo es
compartiendo una zona de memoria; sin embargo, debido a que son procesos independientes, no es
posible establecer esta área de memoria. Otra manera de comunicar procesos es mediante los
servicios del sistema, este servicio se conoce como pipe (tubería).
Pipes
Un pipe no es mas que un canal de comunicación entre dos procesos, en este caso entre el proceso
padre y el hijo. Dicho canal de comunicación solo tiene dos acciones: escritura y lectura. Un pipe es
un descriptor o manejador a una especie de archivo el cual podemos leer y escribir. El pipe se
compone de un arreglo de dos elementos que contendrá al descriptor de lectura y al de escritura:
int iPipe[2];
. . .
Pipe (iPipe);
/*Arreglo que contendrá a los descriptores*/
/*Se crea el pipe para lectura y escritura*/
El descriptor de lectura estará dado por iPipe[0], mientras que el descriptor de escritura será
iPipe[1]. Para leer u escribir en el pipe, se emplean las funciones read y write respectivamente, y
se usan como si se trataran de las correspondientes operaciones con archivos. El formato de las
funciones es:
write (descriptor, datos, tamaño_datos);
read (descriptor, datos, tamaño_datos);
Por ejemplo para leer y escribir una variable de tipo int:
int iVar;
. . .
write (iPipe[1], &iVar, sizeof(int));
. . .
read (iPipe[0], &iVar, sizeof(int));
El siguiente ejemplo muestra un proceso padre comunicándose con su proceso hijo:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define READ
#define WRITE
0
1
int main ()
{
int iId, iWait;
int iPipe[2];
pipe (iPipe);
iId = fork ();
if (iId) /*Padre*/
{
printf ("Soy el padre, y mi hijo tiene ID: %d\n", iId);
printf ("No voy a leer solo a escribir, entonces cierro para lectura.\n");
close (iPipe[READ]);
write (iPipe[WRITE], &iId, sizeof(int));
printf ("Envíe dato, cierro escritura, espero a mi hijo y termino.\n");
close (iPipe[WRITE]);
wait(&iWait);
}
else /*Hijo*/
{
printf ("Soy el hijo, y solo voy a leer, cierro escritura\n");
close (iPipe[WRITE]);
read (iPipe[READ], &iId, sizeof(int));
printf ("Me enviaron mi ID: %ld, cierro lectura y termino.\n", iId);
close (iPipe[READ]);
exit (0);
}
return (0);
}
Un punto importante a considerar es que la lectura es bloqueante, es decir, un proceso que va a leer
un pipe no puede continuar si el pipe esta vacío (no se han escrito datos). Más aun, como el manejo
de pipes requiere de descriptores, entonces todos comparten el mismo descriptor; esto se traduce en
que todos los procesos, que provengan de un padre que creo pipes pueden comunicarse al compartir
el mismo descriptor del pipe.
Procesos ligeros (hilos)
Un hilo o proceso ligero forma parte de un proceso, cuya característica es que comparte una misma
región de memoria con otros hilos del mismo proceso.
Un proceso puede tener uno o más hilos. Cuando un proceso tiene solo un hilo decimos que es un
proceso monohilo; si el proceso tiene más de un hilo, se dice que es un proceso multihilado.
Actualmente, la mayoría de las aplicaciones tienden a ser multihiladas con la finalidad de aumentar
su rendimiento, o bien para incorporar funciones sin perder su rendimiento actual. Lo anterior es
cada vez es más evidente con el uso de procesadores con más de un núcleo (multicore), ya que una
aplicación multihilada puede explotar eficientemente este tipo de procesadores.
Debido a los que los hilos comparten la misma región de memoria, es posible que dos o más hilos
accedan de manera concurrente; por ejemplo, a una misma variable. Este es un problema un tanto
complicado, el determinar en que orden deben acceder los hilos a una misma zona de memoria; en
otras palabras, requerimos de sincronización entre los hilos.
A diferencia de la llamada al sistema fork, los hilos son implementados mediante bibliotecas, por lo
que no forman parte directamente de los servicios que ofrece un sistema operativo.
Hilos en lenguaje C
La biblioteca de hilos posee diferentes funciones, las cuales están declaradas en el archivo de
cabecera:
#include <pthread.h>
A continuación describiremos algunas de estas funciones:

int pthread_create (pthread_t *tid, const pthread_attr_t *attr,
void *(*star_routine)(void *), void *arg);
Los hilos se crean con la función pthread_create, y son colocados en una cola de hilos listos para su
ejecución, los argumentos de esta función son:
1. pthread_t *tid Indica que se debe colocar la dirección de una variable de tipo pthread_t, para
almacenar en ella el identificador del hilo (si es que la llamada tuvo éxito). En caso de que se
desee crear varios hilos, se puede emplear un arreglo para almacenar todos los
identificadores.
2. const pthread attr_t *attr Los atributos del hilo se encapsulan en el objeto atributo al que
apunta attr. Si attr es NULL, el nuevo hilo tendrá los atributos por omisión.
3. void *(*star_routine)(void *) El tercer argumento, start_routine es el nombre de una función
a la que el hilo invoca cuando inicia su ejecución. La función debe devolver un puntero de
tipo (void *), y solo puede tener un argumento que debe ser un puntero sin tipo. Esto no es
una limitación, sino una ventaja, ya que se puede emplear como argumento cualquier tipo de
dato o estructura, y después en la función recuperar el tipo de dato haciendo un cast.
4. void *arg El último argumento es la dirección de alguna variable que se desea pasar como
parámetro de la función.

void pthread_exit (void *value_ptr);
La función pthread_exit termina el hilo que lo invoca. El valor del argumento value_ptr queda
disponible para pthread_join (si esta tuvo éxito). El argumento value_ptr en pthread_exit debe
apuntar a datos que existan después que el hilo ha terminado, así que no puede asignarse como
datos locales para el hilo que está terminando. La función pthread_exit invoca a controladores de
terminación de hilos, cosa que return no hace.

int pthread_join (pthread_t thread, void **value_ptr);
La función pthread_join suspende la ejecución del hilo invocador hasta que el hilo identificado con
thread termine, ya sea porque llamo a pthread_exit o porque fue cancelado. Esta función es similar a
waitpid en el nivel de procesos. Si value_ptr no es NULL, entonces el valor retornado por thread es
almacenado en la ubicación apuntada por value_ptr.

pthread_t pthread_self (void);
La función pthread_self devuelve el identificador del hilo que esta invocando la función.
Ejemplos
“Hola mundo” con hilos
#include <stdio.h>
#include <stdlib.h>
#include "pthread.h"
void imprime();
int IGlobal; /*Una variable global*/
int main()
{
pthread_t hilo1;
pthread_t hilo2;
IGlobal = 10;
if((pthread_create( &hilo1, NULL,(void *)&imprime,NULL))!=0)
{
printf("Error en la creación de hilos\n");
exit(0);
}
if((pthread_create( &hilo1, NULL,(void *)&imprime,NULL))!=0)
{
printf("Error en la creación de hilos\n");
exit(0);
}
pthread_join(hilo1,NULL);
pthread_join(hilo2,NULL);
return (0); /*El proceso puede terminar con return*/
}
void imprime()
{
IGlobal++;
printf("Hola mundo\n\nLa variable global es: %d", IGlobal);
sleep(100);
pthread_exit(NULL); /*En lugar de emplear exit, se usa esta función*/
}
Suma de matrices con hilos
#include <stdio.h>
#include <stdlib.h>
#include "pthread.h"
#include <time.h>
#define TAM 2
void imprime(int matriz[2][2]);
void suma(void *id);
void inicializa_matriz(int modulo,int matriz[2][2]);
int matA[TAM][TAM];
int matB[TAM][TAM];
int matC[TAM][TAM];
int main()
{
pthread_t hilo1;
pthread_t hilo2;
int id_thread0=0;
int id_thread1=1;
srand(time(NULL));
inicializa_matriz(3,matA);
inicializa_matriz(3,matB);
inicializa_matriz(1,matC);
imprime(matA);
imprime(matB);
if ((pthread_create( &hilo1, NULL,(void *)&suma,(void *)&id_thread0))!=0)
{
printf("Error en la creación de hilos\n");
exit(0);
}
if((pthread_create( &hilo1, NULL,(void *)&suma,(void *)&id_thread1))!=0)
{
printf("Error en la creación de hilos\n");
exit(0);
}
pthread_join(hilo1,NULL);
pthread_join(hilo2,NULL);
imprime(matC);
return 0;
}
void inicializa_matriz(int modulo,int matriz[2][2])
{
int i,j;
for(i=0;i<TAM;i++)
for(j=0;j<TAM;j++)
matriz[i][j]=rand()%modulo;
}
void suma(void *id)
{
int i;
int id_thread=*((int *)(id));
for(i=0;i<TAM;i++)
matC[id_thread][i] = matA[id_thread][i] + matB[id_thread][i];
pthread_exit(NULL);
}
void imprime(int matriz[2][2])
{
int i,j;
for(i=0;i<TAM;i++)
{
for(j=0;j<TAM;j++)
printf("%d ",matriz[i][j]);
printf("\n");
}
printf("\n");
}
Como se ha mencionado, en ocasiones los hilos comparten información al acceder a bloques de
memoria compartidos (a diferencia de procesos con fork que no comparten). Cuando el acceso se
realiza para lectura, cada uno de los procesos podrá ir a la memoria y recuperar el dato sin el peligro
de que pueda encontrar inconsistencias, pero si uno o más procesos acceden a la memoria para
modificar un determinado dato, entonces se pueden generar problemas de inconsistencia. La
solución a este problema es mediante el empleo de mecanismos de sincronización, de tal forma que
se pueda asegurar el acceso a un dato compartido de manera única.
Ejercicios
1. Considere un proceso padre con cinco hijos, de tal forma que los cinco hijos se puedan comunicar
mediante pipes, de la siguiente manera:
a) Enumere los hijos de 0 a 4.
b) La comunicación solo se pueda realizar entre las parejas (y en esa dirección): 0 a 2, 2 a 4, 4 a
1, 1 a 3, y de 3 a 0. Observe que un dato x podrá pasarse desde el proceso 0 por todos los
procesos, hasta llegar nuevamente al proceso 0.
c) Su programa deberá solicitar un número N mayor a 0.
Para su programa, el dato inicial será x = 1, y cada proceso que reciba dicho dato deberá
incrementarlo en una unidad y pasarlo a todos los demás procesos. Todos los procesos deberán
terminar cuando alguno de ellos reciba el dato x, y este sea igual a N. Note que una vez que N = x, se
les deberá informar a todos los procesos que deben terminar, y el proceso que encontró que N = x,
deberá imprimir su identificador.
2. Realice el ejercicio 2 de la práctica 1, pero empleado pipes. No se limite ahora por el tamaño del
arreglo.
3. Desarrolle el mismo programa anterior, pero en lugar de emplear procesos hijos utilice hilos. El
proceso padre creador de los hilos, deberá recibir el resultado y presentarlo en pantalla.
4. Considere una variable de tipo unsigned long llamada A, y 15 hilos que tienen acceso a dicha
variable compartida. Cada uno de los 15 hilos implementara un ciclo for que contará desde 0 hasta
10000000, e incrementar en una unidad la variable compartida A. Si la variable compartida se inicia
en cero, entonces al incrementar en una unidad la variable A por los 15 hijos, la dejarán con el valor
igual a 150000000. El proceso padre que crea los hijos, deberá imprimir el valor de A. Ejecute el
programa varias veces, ¿siempre se presenta el mismo resultado?
5. Investigue como generar números aleatorios en C, y genere un arreglo de tipo long de 107
localidades, llenándolo con números aleatorios entre 0 y 100. Con lo anterior realice dos programas
que:
a) Haga una búsqueda secuencial para determinar cuantos números x hay en el arreglo. Desde
luego x lo proporciona el usuario.
b) Y otro programa que haga una búsqueda empleando hilos, para que también se pueda
determinar cuántos números x hay en arreglo. Para este caso, emplee al menos 5 hilos que se
repartan el arreglo.
Nota: es muy recomendable que usted empiece a investigar por su cuenta los servicios y llamadas al
sistema que ofrece el sistema operativo; lo que aquí se presenta es solo lo básico para que usted
pueda iniciar. Además se recomienda que inicie la práctica con tiempo suficiente.
Fecha de entrega: 05/06/2014.
Descargar