Cursul 14 Comunicarea interprocese Pipe FIFO Pipeuri cu

  • Slides: 29
Download presentation
Cursul 14 Comunicarea interprocese (Pipe)

Cursul 14 Comunicarea interprocese (Pipe)

 • • • FIFO (Pipe-uri cu nume) Una din limitările pipe-urilor, este necesitatea

• • • FIFO (Pipe-uri cu nume) Una din limitările pipe-urilor, este necesitatea ca procesele care comunică să fie înrudite; se elimină prin utilizarea unui pipe cu nume, numit pe scurt FIFO, care este un tip special de fişier. Fişierele FIFO combina caracteristicile fişierelor obişnuite cu cele ale fişierelor PIPE. Fişierele FIFO sunt caracterizate de faptul că fiecărui astfel de fişier îi corespunde o intrare în unul din cataloagele sistemului de fişiere, lucru care face posibilă referirea unui astfel de fişier prin intermediul numelui asociat acestuia. De asemenea, un fişier FIFO este deschis la fel ca fişierele obişnuite. După deschidere, fişierul FIFO capătă caracteristicile unui fişier PIPE. Datele se citesc strict în ordinea în care au fost depuse, iar apelurile de scriere şi citire în fişier se garantează a fi atomice. Nu este posibilă poziţionarea în fişier utilizând apelul de sistem lseek. Putem crea un FIFO in shell folosind comanda mkfifo, care sintaxa: $mkfifo [ -m mode ] pathname este numele FIFO-ului ce va fi creat; –m option este folosit pt. a specifica drepturile, in acelasi mod utiliz. de comanda chmod.

 • Exemplu. Crearea unui FIFO si afisarea informatiilor despre el: $mkfifo /tmp/my_fifo $ls

• Exemplu. Crearea unui FIFO si afisarea informatiilor despre el: $mkfifo /tmp/my_fifo $ls -l /tmp/my_fifo prw-rw-rw- 1 ion users 0 Jan 16 14: 04 /tmp/my_fifo • Obs. Primul caracter al iesirii lui ls este p, indicind tipul fisierului (p-pipe). • Scrierea intr-un FIFO se poate realiza cu o comanda cat, la fel ca intr-un fisier obisnuit, sau prin redirectarea iesirii unei comenzi. • Exemplu. $echo “sdsdfasdf” >/tmp/my_fifo • Citirea dintr-un FIFO se poate realiza cu o comanda cat, prin redirectarea iesirii. • Le putem lansa simultan, punind prima comanda in background. • Exemplu. $cat < /tmp/my_fifo & [1] 1316 $echo “sdsdfasdf” > /tmp/my_fifo sdsdfasdf [1]+ Done cat </tmp/my_fifo $ • Obs. 1. La inceput procesul cat este blocat in background. Cind echo produce date disponibile, cat le citeste si le afiseaza la iesirea standard. • 2. Dupa citire, cat se termina, fara a mai astepta alte date.

 • Apelul sistem pentru crearea unui FIFO este: int mkfifo(const char *pathname, mode_t

• Apelul sistem pentru crearea unui FIFO este: int mkfifo(const char *pathname, mode_t mode); • Primul argument este numele FIFO, iar al doilea defineste drepturile de proprietate, care sunt aceleaşi ca şi la fişiere. • Exemplu: Crearea unui FIFO (fifo 1. c). #include <unistd. h> #include <stdlib. h> #include <stdio. h> #include <sys/types. h> #include <sys/stat. h> int main() { int res = mkfifo(“/tmp/my_fifo”, 0777); if (res == 0) printf(“FIFO createdn”); exit(EXIT_SUCCESS); }

 • Daca vrem sa vedem inf. despre FIFO-ul creat, tastam: $ls -l. F

• Daca vrem sa vedem inf. despre FIFO-ul creat, tastam: $ls -l. F /tmp/my_fifo prwxr-xr-x 1 ion users 0 July 10 14: 55 /tmp/my_fifo| • Obs. ca primul caracter al liniei este p, indicind un pipe; simbol |, de la sfirsit este adaugat de catre optiunea –F a comenzii ls si indica un pipe. • Modul de Lucru. Programul utilizează functia mkfifo pt. a crea fisierul special. Drepturile asupra fisierului sunt determinate scazind din constanta octala 0777, valoarea stabilita prin umask; implicit aceasta este 022 si in acest caz drepturile vor fi codificate de 0755. • Obs. • 1. Spre deosebire de o conductă creata cu un apel pipe, un FIFO există ca un fişier cu nume, nu ca un descriptor de fisier şi trebuie să fie deschis înainte de a putea fi citite/scrise date din/in el. • 2. Pt. deschiderea si inchiderea unui FIFO, se utilizeaza aceleaşi funcţii folosite pt. fişiere, cu unele funcţionalităţi suplimentare; in apelul de deschidere este trecuta calea catre FIFO si nu numai numele de fisier. • După ce FIFO a fost creat, el poate fi deschis prin open, apoi toate operaţiile cu fişiere (close, read, write, unlink, etc. ) i se pot aplica, la fel ca altor fişiere.

 • Deschiderea fisierelor cu apelul open. • Deschiderea unui FIFO are o semantică

• Deschiderea fisierelor cu apelul open. • Deschiderea unui FIFO are o semantică oarecum neobişnuită. • În general, utilizarea unui FIFO este de a avea un proces de citire şi unul de scriere la fiecare capăt. Prin urmare, în mod implicit, deschiderea unui FIFO pentru citire (open() cu fanionul O_RDONLY) blocheaza procesul până cind un alt proces deschide FIFO pentru scriere (open() cu fanionul O_WRONLY). • Restricţia principala privind deschiderea unui FIFO, este faptul că un program nu poate deschide un FIFO pentru citire şi scriere cu O_RDWR. Dacă un program face acest lucru, rezultatul este nedefinit. Acest lucru este o restricţie nesemnificativa, deoarece, în mod normal, vom folosi un FIFO doar pentru a trimite date într-o singură direcţie, aşa că nu avem nevoie de O_RDWR. • Dacă vrem să transmitem date în ambele direcţii între programe, este mult mai bine să utilizam o pereche de FIFO-uri sau de conducte, una pentru fiecare direcţie sau sa schimbam în mod explicit direcţia fluxului de date prin închiderea şi redeschiderea FIFO.

 • Modul de comportare al unui FIFO după deschidere este afectat de fanionul

• Modul de comportare al unui FIFO după deschidere este afectat de fanionul O_NONBLOCK astfel: 1. Dacă O_NONBLOCK nu este specificat (cazul normal), atunci un open pentru citire se va bloca până când un alt proces deschide acelasi FIFO pentru scriere. Dacă deschiderea este pentru scriere, se poate produce blocare până când un alt proces il deschide pentru citire. Cu alte cuvinte, două procese care comunică printr-un fişier FIFO se aşteaptă unul pe celalalt la deschidere, pentru sincronizare, înainte de începerea comunicaţiei propriu-zise. Aceasta aşteptare nu mai are loc daca la deschidere se specifica semaforul O_NDELAY, caz în care daca la celalalt capăt deja exista un proces, atunci se produce sincronizarea, altfel apelul de deschidere este terminat cu eroare. 2. Dacă se specifică O_NONBLOCK, din deschiderea pentru citire se revine imediat, dar o deschidere pentru scriere poate returna eroare cu errno la valoarea ENXIO, dacă nu există un alt proces care a deschis acelaşi FIFO pentru citire.

 • Exista 4 combinatii valide ale fanioanelor O_RDONLY, O_WRONLY si O_NONBLOCK. 1. open(const

• Exista 4 combinatii valide ale fanioanelor O_RDONLY, O_WRONLY si O_NONBLOCK. 1. open(const char *path, O_RDONLY); • In acest caz, apelul open se va bloca; debl. se va face, când un alt proces deschide acelaşi FIFO pentru scriere. • Acesta este la fel cu exemplul legat de cat. 2. open(const char *path, O_RDONLY | O_NONBLOCK); • Apelul open se exec. cu succes, chiar dacă FIFO-ul nu a fost deschis pentru scriere de catre alt proces. 3. open(const char *path, O_WRONLY); • In acest caz, apelul open se va bloca; debl. se va face, când un alt proces deschide acelaşi FIFO pentru citire. 4. open(const char *path, O_WRONLY | O_NONBLOCK); • In acest caz , revenirea se va realiza imediat, dar în cazul în care nici un proces nu are FIFO deschis pentru citire, se va reveni cu o eroare ( -1), iar FIFO nu va fi deschis. Dacă un proces are deschis FIFO pentru citire, fişierul descriptor returnat poate fi folosit pentru a scrie date in FIFO.

 • Exemplu. Deschiderea de fisiere FIFO( fifo 2. c ). Cum putem folosi

• Exemplu. Deschiderea de fisiere FIFO( fifo 2. c ). Cum putem folosi comportarea lui open cu fanionul O_NONBLOCK, pt. a sincroniza două procese. Programul prezintă comportarea unui FIFO prin transmiterea a diferiti parametri. #include <unistd. h> #include <stdlib. h> #include <stdio. h> #include <string. h> #include <fcntl. h> #include <sys/types. h> #include <sys/stat. h> #define FIFO_NAME “/tmp/my_fifo” int main(int argc, char *argv[]) { int res; int open_mode = 0; if (argc < 2) { fprintf(stderr, “Usage: %s <some combination of O_RDONLY O_WRONLY O_NONBLOCK>n”, *argv); exit(EXIT_FAILURE);

 • /* Presupunând că programul a trecut testul, vom seta valoarea lui*/ /*open_mode

• /* Presupunând că programul a trecut testul, vom seta valoarea lui*/ /*open_mode , pentru acele argumente*/ argv++; if (strncmp(*argv, “O_RDONLY”, 8) == 0) open_mode |= O_RDONLY; if (strncmp(*argv, “O_WRONLY”, 8) == 0) open_mode |= O_WRONLY; if (strncmp(*argv, “O_NONBLOCK”, 10) == 0) open_mode |=O_NONBLOCK; argv++; if (*argv) { if (strncmp(*argv, “O_RDONLY”, 8) == 0) open_mode |= O_RDONLY; if (strncmp(*argv, “O_WRONLY”, 8) == 0) open_mode |= O_WRONLY; if (strncmp(*argv, “O_NONBLOCK”, 10) == 0) open_mode |= O_NONBLOCK; }

/* verificam daca FIFO-ul exista si il vom crea daca este necesar */ if

/* verificam daca FIFO-ul exista si il vom crea daca este necesar */ if (access(FIFO_NAME, F_OK) == -1) { res = mkfifo(FIFO_NAME, 0777); if (res != 0) { fprintf(stderr, “Could not create fifo %sn”, FIFO_NAME); exit(EXIT_FAILURE); } } printf(“Process %d opening FIFOn”, getpid()); res = open(FIFO_NAME, open_mode); printf(“Process %d result %dn”, getpid(), res); sleep(5); if (res != -1) (void)close(res); printf(“Process %d finishedn”, getpid()); exit(EXIT_SUCCESS); }

 • Cum lucrează. Acest program ne permite sa specificam in linia de comanda

• Cum lucrează. Acest program ne permite sa specificam in linia de comanda combinatii de O_RDONLY, O_WRONLY şi O_NONBLOCK pe care dorim sa le folosim. • Face acest lucru, comparind sirurile cunoscute cu parametri din linia de comandă şi prin setare (cu |=) a fanionului potrivit daca sirul se potriveşte. • Program foloseşte functia access ptr. a verifica daca fisierul FIFO deja exista si îl va crea dacă este necesar. • Nu se distruge FIFO-ul, deoarece nu se poate sti daca un alt program nu foloseste FIFO-ul.

 • Exemple de apeluri. Observ. lansarea în background a programului. 1. O_RDONLY şi

• Exemple de apeluri. Observ. lansarea în background a programului. 1. O_RDONLY şi O_WRONLY fără O_NONBLOCK $. /fifo 2 O_RDONLY & [1] 152 Process 152 opening FIFO $. /fifo 2 O_WRONLY Process 153 opening FIFO Process 152 result 3 Process 153 result 3 Process 152 finished Process 153 finished • Acesta este cel mai întâlnit mod de utilizare a pipe-urilor. El permite cititorului să înceapă şi să aştepte în apelul open şi apoi ambelor programe să continue când scriitorul deschide FIFO-ul. Obs. sincronizarea ambelor programe la apelul open. • Cînd un proces este blocat, el nu consumă resurse CPU, deci metoda este eficientă d. p. d. v. al CPU.

 • 2. O_RDONLY cu O_NONBLOCK şi O_WRONLY • În acest moment, procesul cititor

• 2. O_RDONLY cu O_NONBLOCK şi O_WRONLY • În acest moment, procesul cititor execută apelul open şi continuă imediat exec. , deşi nu este prezent nici-un proces scriitor. De as. scriitorul imediat efect. apelul open, deoarece FIFO-ul este deschis pentru citire. $. /fifo 2 O_RDONLY O_NONBLOCK & [1] 160 Process 160 opening FIFO $. /fifo 2 O_WRONLY Process 161 opening FIFO Process 160 result 3 Process 161 result 3 Process 160 finished Process 161 finished [1]+ Done fifo 2 O_RDONLY O_NONBLOCK • Obs. Aceste doua exemple sunt cele mai frecvente combinatii a utiliz. open.

 • Citirea şi scrierea din/in FIFO-uri • O_NONBLOCK afecteaza comportamentul apelurilor de citire/scriere

• Citirea şi scrierea din/in FIFO-uri • O_NONBLOCK afecteaza comportamentul apelurilor de citire/scriere pe FIFO-uri. • O citire pe un FIFO vid blocat ( adica unul care nu a fost deschis cu O_NONBLOCK ) va astepta pina cind vor fi date care pot fi citite. Invers, o citire dintr-un FIFO neblocat care nu contine date va returna 0 octeti. • O scriere pe un FIFO plin si blocat va astepta pina cind datele pot fi scrise. O scriere pe un FIFO ce nu poate accepta toti octetii ce urmeaza a fi scrisi, va: - esua, daca cererea este de cel mult PIPE_BUF octeti; - scrie o parte din date, daca cererea este mai mare de PIPE_BUF octeti, returnind numarul octetilor scrisi. • Dimensiunea unui mesaj FIFO este limitata de PIPE_BUF, definita in limits. h. In mod normal, aceasta este de 4096 octeti. • Daca se cere scrierea a PIPE_BUF octeti (sau mai putini) pe un FIFO deschis cu O_WRONLY (adica blocat), ori se vor scrie toti octetii ori niciunul. • Aceasta limitare este importanta, daca exista mai multi scriitori si un singur cititor.

 • Comunicarea inte procese cu FIFO (fifo 3. c, fifo 4. c). •

• Comunicarea inte procese cu FIFO (fifo 3. c, fifo 4. c). • 1. Primul program este producatorul; el creaza conducta (daca se cere) si apoi scrie date cit mai repede posibil. • Liniile scrise cu italic arata schimbarile, fata de fifo 2. c #include <unistd. h> #include <stdlib. h> #include <stdio. h> #include <string. h> #include <fcntl. h> #include <limits. h> #include <sys/types. h> #include <sys/stat. h> #define FIFO_NAME “/tmp/my_fifo” #define BUFFER_SIZE PIPE_BUF #define TEN_MEG (1024 * 10)

int main() { int pipe_fd; int res; int open_mode = O_WRONLY; int bytes_sent =

int main() { int pipe_fd; int res; int open_mode = O_WRONLY; int bytes_sent = 0; char buffer[BUFFER_SIZE + 1]; if (access(FIFO_NAME, F_OK) == -1) { res = mkfifo(FIFO_NAME, 0777); if (res != 0) { fprintf(stderr, “Could not create fifo %sn”, FIFO_NAME); exit(EXIT_FAILURE); } } printf(“Process %d opening FIFO O_WRONLYn”, getpid()); pipe_fd = open(FIFO_NAME, open_mode); printf(“Process %d result %dn”, getpid(), pipe_fd); if (pipe_fd != -1) {

while(bytes_sent < TEN_MEG) { res = write(pipe_fd, buffer, BUFFER_SIZE); if (res == -1) {

while(bytes_sent < TEN_MEG) { res = write(pipe_fd, buffer, BUFFER_SIZE); if (res == -1) { fprintf(stderr, “Write error on pipen”); exit(EXIT_FAILURE); } bytes_sent += res; } (void)close(pipe_fd); } else {exit(EXIT_FAILURE); } printf(“Process %d finishedn”, getpid()); exit(EXIT_SUCCESS); }

 • 2. Al doilea program, consumator, citeste datele din FIFO. #include <unistd. h>

• 2. Al doilea program, consumator, citeste datele din FIFO. #include <unistd. h> #include <stdlib. h> #include <stdio. h> #include <string. h> #include <fcntl. h> #include <limits. h> #include <sys/types. h> #include <sys/stat. h> #define FIFO_NAME “/tmp/my_fifo” #define BUFFER_SIZE PIPE_BUF int main() { int pipe_fd; int res; int open_mode = O_RDONLY; char buffer[BUFFER_SIZE + 1]; int bytes_read = 0;

memset(buffer, ‘�’, sizeof(buffer)); printf(“Process %d opening FIFO O_RDONLYn”, getpid()); pipe_fd = open(FIFO_NAME, open_mode); printf(“Process

memset(buffer, ‘’, sizeof(buffer)); printf(“Process %d opening FIFO O_RDONLYn”, getpid()); pipe_fd = open(FIFO_NAME, open_mode); printf(“Process %d result %dn”, getpid(), pipe_fd); if (pipe_fd != -1) { do { res = read(pipe_fd, buffer, BUFFER_SIZE); bytes_read += res; } while (res > 0); (void)close(pipe_fd); } else { exit(EXIT_FAILURE); } printf(“Process %d finished, %d bytes readn”, getpid(), bytes_read); exit(EXIT_SUCCESS); }

 • Ptr. lansarea in executie a programelor in acelasi timp, se utilizeaza a

• Ptr. lansarea in executie a programelor in acelasi timp, se utilizeaza a comanda time $. /fifo 3 & [1] 375 Process 375 opening FIFO O_WRONLY $time. /fifo 4 Process 377 opening FIFO O_RDONLY Process 375 result 3 Process 377 result 3 Process 375 finished Process 377 finished, 10485760 bytes read real 0 m 0. 053 s user 0 m 0. 020 s sys 0 m 0. 040 s [1]+ Done fifo 3

 • Cum lucreaza. Ambele programe folosesc FIFO-ul in modul blocare. Se lanseaza primul

• Cum lucreaza. Ambele programe folosesc FIFO-ul in modul blocare. Se lanseaza primul producatorul (fifo 3) , care se blocheaza asteptind ca cititorul sa deschida FIFO-ul. • Cind fifo 4 (consumatorul) este lansat, scriitorul este deblocat si incepe scrierea datelor in pipe. In acelasi timp, cititorul incepe citirea datelor din pipe. • Planificatorul realizeaza executia celor doua procese atunci cind este posibil; de as. scriitorul este blocat cind pipe-ul este plin si scriitorul este blocat cind pipe-ul este vid. • Iesirea comenzii time afiseaza timpii in executie si cantitatea de date citita.

 • Aplicatie Client/Server folosind FIFO • Vom considera un singur proces server ce

• Aplicatie Client/Server folosind FIFO • Vom considera un singur proces server ce accepta cereri, le proceseaza si returneaza datele rezultate din prelucrare procesului client. Presupunem ca exista mai multe procese client. • Datele transmise sunt impartite in blocuri, de dimensiune cel mult PIPE_BUF octeti. • Deoarece server-ul va prelucra, la un moment dat numai un bloc de informatii, va exista un singur FIFO din care citeste server-ul si in care scriu mai multi clienti. • Prin deschiderea FIFO-ului in modul blocare, server-ul si clientii se vor bloca automat atunci cind este necesar. • Pentru returnarea datelor catre clienti, va exista cite un pipe pentru fiecare client. • Prin transmiterea PID-ului clientului in cadrul datelor trimise catre server, ambele parti il pot folosi pt. a genera un nume unic ptr. pipe-ul de retur.

 • 1. Mai intii, avem nevoie de un fisier antet ( client. h

• 1. Mai intii, avem nevoie de un fisier antet ( client. h ), ce defineste datele comune atit pt. progr. client cit si server. El include antetele de sistem necesare. #include <unistd. h> #include <stdlib. h> #include <stdio. h> #include <string. h> #include <fcntl. h> #include <limits. h> #include <sys/types. h> #include <sys/stat. h> #define SERVER_FIFO_NAME “/tmp/serv_fifo” #define CLIENT_FIFO_NAME “/tmp/cli_%d_fifo” #define BUFFER_SIZE 20 struct data_to_pass_st { pid_t client_pid; char some_data[BUFFER_SIZE - 1]; };

 • 2. In programul server (server. c) se creaza si se deschide pipe-ul

• 2. In programul server (server. c) se creaza si se deschide pipe-ul server, setat read-only, cu blocare. Dupa “adormire”, server-ul citeste date de la client, de tipul data_to_pass_st. #include “client. h” #include <ctype. h> int main() { int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data; int read_res; char client_fifo[256]; char *tmp_char_ptr; mkfifo(SERVER_FIFO_NAME, 0777); server_fifo_fd = open(SERVER_FIFO_NAME, O_RDONLY); if (server_fifo_fd == -1) { fprintf(stderr, “Server fifo failuren”); exit(EXIT_FAILURE); } sleep(10); /* lets clients queue for demo purposes */ do { read_res = read(server_fifo_fd, &my_data, sizeof(my_data)); if (read_res > 0) {

 • 3. In aceasta urm. etapa, se prel. datele care au fos citite

• 3. In aceasta urm. etapa, se prel. datele care au fos citite de la client: Se convertesc toate caract. in some_data si combina CLIENT_FIFO_NAME cu client_pid primit. tmp_char_ptr = my_data. some_data; while (*tmp_char_ptr) { *tmp_char_ptr = toupper(*tmp_char_ptr); tmp_char_ptr++; } sprintf(client_fifo, CLIENT_FIFO_NAME, my_data. client_pid); • 4. Se transmit datele procesate inapoi, deschizind pipe-ul client in modul write-only, cu blocare. In final, se opreste FIFO-ul server, inchizind fisierul si anulind legatura la FIFO. client_fifo_fd = open(client_fifo, O_WRONLY); if (client_fifo_fd != -1) { write(client_fifo_fd, &my_data, sizeof(my_data)); close(client_fifo_fd); } } } while (read_res > 0); close(server_fifo_fd); unlink(SERVER_FIFO_NAME); exit(EXIT_SUCCESS); }

 • 5. Aici este prez. programul client ( client. c ). In prima

• 5. Aici este prez. programul client ( client. c ). In prima parte a acestui program se deschide server-ul FIFO, daca acesta exista, ca un fisier. Apoi isi obtine PID-ul sau, care face parte din datele care se transmit catre server. Este creat FIFO-ul client, ptr. a fi utiliz. in urm sectiune. #include “client. h” #include <ctype. h> int main() {int server_fifo_fd, client_fifo_fd; struct data_to_pass_st my_data; int times_to_send; char client_fifo[256]; server_fifo_fd = open(SERVER_FIFO_NAME, O_WRONLY); if (server_fifo_fd == -1) { fprintf(stderr, “Sorry, no servern”); exit(EXIT_FAILURE); } my_data. client_pid = getpid(); sprintf(client_fifo, CLIENT_FIFO_NAME, my_data. client_pid); if (mkfifo(client_fifo, 0777) == -1) { fprintf(stderr, “Sorry, can’t make %sn”, client_fifo); exit(EXIT_FAILURE); }

 • 6. Pentru fiecare din urm. 5 ciclari, datele client sunt transmise server-ului.

• 6. Pentru fiecare din urm. 5 ciclari, datele client sunt transmise server-ului. Apoi FIFO-ul client este deschis ( read-only, modul blocare ) si datele sunt transmise inapoi. In final, FIFO-ul server este inchis si FIFO-ul client este sters. for (times_to_send = 0; times_to_send < 5; times_to_send++) { sprintf(my_data. some_data, “Hello from %d”, my_data. client_pid); printf(“%d sent %s, “, my_data. client_pid, my_data. some_data); write(server_fifo_fd, &my_data, sizeof(my_data)); client_fifo_fd = open(client_fifo, O_RDONLY); if (client_fifo_fd != -1) { if (read(client_fifo_fd, &my_data, sizeof(my_data)) > 0) { printf(“received: %sn”, my_data. some_data); } close(client_fifo_fd); } } close(server_fifo_fd); unlink(client_fifo); exit(EXIT_SUCCESS); }

 • Pt. a testa acesta aplicatie, avem nevoie sa executam o singura copie

• Pt. a testa acesta aplicatie, avem nevoie sa executam o singura copie a server si clientii severului. Lansarea in executie a unui server si cinci clienti, se face prin: $server & $for i in 1 2 3 4 5 do client & done $ • Iesirile clientilor: 531 sent Hello from 531, received: HELLO FROM 531 532 sent Hello from 532, received: HELLO FROM 532 529 sent Hello from 529, received: HELLO FROM 529 530 sent Hello from 530, received: HELLO FROM 530 531 sent Hello from 531, received: HELLO FROM 531 532 sent Hello from 532, received: HELLO FROM 532 • Obs. Iesirile sunt afisate intr-o ordine aleatoare.