Sistemas Operativos Ingenierı́a de telecomunicaciones Sesión 3: Concurrencia Calendario Comienzo: Lunes 2 de noviembre y miércoles 4 de noviembre. Entrega: 16 de noviembre y 18 de noviembre. 1. Requisitos de teorı́a Comprender los problemas de la concurrencia. Comprender los mecanismos de gestión de la concurrencia como semáforos y exclusiones mutuas. 2. Objetivos Al finalizar esta sesión: Identificarás correctamente situaciones conflictivas de concurrencia. Conocerás las llamadas al sistema que manejan los semáforos. Diseñarás correctamente esquemas de sincronización para solucionar situaciones conflictivas. 3. Motivación: paralelismo y sus problemas Cuando trabajamos de forma paralela, y esto es algo constante en sistemas multitarea como los actuales, es bueno pensar que “nadie nos garantiza nada”. Ya hemos aprendido que nadie garantiza el orden de ejecución de los procesos, ni siquiera que todos ellos 1 se ejecuten el mismo número de veces. Por lo tanto, para no dar nada por supuesto, como programadores tenemos que ocuparnos de garantizar que se cumple lo que nosotros queremos que se cumpla. En esta práctica vamos a ver qué problemas surgen cuando varios procesos acceden a un mismo recurso y cómo podemos resolverlos. 4. 4.1. Conceptos Sincronización Por sincronización entendemos un orden o una dependencia temporal de los hechos. Por ejemplo, no puedes retirar el dinero de un cajero antes de que él te lo dé. Existe una sincronización entre las acciones del usuario y las del cajero, ya que ese orden se garantiza siempre. En los sistemas paralelos muchas veces necesitaremos garantizar el orden de las acciones que realicen nuestros procesos. Para ello utilizaremos mecanismos que el sistema operativo nos ofrece. 4.2. Comunicación entre procesos Existen una serie de mecanismos llamados IPC (Inter Process Communication) que nos van a ayudar en la tarea de comunicar y coordinar procesos. En esta práctica vamos a utilizar sólo dos de los que existen: memoria compartida y semáforos. El comando ipcs nos muestra información sobre los objetos IPC (Inter Process Communication) que existen actualmente en el sistema. En particular nos va a interesar la parte destinada a los semáforos. Si lo ejecutais en una terminal podéis ver algo como (el formato exacto es dependiente del sistema): Semaphores: T ID s 65536 KEY MODE OWNER 1297581835 --rw------- fherrero GROUP fherrero El comando ipcrm permite borrar un conjunto de semáforos desde la lı́nea de comandos (con la opción -s). 4.3. Semáforos Un semáforo es un objeto IPC, con una clave y un identificador asociados. Las operaciones que podremos realizar sobre él son signal o post y wait. Os remito a la parte teórica de la asignatura para entender qué hace cada operación. 2 Veamos con un pequeño ejemplo cómo crear un conjunto de semáforos y cómo operar con él: #include #include #include #include <s y s / t y p e s . h> <s y s / i p c . h> <s y s /sem . h> <s t d i o . h> int main ( ) { k e y t key = 3 4 ; int semid = semget ( key , 1 , IPC CREAT | 0 6 0 0 ) ; s e m c t l ( semid , 0 , SETVAL, 0 ) ; struct sembuf o p e r a t i o n ; o p e r a t i o n . sem num = 0 ; o p e r a t i o n . sem op = −1; operation . sem flg = 0; semop ( semid , &o p e r a t i o n , 1 ) ; p r i n t f ( ‘ ‘ Hasta a q u i l l e g a m o s \n ’ ’ ) ; return 0 ; } La función semget() nos devuelve el identificador de un objeto de tipo “conjunto de semáforos”. La opción IPC CREAT le dice que lo cree si no existe ya (probad a quitarle la opción y utilizad una clave nueva). Los otros parámetros son una clave asociada al objeto IPC y el número de semáforos que contendrá este conjunto que queremos compartir. Con semctl() (como siempre, usad el man), podemos realizar varias operaciones, como inicializar el valor de un semáforo del conjunto (como en este caso) o eliminar el conjunto con IPC RMID. Consultad la página man para saber cómo. Compilad y ejecutad este ejemplo. ¿Qué pasa? ¿Que pasarı́a si en lugar de 0 pusiéramos 1 en el último argumento de semctl()? Probad lo mismo con el siguiente ejemplo: #include #include #include #include #include <s y s / t y p e s . h> <s y s / i p c . h> <s y s /sem . h> <s t d i o . h> < s t d l i b . h> void h i j o ( int semid ) { struct sembuf o p e r a t i o n ; 3 o p e r a t i o n . sem num = 0 ; o p e r a t i o n . sem op = 1 ; operation . sem flg = 0; p r i n t f ( ” Soy e l h i j o , e s p e r a n d o t r e s s e g un d o s \n” ) ; sleep (3) ; semop ( semid , &o p e r a t i o n , 1 ) ; exit (0) ; } int main ( ) { k e y t key = 3 4 ; int semid = semget ( key , 1 , IPC CREAT | 0 6 0 0 ) ; s e m c t l ( semid , 0 , SETVAL, 1 ) ; struct sembuf o p e r a t i o n ; o p e r a t i o n . sem num = 0 ; o p e r a t i o n . sem op = −1; operation . sem flg = 0; p i d t pid = f o r k ( ) ; i f ( p i d == 0 ) { h i j o ( semid ) ; } semop ( semid , &o p e r a t i o n , 1 ) ; p r i n t f ( ” Padre , h a s t a a q u i l l e g a m o s \n” ) ; return 0 ; } 4.4. Unicidad ¿Qué pasarı́a si ya existiese un conjunto de semáforos con la clave que nosotros queremos usar? Puede ser que, por casualidad, hayamos escogido una clave que ya está en uso. Con un poco de imaginación y la función ftok() podemos conseguir claves únicas que harán a nuestros programas más robustos. 4 4.5. Sincronización en hilos Los hilos siguen teniendo los mismos problemas de sincronización que los procesos, eso es algo inherente al paralelismo. Existen multitud de mecanismos de sincronización para hilos. Combinándolos se pueden resolver multitud de problemas de paralelismo. Uno de ellos son los objetos de exclusión mutua. Son similares a los semáforos en ciertos aspectos, aunque tienen algunas diferencias: 1. Un objeto de exclusión mutua sólo puede tener dos valores: libre o bloqueado. 2. Sólo puede hacer una operación de desbloqueo un hilo que ya lo haya bloqueado. Las exclusiones mutuas o mutexes se manejan con pthread mutex init, pthread mutex lock, pthread mutex unlock y pthread mutex destroy. Otro mecanismo que nos será muy útil para esta práctica son las condiciones. Los objetos de condición son un mecanismo útil para que un hilo indique a los demás que algo ha ocurrido. Como en el caso de los semáforos, un hilo puede quedarse a la escucha de lo que pasa con esa condición (pthread cond wait) y otro puede avisarle de que se ha cumplido la condición (pthread cond signal). Adicionalmente, con objetos de condición se puede avisar a varios hilos a la vez, usando pthread cond broadcast. Normalmente los objetos de condición se utilizan como complemento a una comprobación de alguna variable. Imagina que un hilo quiere comprobar si se ha cumplido algo, y si todavı́a no ha ocurrido, que alguien le avise cuando eso pase (pseudocódigo): if(variable == algun_valor){ variable = otro_valor } else{ espera a que variable alcance algun_valor } Como variable será una variable compartida, tenemos que garantizar que los accesos no generan problemas. Para ello hay que utilizar siempre mutexes que protejan el acceso a variable: Bloquea mutex de variable if(variable == algun_valor){ variable = otro_valor libera mutex } 5 else{ libera mutex y espera a que variable alcance algun_valor } 5. Comprobación de errores Aunque sea redundante, vuelvo a insistir en la necesidad de comprobar errores: si queremos reservar algo y luego lo utilizamos, tenemos que asegurarnos de que lo hemos reservado bien, etc. Para ello comprobad en las páginas man los valores devueltos de las llamadas al sistema. Si ahora miráis la salida de ipcs veréis que todos los recursos IPC que hayáis creado ejecutando vuestras prácticas siguen ahı́, no se liberan automáticamente. Por eso es muy importante esta vez que los libere vuestro proceso (siempre lo es, pero esta vez con razón). Tenéis que tener cuidado, eso sı́, de no borrarlos antes de que el resto de procesos hayan terminado de usarlos. Más páginas man interesantes relacionadas con el control de errores: errno(3) y perror(3), en la sección 3 del manual. 6. Ejercicios 6.1. Conocimiento Cada pregunta vale 1 punto. Serán valoradas como correctas o incorrectas. ¿Por qué pthread cond wait libera un mutex antes de bloquear al hilo? ¿Se podrı́a hacer también usando pthread mutex unlock? ¿Qué es un esquema de sincronización productor/consumidor ? ¿Qué llamada al sistema y con qué parametros utilizarı́as para obtener el valor que tiene un semáforo IPC? ¿Cómo se puede averiguar en linux cuántas CPUs tiene la máquina? Argumenta tu respuesta con un ejemplo real. 6.2. Experimentación Las máquinas del laboratorio tienen dos CPUs. ¿Por qué? ¿Por qué es mejor tener dos que una? Teniendo en cuenta los dolores de cabeza que da asegurarse de que las cosas se ejecutan en su orden, ¿realmente merece la pena tener más de una? Vamos a intentar comprobarlo. 6 (2 puntos) Vamos a escribir un programa que cree varios hilos y vaya repartiendo una tarea entre ellos. En concreto, queremos calcular la función f (x) = exp(x) − 1/ exp(x) entre −3 y 3. El programa deberá recibir como argumentos por lı́nea de comandos el número de hilos a crear y el número de puntos a evaluar de la función. Es decir, la siguiente lı́nea: $> program 4 1000000 creará cuatro hilos y evaluará la función f (x) en un millón de puntos en el intervalo [−3, 3). La manera de repartir las tareas entre los hilos es fácil: cada hilo deberá calcular un tramo de la función, por ejemplo el hilo 0 calculará [−3, −1,5), el hilo 1 [−1,5, 0), etc. Cada hilo imprimirá por pantalla el valor de x y el valor de f (x), de tal forma que al final se pueda comprobar que se está calculando correctamente la función. El ejercicio consiste en estudiar el comportamiento del programa en función del número de hilos y la complejidad del problema. Vamos a usar el comando time para ver cuánto tiempo tarda en ejecutarse en cada caso. Puedes escribir un script de shell como el siguiente: for t h r e a d s i n 1 2 4 8 do i t e r a t i o n s =1000 f o r n i n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 do i t e r a t i o n s=$ ( ( $ i t e r a t i o n s ∗ 2 ) ) echo −n $ t h r e a d s $ i t e r a t i o n s ; time prog0 $ t h r e a d s $ i t e r a t i o n s > out done echo done Representa gráficamente los tiempos de usuario y tiempos de sistema y contesta razonadamente a las siguientes preguntas: • ¿Compensa el uso de hilos frente a un proceso monohilo? (0,5 puntos) • ¿Qué significa el tiempo de usuario? (0,25 puntos) • ¿Qué significa el tiempo de sistema? (0,25 puntos) • ¿Cómo explicarı́as el comportamiento del sistema? (0,5 puntos) 6.3. Programación Vamos a experimentar con los semáforos y las exclusiones mutuas. Las utilizaremos para resolver algunos problemas comunes de sincronización. 7 Para este ejercicio utilizarás hilos. Escribe un programa que cree varios hilos. Cada uno de ellos escribirá su identificador. Deberás diseñar un mecanismo de sincronización de tal forma que cada hilo pueda ejecutarse cuando quiera, pero deberán esperar al resto de los hilos antes de volver a imprimir su identificador. (3 puntos) 8