Práctica 2

Anuncio
Sistemas Operativos
Ingenierı́a de telecomunicaciones
Sesión 2: Procesos e hilos (modificado 29/10)
Calendario
Comienzo: Lunes 19 de octubre y miércoles 21 de octubre.
Entrega: 2 de noviembre y 4 de noviembre, hasta la hora de clase.
1.
Objetivos
Al finalizar esta sesión:
Comprenderás el mecanismo de creación de un proceso en un entorno UNIX real.
Conocerás el funcionamiento y las opciones más comunes de los comandos que proporcionan información sobre el estado de los procesos: top y ps. Además, interpretarás correctamente la información que proporcionan.
Conocerás las llamadas al sistema relacionadas con la creación de procesos y llamadas que devuelven información adicional como su PID o el de su padre.
Escribirás programas en C que creen procesos en un entorno real UNIX y que esperen
a que mueran.
Identificarás las ventajas y los incovenientes derivados de los sistemas concurrentes.
2.
Identificando un proceso
Seguro que has leı́do una y otra vez la definición de un proceso, y seguro que la has
entendido ;) Seguro que sabes qué es un proceso, pero ¿sabes dónde está un proceso?
¿Podrı́as identificar un proceso en tu ordenador ahora mismo?
Cuando arrancas un programa en tu ordenador, por ejemplo un navegador web, el
reproductor de música o el messenger, el sistema operativo empieza a crear procesos. Es
1
posible que un solo programa arranque varios procesos, pero por lo general arrancar un
programa crea un único proceso.
Consulta la página man del comando ps y piensa para qué puede servir.
Vamos a ver qué procesos existen en nuestro ordenador ahora mismo. Abre un programa que no tengas abierto todavı́a: firefox, gedit, kedit, cualquiera. Ahora ejecuta el
comando ps aux. Fı́jate en la salida del comando y busca el nombre del programa que
acabas de ejecutar. ¿Cuántas veces aparece? ¿Sabrı́as interpretar la información que te
proporciona el comando?
Existe otro comando, top, que te dará información adicional acerca del estado en el que
se encuentran los procesos. Además, este comando incluye información sobre los recursos
del sistema. La única diferencia es que top proporciona información que se actualiza
automáticamente.
3.
Programando
Vamos a ver cómo podemos gestionar los procesos creando nuestros propios programas.
3.1.
Creando nuestro primer proceso
Vamos a crear un programa muy simple y a comprobar cómo cuando lo arrancamos,
estamos creando un nuevo proceso en el sistema. Hagamos un programa muy simple que
no termine nunca:
int main ( )
{
while ( 1 )
{
};
return 0 ;
}
Esto lo guardaremos en un archivos main.c. Para compilarlo ejectuamos:
$>gcc main.c -o main
con lo que obtenemos un ejecutable llamado main. Si lo invocamos desde la lı́nea de
comandos veremos cómo la terminal se queda parada, sin responder a ninguna petición
más. Para parar el programa que acabamos de arrancar pulsamos ctl+c (la tecla de
control y la tecla ’c’ a la vez).
Ahora lo lanzaremos de la siguiente manera:
$>main &
2
El ampersand (&) le dice al intérprete de comandos que ejecute el proceso “en el fondo”
y que siga interpretando comandos mientras tanto, sin esperar a que nuestro programa
termine.
Ahora hacemos de nuevo un ps:
PID PPID USER COMMAND
1330
1158 fherrero bash
1341
1330 fherrero main
Lo que nos interesa ahora es saber cómo nuestro proceso puede crear otros procesos
igual que hace bash.
3.2.
Clones
En un programa nosotros utilizamos datos que guardamos en variables y funciones
que operan sobre esos datos. Cuando ejecutamos un programa creamos un proceso que,
entre otras cosas, tiene una zona de la memoria del ordenador reservada para él donde
guarda nuestros datos y las instrucciones que ha de ejecutar.
Si en nuestro programa llamamos a fork(), le estaremos pidiendo al sistema operativo
que cree un nuevo proceso exactamente igual al que llama a fork().
Primero vamos a crear un programa que recoge información como ps:
#include <s y s / t y p e s . h>
#include <u n i s t d . h>
#include <s t d i o . h>
int main ( )
{
p r i n t f ( ‘ ‘ \ nHola , soy e l p r o c e s o %d y mi padre e s e l %d\n ’ ’ ,
getpid () , getppid () ) ;
return 0 ;
}
Compiladlo y ejecutadlo como siempre.
Ahora veremos cómo funciona fork()
#include <s y s / t y p e s . h>
#include <u n i s t d . h>
#include <s t d i o . h>
int main ( )
{
fork () ;
p r i n t f ( ‘ ‘ \ nHola , soy e l p r o c e s o %d y mi padre e s e l %d\n ’ ’ ,
getpid () , getppid () ) ;
3
return 0 ;
}
Compiladlo y ejecutadlo. Ahora obtenemos algo parecido a:
Hola, soy el proceso 1431 y mi padre es el 1330
Hola, soy el proceso 1432 y mi padre es el 1431
Veamos qué está sucediendo. Cuando invocamos a main, creamos un proceso con
PID 1431 en este caso. La llamada a fork() hace que el sistema cree un proceso nuevo
exactamente igual al anterior. Esto implica entre otras cosas, que el código que ejecutan es
exactamente igual. Hay que tener muy claro que después de fork() son dos los procesos
ejecutando nuestro programa. Es por eso que la lı́nea printf(...) se muestra dos veces
en la terminal, una por cada proceso. Más aún, se puede ver que en una de las lı́neas un
proceso dice: “...mi padre es el 1431”. Este proceso es el que fork() ha creado, y que no
existı́a antes de él. El proceso 1431 es en este caso el padre y el 1432 el hijo.
3.3.
Distinguiendo entre padres e hijos
No parece que fork() sea muy útil si los dos procesos van a hacer exactamente lo
mismo. Es posible, sin embargo, que utilizando el mismo código, cada proceso tome una
decisión diferente sobre lo que debe hacer.
Probad el siguiente código:
#include
#include
#include
#include
<s y s / t y p e s . h>
<u n i s t d . h>
< s t d l i b . h>
<s t d i o . h>
void padre ( )
{
p r i n t f ( ‘ ‘ \ nSoy e l padre con p i d %d\n ’ ’ , g e t p i d ( ) ) ;
exit (0) ;
}
void h i j o ( )
{
p r i n t f ( ‘ ‘ \ nSoy e l h i j o con p i d %d y padre %d\n ’ ’ , g e t p i d ( ) ,
%g e t p p i d ( ) ) ;
exit (0) ;
}
int main ( )
{
4
p i d t pid = f o r k ( ) ;
i f ( p i d == 0 ) /∗ Soy e l h i j o ∗/
{
hijo () ;
}
i f ( p i d > 0 ) /∗ Soy e l padre ∗/
{
padre ( ) ;
}
return 0 ;
}
La salida de este programa:
Soy el hijo con pid 1471 y padre 1470
Soy el padre con pid 1470
Para más información sobre el funcionamiento de fork() consultad man fork. Es muy
importante entender qué valor devuelve y cómo el padre y el hijo son capaces de decidir
quién es quién.
3.4.
Datos y memoria
Hemos dicho que el proceso hijo es una copia exacta de su padre, lo que implica ejectuar
un código igual y tener una estructura en memoria igual. En este programa vamos a crear
tres procesos y a comprobar algunas cosas sobre la memoria. Compila, ejecuta, y analiza
los resultados que obtengas.
#include
#include
#include
#include
<s y s / t y p e s . h>
<u n i s t d . h>
< s t d l i b . h>
<s t d i o . h>
int main ( )
{
int x = 3 ;
p i d t pid = f o r k ( ) ;
i f ( p i d == 0 ) /∗ Soy e l h i j o ∗/
{
p r i n t f ( ‘ ‘ Soy e l h i j o . X v a l e a l p r i n c i p i o : %d\n ’ ’ , x ) ;
5
x = x − 1;
p i d t pid2 = f o r k ( ) ;
i f ( p i d 2 == 0 ) /∗ Soy e l h i j o d e l h i j o : e l n i e t o ∗/
{
p r i n t f ( ‘ ‘ Soy e l n i e t o . X v a l e : %d\n ’ ’ , x ) ;
exit (0) ;
}
i f ( p i d 2 > 0 ) /∗ Soy e l padre d e l n i e t o : e l h i j o ∗/
{
p r i n t f ( ‘ ‘ Soy e l h i j o . X v a l e : %d\n ’ ’ , x ) ;
exit (0) ;
}
}
i f ( p i d > 0 ) /∗ Soy e l padre ∗/
{
p r i n t f ( ‘ ‘ Soy e l padre . X v a l e : %d\n ’ ’ , x ) ;
exit (0) ;
}
return 0 ;
}
3.5.
Cuando terminan los hijos
Probad qué pasa cuando el padre termina y el hijo sigue trabajando. Haced $>ps -eo
pid,ppid,comm | grep main después de ejecutar vuestro programa. ¿Qué ha pasado con
el padre? Si el padre muere, ¿Cuál es el ppid del proceso hijo?
#i n c l u d e <s y s / t y p e s . h>
#i n c l u d e <u n i s t d . h>
int main ( )
{
p i d t pid = f o r k ( ) ;
i f ( p i d == 0 ) /∗ Soy e l h i j o ∗/
{
while ( 1 ) ;
}
i f ( p i d > 0 ) /∗ Soy e l padre ∗/
6
{
}
return 0 ;
}
Probemos ahora el otro caso:
#i n c l u d e <s y s / t y p e s . h>
#i n c l u d e <u n i s t d . h>
int main ( )
{
p i d t pid = f o r k ( ) ;
i f ( p i d == 0 ) /∗ Soy e l h i j o ∗/
{
return 0 ;
}
i f ( p i d > 0 ) /∗ Soy e l padre ∗/
{
while ( 1 ) ;
}
return 0 ;
}
Para ejecutarlo y poder seguir trabajando con la shell sin esperar a que el padre muera,
recordad poner ’&’ al final: $>main &. Comprobad de nuevo con ps qué le ha pasado al
hijo.
Si todo ha funcionado como debiera, el proceso correspondiente al hijo estará marcado
como zombie o difunto. Esto porque ha terminado mientras que el padre sigue trabajando.
Para que el proceso hijo pueda alcanzar la paz eterna, es necesario que el padre recoja
la información adecuada con wait():
#include <s y s / t y p e s . h>
#include <u n i s t d . h>
int main ( )
{
p i d t pid = f o r k ( ) ;
i f ( p i d == 0 ) /∗ Soy e l h i j o ∗/
{
return 0 ;
}
7
i f ( p i d > 0 ) /∗ Soy e l padre ∗/
{
int c h i l d s t a t u s ;
w a i t (& c h i l d s t a t u s ) ;
while ( 1 ) ;
}
return 0 ;
}
El padre puede utilizar wait() como mecanismo de comunicación con su hijo mediante
la función exit() (man 3 exit). Para ver información sobre wait(), busca en la sección
2 del manual: man 2 wait.
4.
Otra forma de paralelismo: hilos
Casi todos los sistemas modernos soportan un nuevo modelo de paralelismo llamado
“hilos” o “hebras” según la traducción del inglés “thread”.
Un hilo es un flujo de control adicional dentro de un mismo proceso. Podemos pensar
que un proceso es la entidad a la que se asocian los recursos, y los hilos son ejecuciones
paralelas del programa que comparten los mismos recursos.
Veamos un ejemplo:
#include <p t h r e a d . h>
#include <s t d i o . h>
void ∗ f u n c i o n h i l o ( void ∗ a r g s )
{
int ∗ v a r i a b l e = ( int ∗ ) a r g s ;
int i ;
for ( i = 0 ; i < 1 0 ; i ++){
∗ variable = i ;
sleep (3) ;
}
}
int main ( )
{
pthread t nuevo hilo ;
int v a r i a b l e = 0 ;
8
p t h r e a d c r e a t e (& n u e v o h i l o , NULL, f u n c i o n h i l o , &v a r i a b l e ) ;
int i ;
for ( i = 0 ; i < 1 0 ; i ++){
p r i n t f ( ‘ ‘ V a r i a b l e v a l e : %d\n ’ ’ , v a r i a b l e ) ;
sleep (1) ;
}
return 0 ;
}
Aquı́ tenemos un único proceso, con dos hilos de ejecución (primero un único hilo,
creamos el segundo con pthread create()). El hilo original imprime una variable cada
segundo y el segundo hilo la modifica cada tres segundos. Daos cuenta que la variable que
estamos modificando es la misma, en el sentido de que es la misma posición de memoria,
que la que imprime el otro hilo.
Al igual que con los procesos, existe una manera en la que el hilo principal puede
esperar a que los demás hilos mueran: pthread join(). La diferencia es que pthread join
espera a que termine un hilo en concreto.
5.
Comprobación de errores
En ninguno de los ejemplos anteriores se han comprobado los posibles errores que
pudieran aparecer. ¿Qué pasa si fork no puede crear un nuevo proceso? Deberéis buscar
en las correspondientes páginas man cómo detectar errores. Por lo general las llamadas
al sistema devuelven un número negativo en caso de error, pero no tiene por qué ser
necesariamente ası́. Algunas de ellas devuelven valores negativos distintos en función del
tipo de error. No será necesario hacer un control exhaustivo de todos los posibles errores,
pero sı́ de los más comunes o probables.
Más páginas man interesantes relacionadas con el control de errores: errno y perror,
en la sección 3 del manual.
6.
Ejercicios
6.1.
Conocimiento
Cada pregunta vale 1 punto. Serán valoradas como correctas o incorrectas.
1. Encuentra un comando que presente todos los procesos ejecutándose actualmente
en el sistema según su relación padre → hijo.
9
2. ¿Existe algún proceso que no tenga ancestros? Especifica cuál.
3. En Unix existe un mecanismo para que los procesos se pasen mensajes simples entre
sı́ llamado “señales”. ¿Qué comando permite mandar una señal a cualquier proceso
desde la shell ? Pon un ejemplo de su uso donde se vea cómo un proceso reacciona
a la señal que le mandas.
4. El comando top, por defecto, sólo muestra información sobre procesos pero no sobre
hilos. ¿Cómo se pueden ver también hilos en top? Escribe un pequeño programa
(que deberás incluir) que demuestre cómo top muestra efectivamente los hilos.
5. Fı́jate en una opción concreta del comando ps: -eo. Esta opción nos permite especificar qué campos queremos tener en la salida. Especifica qué argumentos hay que
pasar a -eo para mostrar a la vez el PID de cada proceso, el de su padre, el usuario
que lo ha ejecutado y el comando con el que se ejecutó.
6. Abre una terminal. Desde ahı́ arranca un firefox. Con la ayuda del comando ps,
investiga el proceso de creación del navegador, indicando todos sus ancestros.
7. Fı́jate en el resto de procesos y encuentra alguno que sea padre de varios procesos. Escribe qué proceso es, y, si puedes, qué función tiene. Identifica también sus
ancestros.
8. Después de todas estas pruebas, describe con tus propias palabras qué es un proceso.
¿Ha cambiado en algo tu percepción respecto a lo que habı́as visto en teorı́a? ¿Qué te
ha aportado? ¿Qué dudas te quedan todavı́a?
6.2.
Experimentación
1. El proceso es la unidad fundamental de asignación de recursos del sistema operativo. Es decir, siempre que un usuario adquiere un recurso, lo hace asociado a
un determinado proceso. Existe una llamada al sistema, getrusage() que permite
conocer los recursos utilizados por un proceso o sus hijos. Consulta su página man
y familiarı́zate con ella.
Escribe ahora un programa que haga lo siguiente (1 punto):
a) Debe recibir como argumento al ejecutarse el número de hijos que debe tener.
b) Cada hijo reservará un bloque de memoria de un millón de bytes con malloc().
Después rellenará cada una de las posiciones con un número aleatorio generado
con “rand()”. Finalmente terminará.
c) El proceso padre deberá esperar a que todos sus hijos mueran y finalmente
recoger sus propias estadı́sticas y las de sus hijos con getrusage().
d ) Deberá imprimir por pantalla las estadı́sticas recogidas.
2. Analiza el comportamiento de los recursos en función del número de hijos y el tiempo
que duermen. (1 punto)
10
6.3.
Programación
1. Crea un programa que sea capaz de leer de teclado y acepte las siguientes órdenes
(2 puntos):
CREAR: creará un nuevo proceso que no hará más que ejecutar un bucle infinito.
MATAR pid: si el proceso con PID pid es hijo suyo lo matará y comprobará que
termina correctamente. Si no (porque no es hijo suyo o porque no existe tal
proceso), informará al usuario convenientemente.
LISTAR: mostrará una lista de todos los PIDs de sus hijos.
SALIR: matará a todos los hijos que queden vivos, comprobando que terminan
correctamente (hay que comprobarlo en el programa, no posteriormente) y
terminará.
El programa deberá estar leyendo de teclado y aceptando órdenes hasta que reciba
la orden SALIR.
Se puede suponer que el lı́mite máximo de procesos que podrá crear será 100.
2. Repetir el mismo ejercicio con hilos (1 punto).
3. Ejecutar los dos ejercicios y demostrar, utilizando las herramientas del sistema, que
realmente se están creando y eliminando los procesos/hilos. (0,5 puntos)
11
Descargar