Diferite tipuri de încuietori sunt utilizate în programare pentru a proteja partea critică a codului de execuția simultană. Prin urmare, blocările sunt cel mai adesea folosite pentru protejarea fragmentelor de cod, mai degrabă decât a zonelor de date, deși, de exemplu, semafoarele (nu cele binare) sunt utilizate în principal pentru a restricționa accesul la date.
În acest articol, vom începe o discuție despre diferitele tipuri de încuietori disponibile la programarea modulelor kernel.
Tipuri de încuietori
Până la apariția și diseminarea pe scară largă a unui lapte praf degresat, atunci când paralelismul fizic nu a existat, de blocare utilizat în modul clasic (așa cum este descris de E. Dijkstra), protejând zonele critice ale programului prin executarea simultană a mai multor procese. Aceste mecanisme funcționează prin suprimarea proceselor solicitante în starea blocată până la momentul eliberării resurselor solicitate. Vom numi astfel de încuietori încuietori pasive, caz în care procesorul oprește executarea procesului curent la punctul de blocare și comută la executarea unui alt proces (eventual inactiv).
Un tip fundamental diferit de blocare activă a blocajelor a apărut împreună cu sistemele SMP atunci când procesorul, în timp ce așteaptă eliberarea unei resurse inaccesibile, nu este pus în stare blocată, ci efectuează cicluri goale. În acest caz, procesorul nu este lansat pentru a efectua un alt proces de așteptare în sistem, dar continuă executarea activă (cicluri "goale") în contextul ramurii de execuție curente.
Aceste două tipuri de încuietori (fiecare dintre acestea cuprinzând mai multe subspecii) diferă în mod fundamental în parametrii cheie:
- posibilitatea de a folosi blocare pasive (schimbare de context), nu poate fi decât o bucată de cod are propriul context (scris de sarcini), pe care le puteți întoarce mai târziu (activitate), și întrerupe sau manipulare Tasklets această condiție nu este îndeplinită;
- eficiență: blocare activă nu este pierde întotdeauna în performanța pasivă, din cauza contextului de comutare în sistem este un proces consumator de timp, astfel încât acestea pot fi chiar mai eficientă decât de așteptare pasivă pentru o perioadă scurtă de timp de blocare activă.
Semaphore (mutexuri)
Semaphorele kernel-ului sunt definite într-un fișier
Spinlocks permite să dețină blocarea doar o singură sarcină la un moment dat, dar numărul de sarcini pentru un semafor (număr), care sunt autorizate să dețină simultan cu ea (propriu semafor) poate fi setat la o declarație de variabilă în câmpul corespunzător al structurii semaforului:
În cazul în care valoarea de numărare este mai mare decât 1, semaforul se numește un semafor de numărare și permite numărul de fire, care sunt în același timp păstrate de blocare nu este mai mare decât valoarea contorului de utilizare (număr). Există situații în care numărul permis de fire, care poate stoca simultan semaforul este egal cu 1 (precum și pentru spin-blocare), și sunt numite semafoarele binare sau blochează reciproc exclusive (mutex, mutex, deoarece garantează accesul reciproc exclusive - excludere reciprocă). semafoarele binare (mutex) este cel mai des utilizat pentru a oferi acces reciproc exclusiv la fragmentele de cod, numit-o secțiune critică.
Indiferent dacă proprietarul terenului este definit, capturat mutex (așa cum se face în mod diferit în diferite sisteme de operare-POSIX), caracteristicile fundamentale ale mutex, spre deosebire de numărabilă este că semafor:
- Mutexul capturat va avea întotdeauna un singur proprietar care la capturat;
- pentru a elibera firele blocate pe mutex (pentru a elibera mutexul), doar un fir de mutex-aware poate.
În cazul unui semafor contabil, firele care sunt blocate pe semafor pot fi eliberate din oricare dintre firele care dețin semaforul.
Definirea statică și inițierea semaforilor se face printr-o macrocomandă:
Pentru a crea o blocare exclusivă (mutex), există o sintaxă mai scurtă:
- în cazul în care ambele cazuri numele este numele unei variabile de tip semafor.
Dar cel mai adesea semaphorele sunt create dinamic, ca parte integrantă a structurilor de date mai mari. În acest caz, se folosește următoarea funcție pentru a inițializa semaforul de numărare:
Și pentru inițializarea semaphorelor binare (mutexuri) se folosesc macrocomenzi:
În sistemul de operare Linux pentru a captura semaforul (mutex) este utilizat în jos de funcționare (). reducându-și contravaloarea cu unul. Dacă valoarea contorului este mai mare sau egală cu zero, atunci de blocare a fost capturat cu succes, iar sarcina poate intra în secțiunea critică. Dacă valoarea contorului (după decrementare) este mai mică decât zero, atunci lucrarea este plasată în coada de așteptare, iar procesorul continuă pentru a efectua alte sarcini. up () metoda este utilizată pentru a elibera semafoarelor (după secțiunea critică finalizare), executarea sa crește contorul unității semaforului, în care unul dintre fluxurile blocate disponibile pot captura de blocare (fundamentală este aceea că este imposibil de a influența modul în care un particular va fi selectat fluxul din numărul blocat). Următoarele sunt alte operații pe semafoare.
- void down (struct semaphore * sem) - transferă sarcina către starea de așteptare blocată cu semnalizatorul TASK_UNINTERRUPTIBLE. În cele mai multe cazuri, acest lucru nu este de dorit, deoarece procesul care așteaptă să fie eliberat semaforul nu va răspunde la semnale.
- int down_interruptible (struct semafor * sem) - încearcă să capteze semaphore. Dacă această încercare nu reușește, sarcina este pusă într-o stare blocată cu semnalizatorul TASK_INTERRUPTIBLE (în structura de sarcină). Această stare a procesului înseamnă că sarcina poate fi returnată la execuție utilizând un semnal, iar această capacitate este de obicei foarte valoroasă. Dacă semnalul apare într-un moment în care sarcina este blocată pe semafor, sarcina este returnată la execuție, iar funcția down_interruptible () returnează valoarea - EINTR.
- int down_trylock (struct semaphore * sem) - utilizat pentru captarea semaforului fără blocare. Dacă semaforul este deja capturat, funcția returnează imediat o valoare diferită de zero. Dacă semaforul este capturat cu succes, valoarea zero este returnată și blocarea este capturată.
- int jos_timeout (struct semaphore * sem, long jiffies) - folosit pentru a încerca să capteze un semafor în intervalul de timp jiffies de căpușe de sistem.
Spinlocks
O încercare de blocare pentru a intra într-o secțiune critică utilizând semnale înseamnă o traducere potențială a sarcinii într-o stare de blocare și schimbarea contextului, ceea ce reprezintă o operație costisitoare. Spin-lock-urile (spinlock_t) sunt utilizate pentru sincronizare în cazurile în care:
- contextul de execuție nu vă permite să intrați în starea blocată (contextul întreruperii);
- sau blocarea pe termen scurt este necesară fără comutarea contextului.
Ele reprezintă o așteptare activă a eliberării într-un ciclu gol. În cazul în care nevoia de sincronizare este asociată numai cu prezența mai multor procesoare din sistem, utilizarea spin-blocare, bazată pe o buclă simplă de așteptare pentru secțiunile critice mici. Spin-lock poate fi doar binar. Definiții referitoare la spinlock_t. distribuite pe mai multe fișiere antet:
Pentru a inițializa spinlock_t și tipul asociat rwlock_t. despre care vor fi discutate în detaliu mai jos, înainte de utilizarea (și în literatură) a macrocomenzilor:
Adică, aceste macrocomenzi sunt declarate neacceptate și pot fi excluse în orice versiune ulterioară. Prin urmare, noi macro-uri (echivalente în sens scris mai sus) sunt folosite pentru a defini și inițializa:
Aceste macrocomenzi sunt definiții statice ale unor variabile separate (autonome), cum ar fi spinlock_t. Și la fel ca și alte primitive, capacitatea de a inițializa dinamic declarat anterior variabilă (cel mai adesea, această variabilă este câmpul ca parte dintr-o structură mai complexă):
Interfața principală spinlock_t conține o pereche de apeluri pentru a captura și a elibera blocarea:
Dacă compilați kernel-ul nu a fost activat de către un SMP (multiprocesor) și codul nu este configurat kernel-ul de preemțiune (necesar ambele condiții), The spinlock_t nu compilează (și în locul lor va rămâne spațiu gol) datorită preprocessor directivelor compilare condiționată.
Notă. Spre deosebire de implementările în alte sisteme de operare, blocările de spin în sistemul de operare Linux nu sunt recursive. Aceasta înseamnă că codul afișat mai jos va duce automat la o situație de blocaj (procesorul va executa la infinit, iar fragmentul va avea loc degradarea sistemului ca numărul de procesoare disponibile în sistem este redus):
Recuperarea recursivă a spinlock-ului poate interveni implicit în dispozitivul de întrerupere, deci trebuie să dezactivați întreruperile pe procesorul local înainte de a captura o astfel de blocare. Acesta este un caz general, deci oferă o interfață specială:
Pentru blocarea spinului, sunt de asemenea definite apeluri ca:
- int spin_try_lock (spinlock_t * sl) - încercați să capturați fără a bloca, dacă blocarea este deja capturată, funcția va reveni la o valoare diferită de zero;
- int spin_is_locked (spinlock_t * sl) - Returnează o valoare diferită de zero dacă blocarea este în prezent blocată.
Blocări de citire și scriere
Special, dar frecvent scenariu, caz de sincronizare sunt „citire-scriere“. „Cititorii“ citit doar de stat a unor resurse, și, prin urmare, poate avea un acces concurent comun la acesta. „Scriitori“ schimba starea resurselor, și așa mai departe scriitorul trebuie să aibă acces exclusiv la resursa, și de resurse de citire pentru toți cititorii în acest moment la fel de bine să fie blocate. Pentru a pune în aplicare încuietori de citire-scriere în kernel-ul Linux, există versiuni separate ale semafoarelor și încuietori de spin. mutex în timp real nu au de punere în aplicare adecvate pentru utilizare în acest scenariu.
Notă. Rețineți că exact aceleași funcționalități pot fi obținute folosind primitive de sincronizare clasice (mutex sau spinlock), pur și simplu capturarea secțiunii critice, indiferent de tipul de operație viitoare. Broastele de citire și scriere sunt introduse din motive de eficiență pentru un scenariu de masă al unei astfel de utilizări.
Pentru semafoare, în locul structurii semaforului struct, se introduce structura structului rw_semaphore. și un set de funcții de interfață pentru captură / eliberare (simple down () / up ()) se extinde la:
- down_read ( rwsem) - o încercare de a lua un semafor pentru citire;
- up_read ( rwsem - eliberarea semaforului pentru citire;
- down_write ( rwsem) - încercarea de a captura un semafor pentru scriere;
- up_write ( rwsem) - eliberarea semaphorelor pentru scriere;
Semantica acestor operații este după cum urmează:
- dacă semaforul nu este încă capturat, atunci orice captură (down_read () sau down_write ()) va avea succes (fără blocare);
- dacă semaforul este deja citit. această încercare ulterioară de a captura de citire semaforul (a down_read ()) se va finaliza cu succes (fără a fi blocat), dar o astfel de cerere pentru a captura semaforul de înregistrare (down_write ()) capăt de blocare;
- dacă semaforul este deja capturat pentru înregistrare. atunci orice încercare ulterioară de a captura semaforul (down_read () sau down_write ()) se va termina cu blocarea;
Un semafor de citire și scriere definit static este creat de o macrocomandă:
Semafoarele de citire și scriere create dinamic trebuie inițializate folosind funcția:
Notă. Din descrierea de inițializare este clar că semafoarele de citire și scriere sunt exclusiv binare (nu sunt numărate), adică (în terminologia Linux) nu sunt semaphore, ci mutexe.
Următoarele sunt un exemplu de modul în care semaphorele de citire și scriere pot fi folosite pentru a lucra (actualiza și citi) listele ciclice Linux (pe care le-am menționat mai devreme):
În același mod ca și pentru semafoare, este introdusă o blocare de citire și scriere pentru spinlock:
Cu un set de operațiuni:
Notă. Dacă compilați kernel-ul nu a fost stabilită și nici un cod de lapte praf degresat configurat de preemțiune nucleu, atunci spinlock_t nu compilează (în locul lor vor rămâne spații goale), și, în consecință, rwlock_t corespunzătoare.
De asemenea, blocarea capturată pentru citire nu mai poate fi crescută până la blocarea care a fost capturată pentru înregistrare:
Această secvență asigură operatorilor ocurență impas, deoarece inspecția periodică va fi efectuată în capturarea unui sistem de blocare pe o înregistrare până când toate firele care captează dispozitivul de blocare pentru citire, nu este eliberat, se aplică la fluxul de curent, ceea ce nu este în stare să facă asta vreodată.
Dar mai multe fire de citire pot capta în siguranță același blocaj de citire și scriere, astfel încât un fir poate de asemenea să capteze în mod sigur recursiv aceeași blocare pentru a citi de mai multe ori, de exemplu, într-un handler de întrerupere fără a întrerupe întreruperile.
concluzie
La momentul creării mecanismului de încuietori de citire și scriere cu implementarea lor, a fost asociată o creștere semnificativă a productivității și au provocat un entuziasm apreciabil al dezvoltatorilor. Dar practica ulterioară a arătat că acest mecanism inerent prezintă un pericol latent că, cu o densitate ridicată și uniformă a solicitărilor de lectură, cererea de modificare (înregistrare) a structurii de înregistrare poate fi întârziată pentru intervale de timp nelimitate. Despre această caracteristică trebuie să vă amintiți, evaluând posibilitatea utilizării acestui mecanism în codul dvs. O atenuare parțială a acestei restricții este încercarea următorului subsistem de încuietori - blocarea serială, pe care o vom lua în considerare în următorul articol.
Obțineți produse și tehnologii
- Testați software-ul de la IBM. Descărcați versiuni de încercare, utilizați demo-uri online, lucrați cu produse într-un mediu de nisip sau în mediul cloud. Sunteți disponibil pentru a studia mai mult de 100 de produse de la IBM.