This shows you the differences between two versions of the page.
so:cursuri:curs-09 [2014/04/14 20:47] razvan.deaconescu [Nevoie de acces exclusiv] |
so:cursuri:curs-09 [2019/04/15 01:04] (current) razvan.deaconescu |
||
---|---|---|---|
Line 1: | Line 1: | ||
====== Curs 09 - Sincronizare ====== | ====== Curs 09 - Sincronizare ====== | ||
- | <html> | + | * [[http://elf.cs.pub.ro/so/res/cursuri/SO%20-%20Curs%2009%20-%20Sincronizare.pdf|Curs 09 - Sincronizare (PDF)]] |
- | <iframe src="http://docs.google.com/viewer?url=http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf&embedded=true" width="600" height="480" style="border: none;"> | + | |
- | </iframe> | + | |
- | </html> | + | |
- | * [[http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf | Curs 09 - Sincronizare (PDF)]] | + | * [[https://drive.google.com/open?id=1PCJp2UD0KSuAOFJ9ZMkB_lMhR9jJ4exD7Pgm-vzSrQA|Notițe de curs]] |
* Suport curs | * Suport curs | ||
- | * Operating Systems Concepts | + | * Operating Systems Concepts Essentials |
* Capitolul 6 - Process Synchronization | * Capitolul 6 - Process Synchronization | ||
* Modern Operating Systems | * Modern Operating Systems | ||
Line 15: | Line 12: | ||
* Secțiunea 2.3 - Interprocess Communication | * Secțiunea 2.3 - Interprocess Communication | ||
* [[http://greenteapress.com/semaphores/|Allen B. Downey - The Little Book of Semaphores]] | * [[http://greenteapress.com/semaphores/|Allen B. Downey - The Little Book of Semaphores]] | ||
+ | * [[https://deadlockempire.github.io/|The Deadlock Empire: Slay dragons, master concurency!]] | ||
+ | * [[http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf|Curs 09 anterior]] | ||
+ | |||
+ | |||
+ | <html> | ||
+ | <center> | ||
+ | <iframe src="https://docs.google.com/viewer?url=https://elf.cs.pub.ro/so/res/cursuri/SO%20-%20Curs%2009%20-%20Sincronizare.pdf&embedded=true" width="600" height="480" style="border: none;"> | ||
+ | </iframe> | ||
+ | </center> | ||
+ | </html> | ||
===== Demo-uri ===== | ===== Demo-uri ===== | ||
Line 44: | Line 51: | ||
</note> | </note> | ||
- | Eroare este cauzată de coruperea pointerilor din lista înlănțuită a programului. Datele sunt accesate concurent, iar în absența sincronizării, vor fi corupte. | + | Eroarea este cauzată de coruperea pointerilor din lista înlănțuită a programului. Datele sunt accesate concurent, iar în absența sincronizării, vor fi corupte. |
Soluția este să folosim un mutex pentru protejarea accesului la listă, lucru realizat în programul ''thread-list-app-mutex''. Dacă vom rula de mai multe ori programul ''thread-list-app-mutex'', nu vom obține niciodată eroare:<code bash> | Soluția este să folosim un mutex pentru protejarea accesului la listă, lucru realizat în programul ''thread-list-app-mutex''. Dacă vom rula de mai multe ori programul ''thread-list-app-mutex'', nu vom obține niciodată eroare:<code bash> | ||
Line 54: | Line 61: | ||
==== Consum de timp mutex și spinlock ==== | ==== Consum de timp mutex și spinlock ==== | ||
- | Dorim să investigăm overheadul produs când folosim spinlock-uri și mutex-uri pentru acces exclusiv. Pentru aceasta accesăm subdirectorul ''spinlock-mutex/''; urmărim conținutul fișierului ''spinlock-mutex.c''. Acest fișiere folosește spinlock-uri sau mutex-uri pentru asigurarea accesului exclusiv. | + | Dorim să investigăm overheadul produs când folosim spinlock-uri și mutex-uri pentru acces exclusiv. Pentru aceasta accesăm subdirectorul ''spinlock-mutex/''; urmărim conținutul fișierului ''spinlock-mutex.c''. Acest fișier are implementare didactică în care folosește spinlock-uri sau mutex-uri pentru asigurarea accesului exclusiv. |
Compilăm cele două programe folosind ''make''. Rezultă două fișiere în format executabil: ''spinlock'' și ''mutex''. | Compilăm cele două programe folosind ''make''. Rezultă două fișiere în format executabil: ''spinlock'' și ''mutex''. | ||
- | * Intrați în directorul ''spinlock-mutex/''. | + | Cele două fișiere au fost generate din același cod sursă (''spinlock-mutex.c''), după cum a fost definit sau nu macro-ul ''USE_SPINLOCK''. Macro-ul îl definim în fișierul ''Makefile''. |
- | * Consultați fișierul ''spin.c''. | + | |
- | * Urmăriți efectul macro-ului ''USE_SPINLOCK''. | + | Pentru a contabiliza timpul de rulare rulăm cele două executabile prin comanda ''time'':<code bash> |
- | * Consultați fișierul ''Makefile''. | + | /usr/bin/time ./spinlock |
- | * Observați unde este definit, ca opțiune de compilare, macro-ul ''USE_SPINLOCK''. | + | /usr/bin/time ./mutex |
- | * Folosiți comanda ''make'' pentru a obține două executabile: ''spin'' și ''mutex''. | + | |
- | * Rulați cele două executabile prin comanda ''time'' pentru a contabiliza timpul de rulare:<code bash> | + | |
- | time ./spin | + | |
- | time ./mutex | + | |
</code> | </code> | ||
- | * Care comandă a durat mai mult? De ce? | + | |
- | * De ce comanda ''./spin'' nu petrece foarte mult în kernel space (//system time// -- ''sys'' în output-ul ''time'')? | + | Observăm că folosirea spinlock-urilor pentru accesul exclusiv la resurse rezultă într-un timp de rulare mai mic decât folosirea mutex-urilor. Aceasta se întâmplă pentru că avem regiune critică mică iar overhead-ul cauzat de operații pe mutex-uri este semnificativ mai mare decât cel cauzat de operații pe spinlock-uri. |
+ | |||
+ | Observăm din output-ul comenzii ''time'' că folosirea mutex-urilor înseamnă semnificativ mai multe schimbări de context: o operație de tip lock pe mutex are șanse mari să blocheze thread-ul curent și să invoce planificatorul pentru schimbarea contextului. De asemenea, observăm că programul ce folosește spinlock-uri petrece mai mult timp în user space (//User time//) și mai puțin în kernel space (//System time//). Aceasta se întâmplă pentru că implementarea de spinlock-uri este realizată în user space și toate acțiunile (inclusiv partea de busy waiting aferentă spinlock-urilor) au loc în user space. | ||
==== Race condition (TOCTTOU) ==== | ==== Race condition (TOCTTOU) ==== | ||
Line 77: | Line 82: | ||
Compilăm programul folosind ''make''. Rezultă fișierul ''tocttou'' în format executabil. | Compilăm programul folosind ''make''. Rezultă fișierul ''tocttou'' în format executabil. | ||
+ | În cadrul programului ''tocttou'' nu folosim sincronizare și deci este posibil să avem condiții de cursă. Pentru a observa acest lucru rulăm de mai multe ori programul până când obținem o valoare negativă pentru numărul de elemente, lucru imposibil la o rulare obișnuită a programului:<code bash> | ||
+ | ./tocttou | ||
+ | Created 50 producers. | ||
+ | Each producer creates one item. | ||
+ | Created 10 consumers. | ||
+ | Each producer removes one item. | ||
+ | Num items at the end: -2 | ||
+ | </code> | ||
+ | |||
+ | Aceasta are loc întrucât avem o perioadă între timpul de verificare, adică linia<code c> | ||
+ | if (num_items >= NUM_ITEMS_CONSUMER) { | ||
+ | </code> | ||
+ | și timpul de utilizare, adică linia<code c> | ||
+ | num_items -= NUM_ITEMS_CONSUMER; /* Consume. */ | ||
+ | </code> | ||
+ | adică o condiție de cursă de tipul TOCTTOU, în care se poate "infiltra" alt thread. Practic este posibil ca două thread-uri să scadă valoarea variabilei ''num_items'' deși condiția ar fi trebuit să se întâmple doar pentru unul. Dacă avem o valoare inițială ''NUM_ITEMS_CONSUMER + 2'', atunci există riscul ca mai multe thread-uri să vadă îndeplinită condiția și apoi toate să scadă valoarea variabilei ''num_items'' rezultând într-o valoare negativă. | ||
+ | |||
+ | O astfel de situație poate duce la un comportament nedeterminst al programului, la coruperea datelor, chiar la încheierea cu eroare a programului. Mai mult o astfel de situație poate fi exploatată de un atacator. | ||
+ | |||
+ | Pentru a preveni apariția condițiilor de cursă, trebuie implementată corespunzător sincronizarea accesului. Din păcate, condițiile de cursă pot apărea foarte greu (adică programul să fie greșit dar să meargă **aproape** tot timpul); acest lucru face nedeterminist comportamentul programului și dificilă investigarea problemei. | ||
==== Deadlock ==== | ==== Deadlock ==== | ||
Line 83: | Line 108: | ||
Compilăm programul folosind ''make''. Rezultă fișierul ''deadlock'' în format executabil. | Compilăm programul folosind ''make''. Rezultă fișierul ''deadlock'' în format executabil. | ||
+ | La rularea programului se va genera deadlock:<code bash> | ||
+ | ./deadlock | ||
+ | </code> | ||
+ | Dacă nu se întâmplă acest lucru rulăm programul de mai multe ori. | ||
+ | |||
+ | Deadlock-ul are loc pentru că cele două mutex-uri folosite în program (''xmutex'' și ''ymutex'') nu sunt achiziționate în aceeași ordine. Unele thread-uri (cele care execută funcția ''xfirst'') achiziționează mutex-urile în ordinea ''xmutex'', ''ymutex''; celelalte thread-uri (cele care execută funcția ''yfirst'') achiziționează mutex-urile în ordinea ''ymutex'', ''xmutex''. În acest fel, la un moment dat un thread va achiziționa mutex-ul ''xfirst'' și imediat după un altul va achiziționa ''ymutex''. În continuare ambele thread-uri vor aștepta după un mutex deținut de alt thread. | ||
+ | |||
+ | <note important> | ||
+ | Pentru a evita apariția deadlock-urilor în momentul în care folosim lock-uri/mutex-uri, o condiție importantă este ca lock-urile să fie achiziționate în aceeași ordine. | ||
+ | </note> | ||
==== Așteptare nedefinită ==== | ==== Așteptare nedefinită ==== | ||
Line 88: | Line 123: | ||
Compilăm programul folosind ''make''. Rezultă fișierul ''indefinite-wait'' în format executabil. | Compilăm programul folosind ''make''. Rezultă fișierul ''indefinite-wait'' în format executabil. | ||
+ | |||
+ | Pentru a observa comportamentul programului îl rulăm:<code bash> | ||
+ | ./indefinite-wait | ||
+ | </code> | ||
+ | Observăm că programul se blochează, deci fie avem deadlock fie unele thread-uri așteaptă fără a fi trezite/notificate. Având un singur mutex folosit corezpunzător, problema nu poate fi deadlock deci este vorba de o așteptare nedefinită. | ||
+ | |||
+ | La o investigație atentă observăm că atât thread-urile producător cât și cele consumator vor aștepta la variabilele condiție aferente (''buffer_empty_cond'' și ''buffer_full_cond'') fără a fi trezite. Nu există apeluri ale funcției [[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_cond_signal.html|pthread_cond_signal]] necesare pentru trezirea thread-urilor. | ||
+ | |||
+ | Rezolvăm această problemă prin adăugarea apelurilor necesare în cadrul funcțiilor ''produce'' și ''consume'' rezultând forma actualizată:<code c> | ||
+ | static void *produce(void *arg) | ||
+ | { | ||
+ | size_t idx = (size_t) arg; | ||
+ | |||
+ | pthread_mutex_lock(&pc_mutex); | ||
+ | if (pc_buffer.size >= MAX_ITEMS) | ||
+ | pthread_cond_wait(&buffer_full_cond, &pc_mutex); | ||
+ | pc_buffer.storage[pc_buffer.size] = ITEM; | ||
+ | pc_buffer.size++; | ||
+ | pthread_cond_signal(&buffer_empty_cond); | ||
+ | printf("Producer %zu created item.\n", idx); | ||
+ | pthread_mutex_unlock(&pc_mutex); | ||
+ | |||
+ | return NULL; | ||
+ | } | ||
+ | |||
+ | static void *consume(void *arg) | ||
+ | { | ||
+ | size_t idx = (size_t) arg; | ||
+ | |||
+ | pthread_mutex_lock(&pc_mutex); | ||
+ | if (pc_buffer.size == 0) | ||
+ | pthread_cond_wait(&buffer_empty_cond, &pc_mutex); | ||
+ | pc_buffer.storage[pc_buffer.size] = NO_ITEM; | ||
+ | pc_buffer.size--; | ||
+ | pthread_cond_signal(&buffer_full_cond); | ||
+ | printf("Consumer %zu removed item.\n", idx); | ||
+ | pthread_mutex_unlock(&pc_mutex); | ||
+ | |||
+ | return NULL; | ||
+ | } | ||
+ | </code> | ||
+ | În cele de mai sus, înainte de operația de unlock pe mutex am apelat funcția [[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_cond_signal.html|pthread_cond_signal]]. Cu ajutorul acestei funcții, thread-ul producător ce tocmai a produs un element trezește un thread consumator care acum are ce consuma; în mod similar thread-ul consumator care a consumat un element trezește un thread producător care acum are loc unde să producă un element. | ||
+ | |||
+ | După actualizarea programului, recompilăm folosind ''make'' și apoi îl rulăm. Acum programul va funcționa corespunzător fără problema așteptării nedefinite. | ||
==== Granularitate regiune critică ==== | ==== Granularitate regiune critică ==== | ||
Line 101: | Line 180: | ||
</code> | </code> | ||
- | Observăm că dureaza semnificativ mai mult rularea executabilului cu granularitate fină. Acest lucru se întâmplă pentru că regiunea critică pe care acesta o protejează este mic, iar cea mai mare parte din timp o va consuma în operațiile cu mutex-ul (//lock contention//). Observăm acest lucru și din numărul mare de schimbări de context (voluntare sau nevoluntare). | + | Observăm că dureaza semnificativ mai mult rularea executabilului cu granularitate fină. Acest lucru se întâmplă pentru că regiunea critică pe care acesta o protejează este mică, iar cea mai mare parte din timp o va consuma în operațiile cu mutex-ul (//lock contention//). Observăm acest lucru și din numărul mare de schimbări de context (voluntare sau nevoluntare). |
<note> | <note> | ||
În general preferăm granularitate fină pentru regiunile critice (adică regiuni critice de mică dimensiune); dar granularitatea fină poate înseamna operații foarte dese pe mutex (//lock contention//). De aceea este recomandat ca folosirea granularității fine să fie echilibrată de un număr redus de thread-uri care să dorească să acceseze la un moment dat regiunea critică, pentru a genera cât mai puțină încărcare (//contention//) pe mutex. | În general preferăm granularitate fină pentru regiunile critice (adică regiuni critice de mică dimensiune); dar granularitatea fină poate înseamna operații foarte dese pe mutex (//lock contention//). De aceea este recomandat ca folosirea granularității fine să fie echilibrată de un număr redus de thread-uri care să dorească să acceseze la un moment dat regiunea critică, pentru a genera cât mai puțină încărcare (//contention//) pe mutex. | ||
</note> | </note> | ||
- |