Differences

This shows you the differences between two versions of the page.

Link to this comparison view

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. Avem acces exclusiv la datele partajate deci am rezolvat problema coruperii datelor din cauza accesului concurent neprotejat.+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> 
 +./​thread-list-app-mutex 
 +</​code>​ 
 + 
 +Avem acces exclusiv la datele partajate deci am rezolvat problema coruperii datelor din cauza accesului concurent neprotejat.
  
 ==== 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țîn directorul ​''​spinlock-mutex/''​. +Cele două fișiere au fost generate din acelaș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 ​contabiliza timpul de rulare rulă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 ​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-urilorau loc în user space.
  
 ==== Race condition (TOCTTOU) ==== ==== Race condition (TOCTTOU) ====
Line 73: 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 79: 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 84: 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 97: 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>​
- 
so/cursuri/curs-09.1397497640.txt.gz · Last modified: 2014/04/14 20:47 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0