IPC
Linux pune la dispoziție urmatoarele mecanisme de comunicare intre procese (IPC - Inter Process Communication):
fisiere
pipe-uri (anonime si cu nume, studiate in lucrarea anterioara)
semafoare (semaphores) - realizează sincronizarea execuțiilor unor procese
cozi de mesaje (message queues) - realizează schimbul de mesaje cu orice proces sau server
memorie partajată (shared memory) - realizează partajarea memoriei între procese
socket (care se vor studia la materia Retele de Calculatoare)
Obiectele de tip IPC pe care se concentrează laboratorul de față sunt gestionate global de sistem și rămân în viață chiar dacă procesul creator moare. Faptul că aceste resurse sunt globale în sistem are implicații contradictorii. Pe de o parte, dacă un proces se termină, datele plasate în obiecte IPC pot fi accesate ulterior de alte procese; pe de altă parte, procesul proprietar trebuie să se ocupe și de dealocarea resurselor, altfel ele rămân în sistem până la ștergerea lor manuală sau până la un reboot. Faptul că obiectele IPC sunt globale în sistem poate duce la apariția unor probleme: cum numărul de mesaje care se află în cozile de mesaje din sistem e limitat global, un proces care trimite multe asemenea mesaje poate bloca toate celelalte procese.
ATENTIE!!! Pentru folosirea API-ului trebuie să includeți la linking biblioteca 'rt' (-lrt).
Pipe-uri
Pipe-urile (canalele de comunicaţie) sunt mecanisme primitive de comunicare între procese. Un pipe poate conţine o cantitate limitată de date. Accesul la aceste date este de tip FIFO (datele se scriu la un capăt al pipe-ului şi sunt citite de la celălalt capăt). Sistemul de operare garantează sincronizarea între operaţiile de citire şi scriere la cele două capete.
Există două tipuri de pipe-uri:
• pipe-uri anonime - pot fi folosite doar de procese înrudite (un proces părinte şi un copil sau doi copii) deoarece este accesibil doar prin moştenire. Aceste pipe-uri nu mai există după ce procesele şi-au terminat execuţia.
• pipe-uri cu nume - există ca fişiere cu drepturi de acces. Aceasta înseamnă că ele vor exista în continuare independent de procesul care le creează şi pot fi folosite de procese neînrudite.
Pipe-uri anonime
Pipe-ul este un mecanism de comunicare unidirecţională între două procese. În majoritatea implementărilor de UNIX un pipe apare ca o zonă de memorie de o anumită dimensiune în spaţiul nucleului. Procesele care comunică printr-un pipe trebuie să aibă un grad de rudenie; de obicei, un proces care creează un pipe va apela după aceea fork, iar pipe-ul se va folosi pentru comunicarea între părinte şi fiu. În orice caz procesele care comunică prin pipe nu pot fi create de utilizatori diferiţi ai sistemului.
Apelul de sistem pentru creare este pipe:
Parametrul filedes returnează 2 descriptori de fişier: filedes[0] deschis pentru citire şi filedes[1] deschis pentru scriere. Se consideră că ieşirea lui filedes[1] este intrare pentru filedes[0]. Apelul returnează 0 în caz de succes şi -1 în caz de eroare. Observaţii:
Citirea/scrierea din/în pipe-uri este atomică dacă nu se citesc/scriu mai mult de PIPE_BUF octeţi.
Citirea/scrierea din/în pipe-uri se realizează cu ajutorul funcţiilor read / write.
Majoritatea aplicaţiilor care folosesc pipe-uri închid în fiecare dintre procese capătul de pipe neutilizat în comunicarea unidirecţională. Dacă unul dintre descriptori este închis se aplică regulile:
O citire dintr-un pipe pentru care descriptorul de scriere a fost închis, după ce toate datele au fost citite, va returna 0, ceea ce indică sfârşitul fişierului. Descriptorul de scriere poate fi duplicat astfel încât mai multe procese să poată scrie în pipe. De regulă, în cazul pipe-urilor anonime există doar două procese, unul care scrie şi altul care citeşte pe când în cazul fişierelor FIFO pot exista mai multe procese care scriu date.
O scriere într-un pipe pentru care descriptorul de citire a fost închis cauzează generarea semnalului SIGPIPE. Dacă semnalul este captat şi se revine din rutina de tratare, funcţia de sistem write returnează eroare şi variabila errno are valoarea EPIPE.
Cea mai frecventă greşeală relativ la lucrul cu pipe-urile constă în faptul că nu se trimite EOF prin pipe (citirea din pipe nu se termină) decât dacă sunt închise TOATE capetele de scriere din TOATE procesele care au deschis descriptorul de scriere în pipe (în cazul unui fork, nu uitaţi să închideţi capetele pipe-ului în procesul părinte).
Alte funcţii utile: popen, pclose.
Exemplu 6. pipe.c
Pipe-uri cu nume
FIFO numite şi pipe-uri cu nume, elimină necesitatea ca procesele care comunică să fie înrudite deoarece acestea nu trebuie să îşi transmită descriptorii. Astfel, fiecare proces îşi poate deschide pentru citire sau scriere fişierul pipe cu nume (FIFO) care este un tip de fişier special care păstrează caracteristicile unui pipe. Comunicaţia se face într-un sens sau în ambele sensuri. Fişierele de tip FIFO pot fi localizate ca având litera p în primul câmp al drepturilor de acces (ls -l) . Apelul de sistem pentru crearea FIFO este:
Parametrii sunt:
pathname - reprezintă numele de cale al fişierului FIFO.
mode - reprezintă un întreg ce indică drepturile de acces ale fişierului FIFO.
Apelul returnează 0 în caz de succes si -1 în caz de eroare.
Observaţii: după ce FIFO a fost creat, acestuia i se pot aplica toate funcţiile pentru operaţii cu fişiere: open, close, read, write la fel ca şi altor fişiere.
Modul de comportare al unui FIFO este afectat de flagul O_NONBLOCK astfel:
Dacă O_NONBLOCK nu este specificat (cazul normal), atunci un open pentru citire se va bloca până când un alt proces deschide acelaşi FIFO pentru scriere. Analog, dacă deschiderea este pentru scriere, se poate produce blocare până când un alt proces efectuează deschiderea pentru citire.
Dacă se specifică O_NONBLOCK, atunci deschiderea pentru citire revine imediat, dar o deschidere pentru scriere poate returna eroare cu errno având valoarea ENXIO, dacă nu există un alt proces care a deschis acelaşi FIFO pentru citire. Atunci când ultimul proces care scrie într-un FIFO îl închide, se va genera un "sfârşit de fişier" pentru procesul care citeşte din FIFO.
Exemplu 7. fifoserver.c
Exemplu 8. fifoclient.c
Semafoare
Semafoarele sunt resurse IPC folosite pentru sincronizarea între procese (e.g. pentru controlul accesului la resurse). Operațiile asupra unui semafor pot fi de setare sau verificare a valorii (care poate fi mai mare sau egala cu 0) sau de test and set. Un semafor poate fi privit ca un contor ce poate fi incrementat și decrementat, dar a cărui valoare nu poate scadea sub 0.
Semafoarele POSIX sunt de 2 tipuri:
cu nume, folosite în general pentru sincronizare între procese distincte;
fără nume, ce pot fi folosite doar pentru sincronizarea între firele de execuție ale unui proces sau între procese înrudite.
In contiunare vor fi luate în discuție semafoarele cu nume. Diferențele față de cele bazate pe memorie constau în funcțiile de creare și distrugere, celelalte funcții fiind identice.
ambele tipuri de semafoare sunt reprezentate în cod prin tipul sem_t.
semafoarele cu nume sunt indenficate la nivel de sistem printr-un șir de forma "/nume".
header-ele necesare sunt <fcntl.h>, <sys/types.h> și <semaphore.h>.
Crearea și deschiderea
Un proces poate crea sau deschide un semafor existent cu funcția sem_open:
Comportamentul este similar cu cel de la deschiderea fișierelor. Dacă flag-ul O_CREAT este prezent, trebuie folosită cea de-a doua formă a funcției, specificând permisiunile și valoarea inițială.
Decrementare, incrementare și aflarea valorii
Un semafor este decrementat cu funcția sem_wait:
Dacă semaforul are valoarea zero, funcția blochează până când un alt proces "deblochează" (incrementează) semaforul. Pentru a incerca decrementarea unui semafor fără riscul de a rămâne blocat la acesta, un proces poate apela sem_trywait:
In cazul în care semaforul are deja valoarea zero, funcția va intoarce -1 iar errno va fi setat la EAGAIN. Un semafor este incrementat cu funcția sem_post:
In cazul în care semaforul are valoarea zero, un proces blocat în sem_wait pe acesta va fi deblocat. Valoarea unui semafor (a contorului) se poate afla cu sem_getvalue:
In cazul în care există procese blocate la semafor, implementarea apelului pe Linux va returna zero în valoarea referită de pvalue. Toate aceste funcții întorc zero în caz de succes.
Inchiderea și distrugerea
Un proces închide (notifică faptul că nu mai folosește) un semafor printr-un apel sem_close:
Un proces poate șterge un semafor printr-un apel sem_unlink:
Distrugerea efectivă a semaforului are loc după ce toate procesele care l-au deschis apelează sem_close sau se termină. Totuși, chiar și în acest caz, apelul sem_unlink nu va bloca!
Exemplu: Implementați două procese care să fie sincronizate folosind un semafor cu nume.
Cele două procese se rulează din console diferite. După rularea primului proces, semaforul va fi creat în ”/dev/shm” cu numele “sem.my_semaphore” și procesul va aștepta apăsarea unei taste pentru a debloca următorul proces.
Exercitiu: Să se modifice aplicația, astfel încât procesele să aștepte după resursele partajate, așa cum este ilustrat în figură, producând interblocarea proceselor, deadlock.
Deadlock-ul este o situație în care două sau mai multe procese sau thread-uri (sau fire de execuție) se blochează reciproc și niciunul dintre ele nu poate face progres. Aceasta este o problemă comună în sistemele concurente, în special în contextul gestionării resurselor partajate.
Un deadlock se produce atunci când următoarele condiții sunt îndeplinite simultan:
Blocare reciprocă (Deadlock Mutual Exclusion): Cel puțin două procese sau thread-uri sunt angajate în așteptarea unor resurse pe care le dețin deja, iar aceste resurse nu pot fi eliberate până când procesul sau thread-ul a obținut resursele suplimentare de care are nevoie. Cu alte cuvinte, fiecare proces sau thread a blocat resursele necesare pentru alții.
Posibilitatea de a elibera resurse (Hold and Wait): Procesele sau thread-urile care au deja resursele pot aștepta și solicita resurse suplimentare. În același timp, ele pot să nu elibereze resursele pe care le dețin până când nu obțin toate resursele necesare.
Nicio eliberare a resurselor (No Preemption): Resursele nu pot fi luate de la un proces sau thread și acordate altuia în mod forțat. Ele pot fi eliberate numai voluntar de către procesul sau thread-ul care le deține.
Așteptare ciclică (Circular Wait): Există un cerc închis de procese sau thread-uri în care fiecare așteaptă resursele eliberate de următorul proces sau thread din cerc. Acest cerc de așteptare creează o situație de impas.
Pentru a evita deadlock-urile, se folosesc diverse tehnici și strategii, cum ar fi evitarea blocării reciproce, detecția deadlock-ului cu eliberare de resurse sau utilizarea algoritmilor de planificare care minimizează șansele de a se produce deadlock. Gestionarea deadlock-ului este o parte esențială a proiectării și dezvoltării sistemelor concurente și a sistemelor de operare.
Cozi de mesaje
Acestea permit proceselor schimbarea de date între procese sub forma de mesaje.
la nivel de sistem sunt indentificabile printr-un string de forma "/nume".
la nivel codului, o coada de mesage este reprezentata de un descriptor de tipul mqd_t.
header-ele necesare pentru lucrul cu aceste obiecte sunt <fcntl.h>, <sys/types.h> si <mqueue.h>.
Crearea și deschiderea
Funcțiile de creare și deschidere sunt similare ca forma și semantică celor de la semafoare:
În funcție de flag-uri (unul din cele de mai jos trebuie specificat), coada poate fi deschisă pentru:
recepționare (O_RDONLY)
trimitere (O_WRONLY)
recepționare și trimitere (O_RDWR)
Daca attr e NULL, coada va fi creată cu atribute implicite. Structura mq_attr arată astfel:
Trimiterea și recepționarea de mesaje
Pentru a trimite un mesaj (de lungime cunoscută, stocat într-un buffer) în coadă se apelează mq_send:
Mesajele sunt ținute în coadă în ordine descrescătoare a priorității. În cazul în care coada este plină, apelul blochează. Dacă este o coadă non-blocantă (O_NONBLOCK), funcția va întoarce -1 iar errno va fi setat la EAGAIN. Pentru a primi un mesaj dintr-o coadă (și anume: cel mai vechi mesaj cu cea mai mare prioritate) se folosește mq_receive:
Dacă priority este non-NULL, zona de memorie către care face referire va reține prioritatea mesajului extras. În cazul în care coada este vidă, apelul blochează. Daca este o coadă non-blocantă (O_NONBLOCK), comportamentul este similar cu cel al mq_send.
ATENTIE!!! La primirea unui mesaj, lungimea buffer-ului trebuie să fie cel puțin egală cu dimensiunea maximă a mesajelor pentru coada respectivă, iar la trimitere cel mult egală. Dimensiunea maximă implicită se poate afla pe Linux din /proc/sys/kernel/msgmax.
Inchiderea și ștergerea
Închiderea (eliberarea "referinței") unei cozi este posibilă prin apelul mq_close:
Ștergerea se realizează cu un apel mq_unlink:
Semantica este similară cu cea de la semafoare: coada nu va fi ștearsă efectiv decât după ce restul proceselor implicate o închid.
Exemplu: Implementați un protocol simplu de comunicație între un client și un server folosind cozi de mesaje. Clientul se va conecta la server și va trimite acestuia numărul 1337 pe care server-ul îl va afișa. Apoi clientul va trimite server-ului un mesaj de închidere.
Memorie partajată
Acest mecanism permite comunicarea între procese prin accesul direct și partajat la o zonă de memorie bine determinată.
la nivelul sistemului, o zonă este identificată printr-un string de forma "/nume";
la nivelul codului, o zona este reprezentată printr-un file descriptor (int).
header-ele necesare pentru lucrul cu aceste obiecte sunt <fcntl.h>, <sys/types.h> și <sys/mman.h>.
Crearea și deschiderea
Apelul de creare/deschidere este similar ca semantica apelului open pentru fișiere "obișnuite":
Ca flag de acces trebuie specificat fie O_RDONLY fie O_RDWR.
Redimensionarea
O zonă de memorie partajată nou creată are dimensiunea inițială zero. Pentru a o dimensiona se folosește ftruncate:
Maparea și eliberarea
Pentru a putea utiliza o zona de memorie partajată după deschidere, aceasta trebuie mapată în spațiul de memorie al procesului. Aceasta se realizează printr-un apel mmap:
Valoarea intoarsă reprezintă un pointer către începutul zonei de memorie sau MAP_FAILED în caz de eșec.
Acest apel are o largă aplicabilitate și va fi discutat în cadrul laboratorului de memorie virtuală. Momentan, pentru a mapa întregul conținut al unei zone (shm_fd) de dimensiune cunoscută (shm_len), recomandăm folosirea apelului
Când maparea nu mai este necesară, prin apelul munmap se realiează demaparea:
Inchiderea și ștergerea
Închiderea unei zone de memorie partajată este identică cu închiderea unui fișier: apelul close.
Odată ce o zonă de memoria a fost demapată și închisă în toate procesele implicate, se poate șterge prin shm_unlink:
Semantica este identică cu cea de la funcțiile *_unlink anterioare: ștergerea efectivă este amânată până ce toate procesele implicate închid zona în cauză.
Exemplu: Folosind memoria partajată, realizați un transfer simplu de informație între două procese astfel: server-ul va crea o zona de 4k de memorie și va pune numărul 1337 începând cu primul octet, clientul va citi și afișa acest număr.
Client pentru memoria partajată
Server pentru memoria partajata
Resurse utile
• Fast User-level Locking In Linux