Sistemas Operativos 6 Sincronización de Procesos Prof. Javier Cañas R. Nota • El texto guía es: Operating System Concepts, Eight Edition, Avi Silberschatz, Peter Baer Galvin, Greg Gagne • Estas PPT están basadas en las PPT originales que el autor del texto guía mantiene en: http://www.os-book.com/ Copyright Note The slides below are copyright Silberschatz, Galvin and Gagne, 2008. The slides are authorized for personal use, and for use in conjunction with a course for which Operating System Concepts is the prescribed text. Instructors are free to modify the slides to their taste, as long as the modified slides acknowledge the source and the fact that they have been modified. Paper copies of the slides may be sold strictly at the price of reproduction, to students of courses where the book is the prescribed text. Any use that differs from the above, and any for profit sale of the slides (in any form) requires the consent of the copyright owners; contact Avi Silberschatz ([email protected]) to obtain the copyright owners consent. Temario 1. Conceptos 2. El Problema de la Sección Crítica 3. La Solución de Peterson 4. Sincronización por Hardware 5. Semáforos 6. Problemas Clásicos ... Temario 7. Monitores 8. Sincronización en Linux 9. Sincronización de Pthreads Objetivos • Introducir el problema de la sección crítica cuyas soluciones se utilizan para asegurar la consistencia de datos compartidos por varios procesos. • Presentar soluciones se software y hardware para solucionarlo. 1 Conceptos • El acceso concurrente a datos compartidos puede generar inconsistencia en los datos. • La mantención de consistencia en los datos requiere de mecanismos para asegurar la ejecución ordenada de procesos que cooperan. Ejemplo • Consideremos el problema del Productor Consumidor con un buffer circular. Queremos una solución que utilice todas las entradas del buffer. Podríamos utilizar una variable count que registre el número de bufers ocupados. Inicialmente está en cero. Se incrementa cuando se agrega un nuevo item y se decrementa cuando se consume uno. Solución Capítulo 3 while (true) { /* Produce an item */ Productor while (((in + 1) % BUFFER SIZE ) ; == out) /* do nothing -- no free buffers */ buffer[in] = item; in = (in + 1) % BUFFER SIZE; } while (true) { while (in == out) Consumidor ; // do nothing -- nothing to consume // remove an item from the buffer item = buffer[out]; out = (out + 1) % BUFFER SIZE; return item; } Usando contador while (true) { /* Productor produce an item and put in nextProduced while (count == BUFFER_SIZE) ; // do nothing buffer [in] = nextProduced; in = (in + 1) % BUFFER_SIZE; count++; } */ ... Usando contador while (true) Consumidor { while (count == 0) ; // do nothing nextConsumed = out = (out + 1) % BUFFER_SIZE; } buffer[out]; count--; /* consume the item in nextConsumed Condiciones de carreras (race condition) count++ could be implemented as register1 = count register1 = register1 + 1 count = register1 count-- could be implemented as register2 = count register2 = register2 - 1 count = register2 Consideremos la ejecución intercalada con “count = 5” inicialmente: S0: productor ejecuta register1 = count {register1 = 5} S1: productor ejecuta register1 = register1 + 1 {register1 = 6} S2: consumidor ejecuta register2 = count {register2 = 5} S3: consumidor ejecuta register2 = register2 - 1 {register2 = 4} S4: productor ejecuta count = register1 {count = 6 } S5: consumidor ejecuta count = register2 {count = 4} Se llega al resultado incorrecto count==4 Soluciones al problema de la Sección Crítica • Se deben cumplir 3 condiciones: 1. Exclusión Mutua: Si el proceso Pi se está ejecutando en su sección crítica, ningún otro proceso puede estar ejecutándose en su sección crítica. 2. Progreso: Si ningún proceso está ejecutándose en su sección crítica y existen procesos que desean entrar a su sección crítica, entonces, la selección de los procesos que deben entrar a su sección crítica después no puede posponerse indefinidamente. ... Soluciones 3. Espera acotada: Debe existir una cota sobre el número de veces que otro proceso tiene permitido entrar a su sección crítica después que un proceso a pedido ingresar a su sección crítica y antes que sea satisfecha. • Adicionalmente: • Los procesos se ejecutan a velocidades distintas de cero • No se asume nada respecto a velocidades relativas de los N procesos 2 El Problema de la Sección Crítica • Cuando un proceso se está ejecutando en su sección crítica, ningún otro proceso tiene permitido ejecutarse en su sección crítica. • No pueden existir dos procesos en su sección crítica al mismo tiempo. do { entry section critical section exit section ! remainder section } while (TRUE) 3 La Solución de Peterson • • Esta solución funciona sólo para dos procesos. • Los procesos comparten dos variables: Se asume que las instrucciones LOAD y STORE son atómicas, es decir, no pueden ser interrumpidas. • • int turn; /*indica turno para entrar a sc*/ boolean flag; /* flag[i]=true indica que Pi está listo para ingresar*/ Algoritmo para Proceso Pi do { ! ! ! ! flag[i] = TRUE; turn = j; ! ! while (flag[j] && turn == j); ! ! ! ! ! ! ! flag[i] = FALSE; ! remainder section critical section ! } while (TRUE); ! 4 Sincronización por HW • La mayoría de los sistemas proveen soporte de hardware para proteger secciones críticas de código. • Uni procesadores: pueden inhibir las interrupciones: • • El código que corre, se puede ejecutar sin interrupciones Muy ineficiente para multiprocesadiores. No es ampliamente escalable ... HW • Arquitecturas modernas proveen instrucciones especiales de máquinas que tienen atomicidad (no son interrumpibles). • Las instrucciones más comunes son: • Test and set: lee una palabra de memoria y fija un valor • Swap: intercambia el contenido de dos palabreas de memoria Solución usando candados (Locks) do { ! ! cerrar ! ! ! ! ! ! abrir critical section remainder section ! } while (TRUE); ! ! ! La instrucción Test and Set • Definición: boolean TestAndSet (boolean *target) { boolean rv = *target; *target = TRUE; return rv: } • v ← test_and_set(x) • El valor de x se copia en v y el valor TRUE se asigna a x dentro del (IBM/360) . mismo ciclo de lectura escritura Solución usando Test and Set • Los procesos comparten la variable booleana lock que es inicializada FALSE. • Solución: do { while ( TestAndSet (&lock )) ; // do nothing // critical section lock = FALSE; // remainder section } while (TRUE); La instrucción Swap • Definición: void Swap (boolean *a, boolean *b) { boolean temp = *a; *a = *b; *b = temp: } Solución usando Swap • Los procesos comparten la variable booleana lock que es inicializada FALSE. Cada proceso tiene una variable local boolena llamada key. • Solución: do { key = TRUE; while ( key == TRUE) Swap (&lock, &key ); // critical section lock = FALSE; // remainder section } while (TRUE); Discusión • Tanto Test and Set como Swap, satisfacen el requerimiento de exclusión mutua de solución del problema de la sección crítica, pero no satisfacen el requerimiento de espera acotada. • Tanto, Test and Set como Swap requieren “busy waiting”, es decir mientras esperan el ingreso, ocupan CPU. • Podría ocurrir que los procesos que esperan estén un tiempo indefinido tratando de ingresar. • Veremos una solución aplicada a Test and Set. Incorporación de espera acotada a Test and Set ! do { ! ! waiting[i] = TRUE; ! ! key = TRUE; ! ! while (waiting[i] && key) ! ! ! ! ! waiting[i] = FALSE; ! ! ! ! ! j = (i + 1) % n; ! ! while ((j != i) && !waiting[j]) ! ! ! ! ! if (j == i) ! ! ! ! ! else ! ! ! waiting[j] = FALSE; ! ! ! // remainder section key = TestAndSet(&lock); // critical section j = (j + 1) % n; lock = FALSE; ! } while (TRUE); 5 Semáforos • Los semáforos son herramientas de sincronización que no requieren “busy waiting”. • • Un semáforo S contiene una variable entera. • Se definen dos operaciones que modifican S: • • wait() (originalmente P()) signal() (originalmente V()) Es más simple que usar Test and Set ... Semáforos • Variables semáforos sólo son accesibles vía dos operaciones indivisibles (atómicas): wait (S) { while S <= 0 ! ! ; // no-op S--; } signal (S) { S++; } Semáforos como herramienta general de sincronización • Hay dos tipos de semáforos: • Contadores: valor entero sobre un dominio sin restricción. • Binarios: sólo valor entero 1 o 0. Simple de implementar. También se conoce como mutex. • Es posible implementar un semaforo binario con un semáforo contador. Exclusión mutua con semáforo binario Semaphore mutex; // initialized to 1 do { ! wait (mutex); // Critical Section signal (mutex); !! // remainder section } while (TRUE); Implementación de Semáforos • La implementación directa de la definición de semáforo presenta “busy waiting”. El proceso que espera gasta ciclos de CPU que podría aprovechar otro proceso. • Estos semáforos se denominan “spinlock” porque quedan “dando vueltas” mientras esperan. Lo bueno es que no genera “Context Switch”. • Para evitar “busy waiting” es necesario modificar la definición de semáforo. Implementación de Semáforos sin “Busy Waiting” cada semáforo se asocia una cola de espera. • Acada entrada en la cola de espera tiene dos items de datos: • • • valor (entero) puntero al siguiente record de la lista Se definen dos operaciones: • • block: pone al proceso que la invoca en una cola de espera apropiada. wakeup: saca un proceso de la cola de espera y lo pone en la cola ready. ... Implementación Implementation of wait: ! ! ! ! ! ! ! ! ! ! ! ! wait(semaphore *S) { ! S->value--; ! if (S->value < 0) { ! ! add this process to S->list; ! ! block(); ! } } Implementation of signal: ! ! ! ! ! ! ! ! ! ! ! ! signal(semaphore *S) { ! S->value++; ! if (S->value <= 0) { ! ! remove a process P from S->list; ! ! wakeup(P); ! } Ejemplo de uso en UNIX • El siguiente ejemplo muestra la forma de utilizar semáforos en UNIX. • El programa fuente se llama sem-ex.c • Para compilar y dejar el ejecutable en sem-ex: gcc -o sem-ex sem-ex.c -Wall -Werror -lpthread • Se trata de entender el código: ¿Qué escribe? definiciones main() thread salida Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread Thread 0: 0: 0: 0: 0: 0: 1: 1: 1: 1: 1: 1: Waiting to enter critical region... Now in critical region... Counter Value: 0 Incrementing Counter... New Counter Value: 1 Exiting critical region... Waiting to enter critical region... Now in critical region... Counter Value: 1 Incrementing Counter... New Counter Value: 2 Exiting critical region... Deadlock y Starvation • Abrazo mortal y Inanición son dos problemas que pueden generar un uso no cuidadoso de semáforos. • Deadlock: dos o más procesos quedan esperando indefinidamente por un evento que sólo lo puede generar un proceso que está en espera. • Starvation: bloqueo indefinido. Un proceso nunca es sacado de la cola en la cual está esperando Prioridad inversa • Esta anomalía se produce por itineración cuando un proceso de baja prioridad retiene el paso de uno de mayor prioridad. • Ejemplo: procesos A, B, C con prioridades A < B < C. Supongamos que C necesita el recurso R que está asignado a A. C debería esperar que A libere R, pero ahora es itinerado B que interrumpe a C. • En síntesis el proceso con menor prioridad, paraliza a uno de mayor prioridad. Ejemplos de deadlock • Sean S y Q dos semáforos inicializados en 1: P0! P1 ! ! wait (S); ! wait (Q); ! ! ! ! wait (Q); ! . ! wait (S); ! ! ! ! . . ! ! . ! . ! ! . ! ! signal ! ! signal (Q); ! (S); ! signal (Q); signal (S); 6 Problemas Clásicos de Sincronización • Los siguientes problemas se denominan clásicos y son tratados en (casi) todos los textos de Sistemas Operativos: • • • El Buffer de capacidad limitada Lectores y escritores Los Filósofos comensales El Buffer de capacidad limitada • Corresponde al problema del Productor Consumidor. • • • Un buffer tiene capacidad para N items de datos. • El semáforo empty se inicializa en N y cuenta el número de entradas vacías. Se inicializa mutex en 1 El semáforo full se inicializa en 0 y cuenta el número de entradas llenas Solución gráfica Buffer N P C full=0 empty=N mutex=1 ... Buffer: Productor do { // produce an item in nextp wait (empty); wait (mutex); // add the item to the signal (mutex); signal (full); } while (TRUE); buffer ... Buffer: Consumidor do { wait (full); wait (mutex); // remove an item from buffer to nextc signal (mutex); signal (empty); // consume the item in nextc } while (TRUE); Lectores y Escritores • Un conjunto de datos es compartido por un número de procesos concurrentes que son: • • Lectores: sólo leen, no pueden modificar nada Escritores: Pueden leer y escribir • El problema: permitir múltiples lectores al mismo tiempo. Sólo un escritor tiene acceso a los datos compartidos al mismo tiempo datos compartidos Conjunto de datos semaforo mutex=1 semaforo wrt=1 semaforo readcount=0 Estructura de proceso escritor do { wait (wrt) ; // writing is performed signal (wrt) ; } while (TRUE); Estructura de proceso escritor ! ! ! ! ! do { wait (mutex) ; readcount ++ ; if (readcount == 1) wait (wrt) ; signal (mutex) // reading is performed wait (mutex) ; readcount-- ; if (readcount == 0) ! signal (wrt) ; signal (mutex) ; } while (TRUE); Filósofos comensales • 5 filósofos comparten un Bowl de arroz. • Para comer cada uno utiliza 2 palillos (chinos). • Cada filósofo puede pensar o comer. Para comer necesita 2 palillos • Datos: Bowl de arroz • semaforo chopstick[5] inicializado en 1 ... Filósofos comensales Filósofo do { wait ( chopstick[i] ); wait ( chopStick[ (i + 1) %5] ); // eat signal ( chopstick[i] ); signal (chopstick[ (i + 1) % 5] ); // think } while (TRUE); Semáforos:discusión • El uso incorrecto puede generar errores. Por ejemplo: • signal(mutex) ........ wait(mutex): deja a muchos procesos en su sección crítica • wait(mutex) ........ wait(mutex): puede ocurrir deadlock • omitir wait(mutex) o signal(mutex)o ambos: se viola la exclusión mutua o se puede generar deadlock 7 Monitores • Un monitor es un tipo abstracto de datos que encapsula datos privados y proporciona métodos públicos • Es una abstracción de mayor nivel que los semáforos y proporcionan un mecanismo conveniente y efectivo para sincronizar procesos. • Sólo un proceso puede estar activo en el monitor al mismo tiempo. Estructura monitor monitor-name { !// shared variable declarations !procedure P1 (…) { …. } !! … !procedure Pn (…) {……} Initialization code ( ….) { … } !! … !} } Vista esquemática de un Monitor Variables de Condición • Los monitores proporcionan mecanismos adicionales de sincronización llamados variables de condición. • • condition x, y; Dos operaciones sobre condiciones: • • x.wait(): el proceso se suspende. Se bloquea por la condición x.signal(): continua uno de los procesos (si hay) que ha invocado x.wait(). Despierta a un proceso bloqueado sobre la condición. Monitor con variables de condición Solución al problema de filósofos comensales monitor DP { ! enum { THINKING, HUNGRY, EATING} state[5] ; ! condition self[5]; //i tiene hambre, pero debe // retardarse ! void pickup (int i) { ! state[i] = HUNGRY; ! test(i); ! if (state[i] != EATING) self[i].wait(); ! } ! void putdown (int i) { ! state[i] = THINKING; // test left and right neighbors ! test((i + 4) % 5); ! test((i + 1) % 5); }! ... Solución al problema void test (int i) { ! if ((state[(i + 4) % 5] != EATING) && ! (state[i] == HUNGRY) && ! (state[(i + 1) % 5] != EATING) ) { ! state[i] = EATING; ! ! self[i].signal(); ! } ! } ! ! ! } } initialization_code() { for (int i = 0; i < 5; i++) state[i] = THINKING; ... Solución al problema • Cada filósofo invoca las operaciones pickup() y putdown() en la siguiente secuencia: DiningPhilosophters.pickup(i); ....... EAT ......... DiningPhilosophers.putdown(i); Implementación de Monitores usando semáforos • Consideraremos una posible implementación de monitores utilizando semáforos. • Se asigna a cada monitor un semáforo mutex. Se usa para controlar el número de procesos en el monitor. • • Antes de entrar: wait(mutex) Después de abandonar el monitor: signal(mutex) ... Implementación • El semáforo next inicializado en 0 se utiliza como una cola de espera de procesos que están en el monitor después de haber sido liberados de una cola de condición por una operación signal de monitor. • next_count es una variable entera inicializada en 0 que cuenta los procesos durmiendo por el semáforo next. ... Implementación: Variables ! ! ! semaphore mutex; // (initially = 1) semaphore next; // (initially = 0) int next-count = 0; ... Implementación • Cada procedimiento F se reemplaza por: wait(mutex); .......... body of F; ........ if (next_count > 0) signal(next) // despierta a proceso else signal(mutex); • next_count sólo se modifica dentro de operaciones de condición. Si no hay condiciones basta wait(mutex) y signal (mutex). • Se asegura la exclusión mutua. ... Implementación • Para cada variable de condición x, tenemos: ! ! • ! ! ! ! ! ! semaphore x_sem; // (initially int x_count = 0; = 0) La operación x.wait() se puede implementar: ! ! ! ! ! ! x_count++; if (next_count > 0) ! signal(next); else ! signal(mutex); wait(x_sem); x_count--; ... Implementación • ! ! ! ! ! La operación x.signal() se puede implementar: ! ! ! ! ! if (x_count > 0) { ! next_count++; ! signal(x_sem); ! wait(next); ! next_count--; } Ejemplo: Monitor para asignar un recurso simple monitor ResourceAllocator { ! boolean busy; ! condition x; ! void acquire(int time) { ! ! if (busy) ! ! ! x.wait(time); ! ! busy = TRUE; ! } ! void release() { ! ! busy = FALSE; ! ! x.signal(); ! } initialization code() { ! busy = FALSE; ! } }! ! ! Cada proceso especifica el máximo de tiempo que planea ocupar el recurso.. El monitor asigna el recurso al proceso que requiere el menor tiempo de asignación. Un proceso que necesita acceso: R.acquire(t); ....... access_the_resource; ........ R.release(); 8 Sincronización en Linux • Antes del kernel versión 2.6, Linux desabilitaba las interrupciones para implementar secciones críticas cortas. • Las versiones 2.6 y posteriores son totalmente interrumplibles (preemtive). • Linux proporciona dos mecanismos: • • semáforos Spin locks (busy waiting) 9 Sincronización de Pthreads • • • Las API de Pthreads son independientes de SO Estas API proporcionan: • • Mutex locks Variables de condición Extensiones no portables incluyen: • • read-write locks Spin locks Sistemas Operativos 6 Sincronización de Procesos Prof. Javier Cañas R.