04.06.2013 Views

Software pentru comunicaţii

Software pentru comunicaţii

Software pentru comunicaţii

SHOW MORE
SHOW LESS

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Capitolul 1. Introducere<br />

1.1. Multiprogramare, multitasking, programare în timp real, calcul paralel,<br />

sisteme distribuite ?<br />

Majoritatea calculatoarelor contemporane sunt formate dintr-o unitate centrală (procesorul)<br />

şi o anumită cantitate de memorie. Exact ca şi primele calculatoare construite pe vremea lui von<br />

Neumann. Totuşi, tehnologia a evoluat mult: în particular, memoria - care pe vremuri era un tambur<br />

magnetic - este acum formată din circuite integrate, adică acelaşi material ca şi unitatea centrală,<br />

dar nu face mai nimica: orice octet lucrează numai atunci când este solicitat de la centru. Per global,<br />

doar o mică parte din hardware este utilizată în fiecare moment. Cum am putea organiza şi exploata<br />

într-un mod mai eficient acest hardware? De exemplu, o tehnologie relativ recentă - FPGA (Field<br />

Programmable Gate Arrays) - permite realizarea unei memorii active (memoria este formată din<br />

mii/milioane de mici unităţi de calcul). Cu asemenea dispozitive se pot rezolva unele probleme la<br />

un raport preţ/performanţă de sute de ori mai bun decât al calculatoarelor obişnuite. Unii cercetători<br />

sînt de părere că numai neştiinţa noastră în a organiza calcule paralele cu zeci (sute) de procese ne<br />

împiedică să realizăm computere mult mai eficiente.<br />

Cum rezolvăm probleme foarte dificile?<br />

Există numeroase probleme matematice şi tehnice <strong>pentru</strong> a căror rezolvare este nevoie de o<br />

putere de calcul mult superioară calculatoarelor obişnuite. De exemplu: rezolvarea sistemelor de<br />

ecuaţii diferenţiale, calculul fluidelor de ardere în motoare termice, simularea globală a<br />

fenomenelor meteo şi multe altele. Soluţia: calculatoare cu mai multe procesoare (sisteme<br />

multiprocesor), sau punerea pe treabă a mai multor calculatoare în mod simultan ( prin programare<br />

distribuită).<br />

Cum utilizăm eficient calculatorul pe care îl avem?<br />

Mai aproape de utilizatorii de PC, cred că oricare dintre noi suferă când o aplicaţie tip WWW sau<br />

FTP ne ţine blocaţi câteva minute bune, în timp ce calculatorul - practic - nu face nimic. Sub noile<br />

versiuni Windows este posibil ceea ce sub Unix a fost posibil întotdeauna: să lucrezi într-o altă<br />

aplicaţie în timp ce altele aflate în aşteptare continuă în paralel (multitasking). Acest lucru este<br />

posibil prin programarea concurentă. În curând vom avea şi PC-uri cu mai multe procesoare, şi<br />

atunci vom putea să profităm şi mai mult de principiile programării paralele.<br />

De ce se face atâta caz astăzi de programarea concurentă? De ce se studiază acest domeniu?<br />

Practic ce rezultate se urmăresc a fi obţinute?<br />

Câteva răspunsuri:<br />

Preocupările în ceea ce priveşte programarea concurentă nu sînt în nici un caz noi. Nou este faptul<br />

că în prezent acestea încep să pătrundă în sfera de preocupare a unui cerc larg de programatori şi<br />

utilizatori şi nu mai este "privilegiul" unor "iniţiaţi".<br />

Calculatoarele pe care s-au implementat iniţial sisteme concurente (care presupun desfăşurarea, în<br />

paralel, a mai multor activităţi) au fost sisteme cu un singur procesor central. Din punct de vedere<br />

fizic deci, o singură activitate se putea desfăşura la un moment dat (la care se adaugă, eventual,<br />

activitatea unui procesor specializat <strong>pentru</strong> I/O). Paralelismul apărea doar la nivel logic, prin faptul


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

că se executau cu schimbul secvenţe de instrucţiuni aparţinând diferitelor activităţi. Au existat<br />

următoarele motive <strong>pentru</strong> care o astfel de implementare a fost utilă, şi <strong>pentru</strong> care programarea<br />

concurentă pe sisteme monoprocesor s-a făcut şi se face şi astăzi:<br />

1. Utilizarea eficientă a procesorului central: în timp ce o activitate aşteaptă un anumit eveniment<br />

(de exemplu încheierea unei operaţii de I/O sau a unui acces la distanţă) procesorul se poate dedica<br />

altei activităţi. Acest lucru se poate realiza de mult pe sisteme care lucrează sub UNIX şi mai nou şi<br />

pe PC-uri ce lucrează sub Windows sau Linux.<br />

2. Deservirea în paralel a mai multor utilizatori (sisteme multiuser): mai mulţi utilizatori pot fi<br />

legaţi prin câte un terminal la acelaşi unic calculator. Procesorul deserveşte <strong>pentru</strong> o anumită cuantă<br />

de timp un utilizator, după care trece la următorul. În mod subiectiv, fiecare utilizator crede că are<br />

acces exclusiv la calculator deşi în realitate activităţile corespunzătoare utilizatorilor se deservesc<br />

succesiv.<br />

3. Aplicaţii care prin natura lor impun o implementare sub formă de activităţi paralele: dacă<br />

precedentele două puncte s-au referit la creşterea eficienţei în utilizarea sistemului sau la<br />

îmbunătăţirea politicii de deservire a utilizatorului, aici este vorba de aplicaţii care prin natura lor<br />

trebuie implementate sub formă de activităţi paralele, chiar dacă aceste activităţi sînt executate, pe<br />

baza unei anumite politici, de către un unic procesor. Este vorba aici, în mod tipic, de aşa numitele<br />

programe în timp real, sau de ceea ce astăzi se numeşte embedded systems, deci sisteme dedicate<br />

controlului într-o anumită aplicaţie (procese industriale, automobile, aviaţie, tele<strong>comunicaţii</strong>,<br />

echipament casnic, etc.). Comun acestor aplicaţii este că ele presupun furnizarea unui răspuns, întrun<br />

anumit interval de timp, la un impuls exterior. Impulsurile exterioare provoacă lansarea unor<br />

activităţi care, cel puţin conceptual, se desfăşoară în paralele cu activităţi lansate ca răspuns la alte<br />

impulsuri. Maniera în care procesorul se dedică diferitelor activităţi este rezultatul unei planificări,<br />

uneori foarte sofisticate, care trebuie să garanteze furnizarea unui răspuns în timp util.<br />

Considerentele prezentate mai sus sînt valabile şi astăzi, atunci când se doreşte utilizarea<br />

eficientă a unei staţii de lucru sau când se implementează un sistem de control folosind un PC sau<br />

un sistem dedicat bazat pe un microprocesor. Fiind însă vorba de sisteme bazate pe un singur<br />

procesor introducerea concurenţei, deşi poate să îmbunătăţească gradul de utilizare global al<br />

calculatorului, nu rezolvă accelerarea execuţiei unui program luat individual (faţă de cazul în care<br />

acest program ar fi fost executat fiindu-i dedicat în exclusivitate sistemul).<br />

Sistemele bazate pe arhitecturi monoprocesor tradiţionale nu pot rezolva actualmente<br />

probleme extrem de complexe şi care presupun prelucrarea unei cantităţi mari de date: calcule<br />

numerice din domeniul ştiinţelor naturale, simulări, proiectare asistată de calculator, prelucrarea<br />

imaginilor, sisteme economice, etc. Rezolvarea unor asemenea calcule într-un timp acceptabil<br />

necesită sisteme cu o arhitectură nouă, bazate pe activitatea concomitentă a mai multor procesoare,<br />

care să asigure în mod efectiv desfăşurarea în paralel a prelucrării datelor. S-au dezvoltat în această<br />

direcţie arhitecturi foarte diverse, de la sisteme cu zeci de mii de procesoare foarte simple la sisteme<br />

cu un număr redus de procesoare foarte complexe. O altă direcţie de dezvoltare a fost aceea a unor<br />

procesoare foarte puternice care, deşi execută un unic flux de instrucţiuni (deci o singură activitate)<br />

pun la dispoziţia programatorului instrucţiuni foarte puternice care acţionează în paralel asupra unei<br />

cantităţi mari de date (procesoare vectoriale sau, într-un sens mai larg, calculatoare bazate pe<br />

paralelism al datelor).<br />

Ca de atâtea ori, şi de aceasta dată dezvoltarea arhitecturilor a luat-o înaintea dezvoltării<br />

limbajelor şi a mediilor de programare. Sunt disponibile deci, la ora aceasta, sisteme de calcul cu<br />

arhitecturi foarte evoluate şi cu performanţe uluitoare, dar într-o cu mult mai mică măsură s-au<br />

dezvoltat uneltele software care să permită utilizarea în mod eficient a acestor sisteme. Aceasta este<br />

Tibor Asztalos<br />

2


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

direcţia în care se aşteaptă în continuare rezultate în domeniul programării concurente (paralele).<br />

Problema este deci ca programatorul să scrie într-o manieră cît mai simplă programe<br />

complexe, care să pună în valoare potenţialul de performanţă al sistemului de calcul şi să permită<br />

obţinerea rapidă a unei soluţii <strong>pentru</strong> un calcul foarte complex. S-au dezvoltat o serie de "limbaje<br />

paralele" în această direcţie, de la dialecte C sau Fortran, limbaje mai sofisticate orientate pe<br />

obiecte, până la limbaje bazate pe concepte noi mai puţin convenţionale. O altă direcţie de<br />

dezvoltare este aceea de a lăsa programatorul să descrie algoritmul în mod tradiţional, fără a ţine<br />

cont de faptul că urmează să fie executat pe un sistem paralel. Compilatoarele disponibile sînt cele<br />

care urmează să preia sarcina de a analiza programul şi de a-l transforma, în mod automat, astfel<br />

încât să fie executat în mod eficient, punând în valoare potenţialul de paralelism al arhitecturii.<br />

Se pune întrebarea dacă aceste probleme sînt interesante din punct de vedere al nostru, care avem o<br />

şansă relativ mică de a se întâlni cu un calculator paralel sofisticat. Răspunsul este DA din cel puţin<br />

următoarele motive:<br />

1. În viitor vom avea la dispoziţie pe orice PC sisteme de operare cu facilităţi paralele, şi deci<br />

aplicaţiile vor trebui să pună în valoare aceste facilităţi (în vederea exploatării eficiente a<br />

calculatorului şi a creşterii gradului de "confort psihic" al utilizatorului).<br />

2. Domeniul programării sistemelor dedicate <strong>pentru</strong> control, bazate pe PC sau pe sisteme şi mai<br />

simple formate din unul sau mai multe microprocesoare, se va dezvolta în mod exploziv în viitorul<br />

foarte apropiat.<br />

3. Şi în occident se constată o tendinţă de a renunţa, pe cît posibil, la calculatoare paralele<br />

sofisticate şi de a utiliza în locul lor (acolo unde performanţele o permit) aşa numite "ferme de<br />

staţii". Cu alte cuvinte, privim o reţea (care trebuie să fie suficient de rapidă!) de staţii ca un<br />

calculator paralel şi dezvoltăm aplicaţii care se execută în paralel pe aceste staţii. Sunt disponibile<br />

medii de programare care facilitează realizarea unor astfel de programe, cum ar fii PVM, bazat pe<br />

RPC.<br />

4. În domenii "speciale", cum sunt tele<strong>comunicaţii</strong>le, sunt necesare echipamente inteligente, ce sunt<br />

de fapt nişte calculatoare "speciale", sisteme multiuser şi multitasking, care deseori operează în timp<br />

real, şi în care apare frecvent necesitatea comunicării inter-proces, (asigurată prin programare<br />

concurentă).<br />

De ce programare concurentă, ce se urmăreşte de fapt, de ce subiectul revine astăzi în actualitate?<br />

Subiectul nu este deloc nou. Este interesant de menţionat în acest context faptul că<br />

majoritatea noţiunilor fundamentale <strong>pentru</strong> programarea concurentă: secţiuni critice, excludere<br />

mutuală, deadlock, semafoare au fost prezentate <strong>pentru</strong> prima dată de către Dijkstra într-un raport<br />

tehnic al Universităţii Tehnice din Eindhoven, Olanda în 1965. Ani de zile, aspectele referitoare la<br />

programarea concurentă au fost discutate numai în cadrul cursurilor de sisteme de operare. Singurii<br />

care aveau ocazia să utilizeze noţiunile de proces, sincronizare, excludere mutuală erau<br />

programatorii de sisteme de operare, deci o parte a elitei programatorilor. Există şi un alt grup de<br />

programatori care utilizează de mulţi ani programarea paralelă. Este vorba de programatorii de<br />

aplicaţii numerice foarte mari care se execută pe maşini paralele. Şi ei constituie o elită, <strong>pentru</strong> care<br />

este important ca un calcul să se termine într-un timp rezonabil, adică în ore şi nu în săptămâni. Şi<br />

<strong>pentru</strong> asta de obicei scriu programele într-un limbaj pe care alţii l-au uitat sau nu au mai apucat să<br />

îl înveţe - Fortran. Această preferinţă nu este un capriciu, <strong>pentru</strong> Fortran 90 există compilatoare care<br />

reuşesc să facă o foarte bună paralelizare a codului, pe care programatorul îl scrie fără precauţii<br />

speciale. Şi deci ce este nou ?- nou este faptul că acest tip de programare nu mai este <strong>pentru</strong> elite, în<br />

Tibor Asztalos<br />

3


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

curând va deveni accesibil (obligatoriu ?) <strong>pentru</strong> programatorii obişnuiţi. Acest fenomen apare<br />

datorită mai multor schimbări. Evoluţia preţurilor va face ca până şi calculatoarele personale să<br />

poată să fie multiprocesor. În general resursele de care poate dispune "un simplu calculator"<br />

(memorie internă, memorie externă, viteză de lucru) devin lună de lună mai importante. În acelaşi<br />

timp majoritatea calculatoarelor (personale sau nu) încep să fie conectate în reţea. Într-o astfel de<br />

reţea calculatoarele (staţiile) nu sunt utilizate tot timpul, dar nici nu au un program fix de<br />

utilizare/încărcare. Există perioade de timp de durate foarte variabile în care puterea de calcul a<br />

unui astfel de calculator poate să fie utilizată <strong>pentru</strong> calcule iniţiate de către altă staţie/utilizator.<br />

Nici această idee nu este foarte nouă dar devine tot mai interesantă pe măsură ce puterea de calcul a<br />

unei staţii la care lucrează un singur utilizator devine tot mai mare. Au fost proiectate sisteme de<br />

operare distribuite care încearcă să utilizeze cât mai eficient această putere de calcul distribuită. Cea<br />

mai simplă soluţie presupune pornirea unor procese pe procesoarele devenite disponibile. Un proces<br />

pornit se va executa până la terminarea sau omorârea sa. În acest fel dacă un procesor nu a fost<br />

utilizat <strong>pentru</strong> un interval dat de timp acesta poate să fie utilizat de către sistemul de operare<br />

distribuit. Ce se întâmplă însă dacă proprietarul încearcă să îşi recupereze staţia ? Evident el nu<br />

poate să fie împiedicat să o utilizeze <strong>pentru</strong> un interval semnificativ de timp, deoarece în acest caz<br />

nu o să mai fie probabil de acord cu utilizarea staţiei de către sistemul de operare distribuit. De aici<br />

decurg o serie de probleme legate de migrarea proceselor sau a aplicaţiilor, dar se lucrează la<br />

rezolvarea lor şi nu va mai dura mult până când vor apărea astfel de sisteme de operare comerciale.<br />

Deci un mediu care să permită programarea concurentă va fii la îndemâna tuturor. Ce utilizări va<br />

avea ..., vom vedea.<br />

Domeniile "calde" legate de programarea concurentă sunt în majoritate cele legate de<br />

utilizarea sistemelor distribuite. De asemenea nu s-a ajuns încă la un consens în ce priveşte<br />

limbajele de programare concurentă. Dacă <strong>pentru</strong> programarea secvenţială din miile (probabil) de<br />

limbaje propuse în timp s-au impus până la urmă câteva, în programarea concurentă încă se mai<br />

caută.<br />

În momentul de faţă limbaje de programare concurentă cum sunt Ada, Linda sau Orca<br />

încearcă să ascundă cât mai multe aspecte legate de implementare de programator. Acest deziderat<br />

reuşeşte în măsura în care aplicaţia tratată se potriveşte cu modelul implementat de limbaj. În<br />

momentul în care această potrivire nu există programatorul va trebui să exprime rezolvarea în<br />

termenii limbajului ceea ce nu neapărat se va face cu performanţe foarte bune. Există implementări<br />

în care se cere de la programator un sprijin mai important. Evident că astfel de sarcini puse în<br />

sarcina programatorului îl împiedică pe acesta să se concentreze asupra aspectelor care ţin de<br />

rezolvarea problemei şi nu de modul în care limbajul sau sistemul folosit sunt implementate.<br />

În general limbajele <strong>pentru</strong> programare paralelă şi/sau distribuită sunt realizate pe scheletul unui<br />

limbaj de programare secvenţial în care se introduc construcţii specifice. Aceste construcţii se<br />

referă, în general, la descrierea paralelismului şi la mecanismele de comunicare şi sincronizare. În<br />

cele ce urmează vom discuta principalele mecanisme de descriere a paralelismului respectiv <strong>pentru</strong><br />

comunicare şi sincronizare fără a considera un limbaj de programare anume.<br />

1.2 Sisteme de operare (SO) multitasking<br />

Problema execuţiei multitasking a fost una dintre problemele care au stat în atenţia<br />

proiectanţilor de sisteme de operare încă de la primele generaţii de sisteme. Ideea care s-a conturat<br />

şi a devenit “clasică” este execuţia “întreţesută” a succesiunilor de instrucţiuni de la mai multe<br />

sarcini de calcul.<br />

Tibor Asztalos<br />

4


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Apariţia multiprocesării (rularea simultană a două sau mai multe programe) este asociată cu<br />

apariţia sistemelor timesharing (1970). Sistemele timesharing rectifică o mulţime de probleme<br />

create de sistemele batch pe care le-au şi înlocuit.<br />

Utilizatorul se confruntă în sistemele timesharing cu un sistem virtual mai lent decât maşina<br />

propriu zisă, <strong>pentru</strong> că aplicaţia sa rulează în paralel sau concurent (terminologia va fi discutată<br />

mai târziu în acest capitol). Cu toate acestea, un motiv subtil le face de preferat sistemele<br />

timesharing: operaţiile I/O lente, realizate fără sprijin din partea procesorului pot mări (uneori<br />

nepermis de mult) timpul de execuţie (per total) al aplicaţiilor. În sistemele timesharing timpul<br />

petrecut de o aplicaţie <strong>pentru</strong> efectuarea operaţiilor de intrare/ieşire este folosit de alte aplicaţii<br />

<strong>pentru</strong> calcule utile, unitatea de procesare fiind neocupată.<br />

În cazul sistemelor de calcul cu multiprogramare unitatea elementară de program se<br />

numeşte task (operaţiune, sarcină de calcul) sau proces şi reprezintă cea mai mică unitate de<br />

program ce se poate executa independent de către echipamentul de calcul. El reprezintă o secvenţă<br />

de instrucţiuni, logic independentă, dintr-un program, care poate fi executată simultan cu alte părţi<br />

ale aceluiaşi program. Această definiţie corespunde noţiunii de proces introdusă de modelul von<br />

Neumann, dar îmbogăţită. Un proces este un program secvenţial în execuţie - el este compus (din<br />

punctul de vedere al sistemului de operare) din codul programului care se execută, datele şi<br />

resursele ocupate, şi este descris de anumite informaţii de stare.<br />

Trebuie făcută diferenţă clară dintre un program şi proces. Procesul este o entitate dinamică,<br />

pe când programul este o entitate statică ce poate fi regăsită pe un mediu de stocare.<br />

Spaţiul de adrese al unui proces este inviolabil, fiind inaccesibil (în condiţii normale) de<br />

către un alt proces. În 1963 M. Conway postulează trei funcţii de bază <strong>pentru</strong> un sistem de operare<br />

multitasking:<br />

• FORK(label) creează un nou proces definit de procedura care se execută la momentul curent.<br />

Acest proces începe cu instrucţiunea referită de label. Procesul iniţial îşi urmează cursul normal<br />

Odată noul proces creat, cele două procese coexistă şi se execută concurent (sau paralel).<br />

• QUIT() este folosită de proces <strong>pentru</strong> terminare. Procesul este distrus, şi toate resursele ocupate<br />

sunt eliberate.<br />

• JOIN(count) este folosit <strong>pentru</strong> a combina două procese în unul singur:<br />

count = count - 1;<br />

if (count!=0) QUIT();<br />

Un singur proces execută apelul JOIN la un moment dat. Odată ce un proces execută JOIN nici un<br />

alt proces nu poate executa aceeaşi instrucţiune.<br />

Un aspect foarte important este faptul că prima definiţie, în varianta iniţială, creează un nou proces<br />

(numit proces fiu) în acelaşi spaţiu de adrese. Dacă această abordare permite unui proces fiu să<br />

folosească în comun datele cu părintele şi procesele înrudite, este imposibil să se realizeze izolarea<br />

contextului a două procese ce rulează simultan. De asemenea este esenţială şi posibilitatea ca<br />

procesul fiu să poată redefini conţinutul propriului spaţiu de adrese.<br />

Acest model este folosit în linii mari de toate sistemele de operare moderne, chiar dacă varii<br />

aspecte au fost modificate.<br />

Aplicaţiile ce rulează pe un astfel de sistem, sunt compuse de regulă dintr-un ansamblu de<br />

taskuri (sau procese) ce se pot executa în paralel şi care interacţionează unele cu altele, fiind<br />

necesar:<br />

• să-şi transmită reciproc informaţii (date), şi<br />

• să-şi sincronizeze execuţiile.<br />

Divizarea ansamblului de programe în taskuri (sau operaţii/procese/sarcini elementare)<br />

simplifică proiectarea şi elaborarea lui, uşurând totodată realizarea cerinţelor în ceea ce priveşte<br />

timpii de răspuns.<br />

Tibor Asztalos<br />

5


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

În general, ordinea strictă de execuţie a taskurilor ce compun programele din aceste aplicaţii<br />

este puţin previzibilă, acestea fiind legate de timpul sistem (numite şi aplicaţii time driven), de<br />

producerea unor anumite evenimente (event - driven) , sau de sosirea unor semnale sau mesaje.<br />

1.2.1. Reprezentarea în memorie a unui proces<br />

In ceea ce priveşte reprezentarea în memorie a unui proces, indiferent de platforma (sistemul de<br />

operare) pe care este operaţional, se disting, în esenţă, următoarele zone:<br />

• Contextul procesului<br />

• Codul programului<br />

• Zona datelor globale<br />

• Zona heap<br />

• Zona stivei<br />

Contextul procesului conţine informaţiile de localizare în memoria internă şi informaţiile de stare a<br />

execuţiei procesului:<br />

• Legături exterioare cu platforma (sistemul de operare): numele procesului, directorul curent în<br />

structura de directori, variabilele de mediu etc.;<br />

• Pointeri către începuturile zonelor de cod, date stivă şi heap şi, eventual, lungimile acestor zone;<br />

• Starea curentă a execuţiei procesului: contorul de program (notat PC -program counter) ce<br />

indică în zona cod următoarea instrucţiune maşină de executat, pointerul spre vârful stivei (notat<br />

SP - stack pointer);<br />

• Zone de salvare a regiştrilor generali, de stare a sistemului de întreruperi etc.<br />

Zona de cod conţine instrucţiunile maşină care dirijează funcţionarea procesului. De regulă,<br />

conţinutul acestei zone este stabilit încă din faza de compilare. Programatorul descrie programul<br />

într-un limbaj de programare de nivel înalt. Textul sursă al programului este supus procesului de<br />

compilare care generează o secvenţă de instrucţiuni maşină echivalentă cu descrierea din program.<br />

Conţinutul acestei zone este folosit de procesor <strong>pentru</strong> a-şi încărca rând pe rând instrucţiunile de<br />

executat. Registrul PC indică, în fiecare moment, locul unde a ajuns execuţia.<br />

Zona datelor globale conţine constantele şi variabilele vizibile de către toate instrucţiunile programului.<br />

Constantele şi o parte dintre variabile primesc valori încă din faza de compilare. Aceste valori iniţiale<br />

sunt încărcate în locaţiile de reprezentare din zona datelor globale în momentul încărcării programului<br />

în memorie.<br />

Zona heap - cunoscută şi sub numele de zona variabilelor dinamice - găzduieşte spaţii de memorare a<br />

unor variabile a căror durată de viaţă este fixată de către programator. Crearea (operaţia new) unei<br />

astfel de variabile înseamnă rezervarea în heap a unui şir de octeţi necesar reprezentării ei şi întoarcerea<br />

unui pointer / referinţe spre începutul acestui şir. Prin intermediul referinţei se poate utiliza în scriere<br />

şi/sau citire această variabilă până în momentul distrugerii ei (operaţie destroy, dispose etc.).<br />

Distrugerea înseamnă eliberarea şirului de octeţi rezervat la creare <strong>pentru</strong> reprezentarea variabilei. In<br />

urma distrugerii, octeţii eliberaţi sunt plasaţi în lista de spaţii libere a zonei heap.<br />

Zona stivă In momentul în care programul apelelează o procedură sau o funcţie, se depun în vârful<br />

stivei o serie de informaţii: parametrii transmişi de programul apelator către procedură sau funcţie,<br />

adresa de revenire la programul apelator, spaţiile de memorie necesare reprezentării variabilelor locale<br />

declarate şi utilizate în interiorul procedurii sau funcţiei etc. După ce procedura sau funcţia îşi încheie<br />

activitatea, spaţiul din vârful stivei ocupat la momentul apelului este eliberat. In cele mai multe cazuri,<br />

Tibor Asztalos<br />

6


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

există o stivă unică <strong>pentru</strong> fiecare proces. Există însă platforme, DOS este un exemplu, care folosesc<br />

mai multe stive simultan: una rezervată numai <strong>pentru</strong> proces, alta (altele) <strong>pentru</strong> apelurile sistem.<br />

În figura 1.1. sunt reprezentate două procese active simultan într-un sistem de calcul.<br />

Context P1<br />

PC<br />

SP<br />

Figura 1.1. Două procese într-un sistem de calcul<br />

Procesele sunt aşadar entităţi de sine stătătoare - faptul că în condiţii normale spaţiul de<br />

adrese al unui proces trebuie să fie inviolabil produce o serie de probleme: anumite procese trebuie<br />

să comunice între ele (procese cooperante). Problemele referitoare la procesele comunicante vor fi<br />

discutate mai în capitolele următoare.<br />

1.2.2. Gestionarea task-urilor într-un context multitasking<br />

Fiecărui task i se ataşează, în contextul timpului real, un indicator de importanţă denumit<br />

prioritate, ce poate fi :<br />

• un atribut fix, constant al task-ului ;<br />

• un atribut a cărei valoare este variabilă şi se stabileşte în momentul activării sale (sau al<br />

re-activării).<br />

În unele SO există un număr limitat de nivele de priorităţi (de ex. 4 - 256) şi fiecare task este<br />

asociat, static sau dinamic, unuia dintre aceste nivele; în cadrul fiecărui nivel de prioritate, dacă<br />

există mai multe task-uri cu aceeaşi prioritate, ele se vor diferenţia între ele prin “vechimea” avută<br />

(din momentul de timp în care au fost activate), de regulă, după criteriul “primul venit - primul<br />

servit” (FIFO - First In First Out).<br />

Tibor Asztalos<br />

PROCESUL P1<br />

Cod P1<br />

Date P1<br />

Heap P1<br />

Stiva P1<br />

Context P2<br />

PC<br />

SP<br />

PROCESUL P2<br />

Cod P2<br />

Date P2<br />

Heap P2<br />

Stiva P2<br />

Aceste priorităţi permit:<br />

• asigurarea unor anumiţi timpi de răspuns,<br />

• utilizarea în comun, de către mai multe task-uri a unor resurse comune (echipamente<br />

periferice, zone de memorie, etc.)<br />

Task-urile ce sunt la un moment dat activate şi încărcate în memoria internă îşi dispută<br />

dreptul de a utiliza unitatea centrală (presupunând un sistem monoprocesor).<br />

Componenta planificator a oricărui sistem de operare dirijează procesele în execuţie aşa<br />

încât în intervalul în care un proces este în aşteptare după o resursă / terminarea unei operaţii de<br />

I/O, un alt proces să beneficieze de procesor. Astfel, instrucţiunile fiecărui proces se execută “pe<br />

secvenţe” alternând cu secvenţe de instrucţiuni de la alte procese. Deci fiecare proces primeşte<br />

controlul, procesorul îi execută câteva instrucţiuni maşină, starea lui este salvată şi controlul este<br />

transmis unui alt proces ş.a.m.d. In acest fel, mai multe procese înaintează simultan spre finalizarea<br />

activităţii lor.<br />

7


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Aceste task-uri sunt plasate de către planificatorul SO într-un şir de aşteptare şi sunt<br />

executate în ordinea de prioritate a acestora.<br />

Ori de câte ori SO preia controlul, el întrerupe sau suspendă execuţia task-ului în execuţie.<br />

După ce SO îşi termină acţiunea, decizia acestuia, în ceea ce priveşte acordarea dreptului de a<br />

utiliza unitatea centrală poate fi:<br />

• de reluare a execuţiei task-ului întrerupt ;<br />

• de trecere la execuţia task-ului cu prioritatea momentan cea mai mare din şirul de<br />

aşteptare;<br />

• de trecere la execuţia unui task oarecare; în cazul în care mai multe task-uri au aceeaşi<br />

prioritate, ele sunt executate conform politicii primul venit - primul servit ;<br />

• în ordinea specifică sistemelor cu acces multiplu (multi-user) unde fiecărui task i se<br />

acordă pe rând o cuantă limitată de timp (politica “time-sharing”).<br />

Unele SO poate oferi facilitatea de “activare în condiţii de criză de timp”, prin creşterea<br />

automată a priorităţii unui task, dacă acesta nu a fost executat de un anumit timp. SO examinează<br />

şirul de aşteptare al task-urilor “gata de execuţie” şi decide alocarea CPU imediat după producerea<br />

şi rezolvarea următoarelor evenimente :<br />

• un eveniment extern (o întrerupere dinspre un proces sau un operator uman);<br />

• un eveniment intern (o întrerupere generată de o operaţie I/O);<br />

• o întrerupere de la dispozitivul “ceas de timp real”;<br />

• apelarea din programe a unei funcţii realizată de SO (apel de sistem);<br />

• terminarea sau suspendarea execuţiei task-ului activ;<br />

• trecerea unui anumit interval de timp;<br />

Pentru a putea relua execuţia unui task, la întreruperea lui se memorează (se salvează)<br />

întregul context, adică toate informaţiile necesare reluării task-ului (conţinutul registrelor, starea<br />

unui indicator etc.).<br />

Tibor Asztalos<br />

Stările task-urilor<br />

ÎN EXECUŢIE<br />

GATA DE EXECUŢIE<br />

BLOCAT<br />

INACTIV<br />

Figura 1.2. Stările task-urilor<br />

Un task se poate găsi la un moment dat într-una<br />

din următoarele patru stări (figura 1.2.) :<br />

a) în execuţie (RUN): utilizează CPU în acel<br />

moment, într-un sistem monoprocesor un<br />

singur task se poate găsi la un moment dat<br />

în această stare;<br />

b) gata de execuţie (READY): are toate<br />

resursele necesare rulării, mai puţin<br />

procesorul; aşteaptă, într-un şir de aşteptare<br />

<strong>pentru</strong> a i se aloca CPU spre a fi executat;<br />

c) blocat (WAIT): are nevoie şi de alte resurse;<br />

aşteaptă să i se aloce memorie, sau<br />

terminarea unei operaţii I/O, producerea<br />

unui eveniment extern, trecerea unui<br />

anumit interval de timp, etc.<br />

În cazul unor SO practice aceste stări pot avea şi o serie de sub-stări.<br />

Sunt posibile următoarele tranziţii între stările unui task :<br />

• inactiv - blocat,<br />

8


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

• inactiv - gata de execuţie,<br />

• blocat - gata de execuţie,<br />

• gata de execuţie - în execuţie,<br />

• în execuţie - gata de execuţie,<br />

• în execuţie - blocat,<br />

• gata de execuţie - blocat,<br />

• în execuţie - inactiv.<br />

Planificarea funcţiei de timp. Unele task-uri trebuiesc executate la anumite momente de<br />

timp, după un anumit interval de timp sau cu o anumită întârziere de timp. Aceste task-uri sunt<br />

plasate corespunzător în anumite şiruri de aşteptare temporale, ce sunt examinate şi actualizate cu o<br />

frecvenţă adecvată. Când se îndeplineşte condiţia de timp de execuţie <strong>pentru</strong> un task din aceste<br />

şiruri de aşteptare, el este transferat în starea “gata de execuţie”.<br />

Trecerea controlului de la un proces la altul se decide folosind una dintre următoarele două tehnici:<br />

• Planificare cooperativă, atunci când procesul în execuţie decide singur momentul în care<br />

transmite controlul dispecerului sistemului de operare care la rândul lui dă controlul altui<br />

proces. Tehnica este folosită doar de sistemele mai vechi şi mai puţin pretenţioase. De exemplu,<br />

Windows3.1 este un astfel de sistem.<br />

• Planificare preemptivă, atunci când nucleul sistemului de operare decide momentul trecerilor de<br />

control. Este metoda cea mai folosită de sistemele actuale.<br />

Dintre metodele preemptive, aplicate mai ales sa sistemele monoprocesor, cel mai des folosită este<br />

multiprogramarea bazată pe priorităţi. In esenţă, este vorba de comutarea proceselor între stările<br />

RUN (procesorul execută o secvenţă de instrucţiuni ale procesului), WAIT (aşteaptă terminarea<br />

unei operaţii I/O) şi READY (proces pregătit spre a fi trecut în starea RUN), iar trecerea este<br />

dirijată de un sistem de priorităţi. In figură ilustrăm un fragment a unei posibile evoluţii a trei<br />

programe în regim de multiprogramare.<br />

P3 RUN WAIT RUN WAIT RUN WAIT<br />

P2 RDY RUN WAI RUN WAI RUN WAIT RUN …<br />

T<br />

T<br />

P1 READY RUN READY RUN WAI<br />

T<br />

RUN READY …<br />

0 1 2 3 4 5 6 7 8 9 10 …<br />

Figura 1.3. Evoluţia stărilor proceselor în multiprogramare<br />

Se observă că în acest exemplu, procesorul central staţionează doar în cel de-al 7-lea cadru. In<br />

schimb, procesul P3 are acces prioritar la procesor, în timp ce procesul P1 este slab prioritar, prinde<br />

procesorul abia după ce celelalte procese îl lasă liber.<br />

Pot fi folosite şi alte tehnici de planificare. Cele mai utilizate sunt cele de tip planificare circulară<br />

(Round-Robin).<br />

Pentru funcţionarea planificării circulare se defineşte o cuantă de timp, de obicei între 10-100<br />

milisecunde. Coada READY a proceselor care aşteaptă la procesor este tratată circular. Pe durata unei<br />

cuante se alocă procesorul unui proces. După epuizarea acestei cuante, procesul este trecut la sfârşitul<br />

cozii, al doilea proces este preluat de către procesor ş.a.m.d.<br />

Există o mare varietate de criterii după care se stabileşte ordinea de prioritate din coada READY.<br />

Noi vom exemplifica cu criteriul folosit de către sistemul de operare Unix, datorită marii lui<br />

9


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

răspândiri. Durata cuantei de timp este, de regulă, 1 secundă. La epuizarea cuantei de timp, se<br />

suspendă activităţile proceselor din sistem. Apoi, <strong>pentru</strong> fiecare proces Pi activ în sistem se<br />

(re)calculează prioritatea lui pi astfel:<br />

timpul de servire de catre procesor a procesului Pi<br />

pi = --------------------------------------------------timpul<br />

de staţionare în memorie a procesului Pi<br />

Apoi, <strong>pentru</strong> cuanta următoare de timp procesorul deserveşte procesul Pi care cel mai mic număr pi. Se<br />

garantează astfel un timp mediu de răspuns, rezonabil <strong>pentru</strong> fiecare proces din sistem. In schimb, nu se<br />

asigură răspuns prompt la o execuţie "preferenţială".<br />

Există şi variante de Round-Robin.<br />

Dacă un proces nu şi-a consumat în întregime cuanta (posibil din cauza unei operaţii de I/O), atunci<br />

locul lui în coada READY se determină invers proporţional cu partea consumată din cuantă. Altfel<br />

spus, cu cât procesul şi-a consumat mai puţin dintr-o cuantă, cu atât va fi plasat în coadă mai aproape de<br />

procesor, după ce îşi termină operaţia I/O.<br />

O altă variantă urmăreşte echilibrarea sistemului folosind aşa-zisul Round-Robin cu reacţie. Când un<br />

proces nou este acceptat, el se rulează mai întâi atâtea cuante câte au consumat celelalte procese, după<br />

care trece la planificarea Round-Robin simplă.<br />

Tibor Asztalos<br />

1.2.3. Funcţiile nucleului unui SO<br />

Cea mai importantă parte a unui sistem de operare, cea care se regăseşte în oricare<br />

implementare, de la MS-DOS la Unix, este NUCLEUL.<br />

Una din primele utilizări la care se gândeşte cineva când este întrebat despre nucleu, este<br />

drept o colecţie de funcţii care sunt necesare marii majorităţi a programelor. Cele mai evidente sunt<br />

funcţiile <strong>pentru</strong> intrare-ieşire (I/O); utilizatorul în loc să dea zeci de comenzi controlerului, să<br />

aştepte întreruperea, să verifice rezultatele, eventual să încerce din nou, să trateze erorile, apelează<br />

o funcţie din nucleu (printr-un apel de sistem - un fel de apel de procedură mai special), procedură<br />

care, de exemplu, ar putea avea numele mnemonic “write_file(…)”, cu nişte argumente simple: un<br />

nume de fişier şi un tablou de caractere.<br />

De altfel în cazul sistemului de operare MS-DOS nucleul - grosso modo - la cam atâta se<br />

rezumă: la a oferi nişte funcţii foarte utile. La MS-DOS nucleul este format din mai multe<br />

componente, una se cheamă BIOS, (Basic Input-Output System) şi este o suită de funcţii înscrise în<br />

memorii nevolatile (PROM sau EPROM). De exemplu, MS-DOS oferă (ca mai toate sistemele de<br />

operare) o serie de funcţii <strong>pentru</strong> accesul discului. Astfel, în loc de a avea de a face cu un obiect<br />

foarte complicat, care are capete, cilindri, etc., avem o structură de directoare şi fişiere, care permite<br />

fişierelor să aibă nume, şi să crească fără să se calce pe picioare între ele. Funcţii de genul “adaugă<br />

un octet la sfîrşitul fişierului cutare” sunt tot părţi din nucleu.<br />

În general, în cazul SO multitasking, apelurile sau comenzile din programele utilizator care<br />

se adresează SO servesc <strong>pentru</strong> comunicarea între aceste programe şi SO <strong>pentru</strong> realizarea<br />

următoarelor acţiuni:<br />

• activarea task-urilor, planificarea în raport cu timpul a task-urilor, sincronizarea,<br />

suspendarea sau terminarea execuţiei task-urilor;<br />

• alocarea sau modificarea priorităţii task-urilor;<br />

• iniţierea operaţiilor de I/O, inclusiv accesarea sistemelor de fişiere;<br />

10


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

• obţinerea de informaţii despre stările task-urilor;<br />

• obţinerea de informaţii despre timpul sistemului etc.<br />

Aceste apelări de servicii SO sunt tratate de rutine speciale, componente ale SO.<br />

De exemplu, apelurile de sistem Unix oferă portabilitate şi o interfaţă eficientă aplicaţiilor<br />

utilizator.<br />

În fond, prin apelurile de sistem, nucleul oferă un suport eficient <strong>pentru</strong> rularea programelor<br />

de aplicaţie pe maşina respectivă. Una din foarte importantele funcţii ale nucleului unui sistem de<br />

operare este de a permite fişierelor să devină programe care se execută (şi care atunci capătă<br />

denumirea de task-uri sau procese).<br />

Una din funcţiile auxiliare acestui scop este gestiunea resurselor calculatorului (memorie, etc.)<br />

în aşa fel încât simultan sau succesiv să poată exista MAI MULTE procese distincte. Sunt avute în<br />

vedere gestionarea task-urilor, a timpului sistem, a memoriei, a evenimentelor, a perifericelor I/O,<br />

suoprt <strong>pentru</strong> <strong>comunicaţii</strong> dintre procese etc.<br />

MS-DOS oferă, de exemplu, un apel de sistem numit “system()”, prin care un utilizator<br />

poate indica nucleului un fişier, al cărui conţinut este un şir de instrucţiuni pe care îl doreşte<br />

executat. E simplu să foloseşti “system()”, dar nucleul are o treabă foarte grea de făcut. El trebuie să<br />

găsească <strong>pentru</strong> noul proces un loc în memorie (poate le dă pe cele vechi afară), să citească întregul<br />

fişier (am văzut că asta nu e deloc simplu), şi apoi să sară la prima instrucţiune citită.<br />

Cum se implementează multitasking-ul ?<br />

Lucrurile se complică şi mai mult dacă programul nou citit trebuie să se execute în paralel cu<br />

altele. Metodele prin care nucleul implementează de obicei multitasking-ul au fost discutate în<br />

secţiunea anterioară. Acolo am subliniat faptul că cel mai frecvent se foloseşte a ceea ce se numeşte<br />

“time sharing” (partajarea timpului de lucru). Să vedem cum se poate ea obţine din instrucţiunile<br />

primitive ale maşinii. Ideea de bază este simplă: toate calculatoarele au un “ceas de timp real” (un<br />

periferic) care generează periodic întreruperi. Fiecare întrerupere cauzează, după cum am spus,<br />

întreruperea programului care tocmai se execută, memorarea PC-ului din această clipă, şi saltul la o<br />

anumită procedură asociată întreruperii. În mod normal toate aceste proceduri, care servesc la<br />

tratarea întreruperilor, sunt tot parte a nucleului (numele lor englezesc este “interrupt handlers”).<br />

Soseşte întreruperea de ceas; tocmai se execută programul nr.1. Se sare la procedura de tratare a<br />

întreruperii din nucleu, care se uită să vadă cine se execută: programul nr.1. Nucleul, memorează<br />

starea acestuia, (valoarea ProgramCounter-ului şi a celorlalţi regiştri), apoi ia din alt loc valoarea<br />

pe care o salvase <strong>pentru</strong> PC-ul programului nr.2, şi face un salt acolo, şi tot aşa. Rezultatul este<br />

executarea întreţesută a instrucţiunilor celor două programe. Lucrurile se petrec foarte frumos<br />

astfel; dacă programele nu accesează nici o zonă de memorie sau fişier sau periferic în comun,<br />

Tibor Asztalos<br />

Programe utilizator (vi, ls, emacs , etc)<br />

Subsistem de<br />

fisiere<br />

Apeluri de sistem<br />

Nucleu<br />

Subsistem de<br />

control al proceselor<br />

Biblioteci<br />

(iostream , stdio, etc)<br />

write, read,close, stat, chmod, etc. fork, exec, wait, signal,etc.<br />

Hardware<br />

Figura 1.4. Apelurile de sistem - o interfaţă dintre aplicaţii şi SO<br />

11


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

fiecare are impresia că este singurul care se execută pe acel calculator. Programele nu trebuie scrise<br />

într-un fel special <strong>pentru</strong> a putea coexista, de aceea sarcina programatorului este mult mai simplă.<br />

Inversa operaţiei de creere a unui proces este “moartea”, oprirea acestuia. Aceasta este<br />

realizată de nucleu scoţând procesul vechi de pe lista proceselor care se vor executa şi eliberând<br />

memoria alocată lui (plus alte resurse folosite, cum ar fi fişiere, etc.).<br />

Cum rezolvă un SO multitasking concurenţa în utilizarea memoriei ?<br />

Memoria este una singură (de obicei), şi este cam greu să scrii un program în aşa fel încât să<br />

se poată împăca cu oricare altul. Dar nucleul preia şi aceste sarcini. Orice sistem de operare<br />

multitasking transformă şi accesul la memorie în aşa fel încât procesele să nu se poată influenţa în<br />

moduri nedorite. Mecanismul pe care îl descriem (numit “memorie virtuală”) este extrem de util<br />

chiar şi în cazul în care nu avem multitasking.<br />

Ideea este de a folosi UMM (Unitate de Management a Memoriei - submodul hardware a<br />

CPU) <strong>pentru</strong> a da impresia fiecărui proces care se execută pe maşină că toată memoria este a lui, de<br />

la adresa 0 la adresa infinit, dacă se poate, şi a nimănui altcuiva, chiar dacă lucrurile nu stau de fapt<br />

de loc aşa.<br />

Soluţia este de a înghesui mai multe procese simultan în memoria disponibilă fizic, şi de a<br />

traduce fiecare acces la adresa X a unuia din procese la o altă adresă.<br />

Această situaţie se prezintă în figură:<br />

Procesul nr. 1 Procesul nr. 2<br />

0 N 0 M<br />

0 N N+1 N+M<br />

Figura 1.5. Mecanismul memoriei virtuale<br />

Când procesul nr.1 face o referinţă la ceea ce el crede că este adresa 0, această referinţa este<br />

trimisă de UMM chiar la adresa 0 din memoria fizică. Pe de altă parte, adresa 0 a procesului nr.2<br />

este trimisă la adresa N+1 din memoria fizică.<br />

Reamintiţi-vă că pe sistemele monoprocesor cele două procese nu se execută niciodată<br />

simultan (Obs: Dacă avem de-a face cu un multiprocesor - un calculator cu mai multe procesoare -<br />

două procese se POT executa simultan, dar în esenţă funcţionarea memoriei virtuale este aceeaşi),<br />

ci pe rând, când le vine rândul. De fiecare dată când comută de la un proces la altul (deci când<br />

tratează întreruperea de la ceas), nucleul trebuie să schimbe şi felul în care UMM traduce adresele.<br />

Acest lucru îl face CPU discutând cu UMM cam în acelaşi fel în care discută cu un controler: îi<br />

transmite tot felul de parametri.<br />

Mecanismul simplu al memoriei virtuale poate fi exploatat şi în alte moduri. De exemplu se<br />

pot executa în time-sharing procese al căror total de memorie necesară depăşeşte cantitatea<br />

existentă. Cum? Simplu: când un proces este înlocuit cu altul, părţi din memoria ocupată de el sunt<br />

mutate pe disc (de exemplu într-un fişier). Când vine din nou la rând, acele părţi sunt aduse la loc<br />

în memoria fizică (discul, de obicei, este mult mai mare decât memoria). Această tehnică se<br />

numeşte “swapping”. Aceasta este cu două tăişuri, <strong>pentru</strong> că scrierea/citirea de pe disc este mult<br />

mai lentă decât cea din memorie, şi nu-ţi poţi permite ca la fiecare zecime de secundă să te muţi la<br />

Tibor Asztalos<br />

adrese virtuale<br />

UMM traduce<br />

adrese fizice<br />

12


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

un alt program, iar mutarea să dureze 10 secunde. Nucleul trebuie să fie zgârcit în astfel de mutări.<br />

Izolarea proceselor în zone de memorie disjuncte face sarcina programatorului mai uşoară şi<br />

calculatorul mai eficient: se pot rula simultan programe ale unor utilizatori diferiţi, care nu se vor<br />

incomoda unul pe altul nicicum. Un program nu poate scrie/citi în/din memoria altuia, <strong>pentru</strong> ca<br />

nici una din adresele pe care el le referă nu este tradusă de UMM în vreo adresă a celuila<br />

Ce înţelegem prin tratarea evenimentelor ?<br />

Tratarea atât a evenimentelor externe cât şi a întreruperilor şi a evenimentelor interne este,<br />

în mare parte, similară. La apariţia unui eveniment controlul este preluat de SO. Acesta va<br />

identifica mai întâi evenimentul, după care, ca răspuns la acest eveniment, are loc una sau alta sau<br />

ambele din următoarele acţiuni:<br />

• se execută o rutină de tratare a întreruperii, fără a se afecta planificarea execuţiei taskurilor;<br />

• se activează un task prin plasarea lui într-un şir de aşteptare, conform priorităţii sale<br />

(<strong>pentru</strong> a i se aloca CPU <strong>pentru</strong> execuţie) sau, în mod excepţional, se poate trece direct<br />

la executarea task-ului.<br />

Rutinele specifice de tratare a evenimentelor pot fi asociate diferitelor tipuri de întreruperi<br />

cu ajutorul unor “tabele de asociere” (tabela vectorilor de întrerupere) şi, eventual <strong>pentru</strong> a asigura<br />

timpii reduşi de tratare a întreruperilor, pot fi şi micro-programate. În majoritatea SO reale, rutinele<br />

de tratare a întreruperilor au prioritate maximă.<br />

Evenimentele externe sunt semnalate sistemului de calcul prin intermediul unor dispozitive<br />

fizice speciale cum sunt “liniile de întrerupere”, “indicatori de evenimente” sau “cuvinte de stare de<br />

întrerupere”.<br />

Cum facilitează un SO comunicarea dintre programe ?<br />

Programele şi, respectiv, task-urile ce alcătuiesc ansamblul de programe al unui sistem de<br />

calcul de tip multitasking comunică destul de intens între ele, atât <strong>pentru</strong> a utiliza date comune cât<br />

şi <strong>pentru</strong> a-şi sincroniza în timp desfăşurarea execuţiei lor. Datele pot fi utilizate în comun cu mai<br />

multe task-uri, prin memorarea lor în zone de memorie internă declarate ca fiind comune mai<br />

multor task-uri şi la care toate task-urile vor avea acces (memorie partajată).<br />

Majoritatea SO actuale oferă un sistem de lucru cu fişiere care permite generarea şi<br />

utilizarea unor colecţii comune de date cu volume sensibil mai mari, la care diferitele task-uri pot<br />

avea acces într-un mod controlabil (fişiere partajate). Dezavantajul acestui sistem este legat de<br />

timpii de acces sensibil mai mari în acest caz.<br />

A treia metodă constă în facilitatea oferită de unele SO de a se transmite între task-uri<br />

anumite mesaje care constau dintr-un volum limitat de date. Pentru un număr limitat de task-uri ce<br />

comunică între ele printr-un asemenea mecanism, acesta poate fi foarte avantajos, asigurând timpi<br />

reduşi de răspuns şi folosind <strong>pentru</strong> executarea inter-comunicării doar locaţii din memoria internă.<br />

Prin acest (ultim) mecanism de transmitere de mesaje între task-uri se poate realiza şi<br />

sincronizarea între task-uri paralele ceea ce este o facilitate foarte utilă <strong>pentru</strong> comunicarea între<br />

task-uri.<br />

O altă formă importantă de comunicare între task-uri este utilizarea în comun a unor<br />

subrutine, în special <strong>pentru</strong> operaţiile cu I/O şi de comunicare cu module ale SO (planificator etc.).<br />

În cazul SO multitasking, în timpul execuţiei unei rutine comune mai multor task-uri, pot<br />

apare întreruperi, re-entranţa acestor rutine trebuind să fie asigurată de către SO. Există mai multe<br />

metode de a trata această comunicare dintre task-uri, cum ar fi :<br />

• blocarea altor task-uri ce ar putea apela subrutina în curs de execuţie;<br />

Tibor Asztalos<br />

13


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

• subrutina să fie re-entrantă (să permită re-instaţierea);<br />

• duplicarea subrutinei (câte o copie a subrutinei <strong>pentru</strong> fiecare task care o foloseşte);<br />

• inhibarea întreruperilor pe durata execuţiei unei subrutine utilizabile în comun de mai<br />

multe task-uri.<br />

14


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

Capitolul 2. Concepte de programare concurentă<br />

Un sistem de prelucrare concurentă se caracterizează prin existenţa simultană a mai multor<br />

programe active care îşi dispută resursa CPU, execuţia lor fiind “întreţesută”, spre deosebire de<br />

prelucrarea secvenţială, în care programele sunt executate succesiv. În cazul prelucrării concurente<br />

mai apare o caracteristică suplimentară şi anume că programele concurente nu sunt programe<br />

absolut independente între ele. În cazul acestor aplicaţii, task-urile sunt porţiuni ale unui program<br />

ansamblu care urmăreşte realizarea unei anumite prelucrări şi care a fost divizat în părţi (denumite<br />

task-uri) care pot fi executate în paralel, concurând la a obţine în acest scop resursa CPU, dar<br />

colaborând între ele (prin comunicare, sincronizare reciprocă etc.) <strong>pentru</strong> realizarea scopurilor<br />

comune, în condiţii de performanţe impuse. Marea majoritate a proceselor şi activităţilor care<br />

alcătuiesc o aplicaţie complexă se desfăşoară sau se pot desfăşura, prin natura lor, în paralel. Pentru<br />

a putea defini şi trata prin program asemenea activităţi paralele, care şi “colaborează” între ele, sunt<br />

necesare mecanisme de descriere şi manipulare a unor procese concurente.<br />

Se poate spune deci :<br />

Concurenţă = Paralelism + Interacţiune<br />

Îmbinarea celor două aspecte este esenţială, deoarece paralelism fără interacţiune reprezintă<br />

un caz particular al multi-tasking-ului iar interacţiune fără paralelism reprezintă cazul particular<br />

simplist al lanţului de mai multe programe secvenţiale.<br />

2.1. Concepte abstracte utilizate în descrierea concurenţei<br />

2.1.1. Paradigme de programare nesecvenţială<br />

Programele sunt adesea modelate ca un număr de task-uri distincte, care interacţionează<br />

<strong>pentru</strong> a executa serviciul sau <strong>pentru</strong> a produce rezultatul dorit. În cazul programării secvenţiale, un<br />

program este implementat ca o singură entitate complexă, care execută mai multe funcţii (părţi<br />

diferite din program). Funcţiile se execută într-o ordine bine determinată: reluarea execuţiei în<br />

acelaşi context conduce întotdeauna la aceeaşi succesiune a funcţiilor.<br />

În general, execuţia unui program secvenţial este deterministă. Astfel, <strong>pentru</strong> acelaşi set de date<br />

de intrare, programul execută aceeaşi secvenţă de instrucţiuni şi produce aceleaşi rezultate.<br />

Paradigma programării secvenţiale are două caracteristici de bază:<br />

• ordinea textuală a instrucţiunilor furnizează ordinea de execuţie a acestora.<br />

• instrucţiuni succesive se vor executa fără a se suprapune, în timp, una cu cealaltă.<br />

Nici una dintre aceste două proprietăţi de bază nu sunt valabile în cazul programării nesecvenţiale.<br />

Programarea nesecvenţială presupune implementarea mai multor entităţi de program, care pot fi<br />

executate în acelaşi timp şi cărora li se asociază task-uri distincte ce furnizează părţi ale rezultatului<br />

final. Deoarece execuţiile entităţilor se pot desfăşura simultan, ordinea lor nu este complet<br />

previzibilă, existând un oarecare grad de nedeterminism în timpul execuţiei.<br />

Paradigmele de programare nesecvenţială diferă între ele prin tipul sistemului de calcul pe<br />

care se execută entităţile concurente: sistem monoprocesor, multiprocesor, sistem distribuit. De<br />

asemenea, paradigmele diferă şi după modul cum sunt partajate de către entităţi informaţiile de<br />

context şi resursele sistemului. In sfârşit, paradigmele mai diferă după gradul de interacţiune între<br />

15


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

entităţi şi în funcţie de existenţa unei "autorităţi tutelare" care să asigure coordonarea entităţilor.<br />

Paradigmele de programare nesecvenţială pot fi grupate în trei mari categorii:<br />

1. paradigme de programare paralelă;<br />

2. paradigme de programare concurentă;<br />

3. paradigme de programare distribuită.<br />

Dintr-un anumit punct de vedere, fiecare dintre ele include pe cele dinaintea ei. In secţiunile<br />

următoare vom vedea ce este specific fiecăreia, avantaje, dezavantaje şi sfere de aplicabilitate.<br />

a) Programare paralelă<br />

Programarea paralelă presupunde dezvoltarea de programe care lansează mai multe sarcini de<br />

calcul: entităţi de execuţie, taskuri, procese, thread-uri (după caz). Acestea sunt executate simultan<br />

şi cooperează în vederea realizării unui scop comun. Deosebirea fundamentală (conceptual vorbind)<br />

a programării paralele faţă de alte paradigme nesecvenţiale constă în faptul că task-urile se<br />

coordonează numai prin operaţii de aşteptare a începerii sau a terminării altor entităţi implicate.<br />

Astfel, lansarea în execuţie a unui task poate fi condiţionată de startul prealabil al altora, respectiv<br />

de terminarea activităţilor altor task-uri.<br />

Punctăm câteva trăsături caracteristice ale programelor paralele:<br />

1. Un program paralel constă din unul sau mai multe sarcini de lucru (task-uri). Aceste task-uri<br />

se execută simultan şi numărul lor poate varia în timpul execuţiei programului.<br />

2. Fiecare task încapsulează un program secvenţial şi o memorie locală. De fapt, un task<br />

reprezintă o maşină virtuală von Neumann. (O maşină von Neumann conţine o unitate<br />

centrală de procesare – CPU - conectată la o unitate de memorare şi execută operaţii de<br />

citire şi scriere asupra unor date din memoria ataşată.)<br />

3. Task-urile pot fi ataşate procesoarelor fizice în diverse moduri, dar această ataşare nu<br />

afectează semantica programului. In particular, unui singur procesor îi pot fi ataşate mai<br />

multe taskuri.<br />

4. Se pot defini interfeţe de comunicare între aceste task-uri şi, de asemenea, modalităţi de<br />

acces la resursele comune, în conformitate cu modelul de paralelism folosit.<br />

În practică sunt utilizate mai multe modele de paralelism, diferenţiate după caracteristicile taskurilor.<br />

Prezentăm în continuare câteva dintre acestea.<br />

Modelul de paralelism bazat pe task-uri şi canale se caracterizează prin faptul că fiecare task are<br />

asociat un set de porturi de intrare (inports) şi un set de porturi de ieşire (outports), conectate între<br />

ele prin cozi de mesaje numite canale. Aceste canale pot fi create sau şterse dinamic. Un task poate<br />

executa operaţiile:<br />

• creare / ştergere canale;<br />

• citiri de la porturile de intrare / scrieri la porturile de ieşire;<br />

• creare / ştergere de alte taskuri.<br />

Figura 2.1. prezintă imaginea globală a unui proces de calcul. El constă dintr-un set de task-uri<br />

reprezentate prin cercuri şi imaginea detaliată a unui singur task. In cadrul unui task s-a evidenţiat<br />

pe de o parte setul lui de instrucţiuni (primul dreptunghi), iar pe de altă parte unitatea de memorie<br />

locală (celălalt dreptunghi). Interfaţa externă este asigurată printr-un set de porturi (ilustrate prin<br />

Tibor Asztalos<br />

16


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

săgeţile ce pleacă de la instrucţiuni spre exterior). Task-urile sunt conectate prin canale,<br />

reprezentate prin săgeţi. Un canal reprezintă o coadă de mesaje în care sunt plasate, respectiv din<br />

care sunt extrase mesaje.<br />

Tibor Asztalos<br />

Figura 2.1. Modelul taskuri şi canale<br />

Modelul bazat pe schimb de mesaje (message-passing) Programele dezvoltate după acest model,<br />

la fel ca şi programele create pe modelul taskuri şi canale, creează task-uri multiple. Fiecare task<br />

este identificat printr-un nume şi încapsulează date locale. Task-urile interacţionează trimiţând şi<br />

primind mesaje la, respectiv de la task-uri specificate prin nume. Cele două modele diferă numai<br />

prin mecanismul folosit <strong>pentru</strong> transferul datelor. La primul model se vorbeşte de "trimis mesaje pe<br />

canalul x”, iar la al doilea "trimis mesaje task-ului n”.<br />

Modelul de paralelism al datelor exploatează concurenţa care derivă din aplicarea aceleiaşi<br />

operaţii mai multor elemente dintr-o structură de date. De exemplu, “adaugă 2 la elementele unui<br />

tablou” sau “măreşte salariul tuturor angajaţilor cu x lei”. Programatorul are sarcina de a preciza<br />

cum sunt partiţionate datele în task-uri.<br />

Modelul memoriei partajate. In acest model, task-urile partajează un spaţiu comun de adrese, în<br />

care scriu şi citesc date. Pentru controlul accesului la memoria partajată se folosesc mecanisme<br />

adecvate, aşa cum vom vedea în secţiunile următoare. Un avantaj al acestui model constă în faptul<br />

că nu se specifică explicit cine este “producătorul” şi cine sunt “consumatorii” unei date.<br />

Din trăsăturile şi din modelele prezentate mai sus, se desprind trei proprietăţi de bază ale<br />

programelor paralele: concurenţă, scalabilitate şi modularitate.<br />

Concurenţa se referă la posibilitatea de a executa mai multe acţiuni simultan. Ea este o<br />

proprietate esenţială, mai ales dacă programul se execută pe un sistem multiprocesor.<br />

Scalabilitatea presupune funcţionarea programului, cel puţin la aceiaşi parametri de<br />

performanţă, dacă numărul de procesoare creşte.<br />

Modularitatea implică descompunerea unor entităţi de execuţie complexe în componente<br />

mai simple. Evident, această proprietate nu este specifică numai programării paralele, ci şi altor<br />

paradigme de programare şi proiectare.<br />

b) Programare concurentă<br />

Elementul esenţial prin care se deosebeşte programarea concurentă de cea paralelă este<br />

faptul că task-urile cooperează între ele în timpul execuţiei. Deşi problema este descompusă în<br />

subprobleme relativ independente, execuţia acestor task-uri poate să fie intercalată şi urmează un<br />

scenariu de paralelism la nivel logic. Două task-uri active pot face schimb de informaţii, pot să<br />

aştepte unul după altul etc.<br />

17


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Pentru comunicarea şi sincronizarea elementelor de execuţie implicate în programul<br />

concurent, s-au dezvoltat diverse mecansime specifice, asupra cărora vom reveni în secţiunile<br />

următoare. Tot în secţiunile următoare vom da câteva exemple de probleme care implică utilizarea<br />

paradigmei programării concurente.<br />

Una dintre problemele tipice de programare concurentă este problema producătorului şi a consumatorului.<br />

Pe scurt, ea se enunţă astfel: Există un recipient capabil să depoziteze în el un număr maxim de obiecte.<br />

Asupra recipientului acţionează două categorii de procese: procese producătoare şi procese consumatoare.<br />

Un producător fabrică un obiect şi îl depune în recipient. Un consumator extrage un obiect din recipient. Se<br />

presupune că există cel puţin un proces producător, cel puţin un proces consumator şi că procesele<br />

producătoare şi consumatoare evoluează în paralel. Pentru disciplinarea accesului la recipient, se impun trei<br />

condiţii:<br />

• In fiecare moment, asupra recipientului acţionează numai un singur proces, de orice tip ar fi el<br />

(acces exclusiv la recipient).<br />

• Dacă recipientul este plin, producătorii care vor să depună în el rămân în aşteptare până când un<br />

consumator extrage un obiect.<br />

• Dacă recipientul este gol, consumatorii care vor să extragă rămân în aşteptare până când un<br />

producător depune un obiect.<br />

In secţiunile următoare vom reveni asupra acestei probleme, precizând inclusiv modelul formal de descriere<br />

a execuţiei. Tot acolo vom prezenta şi alte probleme tipice.<br />

Atât concurenţa cât şi paralelismul necesită acces controlat la resurse partajate precum:<br />

dispozitive de I/O, fişiere, înregistrări din baze de date, structuri de date globale, etc. Conceptul de<br />

paralelism este implicit mai apropiat de hardware, fiind dependent mai mult de caracteristicile<br />

maşinii. Conceptul de concurenţă este mai apropiat de software, fiind o proprietate stabilită în<br />

principal de proiectantul programului.<br />

Tehnicile de programare concurentă se folosesc <strong>pentru</strong> a structura programe care implică<br />

mai multe activităţi, consumatoare de timp sau computaţional intensive, executate concurent sau<br />

programe care tratează evenimente asincrone.<br />

Pentru a concepe modele de programare concurentă independente de hardware, se asociază<br />

fiecărei entităţi executabile din program un procesor logic. Fiecare procesor logic este o maşină<br />

secvenţială care execută pe rând instrucţiunile din procesul alocat. Mai multe procesoare logice<br />

corespund unui procesor fizic. Acesta are implementat un mecanism de planificare <strong>pentru</strong> a da<br />

controlul procesoarelor logice aflate în gestiunea sa.<br />

Nu se fac ipoteze cu privire la vitezele relative ale operaţiilor corespunzătoare procesoarelor<br />

logice. Din acest punct de vedere, paralelismul poate fi considerat un caz particular al concurenţei,<br />

<strong>pentru</strong> sisteme multiprocesor, în care fiecărui procesor logic îi corespunde un procesor fizic. De<br />

obicei, sistemele de operare implementează concurenţa.<br />

Din cele expuse rezultă că gestiunea entităţilor concurente presupune, pe lângă creare şi terminare,<br />

operaţii de sincronizare, adică de coordonare a task-urilor care nu sunt complet independente, de<br />

comunicare, schimb de informaţii între taskuri şi de planificare, adică stabilirea priorităţilor taskurilor<br />

de executat.<br />

Aceste operaţii, precum şi mecanismele specifice, formale sau implementate pe unele platforme,<br />

sunt subiectele principale ale prezentei abordări.<br />

Printre avantajele paradigmei de programare concurentă amintim:<br />

• Controlul unor activităţi multiple, relativ independente şi gestiunea unor evenimente<br />

asincrone, externe. Programele sunt adesea scrise <strong>pentru</strong> a simula sau răspunde la<br />

evenimente din lumea reală, iar în lumea reală, concurenţa este un lucru obişnuit. Modelarea<br />

unui astfel de comportament este posibilă dacă mediu de programare suportă noţiunea de<br />

Tibor Asztalos<br />

18


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

concurenţă.<br />

• Creşterea eficienţei programelor consumatoare de timp. Operaţiile de I/O, sau de aşteptare<br />

(de exemplu, sleep) vor bloca numai procesul/thread-ul apelant până la terminarea operaţiei<br />

respective.<br />

• O reacţie mai rapidă a calculatorului la acţiunile utilizatorului. Pentru aceste cereri se vor<br />

crea procese sau thread-uri cu prioritate mai mare.<br />

• Imbunătăţirea performanţei. Dacă sunt disponibile procesoare multiple, care lucrează în<br />

paralel, execuţia programului este mult mai rapidă. Această situaţie poartă numele de<br />

concurenţă reală.<br />

• Pe sistemele uniprocesor este posibilă îmbunătăţirea performanţei prin modelare concurentă,<br />

dacă programul efectuează operaţii consumatoare de timp. Cât timp o activitate aşteaptă<br />

terminarea unei operaţii de I/O, procesorul poate executa alte sarcini de calcul. În acest caz,<br />

este vorba de o concurenţă logică.<br />

c) Programare distribuită<br />

Programarea distribuită implică procesarea logică, simultană, la distanţă, a unor task-uri pe<br />

platforme eterogene, plasate în diferite puncte din reţea. Contextele proceselor distribuite sunt strict<br />

separate. Procesele nu partajează între ele părţi ale mediului de execuţie. Două procese active pot<br />

schimba informaţii numai prin transfer de mesaje. Spre deosebire de programarea concurentă, în<br />

contexte distribuite nu există o autoritate centrală de coordonare a proceselor, şi nici nu se<br />

gestionează stări globale ale proceselor.<br />

În plus faţă de celelalte paradigme de programare nsecevenţială, în cazul programării<br />

distribuite pot să apară probleme de gestiunea întârzierilor de comunicare. De asemenea, pot să<br />

apară probleme de administrare a reţelelor (securizate mai mult sau mai puţin), sau a calculatoarelor<br />

din reţea în momentul când acestea nu mai funcţionează la parametrii normali.<br />

Metodele şi tehnicile specifice programării distribuite au astăzi un spectru extrem de larg. In<br />

prezent, sub termenul de middleware sunt cunoscute tehnologiile de nivel 6 din modelul OSI. In<br />

prezenta lucrare vom aborda doar tangenţial, în ultimul capitol, elemente de programare distribuită.<br />

Printre cele mai reprezentative exemple de probleme care implică o abordare distribuită<br />

sunt: serviciile Internet, aplicaţii client/server în care programul server şi aplicaţiile client se găsesc<br />

pe maşini diferite dar interconectate, soluţii client/server ale bazelor de date aflate la distanţă, agenţi<br />

mobili, etc.<br />

În modelul de aplicaţii distribuite client/server, programul client se execută pe maşina<br />

locală şi comunică printr-o conexiune de reţea cu programul server, executat pe maşina aflată la<br />

distanţă. Programul client implementează o interfaţă utilizator şi gestionează interacţiunile cu<br />

utilizatorul. Utilizatorul vede din întreaga aplicaţie doar programul client, care acceptă intrări şi<br />

produce rezultate. De fapt, aplicaţia client trimite cererile utilizatorului la server şi prezintă<br />

utilizatorului răspunsurile primite de la server. In unele cazuri, clientul participă, de asemenea, la<br />

procesarea cererilor şi răspunsurilor. De exemplu, filtrează sau sortează răspunsurile serverului, pe<br />

baza unui profil al utilizatorului, cunoscut numai programului client. In figura 3. este schiţat<br />

modelul client/server <strong>pentru</strong> aplicaţii distribuite.<br />

Tibor Asztalos<br />

19


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

user<br />

cerere<br />

raspuns<br />

interfata<br />

client<br />

cereri<br />

retea<br />

raspunsuri<br />

Figura 2.2. Client / server în context distribuit<br />

O clasă importantă de aplicaţii distribuite o reprezintă programele care au la bază arhitecturi<br />

precum CORBA (Common Object Request Broker Architecture), standardizată de OMG (Object<br />

Management Group) sau DCOM (Distributed Component Object Model) dezvoltată de Microsoft.<br />

Aceste arhitecturi de tip middleware oferă suportul tehnic necesar <strong>pentru</strong> ca aplicaţiile distribuite să<br />

coopereze indiferent de protocolul de comunicaţie sau de arhitectura sistemului gazdă.<br />

CORBA reprezintă o arhitectură multiplatformă care permite crearea de aplicaţii orientateobiect,<br />

distribuite, – obiecte diferite se pot găsi pe maşini diferite din reţea- şi eterogene –diferite<br />

tehnologii de reţea, platforme hardware sau sisteme de operare; implementarea obiectelor poate<br />

folosi limbaje de programare diferite.<br />

Tehnologia Microsoft OLE/COMoferă un model de gestiune a unor componente integrabile<br />

în interfeţe utilizator. Arhitectura DCOM (Distributed COM) extinde tehnologia COM, <strong>pentru</strong> a<br />

oferi suport <strong>pentru</strong> comunicarea obiectelor aflate pe calculatoare diferite, dintr-o reţea LAN, WAN<br />

sau chiar din Internet.<br />

Programarea distribuită este o paradigmă puternică de programare, printre avantajele ei de bază<br />

putem enumera:<br />

• Serviciile şi datele unei aplicaţii distribuite sunt adesea duplicate, astfel încât aplicaţia să-şi<br />

poată continua execuţia în siguranţă, chiar dacă o parte din sistem nu funcţionează. Dacă<br />

datele şi serviciile sunt duplicate în totalitate, utilizatorii nu vor detecta eventualele<br />

disfuncţionalităţi din sistem. În cazul unei duplicări parţiale, sistemul va continua să<br />

funcţioneze, oferind un număr restrâns de servicii.<br />

• Un sistem care include numeroase organizaţii diferite este mai uşor de administrat ca o<br />

entitate distribuită. Fiecare organizatie îşi pote structura, gestiona şi controla partea sa din<br />

sistem, conform propriilor legi, reguli şi preferinţe. Avantajul administrativ al unui sistem<br />

distribuit este evident în sistemul World-Wide Web, unde fiecare site este gestionat separat.<br />

• Performanţa mărită a unui sistem distribuit se obţine datorită posibilităţii de a executa mai<br />

multe programe, în acelaşi timp, pe maşini diferite. O problemă computaţională complexă<br />

poate fi divizată în sub-probleme (mai mult sau mai puţin independente) , care se pot<br />

executa pe maşini diferite, iar rezultatele vor fi combinate, pe una dintre maşini, <strong>pentru</strong> a<br />

obţine un răspuns final.<br />

• O arhitectură distribuită permite colaborarea la distanţă a unor persoane implicate în<br />

rezolvarea unei anumite probleme. Operând pe maşini diferite, aceste persoane pot observa<br />

şi manipula informaţii partajate (programe, date, documente, etc).<br />

server<br />

20


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

2.1.2. Interacţiunea task-urilor<br />

Un proces sau task, este un calcul care poate fi executat concurent (în paralel) cu alte calcule. El este o<br />

abstractizare a activităţii procesorului, fiind considerat ca un program în execuţie. Existenţa unui proces<br />

este condiţionată de existenţa a trei factori:<br />

• o procedură - o succesiune de instrucţiuni dintr-un set predefinit de instrucţiuni, cu rolul de<br />

descriere a unui calcul - descrierea unui algoritm.<br />

• un procesor - dispozitiv hardware/software ce recunoaşte şi poate executa setul predefinit de<br />

instrucţiuni, şi care este folosit, în acest caz, <strong>pentru</strong> a executa succesiunea de instrucţiuni<br />

specificată în procedură;<br />

• un mediu - constituit din partea din resursele sistemului: o parte din memoria internă, un spaţiu<br />

disc destinat unor fişiere, periferice magnetice, echipamente audio-video etc. - asupra căruia<br />

acţionează procesorul în conformitate cu secvenţa de instrucţiuni din procedură.<br />

Trebuie deci făcută deosebirea dintre proces şi program. Procesul are un caracter dinamic, el<br />

precizează o secvenţă de activităţi în curs de execuţie, iar programul are un caracter static, el numai<br />

descrie textual această secvenţă de activităţi.<br />

Evoluţia în paralel a două procese trebuie înţeleasă astfel:<br />

Dacă Ii şi Ij sunt momentele de început a două procese Pi şi Pj, iar Hi şi Hj sunt momentele lor de<br />

sfârşit, atunci Pi şi Pj sunt executate concurent dacă:<br />

max (Ii, Ij)


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Interacţiunea între task-uri concrete se manifestă sub trei forme principale, care vor fi<br />

examinate în capitolele ce urmează, şi anume:<br />

• excludere reciprocă,<br />

• sincronizare,<br />

• inter - comunicare.<br />

2.2. Situaţii de excepţie generate de concurenţă<br />

Execuţia simultană a mai multor procese/thread-uri, care accesează pe durata vieţii lor o serie de<br />

resurse (variabile) comune, pot genera situaţii ciudate de comportament al execuţiei programelor.<br />

Avem în vedere cazuri ce nu se întâlnesc în programarea secvenţială clasică. În cele ce urmează<br />

ilustrăm câteva astfel de situaţii.<br />

2.2.1. Rezultate inconsistente<br />

Cunoscută şi sub numele race-condition – este o situaţie generată de lipsa unei mecanism de<br />

sincronizare a task-urilor. Într-un context adecvat, execuţia intercalată a acestora generează<br />

rezultate inconsistente şi incorecte.<br />

Să presupunem că există două procese P1 şi P2 care au dreptul să modifice o aceeaşi variabilă v sub<br />

forma:<br />

P1: v = v+1 şi P2 :v = v+1<br />

Putem presupune că fiecare dintre cele două procese execută această incrementare concurent şi de un<br />

număr neprecizat de ori. De exemplu, cele două procese pot fi două agenţii de voiaj CFR care dau<br />

locuri simultan la acelaşi tren. Variabila v poate reprezenta numărul curent al locului vândut.<br />

Pentru a ilustra acţiunile repetate ale celor două procese, le vom descrie împreună, în specificarea<br />

PARBEGIN-PAREND din figura 2.4.<br />

PARBEGIN<br />

P1: . . . v = v + 1; . . .<br />

|<br />

P2: . . . v = v + 1; . . .<br />

PAREND<br />

Figura 2.4 Două procese accesează aceeaşi variabilă<br />

Dacă cele două procese interferează în execuţia acestei instrucţiuni, rezultatele nu vor fi cele aşteptate.<br />

Pentru clarificare, să presupunem (ceea ce corespunde realităţii) că execuţia unei instrucţiuni maşină nu<br />

poate fi întreruptă. Pentru execuţia atribuirii v=v+1 sunt necesare trei instrucţiuni maşină. Se foloseşte<br />

un registru r al maşinii şi o instrucţiune maşină de adunare a unui număr la un registru r=r+1. Secvenţa<br />

celor trei instrucţiuni este dată în figura 2.5.<br />

Tibor Asztalos<br />

r=v ;<br />

r=r+1;<br />

v=r;<br />

Figura 2.5 O secvenţă de incrementare<br />

Presupunem că fiecare dintre cele două procese P1 şi P2 are câte un registru de lucru r1 şi r2. În figura<br />

2.6 şi 2.7 sunt date două secvenţe posibile a fi executate de către cele două procese.<br />

22


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

Figura 2.6 Prima secvenţă de dublă incrementare<br />

Figura 2.7 A doua secvenţă de dublă incrementare<br />

Presupunem că la începutul fiecărei secvenţe variabila v are valoarea 5. Executându-se prima<br />

secvenţă (fig.2.6), v va avea în final valoarea 7, primind valoarea 6 de la procesul P1, apoi 7 de la P2.<br />

Executându-se secvenţa a doua (fig.2.7), variabila v va avea valoarea 6, deşi a fost incrementată<br />

de către două procese! (V-ar conveni să fiţi unul dintre cei doi cumpărători, sosiţi în acelaşi timp la<br />

două agenţii, după ce a fost deja vândut locul 5 şi primiţi amândoi acelaşi loc 6?).<br />

Pentru evitarea acestor rezultate inconsistente, este necesară includerea unor mecanisme de<br />

sincronizare, astfel încât secveneţele de cod corespunzător atribuirilor v=v+1 să nu fie executate<br />

întreţesut.<br />

Există multe prelucrări în care apar acest gen de situaţii. De exemplu, un task citeşte<br />

valoarea unei date dintr-o locaţie, în timp ce alt task atribuie o altă valoare <strong>pentru</strong> această dată.<br />

Un alt exemplu: două procese accesează simultan acelaşi articol al aceluiaşi fişier de pe disc.<br />

Fiecare dintre ele citeşte conţinutul articolului, îl prelucrează şi depune conţinutul înapoi. Rezultatul<br />

final este dat de ultima scriere a articolului. Normal, acest rezultat depinde de succesiunile celor<br />

două citiri şi scrieri.<br />

2.2.2 Live-lock<br />

P1: r1=v; P2: . . .<br />

r1=r1+1; . . .<br />

v=r1; . . .<br />

. . . r2=v;<br />

. . . r2=r2+1;<br />

. . . v=r2;<br />

P1: r1=v; P2:. . .<br />

r2=v<br />

r1=r1+1;<br />

r2=r2+1;<br />

v=r1;<br />

. . . v=r2;<br />

Sub denumirea de înfometare (starvation, live-lock) este precizată situaţia în care mai multe procese<br />

aşteaptă să obţină o resursă critică, dar accesul la ea NU este oferit într-o manieră echitabilă.<br />

Spunem că se află în starea starvation acele procese care aşteaptă relativ mult, în raport cu altele<br />

care chiar se pot termina de executat, sau procesele acelea care aşteaptă practic un timp indefinit<br />

după resursa respectivă.<br />

De exemplu, să ne imaginăm o situaţie în care mai multe procese doresc să acceseze diverse<br />

sectoare de pe acelaşi disc. Coordonatorul proceselor a stabilit o disciplină de acces prin care să<br />

reducă numărul total de deplasări a furcii cu capete de citire/scriere a discului. Regula de acces este:<br />

dintre toate procesele care solicită acces la disc, va fi selectat să acceseze discul procesul care<br />

solicită acces la sectorul cel mai apropiat de precedentul sector citit. Evident că prin această regulă<br />

se reduce numărul total de deplasări a furcii.<br />

Să luăm un exemplu concret: la un moment dat este accesat sectorul 1000. Cererile la disc<br />

sunt: un proces cere acces la sectorul 2000, în timp ce alte două procese solicită, în mod repetat,<br />

acces la sectoarele 999 şi 1001. In baza regulii de acces, vor primi dreptul de acces numai ultimele<br />

două procese, în timp ce primul proces are mari şanse să aştepte indefinit!<br />

23


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Infometarea este o situaţie de nedorit în programarea concurentă. Evitarea ei se poate face, spre<br />

exemplu, dacă din când în când se schimbă, într-o manieră echitabilă, regulile de acces la resursă.<br />

2.2.2. Impas<br />

Să presupunem că într-un sistem concurent se execută două procese P1, P2 care au nevoie,<br />

în diverse stadii ale execuţiei de aceleaşi două resurse nepartajabile R1, R2. Procesele ocupă<br />

resursele în diverse stadii ale execuţiei lor şi le eliberează la terminare. Procesul P1 solicită mai<br />

întâi resursa R1, iar după un timp solicită şi ocuparea resursei R2 fără să elibereze pe R1. Procesul<br />

P2 solicită mai întâi ocuparea resursei R2, iar după un timp solicită şi ocuparea resursei R1 fără să<br />

elibereze pe R2.<br />

Deci, la un moment dat, fiecare dintre procese va ocupa ambele resurse. Evident, pe perioada în<br />

care un proces ocupă ambele resurse, celălalt proces rămâne în aşteptare.<br />

Să ne imaginăm următorul scenariu de evoluţie în timp a celor două procese:<br />

0. Procesele P1 şi P2 sunt lansate în execuţie, iar resursele R1 şi R2 sunt ambele libere.<br />

1. Procesul P1 solicită resursa R1 şi o ocupă, ea fiind liberă.<br />

2. Procesul P2 solicită resursa R2 şi o ocupă, ea fiind liberă.<br />

3. Procesul P1 solicită resursa R2 şi intră în starea de aşteptare, deoarece R2 este deja ocupată<br />

de către procesul P2.<br />

4. Procesul P2 solicită resursa R1 şi intră în starea de aşteptare, deoarece R1 este deja ocupată<br />

de către procesul P1.<br />

Din acest moment, ambele procese se află în starea de aşteptare, din care teoretic nu vor mai putea<br />

ieşi niciodată!<br />

Acest fenomen este cunoscut în literatură sub mai multe denumiri: impas, interblocare, deadlock,<br />

deadly embrace, etc. Impasul este o stare gravă care conduce la eşecul execuţiei întregii aplicaţii. Din<br />

această cauză, proiectanţii de aplicaţii concurente trebuie să acorde o mare atenţie coordonărilor, spre a<br />

nu se ajunge la astfel de situaţii.<br />

In 1971, Coffman, Elphic şi Shoshani au indicat patru condiţii necesare <strong>pentru</strong> apariţia impasului:<br />

• -procesele solicită controlul exclusiv asupra resurselor pe care le cer (condiţia de excludere<br />

mutuală);<br />

• -procesele păstrează resursele deja ocupate atunci când aşteaptă alocarea altor resurse (condiţia<br />

de wait for);<br />

• -resursele nu pot fi şterse din procesele care le ţin ocupate, până când ele nu sunt utilizate<br />

complet (condiţia de nepreempţie);<br />

• -există un lanţ de procese în care fiecare dintre ele aşteaptă după o resursă ocupată de altul din<br />

lanţ (condiţia de aşteptare circulară);<br />

Acestei probleme i se acordă o mare importanţă mai ales în domeniul sistemelor de operare. Odată cu<br />

apariţia a numeroase aplicaţii concurente, impasul a devenit important şi <strong>pentru</strong> ele. Firesc,<br />

proiectanţilor de aplicaţii concurente trebuie să aibă în vedere impasul. Practica reliefează câteva<br />

abordări posibile:<br />

1. prima abordare, a cărei utilizare nu este prea recomandată, constă în ignorarea impasului, în<br />

speranta că acesta nu se va produce. Si dacă totuşi apare sistemul este oprit în mod forţat.<br />

2. a doua abordare permite producerea impasului, însă detectează apariţia lui. Odată ce a fost<br />

Tibor Asztalos<br />

24


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

detectat impasul, procesele sunt terminate selectiv sau sunt readuse la o stare anterioară şi<br />

suspendate temporar până când “pericolul” a trecut. Această soluţie este parţial acceptabilă;<br />

în schimb NU este potrivită, spre exemplu, <strong>pentru</strong> sistemele în timp real.<br />

3. A treia abordare constă în prevenirea impasului prin modificarea unor condiţii care pot<br />

conduce la impas. Prezentăm mai jos câteva astfel de tehnici.<br />

O primă posibilitate de prevenire a impasului este să se impună ca fiecare proces să ocupe din start<br />

toate resursele care-i sunt necesare, indiferent de momentele la care le utilizează efectiv. Natural,<br />

aceasta are ca efect secundar o utilizare nejudicioasă a resurselor.<br />

O a doua posibilitate constă în stabilirea de către sistemul de operare a unei ordini de solicitare a<br />

resurselor, la care trebuie să se alinieze toate procesele. Dacă R1, R2, ... Rk sunt toate resursele<br />

sistemului, atunci fiecare proces trebuie să le solicite, pe cele dorite, numai în această ordine. De<br />

exemplu, dacă un proces are nevoie de resursele Ri şi Rj, cu i < j, atunci el trebuie să le solicite în<br />

această ordine, NU întâi Rj şi apoi Ri, indiferent de momentele în care are nevoie efectiv de ele!<br />

Soluţia este funcţională, dar are în ea o mare doză de artificial.<br />

A treia posibilitate este ca sistemul să controleze, înainte de fiecare alocare, dacă nu cumva este<br />

posibilă apariţia impasului. Pentru aceasta se construieşte un graf de alocare a resurselor. Acesta<br />

este un graf orientat (X, U), având ca noduri procesele şi resursele, iar arcele sunt numai între procese<br />

şi resurse, astfel:<br />

Tibor Asztalos<br />

X = {P1, P2, ..., Pn, R1, R2, ..., Rm}<br />

Există un arc (Rj, Pi) în U dacă procesul Pi a ocupat resursa Rj.<br />

Există un arc (Pi, Rj) în U dacă procesul Pi aşteaptă să ocupe resursa Rj.<br />

In acest graf, dacă există un ciclu, atunci este posibil să apară impasul. In consecinţă, dacă în urma<br />

alocării unei resurse se ajunge la un graf ciclic, alocarea este anulată şi procesul solicitant este pus în<br />

aşteptare până la eliberarea unei resurse, după care se încearcă din nou alocarea ş.a.m.d.<br />

2.3. Mecanisme de control al concurenţei, comunicare şi sincronizare<br />

În practică, aplicaţiile concurente, atât la nivel de sistem de operare, cât şi la nivel de program s-au<br />

proiectat şi implementat folosindu-se diverse mecanisme de control al concurenţei. Astfel, un<br />

program concurent este "aproape" ca şi unul secvenţial clasic, numai că, din loc în loc apelează la<br />

serviciile unuia sau altuia dintre mecanismele de control, spre a se sincroniza execuţiile simultane<br />

ale proceselor. In funcţie de situaţie, un proces trebuie să aştepte după un altul.<br />

In cele ce urmează vom defini, la nivel conceptual, câteva astfel de mecanisme. De asemenea, vom<br />

analiza relaţiile dintre aceste mecanisme. În capitolele următoare vom prezenta diverse<br />

implementări ale acestora pe platformele Unix.<br />

2.3.1. Semafoare<br />

Conceptul de semafor a fost introdus de Dijkstra, <strong>pentru</strong> a facilita sincronizarea proceselor,<br />

prin protejarea secţiunilor critice şi asigurarea accesului exclusiv la resursele pe care procesele le<br />

accesează.<br />

Formal, un semafor se poate defini ca o pereche (v(s),c(s)) unde v(s) este valorea<br />

semaforului iar c(s) o coadă de aşteptare la semafor.Valoarea v(s) este un număr întreg care<br />

primeşte o valoare iniţială v0(s) iar coada de aşteptare conţine referinţe la procesele care aşteaptă<br />

25


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

la semaforul s. Iniţial coada este vidă, iar disciplina cozii depinde de sistemul de operare (LIFO,<br />

FIFO, priorităţi, etc.)<br />

Pentru gestiunea semafoarelor, se definesc două operaţii indivizibile, notate: P(s) şi V(s).<br />

Operaţia P(s), executată de un proces A, are ca efect decrementarea valorii semaforului s cu o<br />

unitate şi obţinerea accesului la secţiunea critică, pe care acesta o protejează. Operaţia V(s)<br />

executată de procesul A realizează incrementarea cu o unitate a valorii semaforului s şi eliberarea<br />

resursei blocate. În unele lucrări aceste primitve sunt denumite “WAIT” respectiv “SIGNAL”. In<br />

figura 2.8 şi figura 2.9 sunt prezentate definiţiile pseudocod ale celor două operaţii:<br />

Tibor Asztalos<br />

v(s)=v(s)-1;<br />

if v(s) < 0 then<br />

begin<br />

STARE(A):=WAIT;<br />

c(s)


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

2.3.2. Variabile mutex<br />

Variabila mutex (mutual exclusion) este un instrument util <strong>pentru</strong> protejarea unor resurse partajate,<br />

accesate concurent de mai multe thread-uri. Variabilele mutex sunt folosite, de asemenea, <strong>pentru</strong><br />

implementarea secţiunilor critice şi a monitoarelor (notiuni pe care le vom defini în secţiunile<br />

imediat următoare).<br />

O variabilă mutex are două stări posibile: blocată (este proprietatea unui thread) sau<br />

neblocată (nu este proprietatea nici unui thread). O variabilă mutex nu este proprietatea mai multor<br />

thread-uri simultan. Un task care vrea să obţină o variabilă mutex blocată de alt task, trebuie să<br />

aştepte până când primul o eliberează.<br />

Operaţiile posibile asupra variabilelor mutex sunt: iniţializarea (static sau dinamic), blocarea<br />

(<strong>pentru</strong> obţinerea accesului la resursa protejată), deblocarea (<strong>pentru</strong> eliberarea resursei protejate) şi<br />

distrugerea variabilei mutex.<br />

Din punct de vedere conceptual, o variabilă mutex este echivalentă cu un semafor s, care poate lua<br />

două valori: 1 <strong>pentru</strong> starea neblocată şi 0 <strong>pentru</strong> starea blocată. (Un astfel de semafor se va numi<br />

semafor binar). Operaţiile asupra unei variabile mutex m se definesc, cu ajutorul semafoarelor,<br />

astfel:<br />

• Iniţializare: se defineşte un semafor m astfel încât v0(m) = 1.<br />

• Blocare: (după o eventuală deblocare de către alt thread): P(m).<br />

• Deblocare: V(m).<br />

• Distrugere: distrugerea semaforului m.<br />

2.3.3. Variabile condiţionale<br />

Variabile condiţionale sunt obiecte de sincronizare şi comunicare între task-urile care aşteaptă<br />

satisfacerea unei condiţii şi task-ul care o realizează. O variabilă condiţională are asociate: un<br />

predicat şi o variabilă mutex. Predicatul dă condiţia ce trebuie să se realizeze şi care de obicei<br />

implică date partajate. Variabila mutex asigură faptul că verificarea condiţiei şi intrarea în aşteptare,<br />

sau verificarea condiţiei şi semnalarea îndeplinirii ei să fie executate ca şi operaţii atomice.<br />

Operaţiile posibile asupra variabilelor condiţionale sunt:<br />

• Iniţializare: care poate fi statică sau dinamică.<br />

• Aşteptare (wait): threadul este pus în aşteptare până când i se va semnala din exterior<br />

îndeplinirea condiţiei.<br />

• Semnalare (notify, broadcast, notifyall): threadul curent anunţă unul dintre thread-urile ce<br />

aşteaptă îndeplinirea condiţiei, sau toate thread-urile ce aşteaptă îndeplinirea condiţiei.<br />

• Distrugere.<br />

2.3.4. Conceptul de monitor<br />

Conceptul de monitor a fost introdus de C.A.R. Hoare în 1974. Acolo, Hoare descrie monitorul ca<br />

fiind un obiect folosit <strong>pentru</strong> realizarea execuţiei neconcurente a unui grup de proceduri. Noţiunea<br />

de monitor combină paradigma programării orientate-obiect cu unele tehnici de sincronizare.<br />

Un monitor este o construcţie similară cu un tip de date abstract. Scopul său principal este<br />

Tibor Asztalos<br />

27


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

de a încapsula variabilele partajate şi operaţiile asupra acestora. Astfel, toate secţiunile critice sunt<br />

concentrate în această structură la care, la un moment dat, are acces unul singur dintre task-uri.<br />

Secţiunile critice sunt extrase din task-urile obişnuite şi devin proceduri sau funcţii ale monitorului.<br />

In modelul lui Hoare, un monitor poate fi descris ca un obiect care conţine:<br />

1. datele partajate<br />

2. procedurile care accesează aceste date<br />

3. o metodă de iniţializare a monitorului<br />

Astfel, fiecare grup de proceduri este controlat de un monitor. În momentul rulării programului<br />

multi-thread, monitorul permite unui singur thread să execute o procedură controlată de el. In<br />

această situaţie, vom spune că threadul a ocupat monitorul. Dacă alte thread-uri invocă monitorul în<br />

timp ce acesta este ocupat, ele sunt suspendate până când procedura monitor apelată de thread-ul<br />

respectiv îşi încheie activitatea, ceea ce coincide cu eliberarea monitorului de către thread.<br />

Primele implementări ale conceptului de monitor au fost realizate în limbajele Pascal Concurent şi<br />

Mesa. Limbajul Modula foloseşte de asemenea acest concept. O variantă a conceptului de monitor a<br />

fost implementată în limbajul Java cu ajutorului modificatorului synchronised.<br />

Monitorul este un concept mai uşor de manevrat decât semaforul, motiv <strong>pentru</strong> care este mai<br />

utilizat în limbajele de programare specifice. Totuşi, din punct de vedere conceptual, un monitor<br />

poate fi descris simplu folosind un singur semafor binar, cu valoarea iniţială 1. Fiecare procedură a<br />

monitorului începe cu P(s) şi se încheie cu V(s):<br />

Tibor Asztalos<br />

var semaphore s = 1;<br />

- - - - - - - - - -<br />

Pentru fiecare procedura a monitorului:<br />

P(s)<br />

codul corpului procedurii<br />

V(s)<br />

2.3.5. Secţiune şi resursă critică; excludere mutuală<br />

Prin excludere mutuală a două procese se exprimă faptul că în fiecare moment numai unul dintre<br />

procese poate să fie activ.<br />

Prin secţiune critică se indică o porţiune de cod care nu poate fi executată la un moment dat decât<br />

de un singur task şi care secţiune, odată iniţiată, execuţia ei trebuie să fie terminată fără a fi<br />

întreruptă de un alt task. Asemenea secţiuni critice trebuie să fie, de regulă, cât mai scurte posibil<br />

deoarece toate celelalte task-uri sunt întârziate până la terminarea execuţiei unei asemenea secţiuni<br />

critice.<br />

Prin resursă critică este indicată o resursă care poate fi ocupată şi folosită la un moment dat numai<br />

de către un singur proces.<br />

De exemplu, secţiunea de cod din figura 2.5 din 2.2.1 trebuie să fie o secţiune critică. Astfel, cele două<br />

procese care o execută trebuie să se excludă mutual, iar variabila în cauză este o resursă critică.<br />

Problema secţiunii critice prezintă un interes deosebit, atât din punct de vedere teoretic, cât şi din punct<br />

de vedere practic. Majoritatea "subtilităţilor" programării concurente se leagă, într-un fel sau altul de ea.<br />

O secţiune critică bine definită trebuie să îndeplinească următoarele condiţii:<br />

28


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

• -la un moment dat, numai un singur proces este în secţiunea critică; orice alt proces solicită<br />

accesul la ea, îl va primi numai după ce procesul ocupant a terminat de executat instrucţiunile<br />

secţiunii critice;<br />

• -vitezele relative ale proceselor care accesează secţiunea critică sunt necunoscute;<br />

• -oprirea oricărui proces trebuie să aibă loc numai în afara secţiunii critice;<br />

• -nici un proces nu va aştepta indefinit <strong>pentru</strong> a intra în secţiunea critică.<br />

Tibor Asztalos<br />

Există diverse modele de implementare a secţiunii critice. Implicit, toate acestea rezolvă<br />

excluderea mutuală şi resursele critice.<br />

In modul cel mai simplu, o secţiune critică trebuie implementată, la fel ca şi la monitor, folosind un<br />

semafor binar s, cu valoarea iniţială 1. Codul secţiunii critice trebuie protejat de operaţiile P şi V<br />

efectuate asupra semaforului:<br />

var semaphore s = 1;<br />

- - - - - - - - - -<br />

P(s)<br />

codul sectiunii critice<br />

V(s)<br />

2.3.6. Regiuni critice condiţionale<br />

Prin regiune critică condiţională se înţelege o secţiune critică plus o resursă critică plus verificarea<br />

unei condiţii înainte de execuţia efectivă. Mai mult, la definirea ei i s-a conferit şi o sintaxă elegantă<br />

de specificare spre a fi folosită în diverse limbaje de programare.<br />

Fiecărei regiuni critice i se asociază o resursă constând din toate variabilele care trebuie protejate în<br />

regiune. Declararea ei se face astfel:<br />

resource r :: v1 , v2 , ..., vn<br />

unde r este numele resursei, iar v1 , ..., vn sunt numele variabilelor de protejat.<br />

O regiune critică condiţională se specifică astfel:<br />

region r [ when B ] do S<br />

unde r este numele unei resurse declarate ca mai sus, B este o expresie booleană, iar S este secvenţa de<br />

instrucţiuni corespunzătoare regiunii critice. Dacă este prezentă opţiunea when, atunci S este executată<br />

numai dacă B este adevărată. Variabilele din resursa r pot fi folosite numai de către instrucţiunile din<br />

secvenţa de instrucţiuni S.<br />

29


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Descrierea prin semafoare a unei regiuni critice condiţionate este dată în figura 2.10. Pentru descriere<br />

sunt necesare două semafoare şi un număr întreg. Semaforul sir reţine în coada lui toate procesele care<br />

solicită acces la regiune. Semaforul exclus asigură accesul exclusiv la anumite secţiuni ale<br />

implementării. Intregul nr reţine câte procese au cerut acces la regiune, la un moment dat.<br />

Tibor Asztalos<br />

Figura 2.10 Codul regiunii critice condiţionale<br />

2.3.7. Conceptul de întâlnire (rendez-vouz)<br />

Conceptul de întâlnire (rendez-vous) este introdus de limbajul Ada <strong>pentru</strong> a facilita comunicarea<br />

între două task-uri. Prezentăm scenariul de utilizare a acestui concept. Presupunem că avem două<br />

procese: A şi B. La un moment dat, execuţia lui A depinde de anumite informaţii furnizate de către<br />

B. Procesul B trebuie să aibă în vedere acest lucru şi să nu altereze informaţiile de care A are<br />

nevoie. Sunt posibile 3 situaţii, ilustrate în figura 2.11.<br />

Figura 2.11 Scenarii ale unui rendez-vouz<br />

Semantica acestor situaţii este:<br />

A:<br />

B:<br />

var semaphore sir = 0;<br />

exclus = 1; int nr = 0;<br />

- - - - - - - - - - - - - -<br />

P(exclus);<br />

nr:=nr+1;<br />

while not B do begin<br />

V(exclus); P(sir); P(exclus);<br />

end;<br />

nr:=nr-1;<br />

S;<br />

for i:=1 to nr do V(sir);<br />

V(exclus);<br />

a) b) c)<br />

a) Procesul B este gata să transmită informaţiile, dar procesul A nu le-a cerut încă. In acest caz,<br />

procesul B rămâne în aşteptare până când procesul A i le cere.<br />

b) Procesul B este gata să transmită informaţiile cerute, iar procesul A cere aceste date. In acest<br />

caz, se realizează un rendez-vous, cele două procese lucrează sincron până când îşi termină<br />

schimbul, după care fiecare îşi continuă activitatea independent.<br />

c) Procesul A a lansat o cerere, dar procesul B nu este în măsură să-i furnizeze informaţiile<br />

solicitate. In acest caz, A rămâne în aşteptare până la întâlnirea cu B.<br />

La fel ca şi la celelalte mecanisme, şi acesta poate fi descris foarte simplu folosind un semafor s.<br />

Acesta are ca valoare iniţială 0. Procesul A execută o operaţie P(s) înainte de punctul de întâlnire,<br />

iar procesul B execută o operaţie V(s) înainte de punctul de întâlnire.<br />

2.4 Implementări ale mecanismului de excludere reciprocă<br />

Modalităţile practice de implementare a mecanismului de excludere reciprocă operează prin:<br />

• inhibarea întreruperilor,<br />

• excluderea reciprocă prin programare,<br />

• instrucţiunea de interschimbare,<br />

30


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

• semafoare binare,<br />

• regiuni critice simple,<br />

• monitoare.<br />

2.4.1 Inhibarea întreruperilor<br />

Este o soluţie hardware. Când un program ajunge într-o secţiune definită critică, se<br />

generează o întrerupere care, prin hardware, inhibă celelalte întreruperi (prin mascare), pe durata<br />

execuţiei secţiunii critice.<br />

Este o metodă simplă şi eficientă, însă presupune o inhibare neselectivă a tuturor<br />

întreruperilor ce poate duce la întârzieri semnificative în executarea task-urilor celorlalte.<br />

2.4.2 Excluderea reciprocă prin programare<br />

Este o metodă software bazată pe ipoteza că accesele individuale la locaţiile de memorie<br />

sunt indivizibile, astfel în cazul în care mai multe task-uri încearcă simultan să acceseze (în mod<br />

citire sau scriere) o locaţie de memorie, hardware-ul va decide care va fi primul task ce o va face.<br />

Există mai multe soluţii de implementare software a excluderii reciproce între două task-uri ciclice,<br />

fiecare având câte o secţiune critică astfel:<br />

1 - prin utilizarea unei variabile comune speciale <strong>pentru</strong> protejarea secţiunilor critice, variabilă<br />

denumită “poartă”, care poate avea două stări (valori) “deschis” şi, respectiv, “închis”. Dacă<br />

valoarea variabilei este “închis” înseamnă că unul dintre task-uri se găseşte în secţiunea ei critică;<br />

“deschis” - înseamnă că task-ul poate executa secţiunea sa critică. Însă este posibil ca cele două<br />

task-uri să găsească, în mod simultan, valoarea “deschis” şi să iniţieze execuţia secţiunii critice<br />

proprii. Astfel, această soluţie nu garantează excluderea reciprocă corectă între două task-uri<br />

concurente.<br />

2 - prin utilizarea unei variabile comune speciale <strong>pentru</strong> a dirija cele două task-uri concurente în<br />

momentul în care ele încearcă să execute secţiunile lor critice. Astfel, o variabilă întreagă, numit<br />

“comutator”, poate avea valoarea 1 când task-ul 1 poate executa secţiunea sa critică şi valoarea 2<br />

când task-ul 2 poate trece la execuţia secţiunii sale critice. Această soluţie garantează excluderea<br />

reciprocă a celor două task-uri concurente, însă impune o execuţie alternantă obligatorie a<br />

secţiunilor critice ale celor două task-uri (tot timpul în ordinea 1,2,1,2,...).<br />

3 - Executarea alternantă obligatorie a secţiunilor critice poate fi evitată prin prevederea <strong>pentru</strong><br />

fiecare task în parte a unei variabile proprii locale care poate avea doar valorile “interior” şi<br />

,respectiv, “exterior”. Valoarea “interior” indică faptul că task-ul respectiv doreşte să execute<br />

secţiunea sa critică, iar valoarea “exterior” indică faptul că task-ul respectiv este în afara secţiunii<br />

sale critice. Fiecare dintre task-urile concurente poate examina valoarea variabilei corespunzătoare<br />

task-ului concurent înainte de a trece la execuţia secţiunii critice. În această metodă nu există nici o<br />

variabilă manipulată în comun şi oprirea unui task în afara secţiunii sale critice nu afectează<br />

desfăşurarea celuilalt task. O problemă apare însă în momentul în care ambele task-uri trec la<br />

asigurarea simultană a valorii “interior” variabilelor proprii. În această situaţie fiecare task aşteaptă<br />

ca celălalt să termine secţiunea sa critică, neîncepută de fapt (vor rămâne în această stare până la<br />

infinit).<br />

31


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

4 - bucla infinită din cazul anterior apare din cauza faptului că task-urile, în momentele de conflict,<br />

se aşteaptă unul pe celălalt să acţioneze. Dacă însă task-ul, care detectează faptul că ambele task-uri<br />

concurente încearcă să treacă la execuţia secţiunilor critice proprii, va modifica valoarea variabilei<br />

locale de control, atunci bucla infinită va fi evitată. Din păcate, însă, şi în acest caz poate apare<br />

blocajul în momentul în care cele două task-uri derulează exact în acelaşi ritm (situaţie similară<br />

cazului în care doi abonaţi telefonici se apelează reciproc în acelaşi moment de timp, primesc tonul<br />

de ocupat şi refac ambii apelul exact după acelaşi interval de timp de aşteptare).<br />

5 - O soluţie ce evită dificultăţile menţionate ale metodelor anterioare poate fi găsită prin<br />

combinarea ultimelor două propuneri. Astfel, fiecare task dispune de o variabilă proprie, care indică<br />

dorinţa de a trece la execuţia secţiunii critice iar în cazul în care ambele task-uri doresc simultan<br />

acest lucru, se utilizează o variabilă întreagă auxiliară <strong>pentru</strong> rezolvarea acestui conflict. În această<br />

metodă, fiecare task acţionează doar asupra variabilei celuilalt task concurent doar dacă variabilă<br />

proprie are valoarea “interior”. El va trece la executarea secţiunii critice proprii numai dacă task-ul<br />

concurent este în afara secţiunii sale critice. Variabila întreagă “comutator” este folosită <strong>pentru</strong> a<br />

rezolva conflictele între task-urile concurente permiţând task-ului, al cărui număr este în acel<br />

moment atribuit ca valoarea variabilei “comutator”, să intre în execuţia secţiunii sale critice. La<br />

ieşirea din secţiunea critică, task-ul va modifica valoarea variabilei “comutator” şi celălalt task va<br />

putea , astfel, să treacă la execuţia secţiunii sale critice.<br />

Din cele arătate mai sus, se pot deduce următoarele condiţii generale <strong>pentru</strong> realizarea<br />

corectă a excluderii reciproce a task-urilor concurente:<br />

• la un moment dat, cel mult un singur task se poate găsi în secţiunea sa critică;<br />

• oprirea unui task în afara secţiunii sale critice nu trebuie să afecteze celelalte task-uri;<br />

• nu se poate face nici o ipoteză asupra vitezei relative de desfăşurare a task-urilor;<br />

• task-urile ce sunt pe cale de a trece la execuţia secţiunilor lor critice, nu trebuie să se<br />

blocheze reciproc la infinit.<br />

Tibor Asztalos<br />

2.4.3 Instrucţiunea de interschimbare<br />

Metodele anterioare, de rezolvare prin software a problemei excluderii reciproce, presupun<br />

doar indivizibilitatea acceselor singulare la locaţiile de memorie. Dacă s-ar putea însă executa două<br />

operaţii anumite fără a fi întrerupte (de exemplu cum ar fi operaţiile de interschimbare a<br />

conţinutului unei locaţii de memorie cu cel al unui registru local al unui task) atunci excluderea<br />

reciprocă s-ar putea realiza mult mai simplu. În acest caz, se foloseşte o variabilă globală<br />

“excludere”, care este iniţializată cu o valoare egală cu unitatea. Înainte de a se executa secţiunea<br />

critică, task-ul trebuie să obţină valoarea unităţii memorate de variabila “excludere” şi să<br />

stabilească valoarea zero acestei variabile, ambele acţiuni printr-o singură operaţie indivizibilă. La<br />

sfârşitul acţiunii critice, task-ul va returna valoarea unitate variabilei “excludere”. Deoarece există<br />

o singură valoare unitate în sistem, cel mult un singur task îl poate obţine. Dacă un task nu este<br />

capabil să treacă la execuţia secţiunii sale critice, el va trebui să stea într-o buclă de aşteptare (în<br />

mod continuu). Când valoarea unitate a variabilei “excludere” este eliberată, una singură dintre<br />

buclele de aşteptare va putea să o folosească.<br />

Metoda este acceptabilă dacă există o solicitare moderată a resurselor şi dacă secţiunile<br />

critice sunt scurte.<br />

32


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

2.4.4 Semafoare binare<br />

Una dintre caracteristicile comune a metodelor anterioare este faptul că, dacă un task doreşte<br />

să execute o secţiune a sa critică şi nu i se permite acest lucru, atunci el pierde dreptul de a utiliza<br />

CPU. În aceste condiţii, este preferabil ca un asemenea task să fie trecut în starea de “blocat”<br />

urmând să fi adus în starea “gata de execuţie” în momentul în care este posibil să treacă la execuţia<br />

secţiunii sale critice. În acest scop pot fi utilizate semafoare binare (booleene sau variabile mutex),<br />

fiind o variabilă întreagă ce poate lua doar două valori : 0 sau 1.<br />

Fiecărei secţiuni critice i se asociază un semafor (iniţial având valoarea 1) iar task-ul care<br />

doreşte să execute secţiunea sa critică, va efectua o operaţie P; task-ului i se va permite execuţia<br />

secţiunii sale critice numai dacă valoarea actualizată (decrementată) a semaforului are valoarea<br />

zero. În caz contrar, task-ul este trecut în starea “blocat”. La ieşirea din secţiunea critică, task-ul va<br />

efectua o operaţie de tip V care va provoca incrementarea cu 1 a valorii variabilei semafor; dacă<br />

există alte task-uri blocate, unul dintre aceste task-uri va putea trece la executarea secţiunii sale<br />

critice. Deci, un task va rămâne blocat până când un alt task îşi va indica posibilitatea să continue.<br />

Operaţiile P şi V sunt indivizibile şi un singur task poate executa la un moment dat una dintre ele<br />

asupra aceluiaşi semafor. Operaţiile P şi V asupra semafoarelor se pot implementa şi prin hardware<br />

dar, de regulă, ele sunt implementate prin software, fiind protejate prin inhibarea întreruperilor.<br />

Pentru a evita aşteptarea, prin buclare, se foloseşte de regulă un şir de aşteptare în care se inserează<br />

şi se extrag task-urile ce se executau în momentul inhibării întreruperilor.<br />

Dezavantajul major al utilizării semafoarelor este legat de necesitatea unei programări foarte<br />

atente; dacă, de exemplu, din neatenţie se scrie o operaţie primitivă P în loc de V, atunci task-urile<br />

se vor bloca reciproc la infinit.<br />

2.4.5 Regiuni critice simple<br />

Pentru a uşura programarea corectă a semafoarelor este posibil să se utilizeze şi o<br />

construcţie de limbaj specifică limbajelor de programare de nivel înalt, cum este, de exemplu,<br />

regiunea critică simplă propusă de Hoare :<br />

with R do S<br />

care permite ca instrucţiunile critice S să opereze asupra variabilei comune R. Compilatorul<br />

respectiv va traduce această construcţie în instrucţiuni de program.<br />

Această construcţie evită necesitatea de a încadra cu operaţii P() şi V() toate secţiunile<br />

critice şi elimină necesitatea verificării lor de la începutul şi sfârşitul secţiunilor critice. Este sarcina<br />

compilatorului de a verifica totodată faptul că variabilele comune sunt utilizate doar în interiorul<br />

regiunii critice, actualizarea valorilor variabilelor comune fiind astfel protejată.<br />

2.4.6 Monitoare<br />

De fiecare dată când un task doreşte să execute secţiunea sa critică va face apel la procedura<br />

corespunzătoare a monitorului; unul singur dintre task-uri se admite la un moment dat să se bucure<br />

de serviciile monitorului, toate celelalte apeluri la monitor aşteptând eliberarea sa. Într-un monitor<br />

asupra variabilelor partajate nu se pot executa decât operaţii "cunoscute" (declarate o dată cu<br />

definirea monitorului) <strong>pentru</strong> care se asigură în mod automat condiţiile de excludere mutuală. Nu<br />

există posibilitatea accesului din exterior la datele locale monitorului decât prin intermediul<br />

operaţiilor. Din acest motiv nu pot să apară erori de sincronizare. Pentru monitoare se utilizează<br />

33


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

construcţii speciale în limbajele de programare astfel încât compilatoarele "ştiu" şi generează cod<br />

corespunzător asigurării excluderii mutuale, şi anume se poate insera în codul executat <strong>pentru</strong><br />

fiecare operaţie o secvenţa care verifică dacă exista un proces care execută deja o operaţie din<br />

monitor, situaţie în care procesul se va bloca. În vederea blocării se pot utiliza semafoare<br />

(inaccesibile direct programatorului).<br />

În programarea cu monitoare se consideră că un program conţine doua tipuri de module:<br />

procese active şi monitoare pasive. Toate variabilele partajate sunt variabile locale monitoarelor.<br />

Interacţiunea dintre procese are loc numai prin intermediul monitoarelor. La un moment dat cel<br />

mult un proces se poate afla în execuţia unui monitor dat. Un monitor operează asupra a trei tipuri<br />

de variabile: variabile permanente, variabile locale şi parametrii de apel. Variabilele permanente<br />

sunt de fapt variabilele partajate. Aceste variabile îşi păstrează valoarea între apelurile operaţiilor<br />

asigurate de monitor. Iniţializarea acestor variabile se face la crearea monitorului. Variabilele locale<br />

sunt variabile utilizate <strong>pentru</strong> realizarea operaţiilor şi au acelaşi tip de proprietăţi ca al variabilelor<br />

locale în proceduri. Ultimul tip de variabile sunt parametrii operaţiilor realizate de către monitor.<br />

Dacă un proces trebuie să execute o operaţie definită într-un monitor şi exista deja un proces<br />

care execută o operaţie din monitorul respectiv atunci procesul trebuie să se blocheze în aşteptarea<br />

eliberării monitorului. Din acest motiv trecerea unui proces printr-un monitor trebuie să dureze cât<br />

mai puţin. Ce se întâmpla însă dacă un proces care începe execuţia unei operaţii dintr-un monitor<br />

descoperă că trebuie să se blocheze în aşteptarea unui eveniment extern ?. Există cel puţin două<br />

abordări posibile. Se poate interzice apariţia unor astfel de situaţii sau se poate considera că dacă un<br />

proces se blochează (este în aşteptarea unui eveniment) în timp ce se află în execuţia unei operaţii<br />

dintr-un monitor atunci un alt proces poate să fie lăsat sa utilizeze monitorul. Evident trebuie să<br />

existe o "evidenţă" a proceselor care aşteaptă apariţia unui eveniment. Lista proceselor care sunt<br />

blocate în aşteptarea unui eveniment este organizată ca o coadă cu o anumită politică. Pentru a<br />

asigura semnalizarea blocării în aşteptarea unui eveniment se utilizează variabile condiţie. O astfel<br />

de variabilă este de fapt o variabilă partajată asupra căreia se pot executa două tipuri de operaţii<br />

primitive (atomice): wait (delay) şi signal (resume). Execuţia unei operaţii wait produce blocarea<br />

procesului care o invocă iar execuţia operaţiei signal anunţă posibilitatea deblocării unuia dintre<br />

procesele care a executat operaţia wait <strong>pentru</strong> variabila respectivă. Atunci când un proces este<br />

blocat ca urmare a execuţiei unei operaţii wait în corpul unei operaţii dintr-un monitor, alte procese<br />

pot să utilizeze monitorul. Se observă deci că operaţiile wait şi signal par să "semene" cu operaţiile<br />

P şi V. Wait blochează un proces iar signal activează un proces blocat. Există însă o deosebire<br />

importantă. Operaţia signal nu are nici un efect dacă nu exista un proces care aşteaptă (ceea ce nu<br />

este cazul cu operaţia V care incrementează valoarea semaforului). Operaţia wait blochează<br />

întotdeauna procesul care o execută.<br />

2.5 Sincronizarea explicită<br />

În acest paragraf se consideră situaţiile în care task-urile concurente doresc să coopereze<br />

între ele, fiind astfel, într-un fel, interesate de desfăşurarea celorlalte task-uri. Chiar şi în aceste<br />

situaţii, task-urile continuă să se concureze <strong>pentru</strong> a obţine dreptul de a intra în execuţia secţiunii<br />

lor critice, dar, odată obţinut acest drept, acţiunea task-ului în timpul execuţiei secţiunii critice<br />

poate conduce la realizarea unei condiţii care, anterior, determinase suspendarea (sau întârzierea)<br />

execuţiei unui alt task. Rezultă deci necesitatea existenţei unei metode care să permită unui task să<br />

indice (să semnaleze) că un anumit eveniment a avut loc sau să aştepte până când are loc un anumit<br />

eveniment. Accentul se pune aici pe asigurarea unor forme de cooperare între task-uri, spre<br />

avantajul lor reciproc.<br />

Tibor Asztalos<br />

34


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Noţiunea de “sincronizare”, în contextul special al programării TR, a devenit tot mai<br />

cuprinzătoare. Două programe se consideră sincronizate nu numai dacă sunt lansate în execuţie<br />

concomitent ci şi dacă se pot stabili relaţii reciproce temporale predictibile între anumite momente<br />

ale desfăşurării lor.<br />

Sensul general al sincronizării este cel de coordonare în timp, de corelare, presupunând o<br />

relaţie reciprocă între task-uri, care au un caracter temporal şi stabil. În acest sens, noţiunea de<br />

sincronizare s-a extins şi mai mult, incluzând şi forma de sincronizare cu timpul absolut sau cel real<br />

(înţelegând prin aceasta că un anumit task este lansat în execuţie în fiecare zi la o oră fixă sau ciclic,<br />

de exemplu, din oră în oră).<br />

Din cele spuse mai sus, rezultă că sincronizarea trebuie să permită activarea şi respectiv,<br />

inhibarea desfăşurării unor programe (task-uri) atât prin cereri iniţiate de alte task-uri cât şi prin<br />

comenzi ale utilizatorilor, introduse de la terminale. Inhibarea execuţiei trebuie să fie posibilă prin<br />

cererea programului însuşi, urmând ca reactivarea să fie făcută de un alt program (task) sau<br />

eveniment extern programului. Momentele de timp în care se iniţiază aceste acţiuni sunt momentele<br />

de sincronizare efectivă. Rezultă că sunt necesare funcţii referitoare la “evenimente”, în sensul<br />

general al cuvântului, şi anume “aşteptarea” până la producerea evenimentului specificat şi<br />

“anunţarea” faptului că acesta s-a produs. Eventuale alte funcţii mai complexe se referă la<br />

aşteptarea primului eveniment dintr-o mulţime precizată de evenimente, aşteptarea şi tratarea<br />

selectivă a mai multor evenimente într-o ordine eventual diferită de cea a apariţiei lor, terminării<br />

condiţionate etc.<br />

Metodele şi mecanismele folosite <strong>pentru</strong> realizarea sincronizării programelor sau task-urilor<br />

concurente se deosebesc sub mai multe aspecte:<br />

- natura sincronizării: sincronizare între programe sau task-uri sau sincronizare cu timpul;<br />

- momentul sincronizării: cu începutul unui program, cu sfârşitul unui program sau cu un<br />

“punct” (moment) oarecare din interiorul programului;<br />

- implementarea sincronizării: prin facilităţi specifice ale limbajelor de programare (şi ale<br />

compilatoarelor asociate), ale limbajului de comandă sau prin facilităţi (servicii) asigurate<br />

de un monitor.<br />

Principalele tehnici şi metode de implementare a sincronizării dintre task-uri concurente<br />

sunt prin:<br />

Tibor Asztalos<br />

2.5.1 Semafoare generale<br />

Un semafor general este o variabilă întreagă, ce poate lua doar valori pozitive şi singurele<br />

operaţii ce se pot efectua asupra lui sunt operaţiile primitive P şi V.<br />

Dacă semaforul are valoarea zero, un program (task) ce încearcă să efectueze o operaţie P<br />

asupra acestuia va fi suspendat (blocat) şi va aştepta până când un alt program va efectua asupra<br />

aceluiaşi semafor o operaţie V. Deosebirea esenţială între semafoarele generale şi cele binare constă<br />

în faptul că în cazul celor generale, mai multe programe (task-uri) pot efectua operaţia P asupra<br />

unui asemenea semafor, după care să continue execuţia acestor programe.<br />

Pentru a ilustra modul de utilizare a semafoarelor generale, să considerăm situaţia în care<br />

mai multe programe sau task-uri denumite “producătoare”, doresc să comunice o serie de date altor<br />

programe sau task-uri concurente, denumite şi “consumatoare” sau receptoare. Aceasta se poate<br />

realiza, de exemplu, folosind o zonă de memorie tampon, de capacitate evident limitată, în care<br />

producătorii vor depune datele lor şi din care consumatorii le vor extrage când acestea devin<br />

35


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

disponibile. Aceste programe trebuiesc evident sincronizate, astfel încât producătorii să nu depună<br />

date noi în zona tampon când aceasta este plină, iar consumatorii să nu extragă date din zona<br />

tampon când aceasta este goală.<br />

Acest exemplu reprezintă o ilustrare a procesului de interpătrundere a mecanismelor de<br />

sincronizare cu cele de comunicare inter-programe (inter-proces) concurente. Sincronizarea<br />

necesară între programe se poate realiza în acest caz folosind semafoare generale care să reflecte<br />

condiţiile posibile de aşteptare (condiţia de “tampon plin” respectiv “tampon gol”). Dacă se<br />

foloseşte, de exemplu, un semafor general “Plin” <strong>pentru</strong> a reprezenta numărul de locaţii încărcate<br />

(pline) ale zonei tampon, atunci un consumator va trebui întârziat dacă Plin = 0. Dacă un task<br />

producător va efectua o operaţie V asupra semaforului “Plin”, după ce a plasat o informaţie în zona<br />

tampon, un task-consumator va fi “servit” în urma efectuării unei operaţii P (va avea acces la<br />

tampon şi va putea “consuma” informaţia anterior introdusă). În mod similar, un al doilea semafor<br />

general “Gol”, poate fi folosit <strong>pentru</strong> a întârzia după necesităţi task-urile producător.<br />

Un task-consumator va trebui însă să semnalizeze momentul în care un task-producător<br />

poate să-şi continue acţiunea şi vice-versa.<br />

Pentru a evita suprascrierile sau săririle peste anumite date din zona tampon, aceste operaţii<br />

trebuiesc efectuate prin excludere reciprocă. În acest scop, după cum s-a văzut, se poate introduce şi<br />

un semafor binar.<br />

În general, dacă un task doreşte să aştepte până ce se realizează o anumită condiţie, acestei<br />

condiţii de aşteptare i se asociază un semafor şi orice task ce ar putea provoca realizarea condiţiei<br />

trebuie să aibă responsabilitatea efectuării unei operaţii V asupra acestui semafor. Din această<br />

cauză, programatorul va trebui să introducă în fiecare task instrucţiuni de sincronizare cu celelalte<br />

task-uri, ceea ce presupune o atenţie specială de lucru cu semafoarele generale.<br />

Dacă mai multe task-uri consumator aşteaptă, ele vor trebui planificate într-un mod neutru<br />

sau după unele criterii de prioritate ale task-urilor consumator.<br />

Tibor Asztalos<br />

2.5.2 Regiuni critice condiţionale<br />

Este o structură de programare de forma :<br />

with R when B do S<br />

unde B este o expresie booleană iar S este o secţiune critică ce operează asupra variabilei comune R.<br />

Această construcţie specifică faptul că secţiunea critică de program S trebuie executată doar<br />

dacă condiţia B este respectată.<br />

2.5.3 Variabile de condiţie<br />

Variabila de condiţie este un tip de dată ce poate fi utilizată doar în cadrul unui monitor. El<br />

este folosit în situaţiile în care un task doreşte să-şi întârzie propria execuţie sau <strong>pentru</strong> a reactiva<br />

un task pus anterior în aşteptare. Variabilele de condiţie sunt folosite <strong>pentru</strong> a identifica şirul de<br />

task-uri în aşteptare ce sunt manipulate prin operaţiile “aşteptare” şi “semnalare”. Operaţia<br />

“aşteptare” dezactivează task-ul şi îl trece într-un şir de aşteptare asociat variabilei de condiţie<br />

respective, anulând excluderea reciprocă care ar fi interzis ca un alt task să obţină serviciile<br />

monitorului.<br />

Operaţia “semnalare” va produce reactivarea primului task din şirul de aşteptare, asociat<br />

variabilei de condiţie respective. Task-ul care efectuează o operaţie “semnalare” şi provoacă<br />

36


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

reactivarea unui alt task (trecut anterior în aşteptare) va fi, la rândul său, suspendat până în<br />

momentul în care task-ul activat va ieşi din monitor sau va executa o operaţie “aşteptare”.<br />

Prin detalierea specifică a condiţiilor în care task-urile pot trece în aşteptare şi, respectiv, pot<br />

fi reactivate se poate realiza mai simplu şi mai explicit planificarea task-urilor decât în cazul<br />

regiunilor critice condiţionale (unde erau nevoie evaluări repetate ale condiţiilor) şi fiecare şir de<br />

aşteptare asociat variabilei de condiţie este tratat după strategia “primul - venit, primul - servit” de<br />

fiecare dată când se execută o operaţie “semnalare” asupra variabilei de condiţie respective.<br />

Tibor Asztalos<br />

2.5.4. Sincronizare prin comunicare<br />

În contextul unui sistem de operare cu programe concurente, comunicarea şi sincronizarea<br />

sunt în realitate doar faţete ale aceluiaşi fenomen: programele comunică între ele nu numai <strong>pentru</strong><br />

a-şi comunica informaţii sub formă de mesaje ci şi <strong>pentru</strong> a se sincroniza. De fapt, un semnal de<br />

sincronizare poate fi considerat şi el că este un mesaj fără conţinut ce se transmite între programe.<br />

Acest efect poate fi obţinut în diferite moduri:<br />

a) Procese secvenţiale comunicante. Rendez-vous simetric<br />

Acest model de intercomunicare presupune că dacă un task A emiţător doreşte să transmită<br />

date unui alt task B receptor, atunci cele două task-uri trebuie să-şi indice reciproc această dorinţă<br />

de a comunica prin emiterea unei comenzi de emisie şi respectiv de recepţie. Dacă se întâmplă ca<br />

task-ul A să întâlnească primul, în timpul execuţiei, o comandă de emitere mesaj, atunci el va fi<br />

suspendat până când task-ul B va întâlni în timpul execuţiei sale o comandă de recepţie mesaj. În<br />

mod similar şi simetric, dacă task-ul B va întâlni primul o comandă de recepţie mesaj, atunci el va fi<br />

suspendat până ce task-ul A va întâlni o comandă de emitere mesaj. În momentul în care ambele<br />

task-uri s-au sincronizat, datele (mesajul) sunt transferate imediat, după care fiecare dintre task-uri<br />

îşi va relua execuţia pe cont propriu, în paralel. Această metodă de sincronizare poartă denumirea<br />

de rendez-vous simetric (procese tratate în mod egal).<br />

b) Procese distribuite. Rendez-vous asimetric<br />

Comunicarea şi sincronizarea între programele concurente se realizează în acest caz similar<br />

cu apelarea prin nume de către programul emiţător a unei proceduri incluse în programul receptor,<br />

lista cu parametri asociaţi acestui apel fiind folosită ca un “canal” de comunicare <strong>pentru</strong><br />

transmiterea de date între cele două programe. În acest caz, spre deosebire de cazul anterior, doar<br />

programul apelat trebuie să cunoască numele programului apelat, nu şi invers. Pentru a putea<br />

provoca sincronizarea prin rendez-vous a celor două programe, în programul apelat vor trebui<br />

inserate “comenzi de acceptare”, care vor avea forma unor proceduri de intrare apelabile de către<br />

programele apelante (task-ul apelat este suspendat în aceste puncte de program). Tipul de rendezvous<br />

este unul asimetric. Aceste forme de rendez-vous nu sunt suficient de adecvate <strong>pentru</strong><br />

programarea aplicaţiilor reale. Sincronizarea mult prea strânsă a task-urilor împiedică orice operare<br />

asincronă a programelor şi astfel inhibă avantajele potenţiale ale unui paralelism dezvoltat. Acest<br />

neajuns se poate elimina prin introducerea posibilităţii de selectare ne-deterministă a comenzilor de<br />

acceptare. Este vorba despre un mecanism pe baza căruia un program receptor poate evita<br />

executarea unei comenzi de acceptare şi să fie suspendat. După necesităţi, se pot introduce condiţii<br />

suplimentare de execuţie a comenzii de selectare.<br />

37


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

2.6 Mecanisme de control asincron sau parţial sincron<br />

Mecanismele prezentate în secţiunile precedente au un caracter sincron. Asta înseamnă că de<br />

fiecare dată, procesele supuse controlului sunt active şi efectele schimbării contextului de către un<br />

proces sunt receptate în acelaşi timp de procesul sau procesele interesate în recepţia acestei<br />

schimbări.<br />

In multe situaţii nu este neapărat necesar ca recepţia efectului schimbării de context să fie<br />

făcută instantaneu. Este suficient ca procesul generator al schimbării să anunţe această schimbare<br />

depunând o informaţie adecvată într-un anume loc: căsuţă poştală, canal specializat etc. Procesul<br />

sau procesele care trebuie să reacţioneze la această schimbare au obligaţia ca în momentul pornirii<br />

lor, să consulte căsuţa poştală sau canalul respectiv. Dacă este prezentă informaţia de schimbare a<br />

contextului, atunci procesul consultant îşi decide evoluţia ulterioară în funcţie de conţinutul acestei<br />

informaţii. Despre astfel de mecanisme spunem că au un caracter asincron.<br />

După cum vom constata, există şi mecanisme mixte, care au atât caracter sincron, cât şi caracter<br />

asincron.<br />

2.6.1 Schimburi de mesaje<br />

Mecanismele de tip schimb de mesaje au apărut cam în acelaşi timp cu semafoarele. Ele<br />

sunt azi utilizate pe scară largă <strong>pentru</strong> comunicarea prin poştă electronică (e-mail). Aceasta este<br />

folosită în principal <strong>pentru</strong> comunicarea între persoane, dar este destul de mult folosită şi <strong>pentru</strong><br />

comunicarea între procese. Există servere în Internet care transmit automat e-mailuri către<br />

utilizatori, după cum utilizatorii se pot adresa direct unor servere prin e-mailuri care respectă<br />

anumite structuri standard.<br />

Sisteme de operare precum Unix, dar nu numai, îşi definesc propriile standarde de mesaje, specifice<br />

comunicării numai între procese. Coada de mesaje este un astfel de exemplu, asupra căruia vom<br />

reveni pe larg în capitolele următoare.<br />

Din punct de vedere structural, un mesaj apare ca în figura 2.12.<br />

Destinaţie Sursă Tip mesaj Conţinut mesaj<br />

Figura 2.12 Structura unui mesaj<br />

Destinaţie şi Sursă sunt adrese reprezentate în conformitate cu convenţiile de adresare ale<br />

implementării mecanismului de mesaje. Tip mesaj dă o caracterizare, în concordanţă cu aceleaşi<br />

convenţii, asupra naturii mesajului respectiv. Aceste trei câmpuri, eventual completate şi cu altele<br />

în funcţie de specific, formează antetul mesajului. Ultimul câmp conţine mesajul propriu-zis.<br />

Dacă sistemul de mesaje se adresează factorului uman, atunci informaţiile sunt reprezentate sub<br />

formă de text ASCII, iar conţinutul nu trebuie să aibă o structură rigidă. Dacă sistemul de mesaje se<br />

adresează proceselor, atunci câmpurile lui au structuri standardizate, evetual chiar conţinut binar.<br />

Indiferent de natura corespondenţilor, scenariul comunicării prin mesaje este acelaşi:<br />

• Emiţătorul mesajului compune mesajul şi îl expediază spre destinatar sau destinatari, după<br />

caz. In situaţia în care corespondenţii se află pe acelaşi sistem de calcul, expedierea<br />

înseamnă depunerea lui într-o zonă specială numită canal sau coadă de mesaje.<br />

• Sistemul care conţine un destinatar recepţionează mesajul. Recepţia se face numai atunci<br />

când sistemul devine operaţional. Pe perioada dintre expediere şi recepţie mesajul este<br />

Tibor Asztalos<br />

38


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

păstrat temporar prin intermediul unui sistem de zone tampon.<br />

• Atunci când procesul (utilizatorul) destinatar devine activ, sistemul îl anunţă de primirea<br />

mesajului respectiv. De asemenea, fiecare proces activ controlează din când în când căsuţa<br />

poştală sau canalul spre a sesiza apariţia de noi mesaje.<br />

• Atunci când procesul consideră de cuviinţă, îşi citeşte mesajul primit. In funcţie de<br />

conţinutul lui, procesul îşi poate modifica execuţia ulterioară.<br />

Din scenariul de mai sus reiese clar caracterul asincron al acestui mecanism. Frecvenţa de<br />

consultare a canalului de mesaje depinde de specificul aplicaţiei. De regulă, după ce un mesaj a fost<br />

consultat de către procesul receptor el este eliminat din canalul de mesaje.<br />

Ordinea de extragere a mesajelor din coadă depinde de asemenea de specificul sistemului de<br />

mesaje. Ordinea cea mai folosită este FIFO – primul venit primul servit. Ea poate fi aplicată fie<br />

asupra întregii cozi, fie asupra mesajelor de un anumit tip, fie asupra mesajelor care îndeplinesc o<br />

anumită condiţie. Există sisteme de mesaje în care procesul însuşi decide care mesaj îl extrage mai<br />

întâi din coadă, deci extragerea poate fi aleatorie.<br />

In funcţie de numărul destinatarilor unui mesaj, se disting:<br />

• Mesaje unicast, cu un destinatar unic: corespondenţă punct la punct.<br />

• Mesaje multicast, în care există un grup de destinatari. Ei se stabilesc fie prin specificarea<br />

tuturor destinatarilor, fie există o listă predefinită de destinatari la care va fi trimis mesajul.<br />

• Mesaje broadcast, în care mesajul este trimis automat tuturor destinatarilor care fac parte<br />

dintr-o anumită categorie: utilizatorii dintr-o reţea LAN sau WAN, angajaţii unui<br />

compartiment sau companii, proceselor care utilizează o anumită resursă etc.<br />

Schimbul de mesaje este folosit pe scară largă mai ales în sistemele distribuite, deci programarea<br />

distribuită operează mai des cu acest mecanism. Există totuşi şi aplicaţii ce se derulează concurent<br />

pe acelaşi sistem şi care comunică între ele prin mesaje specializate.<br />

2.6.2 Fluxuri de octeţi<br />

Un flux de octeţi este un canal unidirecţional de date, de dimensiune limitată, asupra căruia<br />

acţionează două categorii de procese: scriitori şi cititori. Disciplina de acces la flux este FIFO, iar<br />

octeţii în care se scrie, respectiv care sunt citiţi, avansează în manieră circulară în buffer.<br />

Pentru a înţelege mai exact această abordare, să presupunem că avem de-a face cu un buffer de n<br />

octeţi, numerotaţi de la 0 la n-1. Pentru manevrarea eficientă a fluxului, este necesară întreţinerea<br />

permanentă a trei parametri:<br />

• s poziţia curentă în care se poate scrie la următorul acces;<br />

• c poziţia curentă de unde se poate citi la următorul acces;<br />

• p un boolean cu valoarea true dacă poziţia de citire din buffer se află înaintea poziţiei de<br />

citire, sau valoarea false dacă în urma unei citiri sau scrieri s-a depăşit ultimul loc din<br />

buffer şi s-a continuat de la începutul lui.<br />

In figurile 2.13, 2.14, 2.15 şi 2.16 sunt prezentate diverse ipostaze ale bufferului, condiţiile ce<br />

trebuie îndeplinite de cei trei parametri şi modificările acestora în urma unei operaţii. Porţiunea<br />

haşurată indică zona octeţilor încă disponibili în buffer.<br />

Figura 2.13 prezintă bufferul gol fără nici un octet disponibil. Dacă fluxul este gol – nu conţine nici<br />

Tibor Asztalos<br />

39


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

un octet <strong>pentru</strong> citit - atunci procesele cititori aşteaptă până când un proces scriitor depune cel puţin<br />

un octet în flux.<br />

Tibor Asztalos<br />

0 c s n-1<br />

--><br />

--> <br />

0 c<br />

s n-1<br />

o<br />

0 c n-1<br />

s --><br />

=n p=false<br />

si =><br />

(s+o)mod n


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Figura 2.16 prezintă condiţiile necesare <strong>pentru</strong> a se putea citi următorii o octeţi. Primele două<br />

situaţii provoacă doar avansarea indicelui c cu o, în timp ce citirea în ultima situaţie îl modifică atât<br />

pe c cât şi pe p.<br />

Tibor Asztalos<br />

<br />

<br />

<br />

<br />

--><br />

0 c o<br />

s n-1<br />

0 c o n-1<br />

s --><br />

<br />

0 c o n-1<br />

s --><br />

Figura 2.16 Situaţii de citire din buffer<br />


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Presupunem că fluxul are capacitatea de 7 octeţi.<br />

Proces<br />

Doreşte<br />

să scrie<br />

l octeţi<br />

Tibor Asztalos<br />

Octeţi<br />

nescrişi<br />

Conţinut<br />

flux<br />

A 9 a8a9 a1a2a3a4a5a6a7<br />

B 5 wait<br />

C 6 wait<br />

b1b2b3b4b5<br />

c1c2c3c4c5c6<br />

A 2 - a8a9<br />

B 5 - a8a9b1b2b3b4b5<br />

C 6 c4c5c6 b3b4b5c1c2c3<br />

C 3 wait<br />

C 3 - c4c5c6<br />

Octeţi<br />

Citiţi<br />

Proces<br />

doreşte<br />

să citească<br />

l octeţi<br />

A1a2a3a4a5a6a7 E 13<br />

D 4 wait<br />

b3b4b5 A8a9b1b2 D 4<br />

B3b4b5c1c2c3 E 6<br />

Lăsăm pe seama cititorului să simuleze accesele de mai sus la flux în diverse variante, în care nu<br />

toţi corespondenţii îşi verifică numărul de octeţi schimbaţi cu fluxul, ci consideră că fiecare operaţie<br />

îi transferă exact atâţia octeţi câţi a dorit să schimbe. De exemplu, în aceste condiţii procesul A nu<br />

ar mai fi executat o a doua scriere de 2 octeţi şi ar fi rămas cu ei nescrişi.<br />

Acesta este mecanismul de comunicare prin flux de octeţi. De regulă, prin intermediul lui se<br />

transmit la un moment dat puţini octeţi în comparaţie cu dimensiunea fluxului. De asemenea,<br />

frecvenţele citirilor şi ale scrierilor sunt relativ apropiate. Din aceaste cauze, evoluţia "relativ<br />

nefirească" exemplificată mai sus apare rar.<br />

2.6.3 Memorie partajată<br />

Conceptul de memorie partajată reprezintă o modalitate utilă de a separa fluxul de control al<br />

execuţiei unei aplicaţii de comunicarea datelor dintre entităţile implicate. Aceste entităţi sunt de<br />

obicei procese, care se execută pe acelaşi sistem (uniprocesor sau multiprocesor) sau pe sisteme<br />

diferite.<br />

In continuare ne vom ocupa numai de situaţia în care procesele se află pe acelaşi sistem. In acest<br />

caz, segmentul de memorie partajată face parte din spaţiul global de memorie a sistemului. Mai<br />

multe procese colaborează prin intermediul acestui spaţiu de memorie comun, în care sunt<br />

memorate o serie de variabile accesibile tuturor proceselor implicate. Unele dintre procese scriu<br />

date în acest spaţiu comun, iar altele citesc aceste date. Unul dintre principiile programării pe orice<br />

platformă este acela că “fiecare proces îşi accesează în mod exclusiv propriul spaţiu de memorie<br />

(internă)”. In contextul concurenţei se impune uneori un schimb rapid de informaţii. Memoria<br />

partajată răspunde pe deplin acestui scop. In figura 2.17 este ilustrat accesul a două procese la un<br />

segment de memorie partajată.<br />

42


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

Proces A<br />

Cheie<br />

Figura 2.17 Accesul la un segment de memorie partajată<br />

Principiul comunicării prin memorie partajată constă în:<br />

1. Un proces crează un segment de memorie partajată. Descriptorul zonei este trecut în<br />

structura de date ataşată zonei.<br />

2. Procesul creator asociază segmentului de zonă partajată o cheie numerică pe care toate<br />

procesele care accesează zona trebuie să o cunoască.<br />

3. Procesul creator asociază zonei drepturile de acces.<br />

4. Orice proces care doreşte să acceseze segmentul de memorie partajată, inclusiv creatorul,<br />

trebuie să şi-o ataşeze la spaţiul lui virtual de adrese. In urma acestei ataşări obţine adresa zonei<br />

de memorie partajată.<br />

Astfel comunicarea între procese se realizează direct. Uneori este necesară protecţia datelor din<br />

segment <strong>pentru</strong> a se evita rezultate inconsistente. Pentru aceasta, se pot folosi mecanismele de<br />

sincronizare cunoscute: semafoare, monitoare etc.<br />

Mecanismul de memorie partajată poate fi privit în acelaşi timp atât unul sincron, cât şi unul<br />

asincron, în funcţie de specificul aplicaţiei.<br />

2.6.4 Evenimente, excepţii, semnale<br />

Segment de memorie partajatã<br />

Proces B<br />

Execuţia unui proces, atât în context secvenţial cât şi în context concurent, poate fi "perturbată" de<br />

intervenţii externe independente de el şi la momente de timp imprevizibile. Indiferent de natura<br />

perturbării, procesul este anunţat de aceasta folosindu-se sistemul de întreruperi existent pe<br />

platforma pe care se execută procesul. Unele dintre aceste perturbări au cauze intrinseci,<br />

dependente de funcţionarea echipamentelor hard, altele apar datorită faptului că în interacţiunea<br />

proceselor cu mediul înconjurător au apărut situaţii deosebite. In context concurent, o parte dintre<br />

perturbările din a doua categorie sunt folosite în acţiuni de sincronizare şi coordonare a proceselor.<br />

Din această cauză considerăm utilă o scurtă prezentare a lor.<br />

In funcţie de "sursele" acestor perturbări se poate face o clasificare, chiar aproximativă, a acestora.<br />

43


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Deşi terminologia nu este unitară, clasificarea următoare este relativ acceptabilă:<br />

• eveniment - generat de o interfaţă grafică sau de un ceas.<br />

• excepţie - programul solicită resurse interne (variabile, operaţii) sau externe (fişiere,<br />

periferice) inexistente.<br />

• eroare - din raţiuni hardware procesul trebuie oprit.<br />

• semnal - procesul primeşte un anunţ din partea altui proces.<br />

Să le luăm pe rând. Termenul de eveniment este utilizat mai ales în legătură cu funcţionarea<br />

interfeţelor grafice utilizator (GUI – Graphics User Interface). Sunt binecunoscute interfeţele<br />

grafice oferite de platformele Microsoft Windows sau cele de tip X-window operaţionale în<br />

principal pe platforme din familia Unix. Platformele Java oferă şi ele posibilitatea de a defini şi<br />

utiliza astfel de interfeţe grafice.<br />

Utilizatorul unui GUI provoacă un eveniment atunci când acţionează din exterior asupra lui prin<br />

intermediul tastaturii, a mouse-ului, track-ballului sau a altor periferice specifice. Efectul unei astfel<br />

de acţiuni conduce fie la modificarea conţinutului informaţiei unuia dintre obiectele grafice de pe<br />

monitor, fie la redimensionarea unei ferestre, închiderea unei aplicaţii prin distrugerea ferestrei ei<br />

principale, etc.<br />

Aceste evenimente sunt prelucrate, de către sistemul de programe aferent, într-o manieră asincronă.<br />

Conceptul principal în jurul căruia gravitează tratarea evenimentelor este coada de evenimente.<br />

Prelucrarea acestei cozi se face prin intermediul buclei de evenimente. O astfel de buclă funcţionează<br />

conform descrierii de mai jos:<br />

while (TRUE) {<br />

Extrage următorul eveniment din coadă<br />

sau aşteaptă până când apare un eveniment<br />

switch (eveniment)<br />

case eveniment de tip 1:<br />

tratează evenimentul de tip 1<br />

- - - - - - - - - - -<br />

case eveniment de tip n:<br />

tratează evenimentul de tip n<br />

}// switch<br />

}//while<br />

Sursa evenimentului pune în coadă o înregistrare specifică evenimentului respectiv. Tratarea<br />

evenimentului se face mai târziu, atunci când el este scos din coadă. De regulă se extrage numai<br />

câte un eveniment odată. Dacă însă evenimentul aşteaptă un răspuns de la sistem, atunci se goleşte<br />

întreaga coadă. Aceasta înseamnă că dacă în coadă sunt câteva evenimente şi apare un altul care<br />

aşteaptă răspuns de la sistem, atunci este procesată mai întâi toată coada după care se aşteaptă<br />

răspunsul.<br />

O categorie aparte de evenimente sunt cele legate de scurgerea unui interval de timp.<br />

Programele pot cere sistemului să fie "puse în adormire" şi să fie trezite după un interval de timp<br />

sau la apariţia unui anume eveniment.<br />

Prin excepţie se înţelege o situaţie deosebită generată de cauze interne ale funcţionării programului:<br />

tentativă de deschidere a unui fişier care nu există, tentativa de utilizare a unui indice de tablou a<br />

cărui valoare este înafara limitelor fixate, tentativă de împărţire la zero, etc.<br />

In majoritatea situaţiilor, execuţia programului este suspendată la apariţia unei excepţii şi se<br />

aşteaptă ca programul sau sistemul de operare să rezolve situaţia. De regulă, dacă se lasă rezolvarea<br />

pe seama sistemului, soluţia acestuia este terminarea anormală a programului.<br />

Tibor Asztalos<br />

44


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Atunci când excepţia este tratată de către programul însuşi, proiectantul trebuie să prevadă o soluţie<br />

de rezolvare specifică <strong>pentru</strong> fiecare excepţie în parte.<br />

Eroare se consideră o situaţie în care, din raţiuni hardware procesul nu-şi mai poate continua<br />

activitatea. De exemplu, atunci când codul unui proces este corupt şi oferă spre execuţie instrucţiuni<br />

care nu există, sau atunci când se solicită o adresă de memorie inexistentă, se semnalează o eroare.<br />

Tratarea unei erori este întotdeauna o operaţie sincronă. cu execuţia programului.<br />

Semnalul poate fi asociat cu emiterea unei întreruperi adresate unui proces. De regulă, procesul care<br />

primeşte un semnal nu ştie sursa acestuia, în schimb primeşte semnalul în manieră sincronă, imediat<br />

ce el a fost emis. Procesul poate să-şi modifice funcţionarea imediat ce a primit semnalul, poate să<br />

ignore semnalul primit sau poate să amâne prelucrarea acestuia.<br />

Sistemele de calcul / de operare mai vechi au folosit ca suport <strong>pentru</strong> semnale chiar sistemul de<br />

întreruperi. Sistemul de operare Unix oferă un mecanism simplu şi elegant de utilizare a semnalelor<br />

cu instrumente pur software.<br />

Exerciţii<br />

1. În §2.4.4. au fost abordate o serie de tehnici ce permit realizarea excluderii reciproce a două sau<br />

mai multe task-uri concurente. Astfel, o primă metodă propusă prevede folosirea unei variabile<br />

comune specială în vederea controlului accesului la secţiunile critice proprii ale taskurilor<br />

concurente. Considerând două astfel de procese, ele pot fi scrise, principial, în felul următor:<br />

Task 1.<br />

#define DESCHIS 1<br />

#define INCHIS 0<br />

int poarta=DESCHIS; // variabila comună ce indică secţiuni critice libere la<br />

… // început<br />

while(poarta); // aşteaptă “deschiderea” porţii<br />

poarta=INCHIS; // blochează accesul şi trece la execuţia secţiunii critice<br />

{<br />

…<br />

// secţiunea critica 1<br />

…<br />

}<br />

poarta=DESCHIS;//deblocheazã accesul (permite executarea altor secţiuni critice)<br />

Task 2.<br />

…<br />

while(poarta); // aşteaptă “deschiderea” porţii<br />

poarta=INCHIS; // blochează accesul şi trece la execuţia secţiunii critice<br />

{<br />

…<br />

// secţiunea critica 2<br />

…<br />

}<br />

poarta=DESCHIS; //deblocheazã accesul<br />

Tibor Asztalos<br />

45


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

a) cele două task-uri sunt lansate în execuţie simultană. Ele rulează, evident, în mod asincron unul<br />

faţă de celălalt. Indicaţi scenariile posibile ale executării secţiunilor critice mai sus indicate.<br />

Identificaţi posibile situaţii de interblocare ale celor două procese.<br />

b) conform exemplului de mai sus, daţi pseudocodul a două procese concurente ce folosesc o<br />

variabilă comună, denumit “comutator”, d dirijare a accesului la secţiunile lor critice (a doua<br />

metodă de excludere reciprocă prin programare, prezentată în §2.4.4.).<br />

2. Introducând, în codul fiecărui task, câte o variabilă de stare locală ce poate avea valorile “extern”<br />

sau “intern”, implementaţi cea de a treia metodă de excludere reciprocă prin programare, descrisă în<br />

§2.4.4. (Obs. variabilele de stare locale sunt inspectabile de către procesul concurent fără însă a<br />

avea acces la alterarea valorii lor, cu alte cuvinte, numai taskul proprietar poate asigna o altă<br />

valoare acestor variabile).<br />

Indicaţi situaţiile posibile în care se ajunge la blocarea reciprocă a taskurilor.<br />

3. Completaţi codul obţinut la problema anterioară cu instrucţiuni de verificare a stării taskului<br />

concurent înainte de trecerea efectivă la modificarea valorii variabilei locale proprii (se va<br />

implementa astfel metoda nr. 4 prezentată în §2.4.4.).<br />

Este posibilă o blocare reciprocă în acest caz ? Dacă da, în ce condiţii ?<br />

4. Combinând metoda cu variabila globală “comutator” cu cea de la problema anterioară să se<br />

implementeze metoda nr. 5 de excludere reciprocă, propusă în §2.4.4.<br />

5. Să presupunem că există o funcţie, denumită interschimbare, cu definiţia:<br />

void inerschimbare(int local, int excludere)<br />

{<br />

int temp;<br />

temp=local;<br />

local=excludere;<br />

excludere=temp;<br />

}<br />

ce se execută în mod indivizibil (neîntreruptibil). Această funcţie permite copierea valorii unei<br />

variabile, transmisă prin argumentul excludere, în cea a variabilei local şi invers. Argumentul<br />

excludere poate fi o variabilă comună ce controlează accesul la secţiunile critice ale unor taskuri<br />

concurente. Rezolvaţi problema excluderii reciproce a două taskuri concurente (similar celei din<br />

problema nr. 2) în ipoteza utilizării acestei funcţii.<br />

6. Un semafor binar poate fi implementat sub forma unei variabile de tip întreg ce poate lua doar<br />

valori de 0 sau 1. Procedurile P şi V ce acţionează asupra unor asemenea semafoare pot fi schiţate<br />

după cum urmează:<br />

int P(int semafor)<br />

{<br />

if semafor>0<br />

semafor=semafor-1;<br />

else<br />

- procesul este blocat, si introdus intr-un sir de asteptare<br />

return(semafor);<br />

}<br />

Tibor Asztalos<br />

46


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

int V(int semafor)<br />

{<br />

semafor=semafor+1;<br />

if (sir de asteptare nu este vid)<br />

{<br />

semafor=semafor-1;<br />

- extrage urmatorul task din sirul de asteptare;<br />

- planificã <strong>pentru</strong> executie taskul extras;<br />

//deblocheazã un proces suspendat printr-o operatie P<br />

}<br />

return(semafor);<br />

}<br />

Indicaţi codul unui task ce face apel la un astfel de semafor binar (şi procedurile aferente) <strong>pentru</strong><br />

implementarea excluderii reciproce.<br />

7. Un semafor general poate fi implementat folosind două semafoare binare (cu procedurile P şi V<br />

aferente - vezi problema anterioară) şi o variabilă contor de tip întreg. Astfel se poate scrie:<br />

int comutator, intarziere;<br />

int n;<br />

comutator=1, intarziere=0; // cele două semafoare binare<br />

n= valoare_semafor_general ; // o variabilă de tip contor<br />

Operaţiile P şi V ale unui semafor general pot fi definite, în termenii celor aferenţi semafoarelor<br />

binare, în modul următor:<br />

P(n)<br />

{<br />

}<br />

V(n)<br />

{<br />

}<br />

Tibor Asztalos<br />

P(comutator);<br />

n=n-1;<br />

if n


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

Capitolul 3. Procese Unix. Sincronizarea în SO Unix<br />

3.1. Controlul proceselor sub SO Unix<br />

Orice sistem de calcul modern este capabil să execute mai multe programe în acelaşi timp.<br />

Cu toate acestea, în cele mai multe cazuri, unitatea centrala de prelucrare (CPU) nu poate executa la<br />

un moment dat decât un singur program. De aceea, sarcina de a rula mai multe programe în acelaşi<br />

timp revine sistemului de operare, care trebuie să introducă un model prin intermediul căruia<br />

execuţia programelor, privită din perspectiva utilizatorului, să se desfăşoare în paralel. Se<br />

realizează, de fapt, un pseudoparalelism, prin care procesorul este alocat pe rând programelor care<br />

trebuie rulate, câte o cuanta de timp <strong>pentru</strong> fiecare, astfel încât din exterior ele par ca rulează efectiv<br />

în acelaşi timp.<br />

Cel mai răspândit model care introduce paralelismul în execuţia programelor este modelul<br />

bazat pe procese (task-uri). Acest model este cel adoptat de sistemul de operare Unix şi va face<br />

obiectul acestui paragraf. Controlul proceselor este un element important în programarea pe sisteme<br />

multitasking (şi multiutilizator). Ea include operaţii de creare de procese, terminare şi sincronizare.<br />

Sistemul Unix este şi un sistem multiutilizator acest lucru fiind un aspect important în dezvoltarea<br />

aplicaţiilor multiutilizator. Controlul proceselor este realizat prin câteva apeluri sistem, care nu sunt<br />

altceva decât funcţii, înglobate în nucleu, accesibile utilizatorului. Utilizarea corectă a apelurilor<br />

este esenţială în controlul corect al proceselor Unix.<br />

Prin proces înţelegem un program în execuţie (sau a unei serii de programe), împreună cu<br />

zona sa de date, stiva şi numărătorul de instrucţiuni (PC- program counter). Trebuie făcuta încă de<br />

la început distincţia dintre proces si program. Un program este, în fond, un şir de instrucţiuni care<br />

trebuie executate de către calculator, în vreme ce un proces este o abstractizare a programului,<br />

specifică sistemelor de operare. Se poate spune că un proces execută un program şi că sistemul de<br />

operare lucrează cu procese, iar nu cu programe. Procesul include în plus faţă de program<br />

informaţiile de stare legate de execuţia programului respectiv (stiva, valorile regiştrilor CPU etc.).<br />

De asemenea, este important de subliniat faptul că un program (ca aplicaţie software) poate fi<br />

format din mai multe procese care să ruleze sau nu în paralel.<br />

Cele mai importante informaţii de stare ale unui proces se găsesc înscrise în zona denumită<br />

context a lui. Contextul este divizat în două: o parte utilizator şi o parte nucleu. Accesul la<br />

informaţiile din context se face prin apeluri sistem specifice.<br />

Printre informaţiile care fac parte din context amintim:<br />

• PID reprezintă identificatorul procesului, fiind un întreg cuprins între 0 şi 32767.<br />

• PPID reprezintă pid-ul procesului părinte,<br />

• UID reprezintă identificatorul utilizatorului care a lansat procesul. El este folosit la<br />

autentificarea utilizatorului şi la comunicarea între utilizatori. Din fişierul /etc/passwd se<br />

poate obţine corespondenţa între UID şi numele de utilizator.<br />

• GID reprezintă identificatorul grupului la care aparţine utilizatorul. Din fişierul /etc/group se<br />

poate stabili o corespondenţă între GID şi numele grupului.<br />

• EUID reprezintă identificatorul efectiv al utilizatorului, folosit <strong>pentru</strong> determinarea<br />

drepturilor de acces la fişierele procesului. De obicei acesta coincide cu UID, dar este<br />

posibil ca unul dintre procesele strămoşi să pretindă să rămână el UID real al procesului.<br />

• EGID reprezintă identificatorul efectiv de grup, relaţia lui cu GID fiind analoagă cu cea<br />

dintre EUID şi UID.<br />

• Directorul curent este un alt element care face parte din context.<br />

• Tabela descriptor de fişiere conţine datele relative la toate stream-urile (fişiere, socketuri,<br />

pipe, fifo, memorii partajate, cozi de mesaje, semafoare etc.) deschise de către proces.<br />

Accesul la tabelă se face printr-un întreg, numit descriptor de fişier (handle).<br />

48


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

• Zona variabilelor de mediu conţine informaţii de legătură cu sistemul de operare. Ele sunt<br />

reprezentate prin string-uri terminate cu zerouri, sub forma nume=valoare.<br />

• prioritatea este un număr între 1 şi 39.<br />

• semnalele disponibile sunt numere între 1 şi 32.<br />

• umask este o configuraţie de biţi ce indică drepturile de acces implicit nepermise la crearea<br />

de fişiere.<br />

Orice proces este executat secvenţial, iar mai multe procese pot să ruleze în paralel (între<br />

ele). De cele mai multe ori, execuţia în paralel se realizează alocând pe rând procesorul câte unui<br />

proces (pe un sistem monoprocesor). Deşi la un moment dat se execută un singur proces, în decurs<br />

de o secundă, de exemplu, pot fi executate porţiuni din mai multe procese. Din această schemă<br />

rezultă că un proces se poate găsi, la un moment dat, în una din următoarele stări :<br />

• În execuţie<br />

• Gata de execuţie<br />

• Blocat<br />

• (Inactiv)<br />

Procesul se găseşte în execuţie atunci când procesorul îi execută instrucţiunile. Gata de execuţie<br />

este un proces care, deşi are toate resursele necesare să îşi continue execuţia, este lăsat în aşteptare<br />

din cauza că un alt proces este în execuţie la momentul respectiv. De asemenea, un proces poate fi<br />

blocat din două motive: el îşi suspendă execuţia în mod voit sau procesul efectuează o operaţie în<br />

afara procesorului, mare consumatoare de timp (cum e cazul operaţiilor de intrare-ieşire - acestea<br />

sunt mai lente şi între timp procesorul ar putea executa părţi din alte procese).<br />

Din perspectiva programatorului, sistemul de operare UNIX pune la dispoziţie un mecanism elegant<br />

şi simplu <strong>pentru</strong> crearea şi utilizarea proceselor. Orice proces trebuie creat de către un alt proces.<br />

Există o singură excepţie de la această regulă, şi anume procesul init, care este procesul iniţial<br />

(procesul 1), creat la pornirea sistemului de operare şi care este responsabil <strong>pentru</strong> crearea<br />

următoarelor procese.<br />

Fiecare proces în Unix are asociat un identificator unic numit identicator de proces,<br />

prescurtat PID. Pentru identificator se utilizează în continuare prescurtarea ID. PID este un număr<br />

pozitiv atribuit de sistemul de operare fiecărui proces nou creat. Un proces poate să determine PIDul<br />

său folosind apelul sistem getpid(). Cum PID-ul unui proces este unic el nu poate fi schimbat, dar<br />

se poate refolosi când procesul nu mai există.<br />

Un proces activ poate genera un nou proces, acesta la rândul său poate genera un al treilea<br />

proces ş.a. Procesul lansat în execuţie de câtre un alt proces se numeşte proces fiu, în timp ce<br />

procesul care l-a lansat în execuţie se numeşte proces tată. Un proces este generat în urma unui apel<br />

al directivei fork(), care generează de fapt o copie a procesului original. Ambele procese (părinte şi<br />

fiu) îşi continuă execuţia de la instrucţiunea ce succede apelul fork(), cu o singură diferenţă: codul<br />

întors de directiva fork() este zero <strong>pentru</strong> procesul fiu şi respectiv o valoare nenulă (valoarea<br />

identificatorului procesului fiu), <strong>pentru</strong> procesul părinte.<br />

Sintaxa de apel a directivei fork() este:<br />

Tibor Asztalos<br />

pid_t fork()<br />

Prin această funcţie sistem, procesul apelant (părintele sau tatăl) creează un nou proces (fiul) care<br />

va fi o copie fidelă a părintelui. Noul proces va avea propria lui zonă de date, propria lui stivă,<br />

propriul lui cod executabil, toate fiind copiate de la părinte în cele mai mici detalii. Rezultă că<br />

variabilele fiului vor avea valorile variabilelor părintelui în momentul apelului funcţiei fork(), iar<br />

49


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

execuţia fiului va continua cu instrucţiunile care urmează imediat acestui apel, codul fiului fiind<br />

identic cu cel al părintelui. Cu toate acestea, în sistem vor exista din acest moment două procese<br />

independente, (deşi identice), cu zone de date şi stivă distincte. Orice modificare făcută, prin<br />

urmare, asupra unei variabile din procesul fiu, va rămâne invizibilă procesului părinte şi invers.<br />

Procesul fiu va moşteni de la părinte toţi descriptorii de fişier deschişi de către acesta, aşa că orice<br />

prelucrări ulterioare în fişiere vor fi efectuate în punctul în care le-a lăsat părintele.<br />

Deoarece codul părintelui şi codul fiului sunt identice şi <strong>pentru</strong> că aceste procese vor rula în<br />

continuare în paralel, trebuie făcută clar distincţia, în interiorul programului, între acţiunile ce vor fi<br />

executate de fiu şi cele ale părintelui. Cu alte cuvinte, este nevoie de o metodă care să indice care<br />

este porţiunea de cod a părintelui şi care a fiului. Acest lucru se poate face simplu, folosind valoarea<br />

returnată de funcţia fork().<br />

Prin urmare, o posibilă schemă de apelare a funcţiei fork() ar fi:<br />

...<br />

if( ( pid=fork() ) < 0)<br />

{<br />

perror("Eroare");<br />

exit(1);<br />

}<br />

if(pid==0)<br />

{<br />

/* codul fiului */<br />

...<br />

exit(0)<br />

}<br />

/* codul părintelui */<br />

...<br />

In programul 2 vom descrie o primă utilizare a lui fork().<br />

Înainte de acest program, introducem rutina numită err_sys, în programul (programul sursă 1).<br />

//afiseaza textul "text", eroarea aparuta si termina programul<br />

int err_sys(char *text) {<br />

perror(text);<br />

exit(1);<br />

}//err_sys<br />

Programul 1. Sursa err_sys.c<br />

Rutina err_sys este apelată în cazul unor apeluri de funcţii care eşuează, întorcând, de exemplu, un<br />

cod de eroare sub forma unui întreg negativ. In acest caz, rutina err_sys afişează un text al<br />

utilizatorului şi un text corespunzător erorii apărute, după care încheie execuţia programului.<br />

#include "err_sys.c"<br />

main(){<br />

Tibor Asztalos<br />

int pid,i;<br />

printf("\nInceputul programului:\n");<br />

if ((pid=fork())


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

Tibor Asztalos<br />

printf("Sfarsit FIU\n");<br />

exit(0);<br />

}<br />

else if (pid>0){//Suntem in parinte<br />

printf("Am creat FIUL(%d)\n",pid);<br />

for (i=1;i


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

Mostenit<br />

Absoluta<br />

Calea spre<br />

executabil<br />

Argumentele Argumentele Argumentele<br />

Figura 3.1 Criterii de implementare ale apelului exec()<br />

Conform acestor criterii sunt posibile maximum 8 tipuri de apeluri exec() dintre care s-au<br />

implementat numai 6. Variantele posibile de apeluri exec() conform criteriilor pe care le satisfac<br />

sunt apelurile: execl(), execlp(), execv(), execvp(), execle(), execve() (cele din fig. 3.1.)<br />

Prototipurile acestor apeluri exec() sunt:<br />

int execv (char *cale, char **argv);<br />

int execl (char *cale, char *arg0,…,char *argn,NULL);<br />

int execve(char *cale, char **argv, char**envp);<br />

int execle(char *cale, char *arg0,…,char *argn,NULL,char **envp);<br />

int execvp(char *fişier, char **argv);<br />

int execlp(char* fişier, char *arg0,…,char *argn,NULL);<br />

Semnificaţia parametrilor folosiţi este următoarea:<br />

fişier - numele fişierului care va fi căutat în directoarele din variabila de mediu PATH.<br />

cale - se precizează calea completă spre fişierul de executat.<br />

Primul argument coincide, după caz, cu argv[0] sau arg0. Excepţiile de la această regulă sunt cu<br />

totul speciale.<br />

Privind cu atenţie aceste funcţii, se observă o serie de elemente comune:<br />

• execlp, execl, execle primesc argumentele ca parametri terminaţi prin pointerul NULL.<br />

• execvp, execv, execve primesc tablouri de pointeri la argumente.<br />

• execlp şi execvp primesc un nume de fişier, care urmează să fie convertit în cale cu<br />

informaţiile din variabila de mediu PATH.<br />

• execle şi execve sunt singurele care primesc şi tablouri de pointeri la variabilele de mediu.<br />

Programul lansat prin exec() moşteneşte de la procesul părinte, pe care-l suprapune:<br />

• PID, PPID, GID, PGID, timpul semnalului de alarmă, directorul rădăcină, directorul curent,<br />

masca de fişier umask, UID, GID, fişierele blocate.<br />

• Atributele EUID şi EGID pot fi schimbate.<br />

Relativa<br />

Mediul Mediul<br />

Mostenit<br />

Nou Nou<br />

Vector Lista Vector Lista Vector Lista<br />

execv execl execve execle execvp execlp<br />

Nu<br />

exista<br />

52


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

In fig. 3.2 se face un sumar a ceea ce se moşteneşte în urma unui fork() şi în urma unui exec().<br />

Atribut Moştenit prin fork Reţinute la exec<br />

PID Nu Da<br />

UID Da Da<br />

EUID Da Depinde de bitul “setuid”<br />

Datele statice Copiate Nu<br />

Stiva Copiată Nu<br />

Heap Copiată Nu<br />

Text (cod) Partajat Nu<br />

Descriptorii fişierelor Copiate; poziţia curentă De regulă Da. Se poate<br />

deschise<br />

în fişiere partajată<br />

evita prin apel “fcntl”<br />

Mediul Da Depinde de tipul exec<br />

Directorul curent Da Da<br />

Tratarea semnalelor Copiată Parţial<br />

Tibor Asztalos<br />

Figura 3.2 Atribute moştenite prin fork şi exec<br />

Prezentăm mai jos, în programele 3 şi 4, două exemple de folosire a apelului exec(). Programele nu<br />

fac altceva decât rezumatul unui director apelând <strong>pentru</strong> asta programul extern /bin/ls. Ca şi<br />

parametri <strong>pentru</strong> apelul extern se folosesc argumentele primite în linia de comandă de către<br />

programul apelator şi preluate de către funcţia main().<br />

#include "err_sys.c"<br />

main(int argc,char *argv[]) {<br />

printf("\nTest Execv\n");<br />

if(execv("/bin/ls",argv)


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

pid_t waitpid(pid_t pid, int *status, int flags)<br />

Apelul wait() suspendă execuţia programului până la terminarea unui proces fiu. Dacă fiul s-a<br />

terminat înainte de apelul wait(), apelul se termină imediat. La terminare, toate resursele ocupate de<br />

procesul fiu sunt eliberate. Modul de terminare este depus în locaţia de 16 biţi punctată de status.<br />

Situaţiile de terminare sunt ilustrate în fig. 2.3, a,b,c:<br />

cod de retur 00000000 cod de retur core nr.semnal nr. semnal 01111111<br />

a) b) c)<br />

Figura 3.3 Conţinuturi ale întregului status, în urma unui wait<br />

Semnificaţia celor trei situaţii este:<br />

a) un proces fiu face exit()<br />

b) un proces fiu a recepţionat un semnal de terminare<br />

c) un proces a fost oprit în cursul depanării<br />

Parametrul status este folosit <strong>pentru</strong> evaluarea valorii returnate, folosind câteva macro-uri definite<br />

special (vezi paginile de manual corespunzătoare funcţiilor wait() şi waitpid()). Este obligatoriu ca<br />

starea proceselor să fie preluată după terminarea acestora, astfel că funcţiile din această categorie nu<br />

sunt opţionale.<br />

Apelul waitpid() pune programul în aşteptare până la apariţia unuia dintre următoarele evenimente:<br />

• terminarea procesului precizat prin argumentul pid;<br />

• recepţionarea unui semnal de terminare a procesului curent;<br />

• apariţia unui semnal cu efect de ieşire din starea de aşteptare.<br />

Semnificaţiile valorilor argumentului pid sunt:<br />

• < -1 - se aşteaptă terminarea tuturor proceselor fiu ale caror GID este egal cu valoarea absolută<br />

a parametrului pid.<br />

• -1- se aşteaptă terminarea tuturor fiilor.<br />

• 0 - se aşteaptă terminarea tuturor proceselor fiu ala căror GID este egal cu GID-ul părintelui<br />

• 0 - se aşteaptă procesul cu PID-ul specificat prin parametrul pid.<br />

Valorile argumentului flags depind de sistemul de operare şi pot fi găsite în manualele sistemului<br />

respectiv. El poate avea valorile:<br />

Constanta Descriere<br />

WNOHANG Apelul nu se blocheaza daca fiul specificat prin pid nu este<br />

disponibil. In acest caz valoarea de retur este 0.<br />

WUNTRACED Daca implementarea permite controlul lucrarilor, starea<br />

fiecarui proces fiu oprit si neraportata este intoarsa.<br />

Macroul WIFSTOPPED determina daca valoarea intoarsa<br />

corespunde unui proces fiu oprit.<br />

Argumentul poate fi şi 0 sau rezultatul unui SAU între constantele simbolice WNOHANG şi<br />

WUNTRACED.<br />

54


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Pentru a analiza starea în care s-a terminat un proces fiu există trei macrouri excluse mutual, toate<br />

prefixate de WIF şi definite în fisierul sys/wait.h. Pe lângă acestea, există alte macrouri <strong>pentru</strong><br />

determinarea codului de exit, număr semnal, etc. Acestea sunt ilustrate mai jos:<br />

Tibor Asztalos<br />

Macro Descriere<br />

WIFEXITED( status) Adevarat daca informatia de stare, status, provine<br />

de la un proces terminat normal. In acest caz se<br />

poate folosi: WEXITSTATUS( status) <strong>pentru</strong> a extrage<br />

octetul mai putin semnificativ.<br />

WIFSIGNALED(status) Adevarat daca informatia de stare, status, provine<br />

de la un proces terminat anormal. In acest caz se<br />

poate folosi: WTERMSIG( status) <strong>pentru</strong> a extrage<br />

numarul semnalului.<br />

In SVR4 si 4.3+BSD macroul WCOREDUMP( status) este<br />

adevarat daca s-a generat fisier core.<br />

WIFSTOPPED(status) Adevarat daca informatia de stare, status, provine<br />

de la un proces temporar oprit. In acest caz se<br />

poate folosi: WSTOPSIG( status) <strong>pentru</strong> a extrage<br />

numarul semnalului care a oprit procesul.<br />

Dacă procesul fiu se termină înaintea procesului părinte, nucleul trebuie să păstreze anumite<br />

informaţii ( pid, starea de terminare, timp de utilizare CPU) asupra modului în care fiul s-a terminat.<br />

Aceste informaţii sunt accesibile părintelui prin apelul wait() sau waitpid(). În terminologie Unix un<br />

proces care s-a terminat şi <strong>pentru</strong> care procesul părinte nu a executat wait() se numeşte zombie. În<br />

această stare, procesul nu are resurse alocate, ci doar intrarea sa în tabela proceselor. Nucleul poate<br />

descărca toată memoria folosita de proces şi închide fişierele deschise. Un proces zombie se poate<br />

observa prin comanda Unix ps care afişează la starea procesului litera 'Z'.<br />

Prin urmare, o posibilă schemă de apelare a funcţiilor fork() şi wait() ar fi:<br />

...<br />

if( ( pid=fork() ) < 0)<br />

{<br />

perror("Eroare");<br />

exit(1);<br />

}<br />

if(pid==0)<br />

{<br />

/* codul fiului */<br />

...<br />

exit(0)<br />

}<br />

/* codul părintelui */<br />

...<br />

wait(&status)<br />

Un exemplu simplu:<br />

#include <br />

#include <br />

#include <br />

int main()<br />

{<br />

if (fork() == 0)<br />

{ /* fiu */<br />

printf("Proces fiu id=%d\n", getpid());<br />

exit(1);<br />

}<br />

55


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

else<br />

{ /* parinte */<br />

int pid_fiu, cod_term;<br />

pid_fiu= wait(&cod_term);<br />

printf("Tata: sfirsit fiul %d cu valoarea %d\n", pid_fiu, cod_term);<br />

}<br />

}<br />

Programul 5. Sursa Wait-Fiu.c<br />

Din punct de vedere al procesului apelant, un apel fork()-exec() este similar cu o instrucţiune<br />

call subroutine, în timp ce un apel al directivei exec() este similar cu instrucţiunea go to.<br />

Consturcţii de directive sau alternative la anumite directive mai sus menţionate o reprezintă<br />

funcţiile system() şi vfork().<br />

Sintaxa de apel a directivei system() este:<br />

Tibor Asztalos<br />

int system(const char *cmd)<br />

Aceasta lansează în execuţie un program de pe disc, folosind în acest scop un apel fork(), urmat de<br />

exec(), împreună cu waitpid() în parinte, şi se foloseşte, în general, <strong>pentru</strong> executarea unor comenzi<br />

de sistem din cadrul unei aplicaţii.<br />

Sintaxa de apel a directivei vfork() este:<br />

pid_t vfork()<br />

Creează un nou proces, la fel ca fork(), dar nu copiază în întregime spaţiul de adrese al părintelui în<br />

fiu. Este folosit în conjuncţie cu exec(), şi are avantajul că nu se mai consumă timpul necesar<br />

operaţiilor de copiere care oricum ar fi inutile dacă imediat după aceea se apelează exec() (oricum,<br />

procesul fiu va fi suprascris cu programul citit de pe disc).<br />

Alte funcţii utile <strong>pentru</strong> lucrul cu procese sunt:<br />

pid_t getpid() - returnează PID-ul procesului curent;<br />

pid_t getppid() - returnează PID-ul părintelui procesului curent;<br />

uid_t getuid() - returnează identificatorul utilizatorului care a lansat procesul curent;<br />

gid_t getgid() - returnează identificatorul grupului utilizatorului care a lansat procesul curent;<br />

pid_t getpgrp() - returnează ID grupului de procese;<br />

Un proces poate să determine PID-ul părintelui prin apelul getppid(). PID-ul procesului<br />

părinte nu se poate modifica. Sistemul de operare Unix ţine evidenţa proceselor într-o structură de<br />

date internă numită tabela de procese. Ea are o intrare <strong>pentru</strong> fiecare proces din sistem. Lista<br />

proceselor din tabela de procese poate fi obţinută prin comanda ps.<br />

Uneori se doreşte crearea unui subsistem ca un grup de procese înrudite în locul unui proces<br />

singular. De exemplu, un sistem complex de gestiune al unei baze de date poate fi împărţit în câteva<br />

procese <strong>pentru</strong> a "câştiga" operaţii de I/O cu discul. Astfel, pe lângă ID asociat unui proces, care<br />

permite identificarea individuală a fiecăruia, fiecare proces are şi un ID de grup de procese,<br />

prescurtat (PGID), care permite identificarea unui grup de procese. PGID este moştenit de procesul<br />

fiu de la procesul părinte. Contrar PID-ului, un proces poate să-şi modifice PGID, dar numai prin<br />

crearea unui nou grup. Acest lucru se realizează prin apelul sistem setpgrp().<br />

int setpgrp();<br />

56


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

setpgrp() actualizează PGID-ul procesului apelant la valoarea PID-ului său şi întoarce noul PGID.<br />

Procesul apelant părăseşte astfel vechiul grup devenind leaderul propriului grup urmând a-şi crea<br />

procesele fiu, care să formeze grupul. Deoarece procesul apelant este primul membru al grupului şi<br />

numai descendenţii săi pot să aparţină grupului (prin moştenirea PGID), el este referit ca<br />

reprezentantul (leaderul) grupului.<br />

Deoarece doar descendenşii leaderului pot fi membri ai grupului există o corelaţie între grupul de<br />

procese şi arborele proceselor. Fiecare leader de grup este rădăcina unui subarbore, care după<br />

eliminarea rădăcinii conţine doar procese ce aparţin grupului. Dacă nici un proces din grup nu s-a<br />

terminat lăsând fii care au fost adoptaţi de procesul init, acest subarbore conţine toate procesele din<br />

grup. Un proces poata determina PGID sau folosind apelul sistem getpgrp(); Apelul întoarce<br />

PGID procesului apelant. Deoarece PID-ul leaderului este acelaşi cu PGID-ul, getpgrp() identifică<br />

leaderul.<br />

Un proces poate fi asociat unui terminal, care este numit terminalul de control asociat<br />

procesului. Acesta este moştenit de la procesul părinte la crearea unui nou proces. Un proces este<br />

deconectat (eliberat) de terminalul său de control la apelul setpgrp(), devenind astfel un leader de<br />

grup de procese (nu se închide terminalul). Ca atare, numai leaderul poate stabili un terminal de<br />

control, devenind procesul de control <strong>pentru</strong> terminalul în cauza.<br />

Raţiunea existentei unui grup este legată de comunicarea prin semnale. În mod uzual, procesele din<br />

acelaşi grup sunt conectate logic în acest fel. De exemplu, managerul unei baze de date multiproces<br />

este constituit dintr-un proces master şi câteva procese subsidiare. Pentru a face din suita proceselor<br />

bazei de date un grup de procese, procesul master apelează setpgrp() înainte de a crea procesele<br />

subsidiare.<br />

Un proces care nu este asociat cu un terminal de control este numit daemon. Spoolerul de<br />

imprimantă este un exemplu de astfel de proces. Un proces daemon este identificat în rezultul<br />

afişării comenzii ps prin simbolul ? plasat în coloana TTY.<br />

3.2 Sincronizarea proceselor în UNIX<br />

Sincronizarea proceselor în UNIX se poate realiza în doua moduri:<br />

controlată de către sistemul de operare sau controlată de către utilizator. În primul caz, mecanismul<br />

clasic utilizat este cel de conductă de comunicaţie (pipe). Sincronizarea controlată de utilizator se<br />

realizează în principal prin intermediul evenimentelor.<br />

Tibor Asztalos<br />

3.2.1 Evenimente<br />

Evenimentul este conceptul de bază în sincronizarea şi planificarea proceselor UNIX. El<br />

reprezintă modalitatea de precizare a momentului în care un proces, anterior blocat (în aşteptarea<br />

terminării unei operaţii de intrare/ieşire cu terminalul, a eliberării unei zone tampon sau a unui inod),<br />

poate trece în starea gata de execuţie (condiţia pe care o aşteaptă s-a îndeplinit).<br />

Blocarea proceselor se face prin intermediul unei funcţii interne, numită sleep (a nu se<br />

confunda cu funcţia de bibliotecă cu acelaşi nume), apelată cu un parametru care reprezintă<br />

parametrul principal. În momentul îndeplinirii condiţiei de deblocare, nucleul, prin intermediul<br />

funcţiei wakeup, trece toate procesele, care aşteptau acea condiţie, în starea gata de execuţie.<br />

Evident, numai unul dintre acestea se va executa efectiv, celelalte trecând din nou în starea blocat.<br />

Evenimentele sunt reprezentate prin numere întregi, alese prin convenţie, astfel încât să fie<br />

chiar adrese virtuale, cunoscute de nucleul UNIX-ului; semnificaţia lor este ca sistemul de operare<br />

se aşteaptă ca anumite evenimente să se mapeze pe anumite adrese ( de exemplu: evenimentul de<br />

terminare a unui proces fiu este reprezentat de adresa intrării corespunzătoare a tatălui său din<br />

tabela de procese).<br />

În afara de producerea propriu-zisă a unui eveniment, acest mecanism nu permite şi<br />

transmiterea altor informaţii, cu evenimentul nefiind asociată memorie; el există doar în momentul<br />

în care este folosit.<br />

57


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Dezavantajul acestei abordări constă în faptul că sincronizarea nu se poate face în funcţie de<br />

o anumită cantitate de informaţie. De exemplu, toate procesele care au făcut cerere de memorie vor<br />

fi planificate <strong>pentru</strong> execuţie la eliberarea unei zone, indiferent de dimensiunea acesteia, deşi zona<br />

eliberată s-ar putea să nu fie suficientă <strong>pentru</strong> multe dintre ele şi deci să fie nevoite să treacă din<br />

nou în aşteptare (în realitate, exista un singur proces care cere memorie, swapper-ul, iar el va fi<br />

activat de către nucleu la eliberarea unei zone de memorie chiar dacă aceasta nu este suficientă).<br />

De asemenea, dacă un proces se blochează în aşteptarea unui eveniment care s-a produs deja, nu<br />

există nici o posibilitate de a specifica acest lucru prin intermediul evenimentelor.<br />

În cadrul sincronizării între procese prin intermediul evenimentelor, se pot identifica mai<br />

multe situaţii: sincronizarea prin semnale, sincronizarea între un proces tată şi fii săi, sincronizarea<br />

prin intermediul unor funcţii de sistem.<br />

Tibor Asztalos<br />

3.2.2 Semnale<br />

Apariţia unor evenimente în sistem este semnalată în UNIX fie de către nucleu, prin<br />

intermediul funcţiei wakeup, fie prin intermediul semnalelor. Acestea din urmă sunt implementate<br />

cu ajutorul unor biţi, memoraţi în tabele de procese şi care pot fi setaţi fie de către nucleu (în cazul<br />

producerii unor evenimente legate de hardware), fie de către utilizator (prin apelul directivei kill).<br />

Nucleul verifică primirea unui semnal (setarea bitului corespunzător acestuia) la trecerea din<br />

mod sistem în mod utilizator, precum şi înaintea şi după blocarea unui proces. Tratarea semnalelor<br />

se face în contextul procesului care le primeşte.<br />

În general, evenimentele ce generează semnale se împart in 3 categorii: erori, evenimente<br />

externe si cereri explicite.<br />

• eroare înseamnă că programul a făcut o operaţie invalidă şi nu poate să-si continue execuţia. Nu<br />

toate erorile generează semnale (de exemplu: erori în operaţiile I/O; apelul de funcţie respectiv<br />

va întoarce un cod de eroare, de obicei -1), ci doar acele erori care pot apare în orice punct al<br />

programului: împarţirea la zero, accesarea unei adrese de memorie invalide, etc.<br />

• Evenimentele externe sunt în general legate de operaţiile I/O sau de acţiunile altor procese, cum<br />

ar fi: sosirea datelor (pe un socket sau un pipe, de exemplu), expirarea intervalului de timp setat<br />

<strong>pentru</strong> un timer (o alarmă), terminarea unui proces fiu, sau suspendarea/terminarea programului<br />

de către utilizator (prin apăsarea tastelor ^Z sau ^C).<br />

• cerere explicită înseamnă generarea unui semnal de către un proces, prin apelul funcţiei de<br />

sistem kill(), a cărei sintaxă o vom discuta mai jos.<br />

Important: semnalele pot fi generate sincron sau asincron. Un semnal sincron este un semnal<br />

generat de o anumită acţiune specifică în program şi este livrat (dacă nu este blocat) în timpul acelei<br />

acţiuni. Evenimentele care generează semnale sincrone sunt: erorile şi cererile explicite ale unui<br />

proces de a genera semnale <strong>pentru</strong> el însuşi. Semnalele asincrone sunt generate de evenimente din<br />

afara zonei de control a procesului care le recepţionează, adică aceste semnale sunt recepţionate, în<br />

timpul execuţiei procesului destinatar, la momente de timp ce nu pot fi anticipate. Evenimentele<br />

care generează semnale asincrone sunt: evenimentele externe, precum şi cererile explicite ale unui<br />

proces de a genera semnale destinate altor procese.<br />

Semnalele nu transferă nici o cantitate de informaţie proceselor, ci sunt forme de<br />

sincronizare (funcţie de tipul semnalului, 19 standard în Unix V). Când un semnal ajunge la un<br />

proces, el este întrerupt din activitatea curentă şi obligat să răspundă. Acesta are trei posibilităţi de<br />

comportare:<br />

a) procesul poate ignora semnalul, continuându-şi activitatea (SIGKILL nu poate fi ignorat).<br />

Sistemul îşi păstrează posibilitatea (dreptul) de a termina procesul.<br />

b) procesul poate lăsa sistemul să execute acţiunea implicită (valabil <strong>pentru</strong> toate semnalele de<br />

terminare a proceselor, excepţie făcând doar SIGCLD şi SIGPWR care sunt explicit ignorate).<br />

c) procesul îşi poate defini o procedură proprie de tratare a semnalului, care va fi automat lansată<br />

la sosirea acestuia (handler).<br />

58


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Setarea unuia dintre cele trei comportamente se face cu ajutorul apelului primitivelor signal() sau<br />

sigaction().<br />

Indiferent de modul cum reacţionează programul la un anumit semnal, după terminarea<br />

acţiunii, dacă nu era ignorat, semnalul este resetat <strong>pentru</strong> viitor la acţiunea implicită, excluzând<br />

semnalele SIGKILL şi SIGTRAP care sunt generate foarte des şi ar fi ineficientă resetarea lor de<br />

fiecare dată. Apelul signal() comunică sistemului care este acţiunea dorită de proces <strong>pentru</strong> un<br />

anumit semnal.<br />

Tibor Asztalos<br />

#include <br />

void (* signal(semnal,functie))(int);<br />

int semnal;<br />

void (* functie)(int);<br />

Parametrul semnal reprezintă semnalul referit şi functie funcţia de tratare a lui. Funcţiile de<br />

tratare nu întorc valori şi au ca parametru unic numărul semnalului sosit. Există 2 valori implicite<br />

<strong>pentru</strong> funcţia de tratare, SIG_IGN (ignorarea semnalului de către proces) şi SIG_DFL (resetarea<br />

funcţiei la valoarea implicită). Apelul signal întoarce vechea funcţie de tratare (poate fi SIG_IGN<br />

sau SIG_DFL) sau -1 în cazul când ceva nu este corect (număr semnal incorect, se încearcă<br />

ignorarea lui SIGKILL, etc.). Această valoare se defineşte:<br />

#define BADSIG (void (*)(int))-1<br />

Valoarea returnată de apelul signal() poate fi folosită <strong>pentru</strong> a restaura starea anterioară<br />

apelului, după ce se iese din zona critică. Programul de mai jos arată cum putem ignora semnalele<br />

SIGINT şi SIGQUIT într-o regiune a programului în care este periculos să se termine anormal<br />

(funcţia ignoraint() şi refaint()).<br />

#include <br />

#include <br />

#include <br />

#include <br />

static void (* intvechi)(int),(* quitvechi)(int);<br />

#define BADSIG (void (*)(int))-1<br />

void ignoraint ()<br />

{<br />

static int first=1;<br />

if (first) /* doar prima data salvam starea */<br />

{<br />

first=0;<br />

intvechi=signal(SIGINT,SIG_IGN);<br />

quitvechi=signal(SIGQUIT,SIG_IGN);<br />

if (intvechi==BADSIG || quitvechi==BADSIG)<br />

perror("signal");<br />

}<br />

else<br />

if(signal(SIGINT,SIG_IGN)==BADSIG||signal(SIGQUIT,SIG_IGN)==BADSIG)<br />

perror ("signal");<br />

}<br />

void refaint()<br />

{<br />

if(signal(SIGINT,intvechi)==BADSIG||signal(SIGQUIT,quitvechi)==BASSIG)<br />

perror("signal"); }<br />

void main(argc,argv) /* inlocuieste în fisierul de pe linia */<br />

/* de comanda caracterele mici cu mari */<br />

int argc;<br />

char **argv;<br />

59


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

{<br />

FILE *in, *out;<br />

char buffer[256]; // buffer comand_ UNIX<br />

char *p;<br />

// parte de cod care poate rula cu intreruperile activate<br />

if (argc!=2)<br />

{<br />

printf("Argumente gresite !\n");<br />

exit(1);<br />

}<br />

in=fopen(argv[1],"r");//atribut read<br />

out=fopen("temporar","w");<br />

if (!in || !out)<br />

{<br />

printf("Nu se pot deschide fisierele !\n\a");<br />

exit(1);<br />

}<br />

while(fgets(buffer,256,in))<br />

{<br />

p=buffer;<br />

while(*p) //converteste bufferul la caractere mari<br />

{<br />

*p=toupper(*p);<br />

p++;<br />

}<br />

fputs(buffer,out);<br />

}<br />

fclose(in);<br />

fclose(out);<br />

ignoraint();<br />

/* sectiune critica */<br />

unlink(argv[1]);<br />

rename("temporar",argv[1]);<br />

refaint();<br />

/* terminare sectiune critica */<br />

}<br />

Programul 6. Sursa Ignora_Sig.c<br />

Apelurile definite într-un proces se păstrează şi în fiu în urma apelului fork(). În cazul<br />

apelului exec() se păstrează doar semnalele setate pe SIG_IGN şi SIG_DFL. Cele care au ataşata o<br />

funcţie a utilizatorului se resetează la valoarea SIG_DFL (în cazul exec se încarcă un nou program<br />

şi segmentul de cod al procesului este modificat şi evident vechile rutine de tratare a semnalelor ori<br />

nu se regăsesc ori sunt la alte adrese).<br />

Semnalele sosite către un proces nu sunt introduse în nici o coadă de aşteptare (dacă au fost<br />

ignorate s-au pierdut). Singura excepţie este SIGCLD care aşteaptă până procesul părinte execută<br />

apelul wait() <strong>pentru</strong> a lua cunoştinţă de terminarea procesului fiu (uneori fiul se termină înainte ca<br />

părintele să execute wait()). Dacă semnalul n-ar fi memorat, procesul părinte ar fi blocat până la<br />

terminarea unui alt fiu! SIGCLD nu este memorat în cazul în care părintele a setat explicit pe<br />

valoarea SIG_IGN rutina de tratare a semnalului. Datorită faptului că semnalele ignorate se pierd,<br />

această formă de sincronizare (comunicare) nu este prea folosita. În cazul când procesul are de<br />

executat mai multe operaţii la terminare (ştergerea fişierelor temporare, restaurarea unui fişier<br />

incomplet prelucrat) procesul trebuie să intercepteze semnalele SIGHUP, SIGINT şi SIGTERM şi<br />

pe ele să execute operaţiile de curăţire. De asemenea, pe parcursul dezvoltării unei aplicaţii,<br />

semnalul SIGQUIT nu trebuie interceptat, <strong>pentru</strong> a putea termina procesul cu CTRL \ însoţit de<br />

core dump. Dar în cazul unui proces care lucrează în background (lansat cu &) acesta rulează cu<br />

semnalele SIGINT şi SIGQUIT implicit ignorate, <strong>pentru</strong> a nu fi întrerupt accidental de apăsarea<br />

tastelor de întrerupere. În acest caz rutina de tratare a acestor semnale trebuie să rămână SIG_IGN.<br />

Rutina setsig() din exemplul de mai jos setează semnalul doar în situaţia când nu a fost anterior<br />

ignorat. Există şi o rutină de terminare anormală a unui proces.<br />

Tibor Asztalos<br />

#include <br />

60


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

#include <br />

#include <br />

#define BADSIG (void (*)(int))-1<br />

void seteaza_semnal(semnal,functie)<br />

int semnal;<br />

void (*functie)(int)<br />

{<br />

void (*sigvechi)(int);<br />

sigvechi=signal(semnal,SIG_IGN);<br />

if(sigvechi==BADSIG)<br />

perror("signal");<br />

else<br />

if(sigvechi==SIG_IGN)<br />

return;<br />

else<br />

if(sigvechi!=SIG_DFL)<br />

{<br />

printf("Semnalul era deja captat!\n");<br />

exit(1);<br />

}<br />

if(signal(semnal,functie)==BADSIG)<br />

perror("signal");<br />

}<br />

void capteaza_semnale()<br />

{<br />

void curata();<br />

seteaza_semnal(SIGHUP,curata);<br />

seteaza_semnal(SIGINT,curata);<br />

seteaza_semnal(SIGQUIT,curata);<br />

seteaza_semnal(SIGTERM,curata);<br />

}<br />

void curata(semnal)<br />

int semnal;<br />

{<br />

if(signal(semnal,SIG_IGN)==BADSIG)<br />

perror("signal");<br />

if(unlink("temporar")==-1)<br />

perror("unlink");<br />

switch(semnal)<br />

{<br />

case SIGHUP:<br />

fprintf(stderr,"Hangup \n");<br />

break;<br />

case SIGINT:<br />

fprintf(stderr,"Interrupt \n");<br />

break;<br />

case SIGQUIT:<br />

fprintf(stderr,"Quit \n");<br />

break;<br />

}<br />

exit(1);<br />

}<br />

int main(argc,argv)<br />

int argc;<br />

char **argv;<br />

{<br />

FILE *file;<br />

int i;<br />

if(argc!=2)<br />

{<br />

printf("Argumente gresite !\n\a");<br />

exit(1);<br />

}<br />

61


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

capteaza_semnale();<br />

file=fopen("temporar","w");<br />

if(!file)<br />

{<br />

printf("Nu pot deschide fisierul temporar !\n");<br />

exit(1);<br />

}<br />

for(i=0;i


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

char buffer[256];<br />

if(signal(SIGINT,SIG_IGN)!=SIG_IGN)<br />

{<br />

if(setjmp(jmpbuf)!=0)<br />

printf("\n intrerupere \n");<br />

if(signal(SIGINT,jumper)==BADSIG<br />

perror("signal");<br />

}<br />

bucla_principala();<br />

}<br />

Programul 8. Sursa Setjmp_Sig.c<br />

Tibor Asztalos<br />

Se poate trimite un semnal către un proces cu ajutorul apelului kill().<br />

int kill(pid,semnal);<br />

int pid;<br />

int semnal;<br />

Parametrul pid este numărul de identificare al procesului, iar semnal reprezintă numărul<br />

semnalului pe care vrem să-l trimitem. Dacă pid=0, semnalul va fi trimis tuturor proceselor din<br />

acelaşi grup cu procesul care lansează apelul. Această proprietate poate fi folosită <strong>pentru</strong> a termina<br />

toate procesele care rulează în background lansate de la un terminal. Dacă pid = -1, semnalul este<br />

trimis către toate procesele care au uid-ul real (userid) egal cu cel al proprietarului procesului care a<br />

lansat apelul. Aceasta foloseşte la terminarea tuturor proceselor care aparţin unui user indiferent de<br />

grupul din care fac parte (câte terminale). Dacă supervisorul execută kill cu pid = -1, toate<br />

procesele<br />

din sistem vor fi terminate cu excepţia lui 0 şi 1 (init, swap). Dacă pid < -1, semnalul este trimis<br />

tuturor proceselor care au numărul de grup egal cu valoarea absolută a lui pid. În practică nu se<br />

foloseşte kill ca apel sistem, ci ca comandă.<br />

Un proces poate sa-şi întrerupă activitatea cu pause() în aşteptarea unui semnal:<br />

void pause()<br />

La ieşirea din pause() procesul nu poate şti ce semnal a cauzat intreruperea, iar errno este<br />

setata pe valoarea EINTR. Cea mai utilă folosire a acestui apel sistem este aşteptarea unui semnal<br />

de alarmă setat de apelul alarm().<br />

unsigned alarm(secunde);<br />

unsigned secunde;<br />

Parametrul reprezintă numărul de secunde după care procesul porneşte semnalul SIGALRM.<br />

Valoarea rezultată este vechea valoare a ceasului (<strong>pentru</strong> fiecare proces este un ceas, un nou apel al<br />

funcţiei alarm distruge vechea valoare). Dacă procesul se răzgândeşte între timp el poate inhiba<br />

semnalul prin alarm(0).<br />

#include<br />

#include<br />

#define BADSIG (void(*)(int)) -1<br />

void sleep2(secunde)<br />

int secunde;<br />

{<br />

if(signal(SIGALRM,nimic)==BADSIG)<br />

perror("signal");<br />

alarm(secunde);<br />

pause();<br />

}<br />

void nimic(semnal);<br />

63


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

int semnal;<br />

main()<br />

{<br />

printf("Se asteapta 5 secunde !\n");<br />

sleep2(5);<br />

printf("Thank you !\n");<br />

}<br />

Programul 9. Sursa Alarm_Sig.c<br />

Tabelul semnalelor definite în UNIX V (aşa cum este definit în /usr/include/signal.h):<br />

SIGHUP(1) Hangup. Acest semnal este trimis, atunci când un terminal este oprit (conexiunea este<br />

întreruptă), către fiecare proces care aparţine terminalului respectiv. El este trimis şi atunci când<br />

procesul părinte al unui grup de procese este terminat, oricare ar fi motivul. Acest semnal ne dă<br />

posibilitatea să simulăm întreruperea conexiunii chiar dacă terminalul nu este conectat la distanţă.<br />

SIGINT(2) Interrupt. Acest semnal este trimis unui proces atunci când la terminalul asociat<br />

procesului s-a apăsat tasta de întrerupere (de obicei DEL). Tasta de întrerupere poate fi dezactivată<br />

sau poate fi modificată prin apelul ioctl. Atenţie, această situaţie nu este echivalentă cu ignorarea<br />

semnalului.<br />

SIGQUIT(3) Quit. Semnalul este similar cu SIGINT, dar este generat la apăsarea tastei CTRL \. La<br />

terminarea procesului se creează o imagine a stării procesului pe disc <strong>pentru</strong> verificări ulterioare.<br />

SIGILL(4) Illegal instruction. Acest semnal este trimis procesului când hardware-ul detectează o<br />

instrucţiune ilegală.<br />

SIGTRAP(5) Trace trap. Semnalul se trimite după fiecare instrucţiune dacă procesul are activată<br />

opţiunea de trasare (urmărire). Este folosit de debuggere sistem.<br />

SIGIOT(6) I/O trap instruction. Acest semnal este trimis când se semnalează o problemă de<br />

hardware (semnificaţia este dependentă de tipul maşinii). Semnalul este folosit de funcţia abort<br />

<strong>pentru</strong> a provoca terminarea procesului cu salvarea stării pe disc.<br />

SIGEMT(7) Emulator trap instruction. Semnalul este trimis când apar unele probleme hard (rar).<br />

SIGFPE(8) Floating point exception. Trimis atunci când hardware-ul detectează o problemă de<br />

lucru cu sistemul de VM, de exemplu când se încearcă folosirea unui număr care are un format<br />

incorect de reprezentare.<br />

SIGKILL(9) Kill. Acest semnal este singurul mod sigur de a termina un proces (nu poate fi<br />

ignorat). Se foloseşte numai în caz de urgenţă (de obicei este preferat lui SIGTERM(15)).<br />

SIGBUS(10) Bus error. Semnal dependent de maşină (când se adresează o adresă impară a unei<br />

structuri de date ce trebuie să se afle la o adresă de cuvânt - pară).<br />

SIGSENV(11) Segmentation violation. Dependent de masină (când se încearcă adresarea datelor<br />

din afara spaţiului de adrese).<br />

SIGSYS(12) Bad argument to DSystem Call. Nu se utilizează acum.<br />

SIGPIPE(13) Write on pipe not open for reading. Semnalul este trimis procesului atunci când acesta<br />

încearca să scrie într-un canal de comunicaţie din care nu citeşte nimeni (se poate folosi <strong>pentru</strong><br />

64


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

terminarea unei întregi înlanţuiri de pipe-uri). Dacă un proces se termină anormal toate celelalte<br />

primesc acest semnal în cascadă.<br />

SIGALRM(14) Alarm clock. Semnalul este trimis procesului când ceasul său a ajuns într-un<br />

moment fixat (fixarea se face cu apelul alarm()).<br />

SIGTERM(15) <strong>Software</strong> termination. Se opreşte un proces. Comanda kill trimite implicit acest<br />

semnal. Un proces care interceptează acest semnal trebuie să execute la primirea lui operaţiile de<br />

salvare şi curăţire necesare, după care se apelează exit.<br />

SIGUSR1(16) User defined signal 1. Acest semnal poate fi folosit de procese pemtru a comunica<br />

între ele (nu prea este utilizat).<br />

SIGUSR2(17) User defined signal 2. Idem.<br />

SIGCLD(18) Death of a child. Este primit de părinte când unul din fii s-a terminat (acţionează<br />

diferit faţă de celelalte deoarece este pus într-o coadă de aşteptare).<br />

SIGPWR(19) Power fail restart. Depinde de implementare (apare când scade tensiunea de<br />

alimentare). Procesul poate să-si salveze starea şi să apeleze exit sau işi salvează starea şi apelează<br />

sleep (dacă se mai trezeste).<br />

Exerciţii<br />

1. Ce afişează programul:<br />

int main( void)<br />

{ int pid, k=7;<br />

pid=fork();<br />

printf("Returnat %d\n", pid);<br />

if ( pid) int k=2;<br />

printf("k= %d\n");<br />

}<br />

2. Să se scrie un program care să demonstreze că două procese aflate în relaţia părinte-fiu sunt<br />

concurente.<br />

3. Să se scrie patru funcţii de sincronizare <strong>pentru</strong> a putea controla execuţia proceselor aflate în<br />

relaţia părinte-fiu.<br />

4. Să se scrie un program care generează un proces zombie.<br />

5. Să se scrie o funcţie sistem similară ca acţiune apelului sistem system() folosind apelurile sistem<br />

fork() şi exec(). Notă: semnalele nu se vor trata.<br />

6. Un program copiază intrarea standard într-un fişier specificat prin linia de comandă, utilizând un<br />

fişier intermediar în acest scop. Scrieţi programul în aşa fel încât, dacă procesul este întrerupt prin<br />

oricare din semnalele SIGINT, SIGQUIT, SIGHUP, SIGTERM, să fie anulate toate efectele<br />

execuţiei programului până la întrerupere.<br />

7. După execuţia lui fork(), nu se ştie ce proces va continua primul. În situaţia în care acestea<br />

accesează o resursă partajată, pot să apară probleme de nesincronizare. Pentru ca două procese<br />

aflate în relaţie părinte-fiu să se sincronizeze, se pot crea următoarele funcţii:<br />

Tibor Asztalos<br />

65


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

TELL_WAIT() realizează anumite iniţializări;<br />

TELL_PARENT(pid) anunţă părintele că poate continua execuţia;<br />

WAIT_PARENT() aşteaptă părintele până ce acesta termină accesul la resursa partajată;<br />

TELL_CHILD(pid) anunţă părintele că a terminat accesul;<br />

WAIT_CHILD( ) aşteaptă procesul fiu până ce acesta termină accesul la resursa partajată;<br />

Utilizând semnalele, să se implementeze aceste funcţii de sincronizare.<br />

8. Să se scrie un program <strong>pentru</strong> a testa sicronizarea părinte-fiu, utilizând funcţiile din programul 7.<br />

Tibor Asztalos<br />

66


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

Capitolul 4. Modalităţi IPC (comunicare inter-proces) în UNIX<br />

Comunicarea între procese sub sistemul de operare UNIX SV se poate realiza prin: fişiere<br />

obişnuite, fişiere de tip FIFO, pipe-uri, cozi de mesaje şi zone (segmente) de memorie partajată.<br />

4.1. Comunicarea intre procese folosind pipe-uri<br />

In încercarea de a rezolva o problemă sau de a implementa o aplicaţie, două sau mai multe<br />

procese care rulează concurent au nevoie să comunice între ele <strong>pentru</strong> a schimba informaţii sau<br />

<strong>pentru</strong> a se sincroniza. Aşa cum am arătat în 2.x.x, una dintre modalităţile de comunicare este<br />

constituirea unui flux de octeţi între procesele protagoniste. Noi vom folosi termenul de pipe în loc<br />

de flux de octeţi, cel puţin <strong>pentru</strong> comoditatea scrierii.<br />

Un pipe este o interfaţă de nivel aplicaţie, care poate fi folosită <strong>pentru</strong> a crea canale IPC<br />

între un client şi un server.<br />

Pipe-urile (conducte de date) sunt canale de comunicaţie unidirecţionale între procese,<br />

informaţia trecând de la un proces la altul printr-un mecanism FIFO (chiar şi COMMAND.COM<br />

din Dos implementează o astfel de tehnică). În DOS însă execuţia are loc secvenţial, în timp ce în<br />

UNIX execuţia are loc concurent, comunicaţia fiind directă. De fapt, conceptul a apărut prima dată<br />

sub Unix, <strong>pentru</strong> a permite unui proces fiu să comunice cu părintele său. De obicei procesul părinte<br />

redirectează ieşirea sa standard, stdout, către un pipe, iar procesul fiu îşi redirectează intrarea<br />

standard, stdin, din acelaşi pipe. Pipe-uri pot fi create şi în shell-ul Unix, ca şi în DOS, prin<br />

concatenarea mai multor comenzi pe aceeaşi linie separate de "|". Prin program se pot crea legături<br />

circulare între procese (bidirecţional).<br />

O metodă foarte des utilizată în UNIX <strong>pentru</strong> comunicarea între procese prin pipe este<br />

folosirea primitivei numită pipe(). Pipe-ul este o cale de legătura care poate fi stabilită între două<br />

procese înrudite (au un strămoş comun sau sunt în relaţia strămoş-urmaş). Ea are două capete, unul<br />

prin care se pot scrie date şi altul prin care datele pot fi citite, permiţând o comunicare într-o singură<br />

direcţie. În general, sistemul de operare permite conectarea a unuia sau mai multor procese la<br />

fiecare din capetele unui pipe, astfel încât, la un moment dat este posibil să existe mai multe<br />

procese care scriu, respectiv mai multe procese care citesc din pipe. Se realizează, astfel,<br />

comunicarea unidirecţionala între procesele care scriu şi procesele care citesc.<br />

4.1.1. Apelul de sistem pipe()<br />

Crearea conductelor de date se face în UNIX folosind apelul sistem pipe() cu sintaxa:<br />

int pipe(int fd[2]);<br />

Funcţia creează un pipe, precum şi o pereche de descriptori de fişier care referă cele două capete ale<br />

acestuia. Descriptorii sunt returnaţi către programul apelant completându-se cele două poziţii ale<br />

tabloului fd trimis ca parametru apelului sistem. Pe prima poziţie va fi memorat descriptorul care<br />

indică extremitatea prin care se pot citi date (capătul de citire), iar pe a doua poziţie va fi memorat<br />

descriptorul capătului de scriere în pipe.<br />

Funcţia returnează zero dacă operaţia de creare s-a efectuat cu succes şi -1 în caz de eroare.<br />

Cei doi descriptori sunt descriptori de fişier obişnuiţi, asemănători celor returnaţi de apelul<br />

de sistem open() (folosit <strong>pentru</strong> deschiderea unui fişier obişnuit). Mai mult, pipe-ul poate fi folosit<br />

67


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

în mod similar fişierelor, adică în el pot fi scrise date folosind funcţia write() (aplicată capătului de<br />

scriere) şi pot fi citite date prin funcţia read() (aplicată capătului de citire). Printre apelurile de<br />

sistem care pot fi aplicate folosind descriptorii de pipe obţinuţi se numără: close(), fcntl(), fstat().<br />

Însă apelurile open() şi creat() nu se folosesc <strong>pentru</strong> pipe, la fel nici lseek() (datele se citesc în<br />

ordinea în care au fost scrise).<br />

In urma creării pipe, legătura user – nucleu prin acest pipe apare ca în figura 4.1.<br />

Tibor Asztalos<br />

proces user:<br />

nucleu:<br />

Figura 4.1 Legătura user – nucleu prin pipe.<br />

Evident, un pipe într-un singur proces nu are sens. Este însă esenţială funcţionarea lui pipe()<br />

combinată cu fork(). Astfel, dacă după crearea lui pipe se execută un fork, atunci legătura celor<br />

două procese cu pipe din nucleu apare ca în figura 4.2.<br />

parinte<br />

read fd[0]<br />

write fd[1]<br />

write fd[1]<br />

kernel<br />

pipe<br />

Figura 4.2 Un pipe leagă două procese înrudite<br />

Fiind implicaţi descriptori de fişier obişnuiţi, dacă un pipe este creat într-un proces părinte,<br />

fiii acestuia vor moşteni cei doi descriptori (aşa cum, în general, ei moştenesc orice descriptor de<br />

fişier deschis de părinte). Prin urmare, atât părintele cât şi fiii vor putea scrie sau citi din pipe. În<br />

acest mod se justifică afirmaţia făcuta la începutul acestui paragraf prin care se spunea că pipe-urile<br />

sunt folosite la comunicarea între procese înrudite. Pentru ca legătura dintre procese să se facă<br />

corect, fiecare proces trebuie să declare dacă va folosi pipe-ul <strong>pentru</strong> a scrie în el (transmiţând<br />

informaţii altor procese) sau îl va folosi doar <strong>pentru</strong> citire. În acest scop, fiecare proces trebuie să<br />

închidă capătul pipe-ului pe care nu îl foloseşte: procesele care scriu în pipe vor închide capătul de<br />

citire, iar procesele care citesc vor închide capătul de scriere, folosind funcţia close().<br />

pipe<br />

read fd[0]<br />

sensul datelor<br />

fiu<br />

read fd[0]<br />

write fd[1]<br />

sen sul datelo r<br />

68


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Această asigurare a unidirecţionalităţii unui pipe cade exclusiv în sarcina programatorului.<br />

Astfel, <strong>pentru</strong> a se asigura sensul datelor din figura 4.2, se impune ca înainte de a transmite prin<br />

pipe, să se execute:<br />

• In procesul părinte să se apeleze close(fd[0]);<br />

• In procesu fiu să se apeleze close(fd[1]);<br />

Natural, dacă se doreşte ordinea inversă, atunci se vor executa operaţiile close(fd[1]) în procesul<br />

părinte şi close(fd[0]) în procesul fiu.<br />

În concluzie: fd este un vector de doi întregi care după apelul directivei va fi completat cu doi<br />

descriptori de fişier:<br />

fd[0] - <strong>pentru</strong> citire<br />

fd[1] - <strong>pentru</strong> scriere<br />

ce vor fi folosiţi <strong>pentru</strong> operaţiile ulterioare de citire/scriere.<br />

După crearea pipe-ului, operaţiile de scriere/citire se fac, din punct de vedere al utilizatorului, ca<br />

<strong>pentru</strong> un fişier obişnuit (utilizând descriptorii corespunzători), cu observaţiile :<br />

• sistemul de operare foloseşte <strong>pentru</strong> fişierul pipe doar un număr limitat de blocuri de<br />

disc (primele zece, de obicei), dimensiunea acestuia fiind deci limitată în memorie (cca.<br />

4 Ko).<br />

• sistemul manipulează blocurile alocate pipe-ului ca pe o coadă circulară, menţinându-se<br />

pointeri de citire/scriere diferiţi <strong>pentru</strong> a se putea respecta politica FIFO (First In First<br />

Out).;<br />

• orice operaţie de scriere într-un pipe măreşte dimensiunea acestuia, iar orice citire o<br />

micşorează. În pipe nu este posibil accesul direct sau repoziţionarea de către utilizator a<br />

pointerului de citire/scriere;<br />

• scrierea/citirea se fac sincronizat în sensul că sistemul de operare blochează (fără a<br />

efectua operaţia) procesele care vor să citească dintr-un pipe gol ca şi pe cele care vor să<br />

scrie într-un pipe în care nu mai este suficient loc;<br />

• dacă se scriu mai mulţi octeti decât este liber, write() se blochează până când cineva<br />

goleşte pipe prin citire, write reluîndu-se până la terminare (afară de setarea prin fcntl() a<br />

comutatorului O_NDELAY);<br />

• un apel read() se termină chiar dacă nu a găsit toţi octeţii de care avea nevoie (valoare<br />

de retur=numărul de octeţi citiţi). În cazul în care pipe-ul este gol, read() se blochează<br />

până la sosirea unor date (excepţie tot cu O_NDELAY setat);<br />

• operaţia de comunicare prin pipe-uri anonime se poate face doar între un proces tată şi<br />

fiii săi;<br />

• un pipe poate fi închis ca un fişier obişnuit prin intermediul directivei close(), apelată cu<br />

unul dintre descriptori ca parametru.<br />

• i-nodul unui pipe anonim este şters în mod automat de către sistemul de operare atunci<br />

când dispar toate procesele care refereau pipe-ul.<br />

• flagul O_NONBLOCK este resetat <strong>pentru</strong> cei doi descriptori.<br />

Un posibil scenariu <strong>pentru</strong> crearea unui sistem format din două procese care comunică prin pipe<br />

este următorul:<br />

Tibor Asztalos<br />

69


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

• procesul părinte creează un pipe,<br />

• părintele apelează fork() <strong>pentru</strong> a crea fiul,<br />

• fiul închide unul din capete (ex: capătul de citire),<br />

• părintele închide celălalt capăt al pipe-ului (cel de scriere),<br />

• fiul scrie date în pipe folosind descriptorul rămas deschis (capătul de scriere),<br />

• părintele citeşte date din pipe prin capătul de citire.<br />

Primitiva pipe() se comportă în mod asemănător cu o structură de date de tip coadă: scrierea<br />

introduce elemente în coadă, iar citirea le extrage pe la capătul opus.<br />

Un exemplu de program scris conform scenariului de mai sus este:<br />

void main()<br />

{<br />

int pfd[2];<br />

int pid;<br />

}<br />

...<br />

if(pipe(pfd)


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Observaţii:<br />

• 1. Cantitatea de date care poate fi scrisă la un moment dat într-un pipe este limitată. Numărul de<br />

octeţi pe care un pipe îi poate păstra fără ca ei să fie extraşi prin citire de câtre un proces este dat<br />

de constanta predefinită PIPE_BUF.<br />

• 2. Un proces care citeşte din pipe va primi valoarea 0 ca valoare returnată de apelul read() în<br />

momentul în care toate procesele care scriau în pipe au închis capătul de scriere şi nu mai există<br />

date în pipe.<br />

• 3. Dacă <strong>pentru</strong> un pipe sunt conectate procese doar la capătul de scriere (cele de la capătul opus<br />

au închis toate conexiunea) operaţiile write() efectuate de procesele rămase vor returna eroare.<br />

Intern, în această situaţie va fi generat semnalul SIG_PIPE care va întrerupe apelul sistem<br />

write() respectiv. Codul de eroare (setat în variabila globală errno) rezultat este cel<br />

corespunzător mesajului de eroare "Broken pipe".<br />

• 4. Dacă dorim să semnalăm procesului cu care comunicăm că s-a atins sfârşitul de fişier,<br />

trebuie să închidem canalul cu apelul de sistem close(). Apelul sistem fstat() returnează numărul<br />

de octeţi disponibili în pipe la un moment dat (este foarte dinamic). fstat() este util la testarea<br />

dacă un descriptor de fişier corespunde sau nu unui pipe, testând dacă numărul de legături este<br />

0. Pipe foloseşte acelaşi mecanism de cache care se foloseşte şi <strong>pentru</strong> fişierele de pe discuri.<br />

Scrierea şi citirea sunt operaţii atomice (scrierea este cu 512 octeţi în general, citirea cu


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Dacă dimensiunea blocului de scris este mai mare decât dimensiunea pipe-ului, se poate<br />

produce deadlock (se poate totuşi rezolva prin citirea unor octeţi din pipe). Un pipe unidirecţional<br />

nu poate duce niciodată la deadlock total. Când avem un pipe între două procese, cel care citeşte<br />

trebuie să fie un fiu al procesului care a deschis pipe-ul <strong>pentru</strong> a moşteni descriptorii de fişiere<br />

(inclusiv al pipe-ului), sau ambele procese fiu al aceluiaşi tată. Dacă unul din procese doar scrie iar<br />

celălalt doar citeşte, atunci avem pipe unidirecţional.<br />

/*p2.c*/<br />

#include <br />

Tibor Asztalos<br />

void main ()<br />

{<br />

int pdescr[2];<br />

char buffer[250];<br />

if(pipe(pdescr)==-1)<br />

{<br />

perror("eroare deschidere pipe\n");<br />

exit(1);<br />

}<br />

switch(fork())<br />

{<br />

case -1 :<br />

perror("eroare fork\n");<br />

exit(1);<br />

case 0 : /* fiu */<br />

if(close(pdescr[1])==-1)<br />

{<br />

perror("eroare close pipe scriere\n");<br />

exit(1);<br />

}<br />

sprintf(buffer,"%d",pdescr[0]);<br />

execlp("./p3","p3",buffer,NULL);<br />

perror("eroare execlp\n");<br />

exit(1);<br />

}<br />

if(close(pdescr[0]==-1)<br />

{<br />

perror("eroare close pipe citire\n");<br />

exit(1);<br />

}<br />

if(write(pdescr[1],"Hello !",7)==-1)<br />

{<br />

perror("eroare write pipe\n");<br />

exit(1);<br />

}<br />

}<br />

}<br />

Programul 2. Sursa p2.c<br />

/*p3.c*/<br />

#include <br />

void main(argc,argv)<br />

inr argc;<br />

char **argv;<br />

{<br />

int fd,nr;<br />

char buffer[256];<br />

fd=atoi(argv[1]);<br />

switch(nr = read(fd,buffer,sizeof(buffer)));<br />

{<br />

case -1;<br />

perror("eroare read\n");<br />

72


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

}<br />

}<br />

Programul 3. Sursa p3.c<br />

exit(1);<br />

case 0;<br />

printf("Nu am atins sfirsitul fisierului pipe \n");<br />

exit(1);<br />

default :<br />

printf("Am citit %d octeti %s \n",nr,buffer);<br />

În primul proces după fork(), în cazul că ne găsim în fiu, canalul de scriere în pipe este închis.<br />

Aceasta <strong>pentru</strong> că cel de-al doilea proces nu va scrie niciodată în pipe şi în acest caz este mai bine<br />

să eliberăm descriptorul de fişiere, care face parte dintr-o resursă limitată (20 de descriptori de<br />

proces).<br />

Această situaţie este înainte de exec() <strong>pentru</strong> că după aceasta, deşi fişierul rămâne deschis, p3 nu<br />

ştie care este acel descriptor. Programul p3 nu ştie nici unde se găseşte descriptorul de citire, de<br />

acea acesta şi este transmis ca parametru în linia de comandă (nu este cea mai buna metoda !). În<br />

procesul tată, la revenirea din fork(), se închide canalul de citire, <strong>pentru</strong> ca tatăl nu va citi niciodată<br />

din pipe. De aceea, intre apelurile de sistem fork() şi exec() se mai pot face unele prelucrări, care<br />

făcute în altă parte ar însemna mult mai mult efort.<br />

Pentru a exemplifica utilizarea pipe, propunem următoarea problemă. Definim o<br />

“propoziţie” ca fiind o succesiune de caractere terminate cu caracterul ‘.’ (punct). Se dau o<br />

succesiune de linii citite de la intrarea standard şi se cere tipărirea la ieşirea standard a propoziţiilor<br />

câte una pe linie. De exemplu, dacă la intrarea standard se dau liniile:<br />

abcd 555. xxxxx shjd asd. d<br />

mmm. t. jj<br />

ee cc rrr<br />

xxx. gg<br />

pppp rr<br />

Ieşirea standard va afişa “propoziţiile”<br />

abcd 555.<br />

xxxxx shjd asd.<br />

d mmm.<br />

t.<br />

jjee cc rrrxxx.<br />

ggpppp rr<br />

Evident, problema are o rezolvare extrem de simplă, de exemplu prin secvenţa C de mai jos, care<br />

transferă caracter cu caracter:<br />

for (;;) {<br />

c = getchar();<br />

if (c == EOF)<br />

break;<br />

putchar(c);<br />

if ( c == ‘.’)<br />

putchar(‘\n’);<br />

}<br />

Noi vom “complica” rezolvarea folosind în acest scop două procese şi un pipe prin care ele<br />

comunică.<br />

Primul proces, scriitorInPipe, citeşte succesiv linii de la intrarea standard şi <strong>pentru</strong> fiecare linie<br />

depune caracterele acesteia într-un pipe. După cum am arătat scrierea în pipe nu asigură, neapărat,<br />

depunerea numărului total de octeţi precizat. De aceea prevedem introducerea în pipe a liniei “pe<br />

bucăţi” fiecare scriere elementară introducând în pipe începând cu primul caracter neintrodus la<br />

73


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

scrierea precedentă.<br />

Al doilea proces, cititorDinPipe, extrage din pipe “pe bucăţi” fragmentele liniei până la întâlnirea<br />

punctului, după care scrie linia la ieşirea lui standard. Apoi continuă cu citirea de fragmente din<br />

propoziţia următoare ş.a.m.d.<br />

Următoarele două programe prezintă sursele funcţiilor/metodelor care implementează cele două<br />

procese. Fiecare dintre ele au ca parametru de intrare un descriptor de fişier, despre care se<br />

presupune că a fost deja deschis <strong>pentru</strong> scriere sau citire în/din pipe. In aceste exemple oferim<br />

cititorulului “mostre” de gestiune a scrierilor/citirilor succssive în/din stream <strong>pentru</strong> transferul unui<br />

număr de octeţi dorit.<br />

#define SMAX 1000 //dimensiune maxima pt un sir de caractere<br />

void scriitorInPipe(int hpipeOut) {<br />

Tibor Asztalos<br />

int noct; //numar octeti scrisi<br />

char linie[SMAX],*plinie;<br />

//citeste cate o linie<br />

for (;;) {<br />

plinie=gets(linie);<br />

if (plinie==NULL)<br />

break;<br />

fflush(stdout);<br />

for (;;) {<br />

//scrie linia pe bucati in pipe<br />

noct=write(hpipeOut,plinie,strlen(plinie));<br />

}<br />

if (noct==strlen(plinie))<br />

break;<br />

plinie+=noct;<br />

}<br />

close(hpipeOut);<br />

}//scriitorInPipe<br />

Programul 4. Sursa scriitorInPipe.c<br />

#define SMAX 1000 //dimensiune maxima pt un sir de caractere<br />

void cititorDinPipe(int hpipeIn) {<br />

int noct; //numar octeti cititi din pipe<br />

char propoz[PMAX],*sir1,*sir2,sirPipe[SMAX];<br />

propoz[0]=0;<br />

for (;;) {<br />

//citeste maximum SMAX octeti din pipe<br />

noct=read(hpipeIn,sirPipe,SMAX);<br />

if (!noct)<br />

break;<br />

sirPipe[noct]=0;<br />

for (sir1=sirPipe;strlen(sir1)>0; ) {<br />

//cauta caracterul '.' in sirul citit din pipe<br />

sir2=(char*)memchr(sir1,'.',strlen(sir1));<br />

if (!sir2) {<br />

strcat(propoz,sir1);<br />

break;<br />

}<br />

strncat(propoz,sir1,strlen(sir1)-strlen(sir2));<br />

*sir2=0;<br />

74


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

}<br />

strcat(propoz,".");<br />

puts(propoz);<br />

propoz[0]=0;<br />

sir1=sir2+1;<br />

}<br />

puts(propoz);<br />

close(hpipeIn);<br />

}//cititorDinPipe<br />

Programul 5. Sursa cititorDinPipe.c<br />

Programul principal:<br />

#include "err_sys.c"<br />

#include "scriitorInPipe.c"<br />

#include "cititorDinPipe.c"<br />

#include <br />

main() {<br />

int fd[2], pid;<br />

if (pipe(fd) < 0)<br />

err_sys("Nu s-a putut deschide pipe");<br />

if ((pid = fork()) < 0)<br />

err_sys("Nu s-a putut face fork");<br />

else if (pid > 0) { // Procesul parinte<br />

close(fd[0]);<br />

scriitorInPipe(fd[1]);<br />

close(fd[1]);<br />

wait(0);<br />

}<br />

else { // Procesul fiu<br />

close(fd[1]);<br />

cititorDinPipe(fd[0]);<br />

close(fd[0]);<br />

exit(0);<br />

}<br />

}<br />

Programul 6. Sursa PropozitiiPipe.c<br />

Pentru a asigura o comunicaţie bidirecţională între două procese sunt necesare două pipe-uri,<br />

folosite fiecare <strong>pentru</strong> câte o direcţie. Paşii necesari sunt:<br />

• creare pipe1 şi pipe2 (presupunem că avem descriptorii fd1 şi fd2)<br />

• execuţie fork()<br />

• în procesul părintele close(fd1[0]) şi close(fd2[1])<br />

• în procesul fiul close(fd1[1]) şi close(fd2[0])<br />

In urma acestor acţiuni, legăturile apar ca în figura 4.4<br />

pãrinte<br />

write fd1<br />

read fd2<br />

pipe1<br />

pipe2<br />

write fd2<br />

read fd1<br />

Figura 4.4 Comunicarea prin două pipe-uri între părinte şi fiu<br />

fiu<br />

75


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Pentru a opri blocarea lui write() în cazul că pipe este plin, putem folosi apelul fcntl():<br />

Tibor Asztalos<br />

#include <br />

if(fcntl(fd,F_SETFL,O_NDELAY)


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

...<br />

fd=open("Fisier.txt", O_WRONLY);<br />

...<br />

if((newfd=dup2(fd,1))


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

Tibor Asztalos<br />

...<br />

close(pfd[0]); /* la sfarsit inchide si capatul utilizat */<br />

exit(0);<br />

}<br />

Programul 7. Sursa DupEx1.c<br />

Funcţia fdopen() a fost folosită <strong>pentru</strong> a putea folosi avantajele funcţiilor de bibliotecă <strong>pentru</strong> lucrul<br />

cu fişiere în cazul unui fişier (capătul de citire din pipe) indicat de un descriptor întreg (în speţa, s-a<br />

dorit să se efectueze o citire formatată, cu fscanf(), din pipe).<br />

În programul următor se lansează două procese interconectate printr-un pipe unidirecţional<br />

(unul scrie altul citeşte - asemănător cu mecanismul utilizat de shell <strong>pentru</strong> a lansa două comenzi<br />

legate printr-un pipe).<br />

#include <br />

void main(argc,argv)<br />

int argc;<br />

char **argv;<br />

{<br />

int pdescr[2];<br />

int status;<br />

if(argc == 3)<br />

printf("Utilizarea unui pipe intre doua procese fiu\n");<br />

else<br />

exit(1);<br />

if(pipe(pdescr)==-1)<br />

{<br />

perror("eroare deschidere pipe\n");<br />

exit(1);<br />

}<br />

switch(fork(0)) :<br />

{<br />

case -1 :<br />

perror("eroare fork 1\n");<br />

exit(1);<br />

case 0 :<br />

/* primul fiu */<br />

close(pdescr[0]);<br />

close(1);<br />

if(dup(pdescr[1])!=1)<br />

{<br />

perror("eroare dup1\n");<br />

exit(1);<br />

}<br />

close(pdescr[1]);<br />

execlp(argv[1],argv[1],NULL);<br />

perror("eroare execlp 1\n");<br />

exit(1);<br />

}<br />

switch(fork(0)) :<br />

{<br />

case -1 :<br />

perror("eroare fork 2\n");<br />

exit(1);<br />

case 0 :<br />

/* al doilea fiu */<br />

close(pdescr[1]);<br />

close(0);<br />

if(dup(pdescr[0])!=0)<br />

{<br />

perror("eroare dup2\n");<br />

exit(1);<br />

}<br />

78


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

close(pdescr[0]);<br />

execlp(argv[2],argv[2],NULL);<br />

perror("eroare execlp 2\n");<br />

exit(1);<br />

}<br />

/* in tata */<br />

close(pdescr[0]);<br />

close(pdescr[1]);<br />

wait(&status);<br />

/* asteapta terminarea primului fiu */<br />

wait(&status);<br />

/* asteapta terminarea celui de al doilea fiu */<br />

}<br />

Programul 8. Sursa DupEx2.c<br />

În cazul când dorim crearea unui pipe bidirecţional (poate duce la deadlock), trebuie create<br />

doua pipe-uri (unul <strong>pentru</strong> citire/scrire, altul scriere/citire). Descriptorii de fişier blocaţi sunt tot doi<br />

<strong>pentru</strong> fiecare proces, ceilalţi doi putând fi închişi ca în exemplele anterioare.<br />

Un alt exemplu. Să considerăm acum comanda compusă Unix (shell):<br />

$ who | sort | lpr<br />

Mecanismul intern de legare a celor trei comenzi este ilustrat în figura 4.5.<br />

proces who<br />

write<br />

Figura 4.5 Schema comenzii “$ who | sort | lpr”<br />

Noi vom prezenta realizarea conexiunii între primele două comenzi: who | sort prin pipe. Procesul<br />

părinte (procesul shell) generează doi fii, iar aceştia îşi redirectează corespunzător intrările / ieşirile.<br />

Primul dintre ele execută who, celălalt sort, iar părintele aşteaptă terminarea lor. Sursa este<br />

prezentată în programul următor.<br />

//whoSort.c<br />

//Lanseaza in pipe comenzile shell: $ who | sort<br />

main (){<br />

pipe1<br />

proces sort proces lpr<br />

write<br />

read<br />

read<br />

pipe2<br />

int p[2];<br />

pipe (p);<br />

if (fork () == 0) { // Primul fiu<br />

dup2 (p[1], 1); //redirectarea iesirii standard<br />

close (p[0]);<br />

execlp ("who", "who", 0);<br />

}<br />

else if (fork () == 0) { // Al doilea fiu<br />

dup2 (p[0], 0); // redirectarea intrarii standard<br />

close (p[1]);<br />

execlp ("sort", "sort", 0); //executie sort<br />

}<br />

else { // Parinte<br />

79


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

}<br />

Tibor Asztalos<br />

close (p[0]);<br />

close (p[1]);<br />

wait (0);<br />

wait (0);<br />

Programul 9. Sursa whoSort.c<br />

4.3. Fişiere de tip FIFO<br />

Un FIFO combină trăsăturile unui pipe cu acelea ale unui fişier obişnuit. Ca şi un fişier,<br />

FIFO-ul are un nume, o poziţie în sistemul de fişiere şi poate fi accesat de orice proces care are<br />

drepturi asupra lui. Spre deosebire de pipe-urile clasice, cu ajutorul unui FIFO pot comunica oricare<br />

două procese indiferent de relaţia lor de rudenie. Din momentul în care a fost deschis însă, FIFO se<br />

comportă ca un pipe. Datele se pot citi în ordinea FIFO, apelurile de tip read/write fiind atomice, cu<br />

condiţia să nu depăşească capacitatea FIFO-ului (>=4 ko). Apelul lseek() nu are efect iar datele nu<br />

mai pot fi scrise înapoi. Atunci când un FIFO este deschis <strong>pentru</strong> citire, kernel-ul aşteaptă până<br />

când un alt proces deschide acelaşi FIFO <strong>pentru</strong> scriere (se aşteaptă unul pe altul la deschiderea<br />

canalului de comunicaţie, rendez-vous, sincronizat înaintea comunicaţiei propiu-zise). La fel ca la<br />

pipe se poate folosi apelul fcntl() <strong>pentru</strong> a seta flagul O_NDELAY. În acest caz deschiderea <strong>pentru</strong><br />

citire returnează imediat, fără să aştepte ca FIFO-ul să fie deschis <strong>pentru</strong> scriere, în timp ce<br />

deschiderea <strong>pentru</strong> scriere returnează eroare (kernel-ul nu poate garanta păstrarea permanentă a<br />

datelor care se înscriu în FIFO-ul care nu este citit imediat). În plus, la închiderea canalului de<br />

comunicaţie fără comunicarea tuturor datelor scrise, acestea se pierd fără a se indica eroare. Flagul<br />

O_NDELAY afectează apelurile de citire/scriere ca la pipe-urile clasice.<br />

Crearea unui FIFO se face cu apelul mknod():<br />

#include <br />

#include <br />

init res;<br />

char *path;<br />

res=mknod(path,S_IFIFO|0777,0);<br />

res = 0 - în caz de succes<br />

res = 1 - în caz de eroare<br />

Path reprezintă numele FIFO (la fel ca la fişiere) şi 0777 drepturi de acces.<br />

mknod() foloseşte <strong>pentru</strong> crearea unor fişiere normale, a unor subdirectoare, sau a unor<br />

fişiere speciale, dar aceste facilităţi sunt accesibile numai supervizorului. Parametrul S_IFIFO este<br />

accesibil oricărui user. Cu apelul fstat() putem prelua starea unui FIFO deschis anterior, iar cu<br />

apelul stat() starea unui FIFO nedeschis încă.<br />

#include <br />

#include <br />

int res,fd;<br />

char *path;<br />

struct stat *sbuf ;<br />

res = fstat(fd,sbuf); // res=0 succes; res=1 eroare<br />

res = stat(path,sbuf);<br />

Informaţiile obţinute prin aceste apeluri: lungime (câte caractere sunt în FIFO), timpul/data<br />

de creare, actualizare, numărul de Inode, numărul de legaturi (links=0 <strong>pentru</strong> pipe clasic, acesta<br />

80


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

neexistând pe disc), uid, gid, etc. O primă aplicare a FIFO este de a implementa un pipe clasic. În<br />

locul folosirii unui pipe vom deschide un FIFO de două ori, o dată <strong>pentru</strong> scriere şi o dată <strong>pentru</strong><br />

citire şi apoi putem trata cei doi descriptori ca la pipe clasic. De fapt FIFO s-au introdus nu <strong>pentru</strong> a<br />

înlocui pipe ci mesajele.<br />

/*p4*/<br />

#include <br />

#include <br />

#include <br />

#include <br />

Tibor Asztalos<br />

#define MAXOPEN 7<br />

#define MAXTRIES 3<br />

#define NAPTIME 5<br />

#define FALSE 0<br />

#define TRUE 1<br />

static char *fifoname(key) /*creeaza un nume de fisier temporar */<br />

long key;<br />

{<br />

static char fifo[20];<br />

sprintf(fifo,"/temp/fifo%ld ",key);<br />

return fifo;<br />

}<br />

static int mkfifos(path) /* creeaza un FIFO */<br />

char *path;<br />

{<br />

return mknod(path,S_IFIFO|0666,0);<br />

}<br />

static int openfifo(key,flags)<br />

long key;<br />

int flags;<br />

{<br />

static struct<br />

{<br />

long key;<br />

int fd;<br />

int time;<br />

}fifos[MAXOPEN];<br />

static int clock ;<br />

int i,avail,oldest,fd,tries;<br />

char *fifo;<br />

extern int errno;<br />

avail=-1; /* caut_ un loc liber */<br />

for(i=0;i


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

Tibor Asztalos<br />

}<br />

fifo=fifoname(key);<br />

if(mkfifos(fifo)== -1 && errno != EEXIT)<br />

return -1;<br />

for(tries=1;tries < MAXTRIES;tries++)<br />

{<br />

if((fd=open(fifo,flags|O_NDELAY)) != -1)<br />

break;<br />

if(errno != ENXIO) return -1;<br />

sleep(NAPTIME);<br />

}<br />

if(fd == -1)<br />

{<br />

errno=ENXIO;<br />

return -1;<br />

}<br />

if(fcntl(fd,F_SETFL,flags)== -1) /* reseteaza O_NDELAY */<br />

return -1;<br />

fifos[avail].key=key;<br />

fifos[avail].fd=fd;<br />

fifos[avail].time=clock;<br />

return fd;<br />

int send(dstkey,buf,nbytes) /* trimite un mesaj */<br />

long dstkey;<br />

char* buf;<br />

int nbytes;<br />

{<br />

int fd;<br />

if ((fd=openfifo(dstkey,O_WRONLY)== -1)<br />

return FALSE;<br />

return write(fd,buf,nbytes) != -1;<br />

}<br />

int receive(srckey,buf,nbytes) /* primeste un mesaj */<br />

long srckey;<br />

char* buf;<br />

int nbytes;<br />

{<br />

int fd,nread;<br />

if((fd=openfifo(srckey,O_RDONLY))==-1)<br />

return FALSE;<br />

while((nread=read(fd,buf,nbytes))==0)<br />

sleep(NAPTIME);<br />

return nread != -1 ;<br />

}<br />

void rmqueue(key)<br />

long key;<br />

{<br />

int errno ;<br />

if(unlink(fifoname(key)) == -1 && errno != ENOENT)<br />

perror("eroare unlink\n");<br />

}<br />

/* Receive.c */<br />

#include"mesaje.h"<br />

void main()<br />

{<br />

MESSAGE m;<br />

setbuf(stdout,NULL);<br />

while(receive(1000L,&m,sizeof(m)))<br />

printf("Received %d from %d \n ",m.number,m.pid);<br />

perror("eroare receive !\n");<br />

82


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

Tibor Asztalos<br />

exit(1);<br />

/* Send.c */<br />

#include"mesaje.h"<br />

void main()<br />

{<br />

MESSAGE m;<br />

m.pid=getpid();<br />

for(m.number = 1 ; m.member │ │ . . . │ │ ───────><br />

┌──>│ │ └──┘ └──┘<br />

┌────────┐ │ └──────┘ ───────────────<br />

│client 2│


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Rolul principal îi revine cozii de mesaje. Unul sau mai multe procese transmit, introduc<br />

mesaje într-o coada de mesaje, iar altele le extrag. Dacă un proces extrage un mesaj din coadă de<br />

mesaje, această informaţie este pierdută <strong>pentru</strong> ceilalţi. Nu există deocamdată un mecanism prin<br />

care un proces să poată trimite un mesaj câtre mai multe procese deodată (broadcast). Ordinea<br />

mesajelor în coada este strictă, mesajele putându-se scoate doar în ordinea în care au fost trimise (o<br />

aplicaţie poate folosi mai multe cozi de mesaje).<br />

Mecanismul de comunicaţie prin mesaje este implementat în nucleu.<br />

Pentru aceasta, kernelul îşi rezervă o zonă tampon de memorie. Fiecare mesaj trimis trece prin<br />

kernel. Procesul care trimite mesajul stabileşte <strong>pentru</strong> acesta o cheie de recunoaştere. Aici apare o<br />

problemă: dacă un alt proces cunoaşte cheia, el poate să preia informaţia din coadă, chiar dacă<br />

informaţia nu-i fusese destinată, ea pierzându-se (o soluţie ar fi codificarea mesajelor).<br />

Transmisia se face cu sincronizare, în sensul că procesele care fac cereri de primire de<br />

mesaje se blochează până când recepţionează mesajul dorit, iar cele care doresc să transmită se<br />

blochează dacă nu există ici un proces care să recepţioneze şi coada de mesaje este plină.<br />

Un mesaj se bazează pe o structură C care cuprinde un identificator şi textul propriu-zis.<br />

┌────┬─────┐ Identificatorul se foloseste <strong>pentru</strong> specificarea<br />

mesaj │mtip│mtext│ tipului mesajului necesar la selectarea lui din<br />

└────┴─────┘ coada de mesaje si se reprezinta cu un long.<br />

Informaţia propriu-zisă este în câmpul mtext, are o lungime variabilă şi se declară de<br />

exemplu prin char[] (structura unui mesaj trebuie să înceapă cu un long, restul după necesităţi =<br />

regula). Pentru a programa cu mesaje, se includ următoarele fişiere standard: ,<br />

, .<br />

Există o serie de primitive sistem care ne permit acest mod de IPC. Ele sunt : msgget(), msgsnd(),<br />

msgrcv(), msgctl().<br />

Aceste primitive lucrează cu o serie de structuri de date, structuri de date ce descriu complet<br />

coada de mesaje. Fiecare coadă de mesaje din sistem este accesat prin intermediul unui identificator<br />

al cozii. Acest identificator (un număr întreg pozitiv) este asociat cu coada la momentul creerii ei.<br />

De asemenea fiecărei cozi (deci şi identificatorul cozii) se asociează câte o structură msg_id definit<br />

în fişierul antet msg.h, cu următoarea semnificaţie :<br />

struct msqid_ds {<br />

struct ipc_perm msg_perm; /*o structură ce defineşte drepturile de<br />

acces la această coadă*/<br />

struct msg *msg_first; /*adresa primului mesaj din coada */<br />

struct msg *msg_last; /*adresa ultimului mesaj din coadă */<br />

time_t msg_stime; /*momentul ultimei transmisii de mesaj */<br />

time_t msg_rtime; /*momentul ultimei recepţii de mesaj */<br />

time_t msg_ctime; //momentul ultimei schimbări a stării cozii<br />

struct wait_queue *wwait;<br />

struct wait_queue *rwait;<br />

ushort msg_cbytes; /* numărul curent de octeţi în coadă */<br />

ushort msg_qnum; /* numărul mesajelor în coadă*/<br />

ushort msg_qbytes; /* numărul maxim de octeţi admis în coadă*/<br />

ushort msg_lspid; /* PID al ultimului proces transmiţător */<br />

ushort msg_lrpid; /* PID al ultimului proces receptor */<br />

};<br />

Această structură este creată şi iniţializată în momentul creerii unei cozi de mesaje apelând<br />

Tibor Asztalos<br />

84


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

primitiva msgget(). Prototipul (sintaxa) acestei funcţii este :<br />

int msgget(key, msgflg)<br />

key key; //cheie de identificare a cozii de mesaje<br />

int msgflg; //identificator al modului şi drepturilor de acces<br />

msgget() returnează identificatorul cozii de mesaje conform valorii key (valori definite în fişierul<br />

antet ipc.h).<br />

Un identificator al unei cozi precum şi structurile asociate cozii sunt create <strong>pentru</strong> key dacă una<br />

dintre următoarele condiţii se verifică:<br />

• key este egal cu IPCPRIVATE.<br />

• key nu are legat de el un identificator al unei cozi, şi (msgflg & IPC_CREAT) este TRUE<br />

(adevărat).<br />

În urma creerii acestei noi cozi structura msgid_ds asociată acestei cozi se iniţializează după<br />

cum urmează :<br />

• msg_perm.cuid, msg_perm.uid, msg_perm.cgid, şi msg_perm.gid sunt iniţializate cu<br />

identificatorii de utilizator (user ID) respectiv de grup (group ID) efective ale procesului<br />

apelant.<br />

• Cei 9 biţi LSB ai msg_perm.mode sunt setaţi la valorile celor 9 biţi LSB ale lui msgflg.<br />

Semnificaţia valorii msgflg este:<br />

• dreptul de citire <strong>pentru</strong> utilizator,<br />

• dreptul de scriere <strong>pentru</strong> utilizator,<br />

• drepturile citire/scriere <strong>pentru</strong> grup,<br />

• drepturile citire/scriere pt. Toţi.<br />

(procesul creator decide cine şi cum are acces la noua coadă de mesaje)<br />

• msg_qnum, msg_lspid, msg_lrpid, msg_rtime, şi msg_stime sunt iniţializaţi cu 0<br />

• msg_ctime setat la valoarea curentă a timpului sistem.<br />

• msg_qbytes setat la o valoare maximă impusă de sistem.<br />

Se observă faptul că <strong>pentru</strong> cozile de mesaje se definesc drepturi de acces exact ca la lucrul cu<br />

fişiere. Acestea sunt specificate în variabila msgflg . Tot <strong>pentru</strong> aceasta sunt definite în <br />

următoarele constante:<br />

IPC_CREAT : cu aceasta se deschide o coadă corespunzătoare cheii date, iar dacă aceasta<br />

nu exista va fi creata. Dacă aceasta constantă lipseşte din apel, coada nu va fi creată şi va fi deschisă<br />

doar dacă există.<br />

IPC_EXCL: se foloseşte împreuna cu IPC_CREAT, coada va fi creată şi deschisă doar dacă<br />

nu există deja. Dacă ea există, se întoarce valoarea -1 de eroare (variabila globală errno va conţine<br />

EEXIST).<br />

În afara de aceste constante, se dau valori normale de drepturi corespunzătoare<br />

proprietarului, grupului şi celorlalţi. De exemplu, dacă msgflg=0660|IPC_CREAT, se deschide o<br />

coadă de mesaje sau se creează dacă nu există, proprietarul şi grupul său având drept de read şi<br />

write.<br />

Mesajele dintr-o coadă sunt menţinute în liste cu ajutorul unor structuri de tip msg definite după<br />

cum urmează :<br />

Tibor Asztalos<br />

85


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

struct msg {<br />

struct msg *msg_next; /*adresa următoarei structuri de tip msg din coadă<br />

*/<br />

long msg_type; /* tipul mesajului */<br />

char *msg_spot; /*adresa unui şir de caractere ce conţine mesajul<br />

*/<br />

short msg_ts; /*lungimea în caractere a mesajului */<br />

};<br />

Un proces poate depune un mesaj în coadă apelând primitiva msgsnd() (caz în care el transmite), şi<br />

poate citi un mesaj din coadă apelând msgrcv() (caz în care el este receptorul). Primitivele celor<br />

două funcţii sunt :<br />

Tibor Asztalos<br />

int msgsnd(msqid, msgp, msgsz, msgflg)<br />

int msqid; // identificatorul cozii de mesaje<br />

struct msgbuf *msgp; // adresă către o structură ce conţinemesajul<br />

int msgsz; // dimensiunea mesajului transmis<br />

int msgflg; // identificator al modului şi drepturilor de acces<br />

int msgrcv(msqid, msgp, msgsz, msgtyp, msgflg)<br />

int msqid;<br />

struct msgbuf *msgp; // adresa unei strucuri unde se va citi mesajul dorit<br />

int msgsz; // dimensiune alocată mesajului ce urmează a fi recepţionat<br />

long msgtyp; // tipul mesajului aşteptat<br />

int msgflg;<br />

Cele două funcţii operează cu structuri de tip msgbuf definite tot în fişierul antet msg.h, după cum<br />

urmează :<br />

struct msgbuf {<br />

long mtype; /* tipul mesajului */<br />

char mtext[]; /* textul mesajului */<br />

};<br />

Funcţia msgsnd va avea rezultatul, dacă se termină cu succes, introducerea în coada de mesaje<br />

identificată prin msgid a mesajului conţinut în buferul adresat de msgp (msgp->mtext) şi având tipul<br />

msgp->mtype. Deasemenea trebuie specificat dimensiunea mesajului transmis (folosit de sistem<br />

<strong>pentru</strong> a aloca o structură tip msg corespunzătoare acestui mesaj) şi msgflg - ce determină acţiunea<br />

sistemului dacă :<br />

• numărul octeţilor din coadă a ajuns la valoarea msg_qbytes, sau<br />

• numărul total de mesaje din toate cozile definite în sistem a atins o limită maximă<br />

(impusă de sistem).<br />

Aceste acţiuni sunt :<br />

- dacă (msgflg & IPCNOWAIT) este TRUE mesajul nu va fi trimis iar procesul apelant va<br />

returna imediat (transmisie ignorată).<br />

- dacă (msgflg & IPCNOWAIT) este FALSE, procesul apelant va fi suspendat până la :<br />

• condiţia din cauza căreia a fost suspendat procesul nu mai există, caz în care se transmite<br />

mesajul;<br />

• identificatorul msgid este revocat din sistem (un alt proces cu drepturi adecvate distruge<br />

86


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

coada de mesaje);<br />

• procesul apelant recepţionează un semnal. În acest caz mesajul nu se mai transmite iar<br />

procesul va acţiona conform modului în care a fost prevăzut ca el să răspundă la<br />

semnalul aşteptat.<br />

La un apel reuşit al directivei msgsnd se reactualizează următoarele câmpuri din structura de date<br />

asociata identificatorului msqid:<br />

• msg_qnum este incrementat cu 1,<br />

• msg_lspid setat la PID al procesului apelant,<br />

• msg_stime reactualizat.<br />

Dacă msgflg =0, apelul va aştepta până când în coada de mesaje va fi loc <strong>pentru</strong> mesajul transmis.<br />

Dacă se introduce constanta NOWAIT, apelul va returna imediat, iar dacă nu este loc <strong>pentru</strong> mesaje<br />

se va întoarce -1, iar în errno va fi depusă valoarea EAGAIN.<br />

Funcţia msgrcv este folosită <strong>pentru</strong> a recepţiona mesaje din coada de mesaje. Mesajul este citit în<br />

structura indicată de pointerul msgp. Dacă dimensiunea de recepţie este mai mică decât cea de<br />

transmisie, mesajul este trunchiat, fără avertizarea utilizatorului. Tipul mesajului care va fi selectat<br />

din coada de mesaje la recepţie este dat de parametrul msgtype, după cum urmează :<br />

• este 0 - se ia primul mesaj din coadă;<br />

• pozitiv nenul - se ia mesajul cu numărul respectiv;<br />

• negativ - se ia primul mesaj al cărui tip are valoarea mai mică decât valoarea absolută a<br />

celui specificat;<br />

Parametrul msgflag are semnificaţie similară ca la transmisie. Dacă msgflag=0 (modul în care se<br />

face apelul), apelul va aştepta până la sosirea unui mesaj de tipul solicitat. Dacă se introduce<br />

IPC_NOWAIT, apelul va întoarce valoarea de eroare -1, dacă nu există mesaj de tipul solicitat.<br />

Variabila globală errno va conţine valoarea EAGAIN. Dacă msgflag=MSG_NOERROR, un mesaj<br />

de lungime mai mare decât cel solicitat va fi trunchiat fără a solicita eroare.<br />

La un apel reuşit al directivei msgrcv se reactualizează următoarele câmpuri din structura de date<br />

asociata identificatorului msqid<br />

• msg_qnum este decrementat cu 1,<br />

• msg_lrpid setat la PID al procesului apelant,<br />

• msg_rtime reactualizat.<br />

Funcţia msgctl() permite un control al cozilor de mesaje din sistem. Sintaxa funcţiei este :<br />

int msgctl (msqid, cmd, buf)<br />

int msqid, cmd;<br />

struct msqid_ds *buf;<br />

msgctl() permite efectuarea unor operaţii de control asupra cozii identificat prin msgid , operaţii<br />

specificate prin parametrul cmd. Următoarele comenzi sunt valide :<br />

IPCSTAT - structura de date tip msgid_ds asociată cu coada este copiată în structura<br />

adresată de buf;<br />

IPCSET - setează valorile următoarelor câmpuri din structura de date asocită cu msgid<br />

87


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

la valorile câmpurilor corespunzătoare din structura adresată de buf :<br />

msg_perm.uid<br />

msg_perm.gid<br />

msg_perm.mode /*nuami cei 9 biţi LSB */<br />

msg_qbytes<br />

Acest lucru poate fi efectuat de către un proces care :<br />

• are identificatorul efectiv de utilizator egal cu cel al super-user-ului (root),<br />

• are PID egal cu msg_perm.cuid sau msg_perm.uid specificat în structura de date<br />

asociată cozii.<br />

Numai super-user-ul poate creşte valoarea lui msg_qbytes.<br />

IPCRMID - revocă identificatorul cozii de mesaj specificat de msgid, distruge coada şi<br />

structurile de date asociate.<br />

Acest lucru se permite doar proceselor care :<br />

- au identificatorul efectiv de utilizator egal cu cel al super-user-ului,<br />

- au PID egal cu msg_perm.cuid sau msg_perm.uid specificat în structura de date asociată<br />

cozii.<br />

Exemplu de program care creează două procese şi transferă date prin mesaje între ele şi apoi le<br />

distruge:<br />

# include <br />

# include <br />

# include <br />

# include <br />

# include <br />

# include <br />

# include <br />

# include <br />

# include <br />

void main()<br />

{<br />

pid_t pid;<br />

int msqid,i;<br />

struct msgbuf *msgp;<br />

char *str="Primul mesaj spre fiu!\n";<br />

msgp = (struct msgbuf *) malloc (sizeof (struct msgbuf));<br />

if ((msqid=msgget(IPC_PRIVATE,0x1FFF))==-1)<br />

{ perror("Nu pot crea coada de mesaje !\n");exit(1); }<br />

if ( (pid=fork())==-1 )<br />

{ perror("Nu pot crea procesul fiu !");exit(1); }<br />

if ( pid==0 )//fiu<br />

{<br />

int len;<br />

if ((len=msgrcv(msqid,msgp,30,0,0))==-1)<br />

{ perror("Eroare la receptie mesaj in fiu !\n");exit(1); }<br />

printf("Am primit [%d] chars: %s",len,msgp->mtext);<br />

}<br />

else //parinte<br />

{<br />

msgp->mtype=1;<br />

88


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

strcpy(msgp->mtext,str);<br />

if ( msgsnd(msqid,msgp,30,0)==-1 )<br />

{perror("Parintele nu poate scrie mesajul");exit(1);}<br />

wait(&i);<br />

struct msqid_ds *r;<br />

if (msgctl(msqid,IPC_RMID,r)==-1)<br />

{ perror("Nu pot elimina coada de mesaje !\n");exit(1);}<br />

}<br />

}<br />

Programul 11. Sursa Ex1_Msg.c<br />

Programul de mai jos este un exemplu de aplicaţie client-server. Programul client trimite<br />

serverului identificatorul său de proces şi apoi aşteaptă de la acesta un răspuns. Când soseşte<br />

răspunsul, clientul îl tipăreşte şi se opreşte. Serverul aşteaptă mesaje de la clienţi şi le răspunde,<br />

transmiţând propriul său identificator de proces. Serverul se opreşte la apariţia oricărui semnal.<br />

Tibor Asztalos<br />

/* Mesaje.h */<br />

#include <br />

#include <br />

#include <br />

#define CHEIE 0100 /* identificatorul cozii */<br />

struct mesaj {<br />

long mtip;<br />

char mtext[256];<br />

};<br />

/* Client */<br />

#include "mesaje.h"<br />

void main()<br />

{<br />

struct mesaj mes;<br />

int id_coada,id_proces,*pint;<br />

/* deschiderea cozii CHEIE */<br />

id_coada=msgget(CHEIE,0660);<br />

/* constructia mesajului */<br />

mes.mtip=1;<br />

pint=(int *)mes.mtext;<br />

*pint=id_proces;<br />

/* transmiterea mesajului fara tratarea erorilor */<br />

msgsnd(id_coada,&mes,256,id_proces,0);<br />

/* tiparirea mesajului primit */<br />

printf("\n Client: serverul este: %d\n",*(int *)mes.mtext);<br />

}<br />

/* Server */<br />

#include "mesaj.h"<br />

int id_coada;<br />

void main()<br />

{<br />

int i,*pint;<br />

extern cleanup();<br />

struct mesaj mes;<br />

/* <strong>pentru</strong> tratarea semnalelor */<br />

for(i=0; i


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

}<br />

printf("\n Server: mesajul este de la client: %d\n",*pint);<br />

/* construirea mesajului de raspuns */<br />

mes.mtip=*pint;<br />

*pint=getpid();<br />

/* transmiterea mesajului */<br />

msgsnd(id_coada,&mes,sizeof(int),0);<br />

}<br />

/* rutina de stergere a cozii la primirea unui mesaj */<br />

cleanup()<br />

{<br />

msgctl(id_coada,IPC_RMD,0);<br />

exit();<br />

}<br />

Programul 12. Sursele exemplu2 mesaje<br />

4.5. Modalitati IPC folosind “Semafoare”<br />

Semafoarele reprezintă a modalitate primitivă de sincronizare a proceselor care indirect<br />

furnizează o modalitate de IPC (Inter-Process Communication -comunicare între procese).<br />

Prin "resursă", termen abstract, se înţelege fie o entitate fizică (dispozitiv periferic) fie o<br />

construcţie logică (variabila de tip simplu sau structurat), care este indispensabilă unui proces<br />

<strong>pentru</strong> a executa. Resursa devine "critică" dacă este indispensabilă mai multor procese, şi acestea<br />

doresc acces la resursa în acelaşi timp.<br />

Prin "regiune critică" se înţelege o zonă de cod în care un proces manevrează o resursă<br />

critică. Prin "excludere mutuală" a două sau mai multe procese se înţelege aceea modalitate care<br />

duce la situaţia în care, la un moment dat, un SINGUR proces se află în regiunea critică<br />

corespunzătoare aceleiaşi resurse critice (vezi capitolul I). Se spune că execuţia procesului respectiv<br />

exclude execuţia celorlalte.<br />

Rolul principal al semafoarelor este de a realiza excluderea mutuală între procese. Pentru<br />

aceasta semafoarele se asociază resurselor sau grupurilor de resurse şi "filtrează" modul de acces la<br />

ele, aşa cum se va vedea în cele ce urmează.<br />

Sub SO Unix semaforul este o structură ce conţine:<br />

• un contor (un întreg),<br />

• o coadă de aşteptare în care sunt introduse procese în aşteptare.<br />

Contorul în funcţie de valoare are următoarele semnificaţii:<br />

- contor>0 semnifică numărul de resurse disponibile asociate semaforului respectiv;<br />

- contor=0 semnifică faptul ca nici o resursă nu mai este disponibilă;<br />

- contor


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

întreruptă şi procesul este introdus în coada de aşteptare a semaforului. Cu alte cuvinte procesul<br />

rămâne blocat până când resursa este disponibilă;<br />

• operaţia up(sem) (operaţia V) - este executată de un proces imediat după eliberarea unei resurse.<br />

Operaţia incrementează contorul. Dacă contorul era pozitiv înainte de incrementare atunci<br />

execuţia continuă ca şi cum nici n-ar fi existat up(). Dacă, însă, contorul era zero sau negativ<br />

înainte de incrementare atunci este relansat în execuţie primul proces din coada de aşteptare a<br />

semaforului, acesta primind acces la resursa proaspăt eliberată.<br />

Reamintim că operaţiile down() şi up() sunt atomice, adică execuţia lor nu poate fi întreruptă.<br />

Dacă programul este scris incorect atunci execuţia poate să ajungă în două stări care exprimă<br />

imposibilitatea de terminare a programului:<br />

Tibor Asztalos<br />

• deadlock - stare ce semnifică blocarea reciprocă a proceselor, întreg programul este<br />

blocat;<br />

• livelock - stare ce semnifică execuţia repetată a unor operaţii, programul rulează dar<br />

execuţia nu avansează către terminare.<br />

Discutăm conceptele de mai sus pe un caz concret. Considerăm o problemă clasică de<br />

concurenţă, producător-consumator. Problema se referă la interacţiunea a două procese. Un proces<br />

"producător" care produce o valoare şi o introduce într-un tablou, şi un proces "consumator" care<br />

preia din tablou o valoare şi o consumă/o foloseşte.<br />

Aşa cum reiese din enunţarea de mai sus, tabloul este o resursă critică. Dăm mai jos conţinutul<br />

programului în pseudocod:<br />

semaphore prod, cons,mutex;<br />

some_type tab[N];<br />

int i=0;<br />

prod.counter=N;<br />

cons.counter=0;<br />

mutex.counter=1;<br />

process producer()<br />

{ some_type item;<br />

}<br />

while (1)<br />

{ item=produce();<br />

down(mutex);<br />

down(prod);<br />

tab[i++]=item;<br />

up(cons);<br />

up(mutex);<br />

}<br />

process consumer ()<br />

{ some_type item;<br />

while (1)<br />

{ down(mutex);<br />

down(cons);<br />

item=tab[i--];<br />

up(prod);<br />

91


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

}<br />

Tibor Asztalos<br />

up(mutex);<br />

consume(item);<br />

Discuţie:<br />

1. Identificaţi tipurile de resurse <strong>pentru</strong> fiecare semafor în parte. Explicaţi de ce s-au iniţializat<br />

contoarele semafoarelor cu valorile de mai sus.<br />

2. Stabiliţi dacă programul se execută corect sau se ajunge la deadlock/livelock.<br />

3. Dacă se ajunge la deadlock, atunci găsiţi o soluţie corectă.<br />

Există o serie de primitive sistem care ne permit acest mod de IPC. Ele sunt apelurile de<br />

sistem: semget(), semctl() şi semop(). Ele operează cu o serie de structuri de date, definite în fişierul<br />

/usr/include/sys/sem.h. Acestea sunt structurile:<br />

struct semid_ds {<br />

struct ipc_perm sem_perm; /*structura drepturilor de acces la semafor<br />

*/<br />

struct sem *sem_base; /* adresa primului semafor din familie */<br />

ushort sem_nsems; /* numarul de semafor din cadrul familiei */<br />

time_t sem_otime; /* momentul ultimului semop asupra semafor<br />

*/<br />

time_t sem_ctime; /* momentul ultimei modificari a semaforului<br />

*/<br />

};<br />

Obs. O singură structură semid_ds se creează în sistem <strong>pentru</strong> fiecare familie de semafoare. Cele<br />

două structuri de date implicate sunt:<br />

struct ipc_perm {<br />

ushort cuid; /*user-id creator*/<br />

ushort cgid; /*group-id creator */<br />

ushort uid; /* user id */<br />

ushort gid; /* group id */<br />

ushort mode; /* permisiuni de acces */<br />

}<br />

struct sem {<br />

ushort semval; /* valoarea curenta semafor */<br />

short sempid; /* PID a ultimului proces */<br />

ushort semncnt; /* # evita semval > cval */<br />

ushort semzcnt; /* # evita semval = 0 */<br />

};<br />

Obs. O structură sem se creează în sistem <strong>pentru</strong> fiecare semafor în parte.<br />

Structura de date necesară apelării directivei semop():<br />

struct sembuf {<br />

short sem_num; /* identificator semafor*/<br />

short sem_op; /* tip operatie aplicat semaforului */<br />

short sem_flg; /* flag-uri ale operatiei */<br />

};<br />

Structura de date necesară apelării directivei semctl():<br />

92


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

union semun {<br />

int val; /* valoare <strong>pentru</strong> SETVAL */<br />

struct semid_ds *buf; /* buffer <strong>pentru</strong> IPC_STAT & IPC_SET */<br />

ushort *array; /* tablou <strong>pentru</strong> GETALL & SETALL */<br />

};<br />

Tibor Asztalos<br />

4.5.1. Apelul de sistem semget()<br />

Sintaxa de apel:<br />

int semget(key, nsems, semflg)<br />

key key;<br />

int nsems, semflg;<br />

semget() returnează identificatorul unei familii de semafoare conform valorii key (valori definite în<br />

fişierul antet ipc.h).<br />

Un identificator de semafor precum şi structurile semid_ds şi sem asociate sunt create <strong>pentru</strong> key<br />

dacă una dintre următoarele condiţii se verifică:<br />

• key este egal cu IPCPRIVATE,<br />

• key nu are legat de el un identificator al unei cozi, şi (semflg & IPCREAT) este TRUE<br />

(adevărat).<br />

În urma creerii acestei noi cozi structura semid_ds asociată acestei cozi se iniţializează după<br />

cum urmează :<br />

• sem_perm.cuid, sem_perm.uid, sem_perm.cgid, şi sem_perm.gid sunt egale cu user-ID<br />

efectiv şi respectiv group-ID efectiv, al procesului apelant,<br />

• Cei 9 biţi LSB ai sem_perm.mode sunt setaţi la valorile celor 9 biţi LSB ale lui semflg,<br />

• sem_nsems este iniţializat cu valoarea nsems,<br />

• sem_ctime setat la valoarea curentă a timpului sistem.<br />

Diferenţa în Unix constă în faptul că funcţia semget() întoarce mulţimi de semafoare şi nu<br />

semafoare individuale, şi că există două contoare diferite, semval numără resursele şi semncnt ce<br />

numără procesele din coadă.<br />

4.5.2. Apelul de sistem semctl()<br />

Ea are sintaxa de apel:<br />

int semctl(semid, semnum, cmd, arg)<br />

int semid, semnum, cmd;<br />

union semun {<br />

val;<br />

struct semids *buff;<br />

ushort *array;<br />

} arg;<br />

93


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Aceasta permite efectuarea unei serii de operaţii de control asupra familiei de semafoare identificate<br />

prin semid. Operaţia curentă este dată de argumentul cmd şi se aplică semaforului semnum din<br />

familie. Operaţiile permise sunt:<br />

GETVAL - returnează valoarea semaforului semnum din familia semid,<br />

SETVAL - stabileşte (poziţionează) valoarea semaforului semnum (semval) la valoarea<br />

arg.val.<br />

GETPID - returnează valoarea sempid.<br />

GETNCNT - returnează valoarea semncnt.<br />

GETZCNT - returnează valoarea semzcnt.<br />

GETALL - returnează valorile tuturor semafoarelor în tabloul arg.array.<br />

SETALL - stabileşte valorile tuturor semafoarelor la cele indicate de tabloul arg.array.<br />

IPCTAT - valoarea curentă semafor este citită în bufferul arg.buff.<br />

IPCET - setează (scrie) valoarea curentă conform conţinutului bufferului arg.buff.<br />

IPCRMID - elimină identificatorul de semafor din sistem.<br />

Astfel, deci, setarea valorii unui semafor se face cu directiva semctl() şi operaţia SETVAL,<br />

referindu-ne la semaforul semnum din mulţimea identificată de semid.<br />

Tibor Asztalos<br />

4.5.3. Apelul de sistem semop()<br />

Funcţia semop() realizează un set de operaţii atomice asupra unei mulţimi de semafoare. Ea are<br />

sintaxa:<br />

int semop(semid, sops, nsops)<br />

int semid;<br />

struct sembuf *sops;<br />

int nsops;<br />

Identificând un semafor individual din mulţime prin câmpul sem_num, al structurii sembuf, se<br />

execută asupra lui operaţia codificată în câmpul sem_op, astfel:<br />

• dacă sem_op > 0 atunci se doreşte întoarcerea de resurse şi valoarea lui sem_op se adună<br />

la valoarea semaforului corespunzător (semval).<br />

• dacă sem_op < 0 atunci se doreşte obţinerea de resurse, şi în funcţie de valorile lui<br />

sem_op, semval şi a flagului IPC_NOWAIT acţiunile efectuate sunt diferite.<br />

• dacă sem_op = 0 înseamnă că procesul doreşte să aştepte până când semval devine 0.<br />

Exemplu de program ce foloseşte semafoare <strong>pentru</strong> a implementa excluderea mutuală:<br />

Problemă: să se scrie un program care crează un pipe şi două procese Unix care accesează capătul<br />

de scriere al pipe-ului. Să se realizeze o excludere mutuală a celor două procese la capătul de<br />

scriere al pipeului. Fiecare proces realizează în mod repetat operaţiile următoare: citesc dintr-un<br />

fişier un număr de octeţi/caractere şi apoi îl scriu în pipe. Procesul care citeşte din pipe afişează<br />

ceea ce primeşte pe ecran.<br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

94


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

#include <br />

#include <br />

#define N 100<br />

#define N1 3<br />

int pp[2];<br />

int sid;<br />

char buf[N];<br />

void down(int s)<br />

{<br />

struct sembuf sops;<br />

int nsops = 1;<br />

sops.sem_num=s;<br />

sops.sem_flg = 0;<br />

sops.sem_op = -1;<br />

semop(s, &sops, 1);<br />

}<br />

void up(int s)<br />

{<br />

struct sembuf sops;<br />

int nsops = 1;<br />

sops.sem_num=s;<br />

sops.sem_flg = 0;<br />

sops.sem_op = +1;<br />

semop(s, &sops, 1);<br />

}<br />

void eroare(char *str)<br />

{<br />

printf("EROARE : %s",str);<br />

exit(1);<br />

}<br />

void fiu(int f)<br />

{<br />

char buff[N1];<br />

int n;<br />

close(pp[0]);<br />

while ( (n=read(f,buf,N1))>0)<br />

{<br />

down(sid);<br />

if (write (pp[1],buf,n) ==-1) eroare("scriere in pipe");<br />

up(sid);<br />

sleep(1);<br />

}<br />

close(pp[1]);<br />

}<br />

void main (void)<br />

{<br />

int f1,f2;<br />

int n;<br />

union semun arg;<br />

pipe(pp);<br />

if ( (f1=open("fis1.txt",O_RDONLY) )==-1) eroare ("deschidere fisier 1...");<br />

Tibor Asztalos<br />

95


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

if ( (f2=open("fis2.txt",O_RDONLY) )==-1) eroare ("deschidere fisier 2...");<br />

sid=semget(IPC_PRIVATE,1,0x1FFF);<br />

arg.val=1;<br />

semctl(sid,0,SETVAL,arg);<br />

if (fork()==0)<br />

fiu(f1);<br />

else<br />

if (fork()==0)<br />

fiu(f2);<br />

else<br />

{<br />

close(pp[1]);<br />

while ( (n=read(pp[0],buf,N-1))>0)<br />

{<br />

buf[n]='\0';<br />

printf(buf);<br />

}<br />

wait();<br />

wait();<br />

close(f1);<br />

close(f2);<br />

close(pp[0]);<br />

semctl(sid,0,IPC_RMID,arg);<br />

}<br />

}<br />

Programul 13. Sursa Ex_sem.c<br />

4.6. Comunicarea inter-procese prin memorie partajată (shared memory).<br />

Unul dintre principiile programării actuale este acela că “fiecare proces îşi accesează în<br />

mod exclusiv propriul spaţiu de memorie (internă)”. Uneori, în aplicaţiile care folosesc IPC, se<br />

impune un schimb rapid de informaţii. În acest scop, diverse sisteme de operare furnizează<br />

mecanisme de acces la un spaţiu comun de memorie internă. Aşa cum se ştie până acum, atunci<br />

când se creează două procese Unix (prin fork()) se realizează o copie a datelor. Deci cele două<br />

procese nu deţin acces la aceleaşi variabile, ele accesează copii ale acestor variabile şi nu pot avea<br />

nici un fel de control asupra copiilor care nu le aparţin. De asemenea, la fiecare dintre metodele de<br />

comunicaţie între procese prezentate până acum, informaţia era transferată între cele două procese<br />

implicate, adică era copiată în afara spaţiului de adresare al procesului sursă şi apoi recopiată în<br />

spaţiul de adresare al procesului destinaţie, prin intermediul unor apeluri sistem. Acest lucru<br />

necesita unele transformări asupra datelor transferate şi uneori, transferarea unor informaţii<br />

suplimentare.<br />

Atâta timp cât cele două procese comunică, ele trebuie să se găsească deja în memorie<br />

împreună cu datele lor. Dacă datele de transmis se află deja în memorie, cele două procese ar trebui<br />

să poată accesa amândouă zona de memorie în care se găsesc datele comune. Această memorie<br />

devine astfel memorie partajată (comună). Sistemul de operare Unix suportă acest concept oferind o<br />

serie de apeluri de sistem în acest sens. Din păcate memoria partajată nu este disponibilă în orice<br />

implementare de Unix System V <strong>pentru</strong> că numai anumite configuraţii hardware permit acest lucru.<br />

De fapt memoria comună este o altă modalitate indirectă de IPC deoarece procesele schimbă date<br />

între ele folosind valorile ale unor variabile comune stabilite în mod static la începutul executării<br />

programului.<br />

Folosirea memoriei partajate oferă o metodă elegantă şi rapidă de comunicaţie între cele<br />

două procese care accesează în comun o zonă de memorie internă. Această zonă poate fi folosită de<br />

oricare dintre procesele din sistem care îi cunosc cheia. Pentru fiecare proces care utilizează<br />

memoria partajată este necesar ca ea să se găsească în spaţiul său de adresare. Acest lucru asigură<br />

Tibor Asztalos<br />

96


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

de fapt eficienţa maximă, în continuare această zonă fiind accesată ca şi o variabilă obişnuită, fără<br />

folosirea apelurilor sistem. Folosirea unor zone de memorie în comun de către două procese impune<br />

desigur şi o serie de restricţii de acces şi necesităţi de sincronizare care se rezolvă de obicei cu<br />

ajutorul semafoarelor. Programarea defectuoasă a accesului la memoria partajată a mai multor<br />

procese poate duce uşor la blocaje (deadlocks).<br />

Din punctul de vedere al programatorului, principiul de lucru cu memoria partajată este<br />

foarte simplu: un segment de memorie partajată trebuie creat şi deschis, apoi ataşat în spaţiul de<br />

adresare al proceselor care au nevoie de el.<br />

Apoi, fiecare dintre procese poate scrie sau citi fără restricţii din zona respectivă. Când un<br />

proces nu mai are nevoie de memoria partajată, el poate detaşa segmentul. Dacă nici unul dintre<br />

procese nu mai are nevoie de el, segmentul de memorie partajată, poate fi eliminat complet. Pentru<br />

operaţiile de creare, deschidere, ataşare, detaşare, eliminare a segmentelor de memorie partajată<br />

există apeluri ale nucleului. Pentru accesarea datelor din segment nu exista apeluri, accesul<br />

făcându-se prin intermediul pointerilor.<br />

Pentru a adresa segmentele de date procesoarele moderne folosesc un registru de<br />

segmentare. Daca este disponibil un astfel de registru, implementarea memoriei partajate este foarte<br />

uşoară (acest lucru este rezolvat de cei care implementează UNIX-ul şi nu de către cei care folosesc<br />

memoria partajată în aplicaţii!). Accesul ulterior la acest segment se face aproape la fel de repede ca<br />

la orice variabila locala a procesului, de aceea memoria partajată este de departe cea mai rapidă<br />

metodă de comunicaţie între procese comparativ cu metodele prezentate până acum.<br />

Desigur, hardware-ul poate să impună şi limitări. De exemplu numărul de segmente de<br />

memorie partajată care pot fi folosite deodată de către un proces este dependent de arhitectura<br />

sistemului. Dacă se doreşte ca aplicaţiile să rămână portabile, nu trebuie folosit decât un singur<br />

element de memorie partajată. La fel hardware-ul determină şi mărimea maximă a unui segment. De<br />

obicei un minim de 64K este disponibil.<br />

Apelurile de sistem ce permit declararea, controlul şi eliminarea din sistem a unor zone de memorie<br />

partajată sunt: shmget(), shmctl(), shmat(), shmdt().<br />

Apelul de sistem shmget() este folosit <strong>pentru</strong> a obţine de la sistem o zonă de memorie comună de<br />

mărime specificată de utilizator.<br />

Apelul de sistem shmctl() are rol de control asupra segmentelor de memorie comună. Un anumit<br />

proces îşi asociază la spaţiul său de adrese zona de memorie comună creată de sistem folosind<br />

apelul shmat(). De obicei ultimele două argumente ale acestui apel sunt zero.<br />

Ultimul apel, shmdt(), este folosit <strong>pentru</strong> dezasocierea segmentului de memorie comună de proces.<br />

Este necesar ca înainte să fi fost utilizat apelul shmat().<br />

Segmentul de memorie comună rămâne activ în sistem până când nu este distrus explicit folosind<br />

shmctl() şi flagul corespunzător.<br />

Structura de date shmid_ds<br />

Este structura de date prin intermediul căreia sistemul gestionează obiectul "segment de memoria<br />

comună". Ea este declarată în fişierul antet shm.h , şi are definiţia:<br />

struct shmid_ds {<br />

struct ipc_perm shm_perm; /* flaguri de permisiune */<br />

int shm_segsz; /* dimensiune segment (bytes) */<br />

time_t shm_atime; /* momentul ultimei asocieri la<br />

memorie */<br />

time_t shm_dtime; /* momentul ultimei dezasocieri de la<br />

Tibor Asztalos<br />

97


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

memoria comună */<br />

time_t shm_ctime; /* momentul ultimei schimbari */<br />

unsigned short shm_cpid; /* PID al procesului creator */<br />

unsigned short shm_lpid; /* PID al ultimului proces care a<br />

lucrat cu memoria comună*/<br />

short shm_nattch; /* numarul de asocieri curente */<br />

/* variabile private */<br />

unsigned short shm_npages; /* dimensiune segment (în pagini) */<br />

unsigned long *shm_pages; /* arie de ptrs către frames -> SHMMAX<br />

*/<br />

struct vm_area_struct *attaches; /* descriptori de asocieri */<br />

};<br />

Constante definite (tot în acest fişier):<br />

sunt fie corespunzătoare unor limitări impuse de sistem:<br />

- SHMMIN, dimensiunea minimă a unui segment de memorie comună;<br />

- SHMMAX, dimensiunea maximă a unui segment;<br />

- SHMMNI, numărul maxim de segmente partajate;<br />

- SHMLBA, divizor comun al tuturor adreselor început de segment;<br />

fie argumente sau comenzi specifice legate de gestiunea unui segment:<br />

- SHM_RDONLY, permite accesarea doar în mod read-only a segmentului;<br />

- SHM_RND, flag de permisiune a "rotunjirii" adresei segmentului;<br />

- SHM_LOCK, flag de invalidare acces la segment;<br />

- SHM_UNLOCK, flag de deblocare acces la segment;<br />

Tibor Asztalos<br />

4.6.1. Apelul de sistem shmget()<br />

int shmget(key_t key, int size, int shmflag)<br />

Pentru crearea şi deschiderea unui segment de memorie partajată de mărime specificată există<br />

apelul de sistem shmget(). Se apeleaza in felul urmator:<br />

#include <br />

#include <br />

#include <br />

key_t cheie;<br />

int marime, permisii, shmid;<br />

...<br />

shmid=shmget(cheie, marime, permisii);<br />

...<br />

La fel ca şi la mesaje, cel mai important parametru este primul, cheie, cel care dă cheia de<br />

recunoaştere globală a segmentului. Orice alt proces care vrea să folosească segmentul de memorie<br />

partajată, creat cu shmget(), trebuie să cunoască această cheie. Cheia este, ca şi mai înainte, de tipul<br />

key_t, dependent de implementare, dar de obicei un long. Al doilea parametru, mărime, este cel care<br />

specifică dimensiunea în octeţi a segmentului de memorie partajată care se doreşte a fi creat.<br />

Dimensiunea segment trebuie să fie în concordanţă cu limitele impuse de sistem precum şi, în cazul<br />

în care cheie corespunde unui segment existent, cu dimensiunea acestui segment. Ultimul<br />

parametru, permisii, specifică drepturile de acces la segment. Apelul returnează în variabila shmid<br />

un identificator al segmentului nou creat, care este local procesului. Dacă apelul eşuează în crearea<br />

segmentului de memorie partajată, este întoarsa valoarea -1, iar eroarea va fi documentată în<br />

variabila globală errno. Valorile de eroare din errno sunt dependente de implementare. Trebuie<br />

consultat manualele on-line <strong>pentru</strong> funcţia shmget(S).<br />

98


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Dacă mărimea specificată la apel <strong>pentru</strong> segment este prea mare sau prea mică, errno va fi<br />

setat pe valoarea EINVAL. Ce înseamnă prea mic sau prea mare <strong>pentru</strong> implementarea dată, trebuie<br />

de asemenea aflat.<br />

Pentru specificarea permisiilor, se pot folosi valori asemănătoare cu cele <strong>pentru</strong> cozi de<br />

mesaje. Trebuie deci specificate drepturile de acces la segment şi câţiva comutatori de creare. De<br />

exemplu poate primi valoarea 066 | IPC_CREAT | IPC_EXCL, deci drepturi de citire/scriere doar<br />

<strong>pentru</strong> proprietar, segmentul de memorie partajată va fi deschis numai dacă nu există deja, caz în<br />

care va fi mai întâi creat. Dacă segmentul există deja se va întoarce valoarea de eroare -1.<br />

Prin acest apel, a fost creat în memorie segmentul dorit. Accesul la el se va face în<br />

continuare prin intermediul identificatorului său, shmid. Acesta este local procesului curent, deci nu<br />

va avea aceeaşi valoare şi <strong>pentru</strong> alte procese care deschid aceeaşi zonă de memorie partajată. La<br />

terminarea acestui apel segmentul nu este înca gata de a fi folosit <strong>pentru</strong> procesul curent. Aceasta<br />

din cauza că segmentul nu se găseşte încă în spaţiul de adresare al procesului. Pentru a ataşa<br />

segmentul de memorie la proces există apelul shmat() (attach shared memory).<br />

Tibor Asztalos<br />

4.6.2. Apelul de sistem shmat()<br />

Un proces ce doreşte utilizarea unui segment de memorie comună va trebui să asocieze cu<br />

aceasta o adresă virtuală din spaţiul său de adresare. Accesarea ulterioară a zonei respective de<br />

memorie comună se va face prin intermediul acestei adrese într-o manieră similară accesării unei<br />

zone de memorie alocată dinamic, prin apelul funcţiei malloc(). Un anumit proces îşi asociază la<br />

spaţiul său de adrese zona de memorie comuna creata de sistem folosind apelul shmat(). Sintaxa ei<br />

este:<br />

char * shmat(int shmid, char * adr, int shmflag)<br />

- realizează conectarea unui segment de memorie identificat în sistem prin shmid, de spaţiul de<br />

adresare a procesului. Returnează adresa la care se face asocierea propriu-zisă. Această adresă nu<br />

trebuie:<br />

- să intre în conflict cu adresele deja utilizate;<br />

- să împiedice extinderea zonei de memorie de date şi cea de stivă;<br />

- să contrazică forma adreselor deduse din dimensiunea unei pagini de memorie (un<br />

segment, de orice dimensiune, trebuie să ocupe un număr multiplu întreg de pagini de memorie).<br />

De obicei ultimele două argumente ale acestui apel sunt zero. Valoarea adr=NULL, este echivalent<br />

cu lăsarea în seama sistemului a alegerii unei adrese. O valoare adr nenulă, transmisă ca şi<br />

argument va însemna:<br />

- asocierea la adresa adr-adr%SHMLBA, dacă flagul SHM_RND este poziţionat , sau<br />

- asocierea la adresa indicată sau cea a paginii următoare, în cazul contrar.<br />

Prima variantă impune ca segmentul să fie ataşat la o adresă care să fie început de pagină de<br />

memorie. Dacă se impune o adresă de ataşare oarecare, aceasta va fi rotunjita la următorul început<br />

de pagină. Daca SHM_RND nu este poziţionat, va fi întors următorul început de pagină disponibil.<br />

Alt comutator disponibil este SHM_RDONLY, care specifică faptul că segmentul trebuie protejat la<br />

scriere. Din păcate numai anumite configuraţii hardware pot asigura o astfel de protecţie. Dacă<br />

parametrul shmflag are valoarea 0, atunci accesul este şi în citire şi în scriere iar adresa nu va fi<br />

rotunjită în nici un fel.<br />

Din acest moment accesul la segmentul de memorie este posibil prin această adresă virtuală.<br />

Se poate scrie şi citi din memorie variabile întregi prin instrucţiuni de genul:<br />

int i, *pint;<br />

99


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

pint=(int *)adresa;<br />

*pint++=i;<br />

...<br />

i=*pint;<br />

...<br />

sau, se poate adresa segmentul caracter cu caracter în felul următor:<br />

int i;<br />

for(i=0;adresa[i];i++)<br />

...;<br />

Tibor Asztalos<br />

4.6.3. Apelul de sistem shmdt()<br />

Apelul shmdt() este folosit <strong>pentru</strong> dezasocierea (detaşarea) segmentului de memorie comună<br />

de proces, şi este necesar ca înainte să fi fost utilizat apelul shmat().<br />

Acesta se apelează astfel:<br />

...<br />

retur=shmdt(adr);<br />

...<br />

După acest apel nu se mai poate adresa în nici un fel segmentul de memorie partajată, decât<br />

după o nouă ataşare. În clipa în care toate procesele care aveau nevoie de segmentul de memorie<br />

partajată s-au detaşat de la acesta, segmentul poate fi distrus complet printr-un apel shmctl().<br />

Apelarea se face astfel:<br />

...<br />

retur=shmctl(shmid, IPC_RMID, 0);<br />

...<br />

Trebuie acordată foarte mare atenţie la distrugerea unui segment de memorie partajată.<br />

Dacă mai există procese care au ataşat acel segment pot apare erori foarte grave, care să ducă chiar<br />

la căderea sistemului !<br />

Un exemplu de program simplu cu memorie comună:<br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#define ARRAY_SIZE 4000<br />

#define MALLOC_SIZE 100000<br />

#define SHM_SIZE 100000<br />

#define SHM_MODE 0x1fff /* se poate scrie si citi in memoria comuna */<br />

char array[ARRAY_SIZE]; /* date neinitializate - bss */<br />

void main (void) {<br />

int shmid;<br />

char *ptr, *shmptr;<br />

printf("array[] de la %xh la %xh \n",&array[0],&array[ARRAY_SIZE]);<br />

printf("Stiva in jurul adresei %xh\n",&shmid);<br />

if ((ptr=(char *)malloc(MALLOC_SIZE))==NULL)<br />

{ perror(); exit(1); }<br />

100


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

printf("Memorie alocata de la %xh la %xh\n",ptr,ptr+MALLOC_SIZE);<br />

if ((shmid=shmget(IPC_PRIVATE,SHM_SIZE,SHM_MODE))


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

cleanup()<br />

{<br />

shmctl(shmid, IPC_RMID, 0);<br />

exit();<br />

}<br />

/* citeste.c */<br />

#include "shmen.h"<br />

int shmid;<br />

main()<br />

{<br />

int i,*pint=0;<br />

char *adresa;<br />

}<br />

}<br />

Tibor Asztalos<br />

/*deschiderea zonei de memorie partajata*/<br />

shmid=shmget(SHMKEY, 2048, 0660);<br />

for(i=0;i < 20;i++) {<br />

/*legarea la spatiul de adresare*/<br />

adresa=shmat(shmid, 0, 0);<br />

while(*pint==0);<br />

/*nu citeste pina nu este gata driverul*/<br />

/*citirea mesajului*/<br />

printf("%s\n",adresa);<br />

/*dezlegarea din spatiul de adresare*/<br />

shmdt(adresa);<br />

sleep(1);<br />

Programul 15. Sursele Exemplu2 shm<br />

Exerciţii<br />

1. Să se scrie un program ce conţine două procese conectate prin pipe, unul dintre procese deschide<br />

un fişier, îl transmite prin pipe celui de-al doilea care îl afişează pe ecran. La terminarea transferului<br />

cele două procese se termină.<br />

2. Să se scrie un program sub sistemul de operare Unix care creează trei procese. Cele trei procese<br />

se conectează prin pipe două câte două într-un lanţ de procese. Unul dintre procese deschide un<br />

fişier text şi trimite conţinutul lui celui de-al doilea, acesta filtrează datele sosite prin pipe, trimiţând<br />

câtre al treilea numai caracterele litere mici. Al treilea proces afişează la consolă ceea ce primeşte<br />

prin pipe.<br />

3. Să se realizeze acelaşi lucru şi într-o rutină shell folosind comanda cat, şi codul celui de al doilea<br />

proces, descris mai sus, ca program separat.<br />

4. Să se realizeze următoarea conexiune între procese folosind pipe-uri.<br />

proc1 ---- proc2 --- proc3 unde --- sau ___ înseamnă conexiune prin pipe.<br />

|_______proc4<br />

Procesul proc1 deschide un fisier text şi îl trimite prin pipe lui proc2. Acesta din urmă, împarte<br />

102


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

datele în două fluxuri câtre cele două procese, pe o cale sunt trimise literele mici, iar pe alta literele<br />

mari din textul<br />

primit. Procesele proc3 şi proc4 afişează la consola datele primite din pipe.<br />

5. Să se construiască un program ce conţine mai multe procese conectate prin pipe după următorul<br />

scenariu:<br />

- un proces citeşte date dintr-un fişier sau de la consolă şi le trimite către un proces, pe care să-l<br />

denumim procesul central.<br />

- procesul central la primirea datelor, buclează pe trei descriptori de pipe scriind aceleaşi date la<br />

toţi descriptorii.<br />

- la fiecare din aceşti trei descriptori este conectat un proces.<br />

- procesele de la celălalt capăt al descriptorilor aşteaptă datele şi le scriu într-un fişier, al cărui<br />

nume se obţine concatenând numărul procesului cu şirul "file".<br />

6. Să se scrie un program care creează trei procese, astfel:<br />

- primul proces (părinte) citeşte dintr-un fişier cu numele date.txt un şir de caractere, până la<br />

sfârşitul fişierului şi le trimite printr-un pipe primului fiu;<br />

- primul fiu primeşte caracterele de la părinte şi selectează din ele literele mici, pe care le<br />

trimite printr-un pipe câtre cel de-al doilea fiu;<br />

- al doilea fiu creează un fişier numit statistici.txt, în care va memora, pe câte o linie, fiecare<br />

literă distinctă întâlnită şi numărul de apariţii a acesteia în fluxul de date primit. În final, va trimite<br />

către părinte, printr-un pipe suplimentar, numărul de litere distincte întâlnite. Părintele afişează pe<br />

ecran rezultatul primit de la al doilea fiu.<br />

7. Să se realizeze un program de tip producători-consumatori astfel:<br />

- programul primeşte ca parametri în linia de comandă două numere: numărul de procese<br />

producător şi numărul de procese consumator;<br />

- resursa comună tuturor proceselor este un pipe creat de procesul părinte;<br />

- părintele este la rândul lui producător;<br />

În general, producătorii scriu în pipe un număr oarecare de caractere, iar consumatorii citesc din<br />

pipe, caracter cu caracter, cât timp acest lucru este posibil.<br />

Producătorii vor avea următoarea formă:<br />

a) Părintele produce un număr de caractere '*';<br />

b) Ceilalţi producători sunt procese independente (existente pe disc ca programe de sine stătătoare)<br />

care generează la ieşirea standard un număr oarecare de caractere '#'. Pentru realizarea scopului<br />

propus, ieşirea standard a acestor procese va fi conectata la capătul de scriere al pipe-ului.<br />

c) Cel puţin unul dintre producători este comanda 'ls'.<br />

Consumatorii citesc caracterele din pipe şi işi afişează identificatorul de proces urmat de caracterul<br />

citit (la un moment dat).<br />

În sistem mai există un pipe prin care consumatorii vor raporta în final rezultatele câtre părintele<br />

tuturor, scriind în el, cu ajutorul funcţiei fprintf(), linii de forma:<br />

număr_proces : număr_octeţi<br />

unde număr_octeţi este numărul total de octeţi citiţi de procesul respectiv din pipe. Părintele va<br />

prelua aceste informaţii şi le va afişa pe ecran.<br />

8. Realizaţi un program care creează două procese şi transfera date prin mesaje între ele şi apoi le<br />

distruge.<br />

Tibor Asztalos<br />

103


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

9. Să se rezolve problema cu enunţul de mai jos folosind oricare din modalităţile de IPC cunoscute:<br />

cozi de mesaje, memorie comună sau semafoare (problema denumită Fum de Ţigarete).<br />

Se consideră un sistem care constă din 3 procese smoker (fumător) şi un proces agent. Fiecare<br />

smoker îşi pregăteşte o ţigaretă şi o fumează. Pentru a face o ţigaretă sunt necesare trei ingrediente:<br />

tutun, hârtie, şi chibrite. Unul din procese are numai hârtie, celălalt are numai tutun şi al treilea<br />

numai chibrite. Procesul agent are o rezervă infinită din toate cele trei ingrediente şi pune câte două<br />

din ingrediente pe o masă. Procesul smoker care are celălalt ingredient poate atunci să-si<br />

confecţioneze şi să fumeze o ţigaretă, semnalând agentului când a terminat, atunci agentul pune alte<br />

două ingrediente pe masă şi ciclul se repetă.<br />

10. Să se rezolve problema cu enunţul de mai jos folosind oricare din modalităţile de IPC<br />

cunoscute: cozi de mesaje, memorie comună sau semafoare (problema Cititori-Scriitori).<br />

Se consideră existenţa unei baze de date (simulată) şi două grupuri de procese, un grup de procese<br />

cititori care doresc numai să consulte baza de date, şi un grup de procese scriitori care doresc să<br />

modifice baza de date. Să se realizeze un program care să sincronizeze accesul proceselor la baza<br />

de date astfel încât să se asigure consistenţa acesteia. În acest scop trebuie sa fie verificate, în orice<br />

moment, următoarele reguli:<br />

- un singur tip de proces (cititor sau scriitor) poate accesa baza de date la un moment dat;<br />

- excluderea mutuală a accesului <strong>pentru</strong> procesele de tip cititor se realizează la nivel de grup de<br />

procese;<br />

- excluderea mutuală a accesului <strong>pentru</strong> procesele de tip scriitor se realizează la nivel de proces.<br />

11. Să se rezolve problema cu enunţul de mai jos folosind oricare din modalităţile de IPC<br />

cunoscute: cozi de mesaje, memorie comună sau semafoare (problema Barbierul Somnoros).<br />

Se consideră o frizerie deservită de un bărbier. Acesta dispune de cinci scaune în holul de aşteptare.<br />

Bărbierul nu poate deservi decât un client la un moment dat, iar în criza de clienţi bărbierul aţipeşte.<br />

Un client în funcţie de situaţia existentă în frizerie în momentul sosirii poate acţiona într-unul din<br />

modurile următoare:<br />

- dacă bărbierul doarme, îl trezeşte şi cere să fie bărbierit;<br />

- dacă bărbierul este ocupat, dar există scaune libere în holul de aşteptare ocupă un scaun aşteptând<br />

să-i vină rândul;<br />

- dacă holul de aşteptare este plin atunci clientul pleacă (nemulţumit!).<br />

Să se scrie un program care sincronizează procesele aşa cum se cere mai sus, şi care afişează o<br />

statistică a clienţilor deserviţi şi a celor nemulţumiţi.<br />

12. Să se rezolve problema cu enunţul de mai jos folosind oricare din modalităţile de IPC<br />

cunoscute: cozi de mesaje, memorie comună sau semafoare (problema Cina Filozofilor).<br />

Se consideră cinci filozofi care stau la o masă rotundă fiecare având în faţă o farfurie cu mâncare.<br />

Fiecare farfurie este încadrată de două furculiţe, care sunt comune cu filozofii vecini. Deci există<br />

cinci farfurii şi cinci furculiţe. Cei cinci filozofi execută trei operaţii, în mod repetat:<br />

- gândesc, şi atunci nu au nevoie de furculiţe;<br />

- mănâncă, şi au nevoie de două furculiţe;<br />

- flămânzesc, adică doresc să mănânce dar nu au furculiţele necesare.<br />

Să se scrie un program <strong>pentru</strong> sincronizarea proceselor astfel încât să nu existe posibilitatea ca un<br />

filozof să moară de inaniţie.<br />

13. Se imaginează următorul scenariu al problemei producător-consumator. Există trei procese,<br />

Tibor Asztalos<br />

104


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

producătorul, consumatorul şi un proces arbitru care deţine tabloul în care sunt păstrate elementele<br />

produse înainte de a fi consumate. Procesele producător şi consumator interacţionează cu arbitrul<br />

prin transfer de mesaje (cozile sunt vehiculul prin care elementele produse sunt trimise la arbitru, şi<br />

respectiv, elementele ce urmează a fi consumate sunt primite de la arbitru). Arbitrul funcţionează<br />

după următorul scenariu, executând operaţiile în ordinea de mai jos:<br />

a) atâta timp cât există elemente trimise de producător le pune în tablou;<br />

b) preia un număr dat de elemente pe care le trimite la consumator;<br />

c) reia punctul a) (dacă tabloul se goleşte rămâne în aşteptare în punctul a) La execuţia unui număr<br />

dat de introduceri/extrageri arbitrul afişează la consola conţinutul tabloului. Sincronizarea<br />

proceselor producător şi consumator are loc la nivelul proceselor producător/consumator şi se face<br />

cu semafoare.<br />

i) care sunt resursele corespunzătoare proceselor consumator/producător ?<br />

ii) oferă scenariul şanse egale ambelor procese în accesul la tablou? Argumentaţi răspunsul.<br />

iii) dacă răspunsul la punctul ii) este NU, atunci imaginaţi un scenariu ce oferă şanse egale<br />

<strong>pentru</strong> procesele producător/consumator.<br />

iv) introduceţi apeluri de sistem sleep(), <strong>pentru</strong> a încetini execuţia fie a producătorului fie a<br />

consumatorului.<br />

14. Rezolvaţi problema consumator-producător introducând tabloul într-un segment de memorie<br />

comună.<br />

Tibor Asztalos<br />

105


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Capitolul 5. Programarea aplicaţiilor de comunicare în reţelele - TCP/IP<br />

Evoluţia sistemelor de operare a fost puternic influenţată de avansul tehnologic în domeniul<br />

<strong>comunicaţii</strong>lor. La ora actuală cu legarea la reţeaua mondială INTERNET este un lucru firesc.<br />

Impactul social pe care îl are interconectarea la nivel global este un amănunt deloc de neglijat. Nici<br />

o aplicaţie modernă nu îşi poate permite să ignore potenţialul tehnic pe care îl oferă INTERNET-ul.<br />

Comunicarea, interconectarea, distribuirea sunt cuvinte de ordine în dezvoltarea de aplicaţii la ora<br />

actuală.<br />

5.1. Internetul<br />

Oamenii au înţeles că existenţa unei reţele mondiale ar înlesni foarte mult atât comunicarea,<br />

cât şi schimbul de informaţii între ei. Astfel, de-a lungul timpului, mai multe reţele au fost<br />

interconectate, <strong>pentru</strong> a face posibil comunicarea între oricare N calculatoare în mod simultan, în<br />

acest mod luând naştere Internet-ul. Internet-ul se defineşte aşadar ca o largă colecţie de reţele sau<br />

ca o reţea de reţele. Interconectarea dintre diferitele reţele (LAN-uri, MAN-uri, WAN-uri) ce<br />

compun Internet-ul sunt realizate, de obicei, prin legături punct la punct mai rapide (nu este<br />

obligatoriu, dar trebuie să existe anumite puncte cheie în reţea, de mare putere în transmitere a<br />

datelor, <strong>pentru</strong> a nu încetini accesul pe distanţe mari). Aceste legături rapide pot fi prin satelit,<br />

circuite de comunicaţie digitale dedicate (ex.: T1, E1, T3, E3, acces ISDN, B-ISDN), etc.<br />

Internet-ul este deci o reţea eterogenă ce face posibilă conectarea calculatoarelor indiferent<br />

de sistemul de operare şi arhitectura hardware.<br />

Toate calculatoarele dintr-o reţea trebuie să comunice între ele pe baza unui set de reguli<br />

fixe, denumit protocol (aşa cum doi oameni trebuie să vorbească şi să înţeleagă amândoi aceeaşi<br />

limbă <strong>pentru</strong> a putea comunica). Pornind de la sensul recunoscut în societatea umană, protocoalele<br />

reprezintă reguli formale de comportare. În cazul calculatoarelor protocoalele reprezintă reguli ce<br />

definesc cum anume trebuie să lucreze aplicaţiile <strong>pentru</strong> a se face înţelese între ele. De-a lungul<br />

timpului s-a dezvoltat o foarte variată serie de protocoale. O dată cu interconectarea a două sau mai<br />

multe reţele, o nouă problemă a apărut, şi anume "înţelegerea" celor două reţele între ele, adică<br />

folosirea aceluiaşi protocol sau folosirea unui aşa-zis program translator, capabil să facă posibilă<br />

transformarea informaţiei dintr-un mod de organizare în altul. În timp s-a constatat mult mai<br />

eficientă prima metodă şi de aici a apărut nevoia standardizării protocolului folosit pe plan mondial.<br />

Comunicarea între două calculatoare se face prin ierarhii de protocoale pe mai multe nivele.<br />

Conform standardelor ISO (International Standards Organization), orice protocol de comunicaţie<br />

dintre sisteme eterogene interconectabile (denumite şi sisteme OSI - Open System Interconnection)<br />

ar trebui (în cazul ideal) să îndeplinească funcţii de comunicare ce pot fi grupate în şapte nivele.<br />

Această structură defineşte modelul arhitectural de referinţă RM-OSI, şi presupune nivelele:<br />

Tibor Asztalos<br />

• Nivelul Fizic (Physical Layer) - fire, conectori şi semnale electrice;<br />

• Nivelul Legătura de Date (Data Link Layer) - împachetarea datelor <strong>pentru</strong> transmiterea<br />

acestora;<br />

• Nivelul Reţea (Network Layer) - conectări între două sisteme separate;<br />

• Nivelul Transport (Transport Layer) - <strong>pentru</strong> transmiterea datelor de la sursă la destinaţie;<br />

106


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

• Nivelul Sesiune (Session Layer) - stabileşte şi termină o sesiune de <strong>comunicaţii</strong>;<br />

• Nivelul Prezentare (Presentation Layer) - converteşte formatul datelor;<br />

• Nivelul Aplicaţie (Application Layer) - asigură mesajele şi comunicarea între programe<br />

(între aplicaţii).<br />

Acest model nu a fost niciodată implementat, dar ideea nivelelor de abstractizare <strong>pentru</strong><br />

protocoale rămâne valabilă.<br />

În momentul în care s-a definit modelul OSI a fost devansat de un alt model de referinţă,<br />

mult mai simplu, definit pe 4 niveluri: TCP/IP simbolizând Transmission Control Protocol/Internet<br />

Protocol. Există câteva deosebiri esenţiale între TCP/IP şi modelul OSI. OSI are şapte straturi.<br />

TCP/IP a lăsat nedefinite cele două straturi inferioare, spre a asigura o mai mare elasticitate, şi a<br />

definit numai trei straturi superioare, nu cinci, ca OSI. Evident, mai sunt şi alte diferenţe, dar asta e<br />

cea mai vizibilă, ce apare în arhitectura sistemului. Motivul: întâiul protocol care a fost creat şi<br />

testat pe scară largă era TCP/IP şi abia apoi a apărut încercarea de standardizare (eşuată) de către<br />

ISO, care a produs modelul OSI.<br />

Avantajul acestui model era simplitatea sa, şi faptul că la momentul apariţiei standardului<br />

ISO OSI era deja funcţional pe reţeaua ARPANET.<br />

Pe baza protocolului IP s-au construit mai multe protocoale: de exemplu TCP este folosit<br />

<strong>pentru</strong> comunicaţia prin flux sigur de date (Transport Control Protocol), UDP este folosit <strong>pentru</strong><br />

transmisia (nesigură) de datagrame (User Datagram Protocol), RDP <strong>pentru</strong> transmisia sigură de<br />

datagrame (Reliable Datagram Protocol), ICMP (Internet Control Message Protocol), IGMP<br />

(Internet Group Management Protocol) etc. Pe sistemele de tip UNIX o listă a protocoalelor<br />

cunoscute (nu neapărat suportate!) se găseşte în mod tradiţional în fişierul /etc/protocols. Lista<br />

oficială se găseşte în documentul RFC 1700.<br />

Modelul de referinţă TCP/IP se aplică tuturor acestor protocoale, el defineşte un model ierarhizare,<br />

lăsând libertatea de decizie asupra protocoalelor de transport. Comunicarea în INTERNET se<br />

bazează exclusiv pe protocolul IP.<br />

5.1.1. Protocoalele de <strong>comunicaţii</strong> Internet: familia TCP/IP<br />

Pentru ca ansamblul calculatoarelor conectate într-o reţea să poată comunica, este necesar<br />

un set întreg de protocoale de comunicaţie care să asigure transferul integral şi corect al datelor de<br />

la un calculator sursă la un calculator destinaţie. Familia protocoalelor TCP/IP reprezintă un set de<br />

astfel de protocoale, organizate pe o structură ierarhică.<br />

TCP şi IP sunt numai două dintre protocoalele familiei TCP/IP, care are peste 100 de astfel<br />

de protocoale. Dar sunt cele considerate principale. Din acest punct de vedere cel mai corect e să-i<br />

spunem "familia sau setul de protocoale Internet". Fiecare dintre aceste protocoale (ale familiei<br />

TCP/IP) asigură transferul datelor prin reţea într-un format diferit şi cu opţiuni diferite (cum este, de<br />

exemplu, verificarea erorilor). În funcţie de necesităţile aplicaţiei, putem utiliza unul sau altul din<br />

protocoalele suitei TCP/IP <strong>pentru</strong> transmiterea informaţiilor pe Internet.<br />

TCP/IP este o mulţime de protocoale ierarhizată. Unele protocoale ale familiei oferă servicii<br />

"low-level" aplicaţiilor ce rulează pe calculator. Dintre acestea fac parte IP, TCP, UDP (User<br />

Datagram Protocol) etc… Altele sunt protocoale destinate unui anumit scop, de exemplu:<br />

- transfer de fişiere între calculatoare (FTP – File Transfer Protocol);<br />

- transmiterea şi recepţionarea mesajelor de poştă electronică (SMTP – Simple Mail<br />

Transfer Protocol);<br />

- obţinerea unor informaţii despre alte calculatoare etc.<br />

Funcţiile celor două cele mai importante protocoale din această ierarhie sunt:<br />

- TCP (Transmission Control Protocol) este protocolul conform căruia un mesaj este fragmentat în<br />

107


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

mai multe datagrame (mesaje de dimensiune strict limitată), transferat prin mediul de comunicaţie<br />

şi asamblat la loc odată pe măsură ce datagramele ajung la destinaţie. Mesajul este transmis de către<br />

un proces care are "acces" la un port de comunicaţie.<br />

- IP (Internet Protocol) este răspunzător cu găsirea rutei (o cale) pe care urmează s-o parcurgă<br />

fiecare datagramă de la sursă la destinaţie pe baza unor adrese formate din patru numere (patru<br />

octeţi), numite adrese IP. Această funcţie este cunoscută sub denumirea de dirijare IP. Transmiterea<br />

datagramei se face de la o reţea locală la alta prin intermediul unor calculatoare sau echipamente<br />

specializate, numite "gateway" (sau router). Datagrama va fi transmisă deci de la sursă la gatewayul<br />

cel mai apropiat care permite transferul mai departe câtre destinaţie (sau câtre un alt gateway).<br />

Dirijarea datagramelor este complet invizibilă utilizatorului.<br />

La nivelele inferioare (nespecificate în stiva de protocoale) arhitectura este compatibilă cu<br />

oricare dintre standardele de LAN-uri existente (de ex. IEEE 802.3,4,5 etc), cel mai răspândit fiind<br />

reteaua Ethernet (IEEE 802.3). Ethernet este protocolul utilizat <strong>pentru</strong> transferul informaţiei în<br />

interiorul unei reţele locale. Este un mediu de tip "broadcast" astfel încât fiecare mesaj transmis<br />

poate fi recepţionat de toate maşinile existente în reţeaua locală. Identificarea maşinii căreia îi este<br />

destinat mesajul se face pe baza unui alt tip de adresa, adresa Ethernet (adresa MAC).<br />

În general aplicaţiile TCP/IP utilizează 4 nivele:<br />

• un protocol de nivel aplicaţie (de exemplu SMTP);<br />

• un protocol ca TCP ce oferă servicii mai multor aplicaţii;<br />

• protocolul IP ce oferă serviciile de bază <strong>pentru</strong> transferul datagramelor la destinaţie;<br />

• protocoalele necesare gestionării unor medii fizice diferite (de exemplu Ethernet, Token-<br />

Ring, legătură punct la punct etc…)<br />

Cum funcţionează de fapt suita de protocoale TCP/IP ?<br />

La un moment dat, un program (o aplicaţie) va încerca să trimită date pe reţea. Până în<br />

momentul în care datele ajung să circule pe reţea, ele vor trebui ambalate (împachetate) într-o formă<br />

care să permită interpretarea corectă atât pe parcurs cât şi la destinaţie. În scopul împachetării,<br />

pachetul de date este cercetat de câtre o serie de examinatori, care impun respectarea regulilor<br />

(protocoalelor). Examinatorii (care sunt de fapt funcţiile diferitelor straturi) se asigură de faptul că<br />

pachetele de date sunt structurate într-un anumit mod, adică aşa cum este nevoie <strong>pentru</strong> transferarea<br />

lor la nivelul imediat inferior. La trecerea prin fiecare strat al stivei de protocoale, pachetului de<br />

date i se asociază o serie de informaţii suplimentare.<br />

Un exemplu: transmiterea unui mesaj electronic (e-mail)<br />

Există un protocol <strong>pentru</strong> e-mail (de exemplu SMTP). Acesta defineşte o mulţime de<br />

comenzi ce sunt transmise de la o maşină la alta (de exemplu comanda <strong>pentru</strong> specificarea<br />

expeditorului, a destinatarului, a conţinutului mesajelor etc.). Acest protocol presupune că există o<br />

cale de comunicare sigură între cele două maşini. El a fost astfel definit încât să colaboreze cu<br />

protocoalele TCP şi IP care au grijă de asigurarea comunicării dintre cele două staţii. TCP este<br />

responsabil de transmisia sigură la destinaţie a datelor (de fapt comenzi şi date). El "ţine minte" ce<br />

s-a mai transmis şi retransmite în cazul în care întârzie confirmarea de la destinaţie. În cazul în care<br />

mesajul transmis este mult prea lung <strong>pentru</strong> a forma un pachet (o datagramă) – entitate gestionată<br />

de protocolul IP, el va fi cel care se ocupă de fragmentarea mesajului în mai multe pachete şi tot el<br />

se ocupă de "reconstrucţia" mesajelor sosite fragmentate. Răspunde direct de corectitudinea acestor<br />

operaţii. Deoarece aceste funcţii, de fapt, sunt mai generale, funcţii de care ar putea avea nevoie şi<br />

alte aplicaţii ele au fost grupate într-un modul separat şi formează protocolul TCP. Din acest punct<br />

de vedere protocolul TCP apare ca o librărie de rutine ce pot fi apelate de câtre aplicaţii în cazul în<br />

Tibor Asztalos<br />

108


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

care acestea au nevoie de o transmisie sigură în reţea. La rândul său TCP va apela la serviciile<br />

nivelului IP. Însa nu toate aplicaţiile au nevoie de serviciile furnizate de TCP. Există anumite<br />

aplicaţii care apelează direct la serviciile IP sau folosesc serviciile altor protocoale de interfaţare cu<br />

IP (de exemplu UDP). Furnizarea unei diversităţi în serviciile oferite justifică, deci, această<br />

organizare ierarhizată a serviciilor de reţea. În vederea îndeplinirii funcţiilor solicitate la fiecare<br />

nivel protocolul în cauza va suplimenta pachetele primite de la nivelul superior cu anumite<br />

informaţii de control (de care are nevoie protocolul pereche la destinaţie). Astfel TCP adaugă un<br />

antet (header) ce conţine informaţii de care are nevoie nivelul TCP de la destinaţie. La rândul său şi<br />

IP va adăuga pachetului primit de la TCP anumite informaţii necesare rutării pachetului în reţea<br />

spre destinaţie. În funcţie de subreţeaua de comunicaţie utilizată <strong>pentru</strong> transmitere (de exemplu se<br />

transmite pe o legătură punct la punct utilizând protocolul HDLC sau, se transmite de pe o staţie<br />

dintr-o reţea locală de tip Ethernet) aceste datagrame primite de la nivelul IP vor fi împachetate<br />

corespunzător protocolului de transmisie de nivel inferior. Servicile nivelului TCP sunt servicii<br />

orientate pe conexiune în timp ce cele oferite de nivelul IP sunt servicii neorientate pe conexiune<br />

(de tip datagramă).<br />

5.1.2. Servicii şi modelul client/server<br />

Comunicarea şi schimbul de informaţii în Internet se realizează prin intermediul a ceea ce<br />

poartă numele de servicii, ce permit exploatarea şi căutarea de informaţii aflate în această uriasă<br />

reţea. Câteva dintre ele sunt: telnet, ssh (Secure Shell), www (World Wide Web), ftp, archie,<br />

gopher, wais (Wide Area Information Service), news, e-mail, irc (Internet Relay Chat), ping, finger.<br />

Pentru oricare dintre aceste servicii există un calculator care solicită informaţii - un client al<br />

serviciului respectiv - de la un alt calculator care furnizează informaţiile cerute, numit server.<br />

Fiecare nod al reţelei Internet, fiecare calculator legat în reţea, poate fi atât client, cât şi server al<br />

oricărui serviciu mai sus menţionat. Termenul de "server" poate fi atribuit unui nod al reţelei (unui<br />

calculator), dacă acesta poate asigura serviciul respectiv prin propriile sale resurse, în timp ce<br />

"client" al unui serviciu poate fi considerat orice program ce permite conectarea la un alt calculator<br />

"server", permiţând astfel transferul de informaţii folosind serviciul respectiv.<br />

În concluzie, serverul este un sistem ce oferă anumite servicii specifice utilizatorului iar<br />

clientul este un alt sistem ce utilizează aceste servicii. Nu este obligatoriu ca serverul şi clientul să<br />

rezide pe diferite calculatoare. Ele pot fi programe diferite rulând pe acelaşi calculator. Acest mod<br />

de organizare a schimburilor de informaţii în reţea defineşte modelul client/server.<br />

Toate aplicaţiile, client sau server, de pe Internet utilizează una sau mai multe protocoale de<br />

nivel aplicaţie.<br />

Tipuri de aplicaţii server ce pot exista pe o staţie Server:<br />

• server de mesagerie electronică (componentă a sistemului e-mail);<br />

• server FTP;<br />

• server gopher;<br />

• server World Wide Web;<br />

• server de baze de date distribuite;<br />

• sistem de fişiere reţea;<br />

• server de tipărire la distanţă;<br />

• execuţie la distantă;<br />

• server de nume;<br />

• server de terminale;<br />

• sisteme grafice orientate pe reţea etc…<br />

Tibor Asztalos<br />

109


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Cum ne conectăm de un server ?<br />

Să presupunem că dorim să expediem un fişier unui calculator ce are adresa sa Internet<br />

128.55.3.23. Pentru a deschide un canal de <strong>comunicaţii</strong> avem nevoie de mai multe informaţii,<br />

deoarece, de fapt, trebuie să ne conectam de serverul FTP de pe calculatorul distant. Când ne vom<br />

conecta de staţia 128.55.3.23 va trebui să specificăm acest lucru. Pentru această problemă s-a găsit<br />

o soluţie elegantă prin asignarea unor numere de porturi fixe şi bine cunoscute fiecărui server.<br />

Fiecare program server va deschide un capăt de canal însă într-un mod pasiv, "de ascultare",<br />

aşteptând sosirea unor cereri din partea aplicaţiilor client. Ansamblul format din adresa Internet a<br />

unei staţii, un număr de port şi un identificator de protocol de serviciu defineşte un socket. Deci<br />

programele server vor folosi, în general, socket-uri bine cunoscute. La rândul său aplicaţia client,<br />

dornic să iniţieze o comunicaţie va crea un socket, deci va asigna un număr de port unui capăt de<br />

canal şi va transmite o cerere cu specificarea, în câmpul destinaţie, a unui socket bine cunoscut. De<br />

obicei aplicaţiile client n-au nevoie să folosească numere de porturi fixate (cunoscute), ele putând<br />

genera aceste numere în mod aleator (probabil nimeni nu se va interesa de ele). În cazul nostru,<br />

dorim să ne conectăm de serverul FTP, vom specifica numărul de port 21 în câmpul destinaţie din<br />

antetul TCP al pachetelor. Aceasta este numărul oficial asignat cu acest serviciu. Numerele de<br />

porturi rezervate unor servicii sunt publicate în documentul "Asigned Numbers" (RFC 1010).<br />

Câteva dintre numerele de port mai uzuala precum şi protocoalele ce le utilizează sunt:<br />

Tibor Asztalos<br />

Decimal Keyword Protocol<br />

------- ------- --------<br />

21 FTP File Transfer [Control]<br />

23 TELNET Telnet<br />

25 SMTP Simple Mail Transfer<br />

37 TIME Time<br />

39 RLP Resource Location Protocol<br />

42 NAMESERVER Host Name Server<br />

43 NICNAME Who Is<br />

49 LOGIN Login Host Protocol<br />

53 DOMAIN Domain Name Server<br />

69 TFTP Trivial File Transfer<br />

79 FINGER Finger<br />

109 POP-2 Post Office Protocol - Version 2<br />

115 SFTP Simple File Transfer Protocol<br />

119 NNTP Network News Transfer Protocol<br />

5.1.3. Reţele bazate pe IP şi UNIX<br />

UNIX este sistemul de operare care a ajutat probabil cel mai mult dezvoltarea reţelei<br />

INTERNET prin strategiile deschise care au fost adoptate în dezvoltarea sa. Într-adevăr sistemele<br />

UNIX (chiar şi cele comerciale) susţin în cea mai mare măsură ideea de libertate care stă la baza<br />

INTERNET-ului. Proiectele FSF-GNU, Linux, FreeBSD îşi justifică existentă şi prin necesitatea de<br />

absolvire a utilizatorului final de unele aspecte comerciale ale accesului la reţea. După aproape 30 de<br />

ani UNIX rămâne încă probabil cea mai eficientă unealtă de comunicare între calculatoare.<br />

Sistemele UNIX sunt foarte puternic integrate cu modelul de lucru TCP/IP. Tocmai această<br />

integrare puternică cu mijloacele de comunicare face din sistemul UNIX platforma cea mai interesantă<br />

<strong>pentru</strong> un sistem software. Foarte multe aplicaţii de bază sunt gândite <strong>pentru</strong> lucrul în reţea, fapt care<br />

oferă o serie de avantaje. Exemplu: aplicaţia Xwindows este gândita ca aplicaţie client-server - iar în<br />

momentul în care un utilizator se conectează nu contează dacă el se află sau nu la calculatorul care<br />

realizează afişarea (render). Un utilizator local va fi văzut de asemenea ca o conexiune de la adresa<br />

127.0.0.1 (adresa calculatorului local). De asemenea, sistemul XFree86 (XWindows) se bazează <strong>pentru</strong><br />

110


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

desenarea caracterelor pe un server de font-uri - care bineînţeles poate fi folosit de oricare din<br />

utilizatorii unei reţele.<br />

O problemă de importanţă majoră este identificarea unei gazde. Pentru aceasta se foloseşte DNS<br />

(Domain Name Service) - o aplicaţie <strong>pentru</strong> identificarea unui calculator după numele său. Protocolul<br />

IP identifică în mod unic fiecare calculator folosind un număr (presupus unic) de 4 octeţi. Aceste<br />

numere sunt însă foarte greu de reţinut, de aceea se preferă asignarea unor denumiri unice <strong>pentru</strong><br />

identificarea gazdelor. De exemplu este mult mai uşor de reţinut adresa hermes.ee.utt.ro decât adresa sa<br />

IP: 193.226.10.90<br />

5.2. Programarea aplicaţiilor de comunicare în reţelele UNIX - TCP/IP<br />

Programele de comunicare în reţea se bazează pe nivelul sesiune al modelului ISO/OSI.<br />

Comunicarea inter-procese poate implica două procese care nu se află în acelaşi spaţiu de adrese, ci pe<br />

două calculatoare diferite. Pentru aceasta se foloseşte (după cum am amintit mai sus) în Internet<br />

modelul TCP/IP. O primă posibilitate este ca o aplicaţie să formeze ea însăşi pachetele de date, să le<br />

încapsuleze în protocolul de transport dorit (de exemplu TCP, sau UDP), apoi să adauge semnătura IP<br />

completată cu datele despre destinaţie, <strong>pentru</strong> a putea transmite direct la dispozitivul de comunicaţie<br />

(placă de reţea, modem) pachetul cu datele. Această posibilitate este însă de evitat <strong>pentru</strong> aplicaţiile<br />

care nu au cerinţe speciale (există aplicaţii care manipulează pachetele de date ce sunt trimise în reţea,<br />

dar în marea majoritate a cazurilor un asemnea efort nu este necesar. În cazul reţelelor de tip TCP/IP<br />

aceasta înseamnă utilizarea interfeţei de programare socket. Interfaţa socket reprezintă o modalitate<br />

generică, independentă de arhitectura hardware sau de protocoalele folosite <strong>pentru</strong> comunicarea între<br />

două procese aflate eventual în spaţii de adrese diferite.<br />

5.2.1. Interfaţa de programare socket - elemente de bază<br />

La începutul anilor 80 agenţia ARPA (Advanced Research Projects Agency) a demarat un<br />

proiect in cadrul University of California <strong>pentru</strong> a porta familia protocoalelor TCP/IP în mediile<br />

UNIX. O parte a acestui proiect a constituit-o realizarea unei interfeţe de programare care să<br />

permită aplicaţiilor utilizarea acestor protocoale în transferul informaţiei. Aceasta interfaţa s-a<br />

numit "interfaţa socket", iar UNIX-ul îmbogăţit cu nucleul de comunicaţie TCP/IP s-a numit iniţial<br />

BSD UNIX (Berkley <strong>Software</strong> Distribution UNIX). Interfaţa socket a apărut prima dată pe<br />

sistemele BSD 5.2, dar acum este disponibilă pe toate sistemele moderne de tip UNIX. Interfaţa nu<br />

este dedicată unui singur set de protocoale, ci este suficient de generală <strong>pentru</strong> a suporta diverse<br />

protocoale.<br />

Abordarea aleasă <strong>pentru</strong> realizarea interfeţei a plecat de la ideea reutilizării a cât mai multor<br />

apeluri sistem deja existente. Acolo unde nu a fost posibil sau acolo unde lucrurile puteau fi privite<br />

mai simplu s-au introdus câteva apeluri noi.<br />

Tibor Asztalos<br />

Aplicaţie<br />

(ex. Web Server)<br />

Berkeley Socket<br />

Transport Protocol<br />

(e.g. TCP)<br />

Network Protocol<br />

(e.g. IP)<br />

...<br />

...<br />

Socket API<br />

Bibliotecă de funcţii, structuri de date şi constante<br />

Astfel Socket este :<br />

• un API şi nu un protocol de reţea<br />

• nu este limitat la TCP/IP<br />

• specific sistemelor UNIX şi platformelor<br />

compatibile<br />

Figura 5.1. Interfaţa Socket<br />

111


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Socket-ii UNIX sunt entităţi care poate suporta mai multe interpretări - ei sunt entităţi ce<br />

există în cadrul unui domeniu de comunicaţie, dar pot fi văzuţi în acelaşi timp şi ca fişiere: pe<br />

socket se poate realiza scriere, citire, selecţie în aceeaşi manieră în care se fac operaţiile clasice<br />

asupra fişierelor.<br />

Operaţia de transfer de informaţie prin intermediul reţelei a fost văzută ca o operaţie de<br />

intrare/ieşire. Urmând deci modelul operaţiilor de intrare/ieşire, <strong>pentru</strong> a realiza transferul<br />

informaţiei este necesar să se obţină o intrare (un descriptor) în tabela de descriptori asociată unui<br />

proces. Astfel, şi un socket este reprezentat (cunoscut prin sistem) printr-un număr întreg numit<br />

descriptor de socket, două aplicaţii putând comunica doar după ce fiecărei aplicaţii i s-a alocat câte un<br />

socket. Exista un număr de descriptori de socket folosiţi de aplicaţiile sistem, restul fiind disponibili<br />

<strong>pentru</strong> utilizatori. Acest descriptor este de un tip mai special, el referind informaţia ce caracterizează<br />

capătul de comunicaţie respectiv:<br />

Tabela de descriptori:<br />

0 - stdin<br />

1 - stdout<br />

2 - stderr<br />

...<br />

x - socket -> familia protocoalelor utilizate,<br />

tipul comunicaţiei (serviciu)<br />

Adresa IP locala<br />

Adresa IP de la distanta<br />

Număr port local<br />

Număr port de la distanta<br />

Socket-ul este un punct final, <strong>pentru</strong> o comunicaţie, ce poate fi numit şi adresat într-o reţea. Din<br />

perspectiva unui program de aplicaţie, un socket este o resursă alocată de sistemul de operare. Fiecare<br />

socket are un tip şi unul sau mai multe procese asociate. Socket-urile există într-un domeniu de<br />

comunicaţie. Un domeniu de comunicaţie este o abstracţie introdusă <strong>pentru</strong> a descrie proprietăţile<br />

comune ale proceselor ce comunică prin socketuri. De exemplu, în domeniul de comunicaţie UNIX,<br />

socket-urile sunt numite cu ajutorul numelor de fişiere; de exemplu un socket poate fi numit<br />

"/tmp/foo". Socket-urile în mod normal schimbă date doar cu socket-uri din acelaşi domeniu (se pot<br />

face traversări de domenii doar dacă există un proces de translatare). Diferitele moduri de adresare<br />

ale socket-urilor din diferitele domenii de comunicaţie definesc familiile de adrese. Toate host-urile din<br />

aceeaşi familie de adrese înţeleg şi folosesc aceeaşi schemă <strong>pentru</strong> adresarea extremităţilor socket-ului.<br />

O asemenea familie de adrese este AF_INET, familie ce defineşte adresarea în domeniul Internet. Pe<br />

lângă acest tip de familie, sistemul UNIX mai recunoaşte şi alte tipuri, cum ar fi AF_UNIX, care<br />

reprezintă modul de adresare specific acestor sisteme.<br />

Interfaţa socket ascunde utilizatorului detaliile reţelei fizice, stiva de protocoale utilizată, ea<br />

fiind caracterizată de serviciile pe care le asigură.<br />

Prezentăm o listă cu cele mai utilizate funcţii sistem implicate în gestiunea entităţilor socket.<br />

În subcapitolele următoare vom detalia modul lor de folosire, iar apoi va fi prezentată o aplicaţie<br />

client-server minimală <strong>pentru</strong> demonstrarea facilităţilor prezentate.<br />

Tibor Asztalos<br />

Primitivă Semnificaţie Destinaţie<br />

socket() Deschide un nou capăt de comunicaţie Client şi Server<br />

connect() Încercare de stabilire a conexiunii Client<br />

bind() Ataşează o adresă locală la un socket Server<br />

112


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

listen() Socket-ul poate accepta conexiuni noi Server<br />

accept() Blocarea până la sosirea unei cereri de conectare Server<br />

send() Trimite date prin socket Client şi Server<br />

receive() Primeşte date din socket Client şi Server<br />

shutdown() Opreşte unidirecţional o legătură de comunicaţie Client şi Server<br />

close() Se închide definitiv conexiunea şi descriptorul de<br />

socket<br />

Client şi Server<br />

În cele ce urmează vom încerca o detaliere a funcţiilor sistem. Prezentarea este general<br />

valabilă <strong>pentru</strong> toate versiunile de interfaţă BSD socket compatibile cu definiţia 4.4BSD.<br />

5.2.1.1 Primitiva socket()<br />

Dacă în cazul fişierelor obţinerea unui descriptor de fişier se realizează prin apelul open(),<br />

în cazul comunicaţiei prin sockeţi obţinerea unui astfel de descriptor se realizează prin intermediul<br />

apelului socket():<br />

#include <br />

#include <br />

int socket(int domeniu, int tip, int protocol)<br />

unde<br />

- domeniu reprezintă familia protocoalelor pe care urmează să le utilizăm în transferul informaţiei.<br />

Valori uzuale :<br />

• PF_UNIX, PF_LOCAL - <strong>pentru</strong> comunicaţia locală UNIX ( comunicaţie prin intermediul unor<br />

fişiere UNIX de un tip special).<br />

• PF_INET - utilizat <strong>pentru</strong> <strong>comunicaţii</strong> folosind protocolul IPv4, fie local, fie la distanţă.<br />

• PF_INET6 - utilizat <strong>pentru</strong> comunicaţia folosind protocolul IPv6.<br />

• PF_NS - protocoale Xerox Network Systems.<br />

• PF_IPX - utilizat <strong>pentru</strong> protocoalele bazate pe IPX - Novell Netware.<br />

• PF_NETLINK - utilizat <strong>pentru</strong> comunicaţia dintre kernelul Linux şi programele utilizator.<br />

Acest tip de socket poate fi folosit <strong>pentru</strong> interogarea şi modificarea tabelelor de rutare, <strong>pentru</strong><br />

primirea pachetelor trimise de codul de firewall IPv4, <strong>pentru</strong> gestionarea tabelului ARP, etc.<br />

• PF_X25 <strong>pentru</strong> protocoalele ITU-T X.25 / ISO-8208<br />

• PF_AX25 <strong>pentru</strong> protocolul de radio-comunicaţie AX.25<br />

• PF_ATMPVC <strong>pentru</strong> comunicarea cu dispozitive mobile folosind Asynchronous Transfer<br />

Mode.<br />

• PF_APPLETALK <strong>pentru</strong> comunicarea folosind protocoalele Appletalk DDP şi AARP<br />

• PF_PACKET <strong>pentru</strong> accesul direct la driver-ul dispozitivului de comunicaţie (la nivelul 2 din<br />

modelul OSI).<br />

- tip indică tipul comunicaţiei (serviciul cerut mediului de comunicaţie). Tipurile curent definite<br />

sunt:<br />

• SOCK_STREAM indică stabilirea unei <strong>comunicaţii</strong> bazată pe construirea unei<br />

conexiuni între sursă şi destinaţie. Interfaţa socket-ului stream defineşte un serviciu<br />

orientat pe conexiuni. Comunicaţia este FIFO, fiabilă şi sigură. Datele sunt transmise fără<br />

erori sau duplicate şi sunt recepţionate în aceeaşi ordine în care au fost transmise. Fluxul de<br />

date prin reţea este realizat astfel încât să se evite depăşirea unei valori maxime. Nu sunt<br />

impuse nici un fel de limitări ale formei datelor, care se consideră a fi şiruri de octeţi.<br />

113


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

Protocolul ftp, de exemplu, utilizează socket-uri de tip stream.<br />

Apl.<br />

3 2 1<br />

• SOCK_DGRAM indică transferul unor mesaje scurte (datagrame) câtre o anume<br />

destinaţie. Comunicaţia nu e bazată pe conexiune (de tip NC), este nesigură (uzual se<br />

implementează un protocol separat <strong>pentru</strong> a comunica prin intermediul datagramelor).<br />

Interfaţa socket-ului datagramă defineşte deci un serviciu fără conexiuni în care<br />

datagramele sunt transmise ca pachete separate. Serviciul fără conexiuni nu asigură garanţia<br />

recepţionării datelor. Datele pot fi pierdute sau duplicate, iar datagramele pot ajunge în altă<br />

ordine de secvenţă faţă de aceea în care au fost transmise. Mărimea unei datagrame este<br />

limitată de numărul de octeţi ce pot fi transmişi într-o singură tranzacţie. Nu se realizează<br />

nici o dezasamblare şi reasamblare a pachetelor. NFS, de exemplu, este unul dintre<br />

protocoalele care folosesc socket-uri datagramă.<br />

Apl.<br />

3 2 1<br />

socket<br />

socket<br />

• SOCK_RAW permite accesul utilizatorului la protocoalele de comunicaţie inferioare<br />

care suportă abstractizarea socket-urilor. Aceste socket-uri sunt orientate datagramă, deşi<br />

caracteristicile lor sunt dependente de interfaţa oferită de protocol. Socket-urile RAW nu<br />

sunt <strong>pentru</strong> utilizatorul general. Ele sunt oferite în mod deosebit celor interesaţi în<br />

dezvoltarea de noi protocoale de comunicaţie sau celor care doresc acces la nişte<br />

facilitaţi mai rar folosite ale unui protocol existent.<br />

• SOCK_SEQPACKET permite comunicaţie bidirecţională cu datagrame de lungime<br />

maximă fixată. La fiecare citire, consumatorul va aduce exact o singură datagramă. Un<br />

SEQUENCED PACKET, de exemplu, este similar cu STREAM SOCKET cu deosebirea<br />

că marginile structurilor sunt păstrate. Această interfaţă nu este oferită în domeniile<br />

UNIX sau Internet.<br />

• SOCK_RDM, transmisie sigură de datagrame.<br />

Uzual vom folosi doar primele două modalităţi de transfer.<br />

Dest.<br />

- protocol reprezintă un anume protocol din familia de protocoale specificate ca prim argument.<br />

Interfaţa socket a fost concepută <strong>pentru</strong> cazul cel mai general în care o familie de protocoale<br />

dispune de mai multe protocoale între care se poate opta. În cazul utilizării domeniului PF_INET,<br />

singurul protocol cunoscut este protocolul identificat prin valoarea 0.<br />

Prefixul PF <strong>pentru</strong> definirea domeniului provine de la prescurtarea sintagmei Protocol<br />

Family. Definiţia BSD foloseşte <strong>pentru</strong> specificarea tipului adreselor <strong>pentru</strong> socket constante cu<br />

aceleaşi denumiri prefixate de AF (Address Family). Paginile de manual BSD însă notează faptul că<br />

D3<br />

D1<br />

D2<br />

114


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

familia protocolului este în general aceeaşi cu familia adresei, şi în general se foloseşte prefixul AF<br />

peste tot.<br />

Valoarea întoarsă de apelul socket() indică descriptorul ce a fost alocat <strong>pentru</strong> realizarea<br />

comunicaţiei. Există mai multe situaţii când apelul rutinei socket poate eşua caz în care se<br />

returnează -1 iar variabila errno conţine codul erorii. Pe lângă situaţiile când nu este memorie<br />

suficientă (ENOBUFS), crearea unui socket poate eşua datorită cererii unui protocol necunoscut<br />

(EPROTONOSUPORT), sau cererii unui tip de socket <strong>pentru</strong> care nu există un protocol<br />

(EPROTOTYPE).<br />

De remarcat faptul că din totalitatea caracteristicilor acestui tip de descriptori nu am completat până<br />

acum decât primele două câmpuri.<br />

Exemplu:<br />

int s;<br />

s = socket(AF_INET, SOCK_STREAM, 0);<br />

...<br />

s = socket(AF_UNIX, SOCK_DGRAM, 0);<br />

Ca o variaţiune pe aceeaşi temă apelul funcţiei socketpair() creează o pereche de socket-uri<br />

interconectate. Ele pot fi folosite de două procese înrudite. Deoarece socket-urile aparţin<br />

domeniului de comunicaţie, nu procesului, nu trebuie închişi descriptorii nefolosiţi după fork().<br />

Asocierea unei adrese complete locale (adresa IP locala şi număr de port local) se realizează<br />

prin apelul bind():<br />

int bind(int sockfd, struct sockaddr *adresa, int lung_adresa);<br />

Acest apel asociază descriptorului de tip socket sockfd adresa locală specificată complet în<br />

câmpurile structurii adresa al cărei pointer este furnizat ca al doilea parametru. Lungimea acestei<br />

adrese este indicată prin valoarea ultimului parametru lung_adresa. Câmpurile structurii sockaddr<br />

şi<br />

apelurile sistem necesare <strong>pentru</strong> completarea acestora vor fi explicate ulterior.<br />

Valoarea întoarsă de această funcţie indică succesul apelului (0) sau o eroare (-1) al cărei cod este<br />

indicat prin valoarea variabilei globale errno.<br />

Specificarea adresei celuilalt capăt de comunicaţie (adresa completa IP şi numărul portului<br />

destinaţie) este realizată diferit în funcţie de tipul de comunicaţie specificat la momentul creerii<br />

descriptorului de socket.<br />

5.2.2. Specificarea adreselor în apelurile interfeţei socket<br />

Interfaţa socket a fost proiectată <strong>pentru</strong> a acoperi cazul cel mai general al comunicaţiei prin<br />

diferite protocoale, fiecare din acestea având propriul mecanism de identificare a unei maşini, şi<br />

deci propriul format de adresă.<br />

Pentru specificarea adresei se foloşeşte un tip de date generic numit sockaddr. Acest tip e folosit<br />

<strong>pentru</strong> stocarea datelor <strong>pentru</strong> orice tip de socket şi e definită după cum urmează:<br />

typedef unsigned short int sa_family_t;<br />

struct sockaddr {<br />

sa_family_t sa_family;<br />

Tibor Asztalos<br />

115


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

unsigned char sa_data[14];<br />

}<br />

Această structură sockaddr este declarată în fişierul antet sockets.h.<br />

Valoarea <strong>pentru</strong> sa_family sunt aceleaşi valori ca la familia protocolului, dar prefixate cu AF în loc<br />

de PF: AF INET, AF IPX, AF UNIX, AF UNSPEC etc. Câmpul sa_data este diferit <strong>pentru</strong> fiecare<br />

familie de adrese. De obicei fiecare familie de adrese defineşte propria sa structură care poate fi<br />

suprapusă peste structura sockaddr. Pentru fiecare familie de protocoale este definită o structură<br />

sufixată de tipul protocolului. De exemplu <strong>pentru</strong> protocolul IP (identificat intern ca familia<br />

"INET") va fi definită structura sockaddr_in (definiţia sa pe sistemele Linux se găseşte în fişierul<br />

header ):<br />

Adresele Internet - formate din 32 de biţi, identifică în mod unic interfaţa de reţea. Un host are atâtea<br />

adrese Internet câte interfeţe de reţea sunt instalate în calculatorul respectiv.<br />

Adresa de socket în domeniul Internet este alcatuită din patru câmpuri: familia de adrese<br />

(AF_INET), o adresă Internet, un port şi un şir de caractere. Structura unei adrese de socket Internet,<br />

definită prin sockaddr_in este:<br />

typedef unsigned short int sa_family_t;<br />

struct in_addr {<br />

unsigned long int s_addr;<br />

};<br />

struct sockaddr_in {<br />

sa_family_t sin_family; /* completat cu AF_INET */<br />

unsigned short int sin_port; /* portul: 0-65535 */<br />

struct in_addr sin_addr; /* adresa IP */<br />

unsigned char sin_zero[8];/* completati cu 0 */<br />

}<br />

Analog, <strong>pentru</strong> diversele protocoale vor exista sockaddr_in6, sockaddr_ipx, sockaddr_x25,<br />

sockaddr_ax25, etc.<br />

Câmpul sin_family este setat la AF_INET. Câmpul sin_port este numărul de port folosit de<br />

aplicaţie. Câmpul sin_addr este adresa Internet a interfeţei de reţea folosite de aplicaţie. Câmpul<br />

sin_zero trebuie setat iniţial cu valoarea 0.<br />

O asemenea structură se foloseşte şi <strong>pentru</strong> specificarea adresei Internet a procesului distant, caz<br />

în care al doilea câmp va indica numărul portului, iar cel de-al treilea câmp va indica adresa IP a<br />

maşinii pe care rulează procesul distant. Deşi acest ultim câmp are forma unei structuri, aceasta<br />

"ascunde" de fapt o variabilă de tip long conţinând în format binar adresa IP respectivă. Cum se<br />

obţine această adresă, considerând cunoscut doar numele simbolic al maşinii UNIX (ex: "hermes"<br />

sau "hermes.ee.utt.ro")?<br />

Apelul sistem:<br />

struct hostent* gethostbyname(char *name);<br />

primeşte ca argument acest nume simbolic sub forma unui şir de caractere şi întoarce un pointer la o<br />

structură ce indică o serie de caracteristici ale maşinii UNIX respective. Această structură e definită<br />

astfel: #include <br />

Tibor Asztalos<br />

struct hostent {<br />

char *h_name;<br />

char **h_aliases;<br />

int h_addrtype;<br />

int h_length;<br />

char **h_addr_list;<br />

}<br />

116


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Numele maşinii respective, ca şi totalitatea celorlaltor nume sub care ea este cunoscută (alias-uri)<br />

sunt conţinute în primele două câmpuri ale acestei structuri. Câmpul h_addrtype conţine în cazul<br />

maşinilor ce au atribuite adrese IP valoarea constantei AF_INET. Câmpul h_length indică lungimea<br />

binară a tipului de adresa utilizat (în cazul adreselor IP această lungime este de 4 octeţi).<br />

Ultimul câmp reprezintă un vector de adrese sub care aceasta maşina e recunoscută în reţea. Fiecare<br />

astfel de adresa este de fapt un şir de h_length octeţi.<br />

5.2.3. Stabilirea conexiunii<br />

Este caracteristică doar socket-urilor de tip stream ce oferă servicii orientate pe conexiune.<br />

Stabilirea conexiunii este de obicei asimetrică. Şi anume unul din cele două procese implicate joacă<br />

rol de "server" iar celălalt joacă rol de "client". Serverul, când doreşte să-şi ofere serviciile, leagă<br />

(folosind apelul bind()) un socket la o adresă bine cunoscută asociată cu serviciul şi apoi să<br />

"asculte", în mod pasiv, pe acel socket cererile ce provin de la clienţi. Mai mult decât atât, în timp<br />

ce serverul este ocupat cu tratarea unei cereri, există posibilitatea de a întârzia cererile ce provin de<br />

la alţi clienţi într-o coadă de aşteptare. Este posibil ca un proces necunoscut să se întâlnească cu<br />

serverul. Clientul cere serviciile serverului iniţiind o conexiune cu socket-ul serverului. Clientul<br />

foloseşte apelul connect() <strong>pentru</strong> a iniţia conexiunea. In domeniul Internet se apelează cu sintaxa:<br />

Tibor Asztalos<br />

struct sockaddr_in server;<br />

...<br />

connect(s, (struct sockaddr *)&server, sizeof (server));<br />

unde structura server trebuie să conţină adresa Internet şi numărul de port al serverului cu care<br />

procesul client doreşte să comunice. Dacă socket-ul clientului este nelegat la momentul apelului,<br />

sistemul va selecta automat şi va lega un nume socket-ului, dacă este necesar. Acesta este modul<br />

obişnuit de legare a adreselor locale la un socket.<br />

O eroare este returnată dacă conexiunea nu a reusit. (orice nume legat automat de sistem<br />

rămâne totuşi). Altfel, socket-ul este asociat cu serverul şi transferul de date poate începe. Câteva<br />

din cele mai obişnuite erori de conexiune sunt:<br />

ETIMEDOUT<br />

După nereuşirea stabilirii conexiunii <strong>pentru</strong> o perioadă de timp, sistemul consideră că nu mai<br />

are rost să mai încerce. Acesta apare de obicei ca urmare a faptului că maşina destinaţie nu este<br />

activ, sau reţeaua are probleme.<br />

ECONNREFUSED<br />

Hostul refuză serviciul. Aceasta poate fi datorită faptului că nu există un proces server la<br />

adresa specificată.<br />

ENETDOWN sau EHOSTDOWN<br />

Aceste erori sunt returnate pe baza informaţiilor de stare întoarse clientului de serviciile de<br />

comunicaţie inferioare.<br />

ENETUNREACH sau EHOSTUNREACH<br />

Aceste erori pot apare fie datorită necunoaşterii hostului sau reţelei, sau nu există ruta până<br />

acolo.<br />

În general socket-urile <strong>pentru</strong> protocoalele orientate pe conexiune se pot conecta o singură<br />

dată. Pentru protocoalele neorientate pe conexiune se pot conecta de mai multe ori <strong>pentru</strong> a-şi<br />

schimba asocierea. Socket-urile fără conexiune pot termina o asociere conectându-se la o adresă cu<br />

membrul sa_family completat cu valoarea AF_UNSPEC.<br />

117


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Pentru ca serverul să primească o conexiune a unui client, trebuie să facă încă doi paşi după<br />

ce şi-a legat adresa locală de socket. Primul pas este să indice dorinţa de a asculta cererile de<br />

conexiune:<br />

Tibor Asztalos<br />

listen(s, nrconn);<br />

Al doilea parametru al apelului listen() specifică numărul maxim de conexiuni ce pot aştepta<br />

acceptarea de server; acest număr poate fi limitat de sistem. Dacă o conexiune este cerută când<br />

coada este plină, aceasta nu va fi rejectată, dar mesajele cererii vor fi ignorate. Acest lucru oferă<br />

serverului timp <strong>pentru</strong> rezolvarea cererilor din coadă, în timp ce clientul reîncearcă să se conecteze.<br />

Dacă apelul connect() întoarce eroarea ECONNREFUSED, clientul nu va şti dacă serverul este<br />

pornit sau nu.<br />

Valoarea întoarsă reprezintă succesul (0) sau eşecul (-1) operaţiei.<br />

Odată lungimea cozii specificate, server-ul se poate pune în aşteptarea unei cereri prin intermediul<br />

apelului accept():<br />

struct sockaddr_in from;<br />

...<br />

fromlen = sizeof (from);<br />

newsock = accept(s, (struct sockaddr *)&from, &fromlen);<br />

Acest apel provoacă blocarea execuţiei server-ului până în momentul recepţionării unei cereri (unui<br />

mesaj) transmis de către un proces client. În acest moment, un socket separat este creat local, el<br />

urmând să servească comunicaţiei server -> client. Un nou descriptor de socket este returnat. Dacă<br />

serverul doreşte să afle care este clientul, poate oferi un buffer <strong>pentru</strong> numele clientului. Parametrul<br />

valoare-rezultat fromlen este iniţializat de server <strong>pentru</strong> a indica cât spaţiu este asociat cu structura<br />

from, apoi modificat la return reflectând dimensiunea reala a numelui. Dacă nu interesează numele<br />

clientului, al doilea parametru poate fi un pointer nul.<br />

În mod normal apelul accept() se blochează sau până când o conexiune este disponibilă sau<br />

până când procesul primeşte un semnal (signal). Mai mult, nu se poate indica dorinţa de conectare<br />

doar cu anumiţi clienţi. Acest lucru trebuie să-l rezolve utilizatorul inspectând adresa clientului şi<br />

închizând conexiunea dacă nu îi convine.<br />

5.2.4. Transferul de date<br />

Odată stabilită conexiunea, se poate face transferul de date. Pentru a trimite sau recepţiona<br />

date există mai multe posibile apeluri. Cum fiecare capăt al conexiunii este bine ancorat, utilizatorul<br />

poate trimite sau recepţiona mesaje fără să mai specifice adresa destinaţie. În acest caz se pot folosi<br />

apelurile read() şi write():<br />

write(s, buf, sizeof (buf));<br />

read(s, buf, sizeof (buf));<br />

Pe lângă read() şi write() se pot folosi noile apeluri send() şi recv():<br />

send(s, buf, sizeof (buf), flags);<br />

recv(s, buf, sizeof (buf), flags);<br />

Deşi send() şi recv() sunt virtual identice cu read() şi write(), parametrul suplimentar flags este<br />

important. Aceste flaguri sunt definite în . Dacă flags este diferit de zero, el poate fi:<br />

• MSG_OOB se cere transmisia/recepţia datelor out-of-band <strong>pentru</strong> socket-urile care suportă<br />

118


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

această opţiune.<br />

• MSG_PEEK cere recepţia datelor din coadă fără a le elimina din coada de aşteptare. Când<br />

MSG_PEEK este folosit într-un apel recv(). orice mesaj prezent este returnat, dar tratat ca încă<br />

'necitit'. Adică următorul recv() sau read() pe acel socket va întoarce acelaşi mesaj.<br />

• MSG_WAITALL cere ca operaţiunea să se blocheze până când o cerere este complet<br />

satisfăcută.<br />

• MSG_TRUNC <strong>pentru</strong> returnarea valorii adevărate a pachetului (opţiune doar <strong>pentru</strong> socket-urile<br />

ce operează cu pachete, nu cu flux de date.<br />

• MSG_NOSIGNAL este dezactivată opţiunea ca la închiderea conexiunii de la celălalt capăt,<br />

procesul curent să primească semnalul SIGPIPE.<br />

• MSG_DONTROUTE Pentru a trimite numai hosturilor cu care hostul este direct conectat<br />

(opţiunea e folosită de obicei doar <strong>pentru</strong> programele de diagnoză, sau programele de rutare).<br />

• MSG_DONTWAIT activează modul de lucru neblocant ( acelaşi efect îl are şi indicatorul<br />

O_NONBLOCK setat cu fcntl() )<br />

• MSG_CONFIRM este un indicator apărut de la versiunea 2.3 a nucleului Linux: se confirmă<br />

nivelului de legătură succesul operaţiei de înaintare a pachetului (IP-forwarding). Dacă nivelul<br />

de legătură nu înţelege aceasta de obicei reverifică identitatea vecinului (de exemplu prin ARP).<br />

Numai socket-urile SOCK_DGRAM şi SOCK_RAW sunt valide, şi momentan sunt<br />

implementate doar <strong>pentru</strong> IPv4 şi IPv6.<br />

Exemplu de transmitere-receptie date pe un socket conectat<br />

Tibor Asztalos<br />

int bytes_sent;<br />

int bytes_received;<br />

char data_sent[256];<br />

char data_received[256];<br />

int send(int socket, char *buf, int buflen, int flags);<br />

int recv(int socket, char *buf, int buflen, int flags);<br />

int s;<br />

…<br />

bytes_sent=send(s,data_sent,sizeof(data_sent),0);<br />

bytes_received=recv(s,data_received,sizeof(data_received),0);<br />

5.2.5 Închiderea socket-urilor<br />

Odată ce un socket nu mai interesează, poate fi închis, aplicând un apel close() desciptorului<br />

de socket.<br />

close(s);<br />

Dacă un socket de tip stream are date asociate atunci când este închis, sistemul va mai încerca o<br />

perioadă să le transfere.<br />

Totuşi, după o perioadă de timp destul de lungă, dacă datele sunt în continuare netransferate, vor fi<br />

descărcate. Dacă un utilizator nu mai are nevoie de nici un mesaj în aşteptare, poate apela<br />

shutdown() pe acel socket înainte să-l închidă.<br />

shutdown(s, how);<br />

unde how este 0 dacă nu mai interesează citirea datelor, 1 dacă nu se mai trimit date sau 2 dacă nu<br />

se mai trimit şi nu se mai citesc date.<br />

119


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

5.2.6. Socketuri fără blocare<br />

Câteodată este nevoie să folosim socket-uri care nu se blochează; adică cererile de I/O care<br />

nu se pot întoarce imediat şi care ar suspenda procesul în aşteptarea terminării lor nu sunt executate<br />

şi întorc un cod de eroare. Un socket creat cu apelul socket() poate fi marcat ca non-blocking astfel:<br />

#include <br />

...<br />

int s;<br />

...<br />

s = socket(AF_INET, SOCK_STREAM, 0);<br />

...<br />

if (fcntl(s, F_SETFL, O_NONBLOCK) < 0)<br />

perror("fcntl");<br />

exit(1);<br />

}<br />

...<br />

Lucrul cu socket-uri non-blocking se face în felul următor: apelurile care ar trebui să se<br />

blocheze se întorc imediat cu eroare (intorc -1) şi cu codul de eroare EWOULDBLOCK în variabila<br />

globală errno.<br />

5.2.7. Multiplexare Sincrona I/O - apelul de sistem select()<br />

Această funcţie este oarecum ciudată, dar foarte utilă. Să considerăm, de exemplu,<br />

următoarea situaţie: un server vrea să asculte cereri de conectare şi să citească în continuare de pe<br />

conexiunile pe care deja le are. Nu este nici o problemă, am putea spune, doar un accept() şi câteva<br />

recv()-uri. Însă ce facem dacă serverul este blocat pe un apel accept() ? Cum mai primeşte date cu<br />

recv() în acelaşi timp ? "Foloseşte un socket non-blocabil!" - Nu e o soluţie eficientă! Consumă<br />

mult timp procesor nefăcând practic nimic.<br />

select() permite controlul a mai mulţi sockeţi în acelaşi timp. El va spune care este gata de citire,<br />

care este gata de scriere, şi care socket a întalnit excepţii.<br />

Sintaxa funcţiei select() este:<br />

#include <br />

#include <br />

#include <br />

int select(int numfds, fd_set *readfds, fd_set *writefds,<br />

fd_set *exceptfds, struct timeval *timeout);<br />

Funcţia controlează seturi de descriptori de fişiere; în particular readfds, writefds, şi exceptfds. Dacă<br />

dorim să vedem dacă se poate citi de la standard input şi un descriptor de socket, sockfd, trebuie<br />

doar să adaugăm descriptorii de fişiere 0 şi sockfd la setul de readfds. Parametrul numfds va trebui<br />

setat la valoarea celui mai mare descriptor plus unu. În acest exemplu, ar trebui setat la sockfd+1,<br />

din moment ce cu siguranţă este mai mare decât standard input (0).<br />

Când select() returnează, readfds va fi modificat <strong>pentru</strong> a reflecta care dintre descriptorii de fişiere<br />

este gata de citire. Acesta poate fi testat cu macroul FD_ISSET(), după cum se va vedea mai jos.<br />

Fiecare set este de tipul fd_set. Următoarele macrouri operează cu acest tip:<br />

• FD_ZERO(fd_set *set) - curăţă setul de descriptori de fişiere<br />

• FD_SET(int fd, fd_set *set) - adaugă fd la set<br />

• FD_CLR(int fd, fd_set *set) - şterge fd din set<br />

• FD_ISSET(int fd, fd_set *set) - testează să vadă dacă fd este în set<br />

Există, de asemenea, o structură de temporizare, denumită struct timeval. Această structură de timp<br />

Tibor Asztalos<br />

120


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

permite specificarea unei perioade de timeout. Dacă timpul a fost depăşit iar select() înca nu a găsit<br />

pregătit nici un descriptor de fişier, se va întoarce <strong>pentru</strong> ca să poată continua procesarea.<br />

Structura struct timeval are următoarele câmpuri:<br />

struct timeval {<br />

int tv_sec; /* seconds */<br />

int tv_usec; /* microseconds */<br />

};<br />

Trebuie doar setat tv_sec la numărul de secunde pe care trebuie să le aştepte, iar tv_usec la numărul<br />

de microsecunde pe care trebuie să le aştepte.<br />

De asemenea, când funcţia iese, timeout poate fi împrospătată <strong>pentru</strong> a arăta timpul care a mai<br />

rămas. Aceasta depinde de versiunea de Unix pe care ruleaza programul.<br />

Un lucru de interes: dacă se setează câmpurile în struct timeval la 0, select() va ieşi imediat, tăind<br />

toţi descriptorii din set. Dacă se setează parametrul timeout la NULL, funcţia nu va mai ieşi<br />

niciodată, ci va aştepta până când primul descriptor este gata.<br />

Urmatoarul fragment de cod aşteaptă 2.5 secunde ca ceva să se întample la standard input (select.c):<br />

#include <br />

#include <br />

#include <br />

#define STDIN 0 /* file descriptor for standard input */<br />

main()<br />

{<br />

struct timeval tv;<br />

fd_set readfds;<br />

tv.tv_sec = 2;<br />

tv.tv_usec = 500000;<br />

FD_ZERO(&readfds);<br />

FD_SET(STDIN, &readfds);<br />

/* don't care about writefds and exceptfds: */<br />

select(STDIN+1, &readfds, NULL, NULL, &tv);<br />

if (FD_ISSET(STDIN, &readfds))<br />

printf("A key was pressed!\n");<br />

else<br />

printf("Timed out.\n");<br />

}<br />

5.3. Comunicaţii orientate pe conexiune prin socket stream<br />

Cea mai simplă modalitate de comunicaţie prin sockeţi stream este:<br />

Server Client<br />

---------------------------------------------s<br />

= socket(..); s = socket(..);<br />

bind(s, adresa_A, ..);<br />

listen(s,..);<br />

t=accept(s,*adresa_client, *lung_adr);<br />

../*server-ul e blocat*/<br />

connect(s, adresa_A,..);<br />

/*server-ul e deblocat, t reprezinta<br />

socket-ul de legatura cu clientul */<br />

write(s,...);<br />

read(t,..);<br />

write(t,...);<br />

read(s,...);<br />

/* transfer repetat de informatie server client */<br />

close(t); close(s);<br />

/*accept-ul poate fi reluat <strong>pentru</strong><br />

Tibor Asztalos<br />

121


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

a trata cererile altor clienti */<br />

Acest scenariu este prezentat în figură:<br />

Exemplu:<br />

Tibor Asztalos<br />

Server Client<br />

socket()<br />

bind()<br />

listen()<br />

accept()<br />

send/recv()<br />

close()<br />

Figura 5.2. Scenariu client/server cu Socket de tip stream<br />

socket()<br />

connect()<br />

recv/send()<br />

close()<br />

a) Un server stream simplu<br />

Tot ceea ce face acest server este să trimită şirul "Hello, World!\n" pe o conexiune stream. Tot ce<br />

este nevoie <strong>pentru</strong> a testa acest server este să fie rulat într-o fereastră, iar cu telnet sa se conecteze la<br />

el un client din altă fereastră:<br />

$ telnet remotehostname 3490<br />

unde remotehostename este numele maşinii pe care rulează serverul.<br />

Codul serverului (tcpserver.c): (Atentie: un backslash la sfarsitul unei linii înseamna că acea linie se<br />

continuă pe următoarea)<br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#define MYPORT 3490 /* the port users will be connecting to */<br />

#define BACKLOG 10 /* how many pending connections queue will hold */<br />

main()<br />

{<br />

int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */<br />

struct sockaddr_in my_addr; /* my address information */<br />

struct sockaddr_in their_addr; /* connector's address information */<br />

int sin_size;<br />

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {<br />

perror("socket");<br />

exit(1);<br />

}<br />

my_addr.sin_family = AF_INET; /* host byte order */<br />

122


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

my_addr.sin_port = htons(MYPORT); /* short, network byte order */<br />

my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */<br />

bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */<br />

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \<br />

== -1) {<br />

perror("bind");<br />

exit(1);<br />

}<br />

if (listen(sockfd, BACKLOG) == -1) {<br />

perror("listen");<br />

exit(1);<br />

}<br />

while(1) { /* main accept() loop */<br />

sin_size = sizeof(struct sockaddr_in);<br />

if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, \<br />

&sin_size)) == -1) {<br />

perror("accept");<br />

continue;<br />

}<br />

printf("server: got connection from %s\n", \<br />

inet_ntoa(their_addr.sin_addr));<br />

if (!fork()) { /* this is the child process */<br />

if (send(new_fd, "Hello, world!\n", 14, 0) == -1)<br />

perror("send");<br />

close(new_fd);<br />

exit(0);<br />

}<br />

close(new_fd); /* parent doesn't need this */<br />

while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */<br />

}<br />

}<br />

b) Un client stream simplu<br />

Tot ceea ce face acest client este să se conecteze la maşina gazdă care a fost specificat în linia de<br />

comandă, pe portul 3490. Preia şirul de caractere pe care acel server îl trimite. Sursa clientului<br />

(tcpclient.c):<br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#define PORT 3490 /* the port client will be connecting to */<br />

#define MAXDATASIZE 100 /* max number of bytes we can get at once */<br />

int main(int argc, char *argv[])<br />

{<br />

int sockfd, numbytes;<br />

char buf[MAXDATASIZE];<br />

struct hostent *he;<br />

struct sockaddr_in their_addr; /* connector's address information */<br />

if (argc != 2) {<br />

fprintf(stderr,"usage: client hostname\n");<br />

exit(1);<br />

}<br />

if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */<br />

herror("gethostbyname");<br />

exit(1);<br />

}<br />

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {<br />

perror("socket");<br />

exit(1);<br />

}<br />

Tibor Asztalos<br />

123


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

their_addr.sin_family = AF_INET; /* host byte order */<br />

their_addr.sin_port = htons(PORT); /* short, network byte order */<br />

their_addr.sin_addr = *((struct in_addr *)he->h_addr);<br />

bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */<br />

if (connect(sockfd, (struct sockaddr *)&their_addr, \<br />

sizeof(struct sockaddr)) == -1) {<br />

perror("connect");<br />

exit(1);<br />

}<br />

if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {<br />

perror("recv");<br />

exit(1);<br />

}<br />

buf[numbytes] = '\0';<br />

printf("Received: %s",buf);<br />

close(sockfd);<br />

return 0;<br />

}<br />

De notat că dacă nu se rulează serverul înainte de client, connect() retrunează "Connection refused".<br />

5.4. Comunicaţii neorientate pe conexiune prin socket datagramă<br />

Până acum ne-au preocupat în mod deosebit socket-urile ce oferă servicii orientate pe<br />

conexiune. Totuşi, există suport <strong>pentru</strong> interacţiuni neorientate pe conexiune cum ar fi datagramele<br />

din reţelele contemporane cu comutare de pachete.<br />

Un socket datagramă oferă o interfaţă simetrică schimbului de date. Deşi procesele sunt încă<br />

'server' şi 'client', nu mai este necesară stabilirea conexiunii. În schimb, fiecare mesaj conţine adresă<br />

destinaţie.<br />

Socket-urile datagramă sunt create ca mai înainte. Dacă o adresă locală particulară este<br />

necesară, apelul bind() trebuie să preceadă primul transfer de date. Altfel, sistemul poate seta adresa<br />

locală şi portul când datele se trimit prima dată. Pentru trimiterea datelor se foloseşte apelul<br />

sendto():<br />

Tibor Asztalos<br />

sendto(s, buf, buflen, flags, (struct sockaddr *)&to, tolen);<br />

unde s, buf, buflen, flags sunt parametri ce se folosesc ca la apelul send().<br />

to şi tolen sunt folosite <strong>pentru</strong> a indica adresa destinaţie.<br />

Când se foloseşte o interfaţă nesigură cum este datagrama, este puţin probabil ca erorile să fie<br />

raportate transmiţătorului. Totuşi, dacă informaţia este netransmisp şi se detectează imposibilitatea<br />

transmiterii (de ex. reţeaua este întreruptă) apelul va întoarce -1.<br />

Pentru a recepţiona mesaje pe un socket datagramă neconectat, se foloseşte apelul<br />

recvfrom():<br />

recvfrom(s, buf, buflen, flags, (struct sockaddr *)&from, &fromlen);<br />

Înca odată, parametrul fromlen este valoare-rezultat, iniţial conţinând dimensiunea bufferului from<br />

şi modificat la întoarcere indicând dimensiunea actuală a adresei de la care s-a recepţionat<br />

datagrama.<br />

Pe lângă cele două apeluri menţionate mai sus, socket-urile datagramă pot folosi şi apelul<br />

connect() <strong>pentru</strong> a asocia un socket cu o adresă destinaţie specifică. În acest caz, orice mesaj trimis<br />

pe acel socket este trimis automat partenerului conectat şi numai mesajele recepţionate de la acel<br />

proces vor fi livrate utilizatorului.<br />

Doar o singură adresă poate fi conectată la un socket la un moment dat. O a două conectare<br />

124


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

va schimba adresa destinaţie, iar o conectare la o adresă nulă (family AF_UNSPEC) va deconecta<br />

socketul.<br />

Cererile de connect() la datagrame se întorc imediat, deoarece sistemul nu face altceva decât<br />

să înregistreze adresa destinaţie (spre deosebire de connect() la socketuri stream, unde se iniţiază<br />

stabilirea unei conexiuni).<br />

Accept() şi listen() nu se folosesc la socketuri datagramă.<br />

Un scenariu clasic de comunicare folosind socket datagramă se prezintă după cum urmează:<br />

Emitător (client) Destinatar (server)<br />

----------------------------------------------------s<br />

= socket(..); s = socket(..);<br />

bind(s, adresa_A,..);<br />

//connect(s, adresa_A,..);<br />

recv/send(s,..);<br />

send/recv(s,..);<br />

/* transfer repetat Emiţător -> Receptor */<br />

close(s); close(s);<br />

Acest scenariu poate fi ilustrat conform figurii:<br />

Tibor Asztalos<br />

Server Client<br />

socket()<br />

bind()<br />

send/recv()<br />

closesocket()<br />

socket()<br />

recv/send()<br />

closesocket()<br />

Figura 5.2. Scenariu client/server cu Socket de tip datagramă<br />

Exemplu:<br />

Prezentăm două programe: talker.c si listener.c. listener este programul server ce stă pe o maşina<br />

asteptând să vină pachete pe portul 4950. talker trimite pachete pe acel port, pe maşina specificată,<br />

care conţin orice introduce utilizatorul în linia de comandă.<br />

Sursa <strong>pentru</strong> listener.c:<br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#define MYPORT 4950 /* the port users will be sending to */<br />

#define MAXBUFLEN 100<br />

main()<br />

{<br />

int sockfd;<br />

struct sockaddr_in my_addr; /* my address information */<br />

struct sockaddr_in their_addr; /* connector's address information */<br />

int addr_len, numbytes;<br />

125


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

Tibor Asztalos<br />

char buf[MAXBUFLEN];<br />

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {<br />

perror("socket");<br />

exit(1);<br />

}<br />

my_addr.sin_family = AF_INET; /* host byte order */<br />

my_addr.sin_port = htons(MYPORT); /* short, network byte order */<br />

my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */<br />

bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */<br />

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct<br />

sockaddr)) \<br />

== -1) {<br />

perror("bind");<br />

exit(1);<br />

}<br />

addr_len = sizeof(struct sockaddr);<br />

if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, \<br />

(struct sockaddr *)&their_addr, &addr_len)) == -1) {<br />

perror("recvfrom");<br />

exit(1);<br />

}<br />

printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));<br />

printf("packet is %d bytes long\n",numbytes);<br />

buf[numbytes] = '\0';<br />

printf("packet contains \"%s\"\n",buf);<br />

close(sockfd);<br />

De reţinut că în apelul funcţiei socket() folosim în final SOCK_DGRAM. De asemenea, de reţinut că nu este nevoie de<br />

listen() sau accept().<br />

Sursa <strong>pentru</strong> talker.c:<br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#include <br />

#define MYPORT 4950 /* the port users will be sending to */<br />

int main(int argc, char *argv[])<br />

{<br />

int sockfd;<br />

struct sockaddr_in their_addr; /* connector's address information */<br />

struct hostent *he;<br />

int numbytes;<br />

if (argc != 3) {<br />

fprintf(stderr,"usage: talker hostname message\n");<br />

exit(1);<br />

}<br />

if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */<br />

herror("gethostbyname");<br />

exit(1);<br />

}<br />

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {<br />

perror("socket");<br />

exit(1);<br />

}<br />

their_addr.sin_family = AF_INET; /* host byte order */<br />

their_addr.sin_port = htons(MYPORT); /* short, network byte order */<br />

their_addr.sin_addr = *((struct in_addr *)he->h_addr);<br />

bzero(&(their_addr.sin_zero), 8); /* zero the rest of the struct */<br />

if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \<br />

(struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {<br />

126


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

Tibor Asztalos<br />

perror("sendto");<br />

exit(1);<br />

}<br />

printf("sent %d bytes to<br />

%s\n",numbytes,inet_ntoa(their_addr.sin_addr));<br />

close(sockfd);<br />

return 0;<br />

Se rulează listener pe o maşina, apoi se rulează talker pe alta iar cele două procese comunică în<br />

mod neorientat pe conexiune.<br />

Există totuşi un singur detaliu mic ce a mai fost menţionat de mai multe ori: conectarea<br />

datagram socket. Să spunem că talker apelează connect() şi specifică adresa lui listener. Din acest<br />

punct, talker poate trimite şi primi mesaje doar la adresa specificată prin connect(). Din acest motiv,<br />

nu mai e nevoie să folosim sendto() şi recvfrom(); se poate folosi simplu send() şi recv().<br />

5.5. Paşii necesari realizării unei aplicaţii cu socket-uri<br />

- 1 - obţinerea unui descriptor de socket cu apelul de funcţie:<br />

int socket(int domain, int type, int protocol);<br />

int s;<br />

s = socket(AF_INET, SOCK_STREAM, 0);<br />

- 2 - atribuirea unui nume unic <strong>pentru</strong> socket cu apelul bind():<br />

int rc;<br />

int s;<br />

struct sockaddr_in myname;<br />

int bind(int s, struct sockaddr *name, int namelen);<br />

memset(&myname, 0, sizeof(myname));<br />

myname.sin_family = AF_INET;<br />

myname.sin_addr = inet_addr("193.226.10.73"); /* adresa specifica */<br />

myname.sin_port = htons(2200); /* numarul portului */<br />

rc = bind(s, (struct sockaddr *) &myname, sizeof(myname));<br />

- acest exemplu atribuie numele myname socket-ului s.<br />

- 3 - aşteptare de conexiuni de la clienţi. Aplicaţia server face acest lucru cu apelul listen():<br />

int s;<br />

int backlog;<br />

int rc;<br />

int listen(int s, int backlog);<br />

rc = listen(s, 5);<br />

Apelul listen() indică software-ului de sistem TCP/IP că serverul este gata să accepte conexiuni<br />

şi că maximum 5 cereri de conectare pot fi trecute în coada de aşteptare a serverului, celelalte cereri<br />

fiind refuzate.<br />

- 4 - lansare de câtre clienţii ce folosesc socket-uri stream a unei cereri de conectare cu apelul connect():<br />

int s;<br />

struct sockaddr_in servername;<br />

int rc;<br />

int connect(int s, struct sockaddr *name, int namelen);<br />

127


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

memset(&servername, 0, sizeof(servername));<br />

servername.sin_family = AF_INET;<br />

servername.sin_addr = inet_addr("193.226.10.73");<br />

servername.sin_port = htons(2200);<br />

rc = connect(s, (struct sockaddr *) &servername, sizeof(servername));<br />

Apelul connect() încearcă să conecteze socket-ul s la server-ul descris de structura servername.<br />

Acesta ar putea fi server-ul care foloseşte apelul bind() în pasul 2.<br />

- 5 - acceptare cerere de conectare :<br />

int clientsocket;<br />

int s;<br />

struct sockaddr clientaddress;<br />

int addrlen;<br />

int accept(int s, struct sockaddr *addr, int *addrlen);<br />

addrlen = sizeof(clientaddress);<br />

clientsocket = accept(s, &clientaddress, &addrlen);<br />

Dacă există cereri de conectare care aşteaptă pe socket-ul s, numele clientului şi lungimea<br />

numelui acestuia sunt întoarse împreuna cu un nou descriptor de socket, care este asociat cu clientul<br />

care a iniţiat conexiunea. După această asociere socket-ul s este din nou disponibil <strong>pentru</strong> a accepta noi<br />

conexiuni.<br />

- 6 - transferul datelor se poate realiza cu patru apeluri : send() şi recv() pe socket-urile conectate şi<br />

sendto() şi recvfrom() pe cele neconectate (de tip datagramă). De exemplu, pe socket-urile conectate :<br />

int bytes_sent;<br />

int bytes_received;<br />

char data_sent[256];<br />

char data_received[256];<br />

int send(int socket, char *buf, int buflen, int flags);<br />

int recv(int socket, char *buf, int buflen, int flags);<br />

int s;<br />

bytes_sent = send(s, data_sent, sizeof(data_sent), 0);<br />

bytes_received = recv(s, data_received, sizeof(data_received), 0);<br />

- 7 - <strong>pentru</strong> socket-uri neconectate trebuie furnizate informaţii suplimentare de adresă şi utilizate<br />

apelurile sendto() şi recvfrom():<br />

int bytes_sent;<br />

int bytes_received;<br />

char data_sent[256];<br />

char data_received[256];<br />

struct sockaddr_in to;<br />

struct sockaddr from;<br />

int addlen;<br />

int sendto(int socket, char *buf, int buflen, int flags, struct sockaddr<br />

*addr, int addrlen);<br />

int recvfrom(int socket, char *buf, int buflen, int flags, struct sockaddr<br />

*addr, int addrlen);<br />

int s;<br />

to.sin_family = AF_INET;<br />

to.sin_addr = inet_addr("193.226.10.73");<br />

to.sin_port = htons(2200);<br />

bytes_sent = sendto(s, data_sent, sizeof(data_sent), 0, (struct sockaddr *)<br />

128


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

&to, sizeof(to));<br />

addrlen = sizeof(from); /* trebuie initializat */<br />

bytes_received = recvfrom(s, data_received, sizeof(data_received), 0, &from,<br />

&addrlen);<br />

- 8 - verificare a stării socket-urilor, dacă au date disponibile <strong>pentru</strong> a fi citite sau sunt gata de emisie<br />

sau sunt în condiţii speciale, cu apelul input_ready():<br />

#define MAX_TIMEOUT 1000<br />

/* input_ready(insock) - verifica daca exista o intrare disponibila pe<br />

socket-ul insock. Returneaza :<br />

1 daca intrarea este disponibila<br />

0 daca intrarea nu este disponibila<br />

-1 in caz de eroare.<br />

*/<br />

int input_ready(insock)<br />

int insock; /* input socket */<br />

{<br />

int socks[1]; /* vector de sockets */<br />

long timeout = MAX_TIMEOUT;<br />

/* pune socket-ul de verificat in socks[] */<br />

socks[0] = insock;<br />

/* verifica disponibilitatea de citire pe acest socket */<br />

return select( socks, 1, 0, 0, timeout);<br />

}<br />

Aplicaţia setează măştile de biţi <strong>pentru</strong> a indica socket-urile testate <strong>pentru</strong> anumite condiţii şi<br />

indică un time-out. Dacă valoarea de time-out este 0, apelul nu aşteaptă un socket disponibil, altfel<br />

funcţia select() va aştepta un timp time-out <strong>pentru</strong> ca cel puţin un socket să fie disponibil în condiţiile<br />

indicate. Procedura este utilă <strong>pentru</strong> aplicaţii cu conexiuni multiple ce nu trebuiesc blocate în aşteptare<br />

pe o singură conexiune.<br />

- 9 - realizare de operaţii asincrone cu apelul ioctl():<br />

int s;<br />

int dontblock;<br />

char buf[256];<br />

int rc;<br />

int ioctl(int s, int command, char *command_data, int datalen);<br />

dontblock = 1;<br />

rc = ioctl(s, FIONBIO, (char *) &dontblock, sizeof(dontblock));<br />

if ( recv(s, buf, sizeof(buf), 0) == -1 && errno == EWOULDBLOCK)<br />

/* datele nu sunt disponibile */<br />

else<br />

/* s-au obtinut date sau a aparut o eroare */<br />

Exemplul plasează socket-ul s în modul fără blocare. Când s este trecut ca parametru la apeluri<br />

care s-ar bloca (de ex. recv() când datele nu sunt disponibile) apelul este returnat cu un cod de eroare şi<br />

variabila globală errno este setată la EWOULDBLOCK.<br />

- 10 - dealocare a socket-ului s cu apelul close() :<br />

Tibor Asztalos<br />

int rc;<br />

int s;<br />

int close(int s);<br />

rc = close(s);<br />

129


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

5.6. Particularităţi de implementare prin programare a modelului client-server<br />

Prin modelul client-server se înţelege un mod de abordare a rezolvării aplicaţiilor de reţea astfel<br />

încât soluţia cuprinde o colecţie de programe care se pot împărţii în două clase. Serverul, de obicei<br />

unul, este programul care este interogat de alte programe şi în urma interogării execută diverse acţiuni.<br />

Este partea cea mai sofisticată a aplicaţiei de reţea. Clientul, unul sau mai mulţi, reprezintă programe, în<br />

general cu un nivel scăzut de complexitate, care în principal reprezintă o interfaţă între utilizator şi<br />

server. Rolul programelor client este de prelua cererile utilizatorului (în modul cel mai evoluat se<br />

foloseşte o interfaţă grafica) şi de a le transmite serverului, după procesarea lor transmiţând<br />

utilizatorului rezultatele.<br />

În continuare se prezintă două scenarii de server, clasice în cazul aplicaţii de reţea construite cu<br />

sockets.<br />

Prin server iterativ se înţelege un server care procesează câte o cerere la un moment dat. Dacă<br />

viteza de sosire a cererilor este mai mare decât viteza de procesare atunci cererile noi sosite se<br />

stochează într-o coadă de aşteptare. Scenariul acestui server este dat mai jos:<br />

int sockfd, newsockfd;<br />

if ((sockfd=socket(...)) < 0)<br />

{ printf ("error ..."); exit(1);}<br />

if (bind(sockfd,...) < 0)<br />

{ printf ("error ..."); exit(1);}<br />

if (listen(sockfd,5) < 0)<br />

{ printf ("error ..."); exit(1);}<br />

for (;;)<br />

newsockfd=accept(sockfd, ...); /* blocare <strong>pentru</strong> asteptare cereri*/<br />

if (newsockfd < 0)<br />

{ printf ("error ..."); exit(1);}<br />

process(newsockfd); /*procesare cerere*/<br />

close (newsockfd);<br />

}<br />

În cazul serverului concurent scenariul este diferit, eliminându-se pierderea de cereri aşa cum se poate<br />

întâmpla în cazul scenariului de mai sus. În acest caz serverul creează un fiu <strong>pentru</strong> fiecare cerere<br />

primită. Transferă execuţia cererii la fiu şi serverul se întoarce la procesarea următoarei cereri.<br />

Scenariul unui astfel de server este :<br />

int sockfd, newsockfd;<br />

if ((sockfd=socket(...)) < 0)<br />

{ printf ("error ..."); exit(1);}<br />

if (bind(sockfd,...) < 0)<br />

{ printf ("error ..."); exit(1);}<br />

if (listen(sockfd,5) < 0)<br />

{ printf ("error ..."); exit(1);}<br />

for (;;)<br />

newsockfd=accept(sockfd, ...); /* blocare <strong>pentru</strong> asteptare cereri*/<br />

if (newsockfd < 0)<br />

{ printf ("error ..."); exit(1);}<br />

if (fork()==0) { /* procesul fiu */<br />

close(sockfd);<br />

process(newsockfd); /*procesare cerere*/<br />

exit(0);<br />

}<br />

Tibor Asztalos<br />

130


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

close (newsockfd); /* parinte */<br />

}<br />

5.7. Programare socket avansată<br />

5.7.1. Utilizarea socket-urilor <strong>pentru</strong> accesare la nivel scăzut<br />

Conceptul de bază în socket-ul de nivel scăzut este trimiterea unui singur pachet la un moment<br />

dat, cu toate header-ele (antetele) protocoalelor completate de program (in locul kernel-ului). Unix<br />

furnizeaza doua feluri de socket care permit accesul direct la retea. Unul este SOCK_PACKET,<br />

care primeste si trimite date in nivelul de legatura. Aceasta inseamna, ca header-ul specific placii de<br />

retea este inclus in datele care vor fi scrise sau citite. Pentru cele mai multe retele, acesta este<br />

header-ul ethernet. Desigur, toate protocoalele următoare vor fi de asemenea incluse in date. Totusi,<br />

tipul de socket pe care il vom utiliza, este SOCK_RAW, care include headerele IP si cele de sub el<br />

in date.<br />

Modelul pe straturi (simplificat) arata cam asa:<br />

Nivelul Fizic -> Nivelul de Legatura sau Dispozitiv (protocolul Ethernet) -> Nivelul Retea (IP) -><br />

Nivelul Transport (TCP, UDP, ICMP) -> Nivelul Sesiune (datele specifice aplicatiei).<br />

O comanda standard <strong>pentru</strong> a crea un socket raw este:<br />

socket(PF_INET, SOCK_RAW, IPPROTO_UDP);<br />

Din momentul in care este creat, poti trimite orice pachet IP prin el, si poti primi orice pachet IP<br />

care a fost primit de masina gazda dupa ce a fost creat socket-ul si daca citesti (read()) de la el. De<br />

retinut: cu toate ca socket-ul este o interfata catre header-ul IP, el ramane specific nivelului de<br />

transport. Asta inseamna, ca <strong>pentru</strong> a asculta trafic TCP, UDP sau ICMP, trebuie create separat 3<br />

raw socket, utilizand IPPROTO_TCP, IPPROTO_UDP si IPPROTO_ICMP (numerele de protocol<br />

sunt 0 sau 6 <strong>pentru</strong> tcp, 17 <strong>pentru</strong> udp si 1 <strong>pentru</strong> icmp). Cu aceste cunostinte, putem deja, de<br />

exemplu, sa creem un mic sniffer, care sa afiseze continutul tuturor pachetelor pe care le primim.<br />

(Header-ele etc. lipsesc, asta este doar un exemplu).<br />

int fd = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);<br />

char buffer[8192]; /* single packets are usually not bigger than 8192<br />

bytes */<br />

while (read (fd, buffer, 8192) > 0)<br />

Tibor Asztalos<br />

printf ("Caught tcp packet: %s\n", buffer+sizeof(struct<br />

iphdr)+sizeof(struct tcphdr));<br />

Observaţii privind utilizarea raw-socket pe maşinile Unix:<br />

• SOCK_RAW permite accesul direct la nivelul IP (IPv4 sua IPv6) trecând astfel peste nivelul<br />

transport al stivei de protocoale TCP/IP.<br />

• Crearea de socket de acest tip cere drepturi de superuser. Această fază poartă caracteristicile<br />

principale:<br />

• SOCK_RAW al doilea argument al apelului socket().<br />

• IP_HDRINCL poate fi setat folosind apelul setsockopt(), opţiune ce va permite aplicaţiei<br />

specificarea şi a antetului IP.<br />

• apelul bind() poate fi folosit <strong>pentru</strong> a specifica adresa locală.<br />

• apelul connect() poate fi invocat (similar cazului SOCK_DGRAM) <strong>pentru</strong> a stabili adresa<br />

destinaţie.<br />

• Transmisia de date prin Raw Socket<br />

• folosind apelurile sendto() sau sendmsg() prin specificarea adresei IP destinaţie.<br />

• Folosind apelurile write(), writev() sau send() <strong>pentru</strong> un socket conectat.<br />

• Permite trimiterea unor datagrame ICMPv4, ICMPv6, IGMPv4 etc.<br />

131


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

• dacă opţiunea IP_HDRINCL nu este setată, adresa de început indicată nucleului trebuie să<br />

indice primul octet de după antetul IP.<br />

Completat de<br />

nucleu<br />

• Dacă opţiuinea IP_HDRINCL este setată, această adresă de început indicată nucleului<br />

trebuie să fie adresa primului octet din antetul IP.<br />

• Nucleul se va ocupa de fragmentarea pachetelor ce depăşesc în lungime MTU al reţelei<br />

locale de acces.<br />

• Tot nucleul este cel care în cazul IPv4 calculează şi completează suma de control a antetului<br />

pachetelor IP.<br />

• Recepţiile Raw Socket<br />

• pachetele raw sockets nu sunt livrate niciodată entităţilor de protocol TCP sau UDP.<br />

• cele mai multe tipuri de pachete ICMP vor fi livrate unui raw socket doar după ce nucleul<br />

sistemului de operare le-a procesat.<br />

• toate pachetele IGMP vor fi livrate unui raw socket doar după ce nucleul sistemului de operare<br />

le-a procesat.<br />

• toate datagramele IP cu câmpul protocol nerecunoscut de către nucleul SO vor fi livrate unui<br />

raw socket.<br />

• nucleul aşteaptă până ce ajung toate fragmentele unei datagrame fragmentate le reasamblează în<br />

datagrama iniţială şi livrează unui raw socket.<br />

• verificările efectuate de către nucleu înaintea livrării datelor unui raw socket sunt:<br />

• dacă o valoare nenulă a câmpului protocol a fost specificat la crearea unui raw socket<br />

acestuia vor fi livrate doar datagramele recepţionate <strong>pentru</strong> acel protocol.<br />

• dacă, prin utilizarea apelului bind() a fost specificată o adresă IP locală conectat de un raw<br />

socket, atunci adresa IP destinaţie a datagramelor IP recepţionate trebuie să corespundă cu<br />

aceasta <strong>pentru</strong> a i se livra.<br />

• dacă o adresă IP destinaţie a fost specificată prin utilizarea apelului connect() , atunci adresa<br />

IP sursă din antetul datagramelor recepţionate trebuie să corespundă cu aceasta.<br />

• dacă un raw socket este creau cu numărul de protocol 0, şi nu s-a făcut nici un appel bind() sau<br />

connect(), atunci socket-ului I se va livra o copie a tuturor datagramelor de tip raw recepţionate.<br />

• nucleul livrează întreaga datagramă, inclusiv antetul IP, unui raw IPv4 socket.<br />

5.7.1.1. Protocoalele IP, ICMP, TCP si UDP<br />

Pentru a injecta propriile pachete, tot ce e nevoie să ştim este structura protocoalelor care se<br />

includ. Mai jos se găseşte o scurtă introducere în header-ele IP, ICMP, TCP şi UDP. Este<br />

recomandat să fie construite pachetele utilizând o structură, astfel încât să se completeze uşor<br />

heder-ele. Sistemele Unix furnizează structuri standard în fişierele antet (de exemplu<br />

). Se poate întotdeauna crea structuri proprii, atât timp cât lungimea fiecărei opţiuni<br />

este corectă.<br />

Tibor Asztalos<br />

IP Header Data<br />

IP Header Data<br />

132


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tipurile/mărimea de date pe care le folosim sunt:<br />

• unsigned char - 1 octet (8 biti)<br />

• unsigned short int - 2 octeti (16 biti)<br />

• unsigned int - 4 octeti (32 biti)<br />

• struct ipheader {<br />

unsigned char ip_hl:4, ip_v:4; /* this means that each member is 4 bits<br />

*/<br />

unsigned char ip_tos;<br />

unsigned short int ip_len;<br />

unsigned short int ip_id;<br />

unsigned short int ip_off;<br />

unsigned char ip_ttl;<br />

unsigned char ip_p;<br />

unsigned short int ip_sum;<br />

unsigned int ip_src;<br />

unsigned int ip_dst;<br />

}; /* total ip header length: 20 bytes (=160 bits) */<br />

Protocolul Internet este protocolul nivelului reţea, utilizat <strong>pentru</strong> dirijarea datelor de la sursă la<br />

destinaţie. Fiecare datagramă conţine un header IP urmat de protocolul nivelului de transport, cum<br />

ar fi tcp.<br />

• ip_hl: lungimea header-ului IP în cuvinte de 32 biţi. Asta înseamnă că o valoare de <strong>pentru</strong><br />

ip_hl reprezintă 20 de octeţi (5*4). Valori diferite de 5 sunt necesare doar dacă header-ul IP<br />

conţine opţiuni (cel mai des <strong>pentru</strong> dirijare)<br />

• ip_v: versiunea IP este întotdeauna 4<br />

• ip_tos: tipul serviciului, controlează prioritatea pachetului. 0x00 este normal. Primii 3 biţi<br />

reprezintă prioritatea de rutare, următorii 4 biţi <strong>pentru</strong> tipul serviciului (întarziere, debit,<br />

siguranţă şi cost)<br />

• ip_len: trebuie să conţină lungimea totală a datagramei IP<br />

• ip_id: număr de identificare, este folosit în special <strong>pentru</strong> reasamblarea datagramelor IP<br />

fragmentate. Când se trimite o singură datagramă, fiecare poate avea identificatori arbitrari<br />

• ip_off: offset-ul fragmentului este utilizat <strong>pentru</strong> reasamblarea datagramelor fragmentate.<br />

Primii 3 biţi sunt flag-ul fragmentului, primul este întotdeauna 0, al doilea bitul a-nu-sefragmenta<br />

(setat cu ip_off |= 0x4000) iar al treilea flagul-continuă sau flagul-urmeazăfragmente<br />

(ip_off |= 0x2000). Următorii 13 biţi sunt offset-ul fragmentului, conţinând<br />

numărul de pachete de 8 octeţi trimise deja<br />

• ip_ttl: timpul de viata, este numărul de hop-uri (rutere prin care trece) înainte ca pachetul să<br />

fie descărcat, şi un mesaj icmp de eroare să fie returnat. Maximul este 255<br />

• ip_p: protocolul din nivelul de transport. Poate fi tcp (6), udp (17), icmp (1), sau orice alt<br />

protocol urmează header-ului IP.<br />

• ip_sum: suma de control a datagramei <strong>pentru</strong> întreaga datagrama IP. De fiecare dată când se<br />

schimbă ceva în datagrama, e nevoie să se recalculeze, sau pachetul va fi descărcat de<br />

următorul ruter.<br />

• ip_src şi ip_dst: adresa IP sursă si destinaţie, convertită la format long, de exemplu de<br />

inet_addr(). Ambele pot fi alese arbitrar.<br />

Protocolul IP nu are nici un mecanism <strong>pentru</strong> stabilirea şi menţinerea unei conexiuni, nici măcar nu<br />

poate conţine date ca încărcătură directă. Protocolul Internet de Control al Mesajelor ICMP<br />

reprezintă o mică adăugare la IP <strong>pentru</strong> a transporta mesaje de erori, de rutare, controlul mesajelor<br />

şi date, şi deseori este considerat ca un protocol al nivelului de reţea.<br />

struct icmpheader {<br />

Tibor Asztalos<br />

unsigned char icmp_type;<br />

unsigned char icmp_code;<br />

133


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

Tibor Asztalos<br />

unsigned short int icmp_cksum;<br />

/* The following data structures are ICMP type specific */<br />

unsigned short int icmp_id;<br />

unsigned short int icmp_seq;<br />

}; /* total icmp header length: 8 bytes (=64 bits) */<br />

icmp_type: tipul mesajului, de exemplu 0 - echo reply, 8 - echo request, 3 - destination<br />

unreachable. Vezi <strong>pentru</strong> toate tipurile<br />

icmp_code: acest câmp este semnificativ când se trimite un mesaj de eroare (unreach), şi<br />

specifică tipul erorii. Din nou, putem consulta fişierul header <strong>pentru</strong> mai multe<br />

icmp_cksum: sumă de control <strong>pentru</strong> header-ul icmp împreuna cu datele. Este la fel ca suma<br />

de control IP.<br />

Observaţie: următorii 32 de biţi dintr-un pachet icmp pot fi folosiţi în diferite moduri.<br />

Aceasta depinde de tipul şi codul icmp. Cea mai des intalnită structură, o secvenţă ID, este<br />

utilizată în cererile şi răspunsurile echo.<br />

icmp_id: utilizat în mesajele echo request/reply, <strong>pentru</strong> a identifica cererea,<br />

icmp_seq: identifică secvenţa mesajelor echo, dacă mai mult de unul este trimis<br />

Protocolul datagramă utilizator UDP, este un protocol de transport <strong>pentru</strong> sesiuni care necesită<br />

schimbul de date. Ambele protocoale de transport, UDP şi TCP furnizează 65535 de porturi sursă şi<br />

destinaţie diferite. Portul destinaţie este utilizat <strong>pentru</strong> conectarea la un serviciu specific pe acel<br />

port. Spre deosebire de TCP, UDP nu este sigur, din moment ce nu utilizează numere de secvenţă şi<br />

conexiune cu stare. Aceasta înseamnă că datagramele UDP nu pot fi sigure (de exemplu pot fi<br />

pierdute fără notificare), din moment ce nu sunt certificate utilizând răspunsuri şi numere de<br />

secvenţă.<br />

struct udpheader {<br />

unsigned short int uh_sport;<br />

unsigned short int uh_dport;<br />

unsigned short int uh_len;<br />

unsigned short int uh_check;<br />

}; /* total udp header length: 8 bytes (=64 bits) */<br />

uh_sport: Portul sursa pe care clientul il atasaza cu bind(), si pe care server-ul contactat va<br />

raspunde, uh_dport: Portul destinatie pe care un server specific poate fi contactat, uh_len:<br />

Lungimea header-ului udp si a datelor incarcate in octeti, uh_check: Suma de control a<br />

header-ului si a datelor, vezi suma de control IP<br />

Protocolul de Control al Transmisiei este cel mai des utilizat protocol care furnizeaza mecanisme de<br />

stabilire sigura a conexiunii cu cateva autentificari de baza, utilizand conexiuni cu stare si numere<br />

de secventa.<br />

struct tcpheader {<br />

unsigned short int th_sport;<br />

unsigned short int th_dport;<br />

unsigned int th_seq;<br />

unsigned int th_ack;<br />

unsigned char th_x2:4, th_off:4;<br />

unsigned char th_flags;<br />

unsigned short int th_win;<br />

unsigned short int th_sum;<br />

unsigned short int th_urp;<br />

134


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}; /* total tcp header length: 20 bytes (=160 bits) */<br />

• th_sport: Portul sursa, care are aceeasi functie ca si la UDP<br />

• th_dport: Portul destinatie, care are aceeasi functie ca si la UDP<br />

• th_seq: Numarul de secventa este utilizat <strong>pentru</strong> enumerarea segmentelor TCP. Datele dintro<br />

conexiune TCP pot fi continute in orice numar de segmente (= 0 singura datagrama tcp),<br />

care vor fi puse in ordine si certificate. De exemplu, daca trimiti 3 segmente, fiecare<br />

continand 32 octeti de date, prima secventa ar trebui sa fie (N+)1, a doua (N+)33 iar a treia<br />

(N+)65. "N+" <strong>pentru</strong> ca secventa initiala este aleatoare.<br />

• th_ack: Fiecare pachet care este trimis si fiecare parte valida a conexiunii este certificata cu<br />

un segment TCP gol care are flag-ul ACK setat (vezi mai jos), iar campul th_ack contine<br />

numarul th_seq anterior<br />

• th_x2: Nu este utilizat si contine zerouri binare<br />

• th_off: Offset-ul segmentului specifica lungimea header-ului TCP in blocuri de 32 biti/4<br />

octeti. Fara optiunile header-ului tcp, valoarea este 5.<br />

• th_flags: Acest camp consta in sase flaguri binare. Utilizand header-ele bsd, ele pot fi<br />

combinate dupa cum urmeaza: th_flags = FLAG1 | FLAG2 | FLAG3...<br />

• TH_URG: Urgent. Segmentul va fi rutat mai repede, este utilizat <strong>pentru</strong> terminarea<br />

unei conexiuni sau <strong>pentru</strong> oprirea proceselor (folosind protocolul telnet).<br />

• TH_ACK: Certificare. Utilizat <strong>pentru</strong> certificarea datelor si in a doua si a treia parte a<br />

unei initalizari de conexiune TCP<br />

• TH_PSH: Push. Stiva IP a sistemului nu va pastra in memorie segmentul ci il va<br />

inainta imediat catre aplicatie (cel mai des folosit cu telnet)<br />

• TH_RST: Reset. Spune perechii ca sa terminat conexiunea<br />

• TH_SYN: Sincronizare. Un segment cu flag-ul SYN setat indica un client care vrea<br />

sa initieze o noua conexiune catre portul destinatie<br />

• TH_FIN: Final. Conexiunea trebuie inchisa, perechea se presupune ca raspunde cu<br />

un ultim segment ce are de asemenea flag-ul FIN setat.<br />

• th_win: Window. Numarul de octeti care pot fi trimisi inainte ca datele sa fie certificate cu<br />

un ACK, inainte de a trimite mai multe segmente<br />

• th_sum: Suma de control a pseudo header-ului, header-ului tcp si a incarcaturii de date.<br />

Pseudo header-ul este o structura continand adresa sursa si destinatie IP, 1 octet setat la<br />

zero, protocolul (1 octet cu valoarea zecimala 6), si 2 octeti (unsigned short) continand<br />

lungimea totala a segmentului tcp<br />

• th_urp: Pointer urgent. Folosit doar daca flagul urgent este setat, alfel zero. Pointeaza catre<br />

sfarsitul incarcaturii de date care trebuie timisa cu prioritate<br />

5.7.1.2 Construirea si injectarea datagramelor<br />

Acum, punand cap la cap cunostintele despre structurile header-elor protocoalelor cu cateva functii<br />

C de baza, este usor sa contruim si sa trimitem orice datagrama. Vom demonstra aceasta cu un mic<br />

program exemplu care trimite constant cereri SYN catre o singura masina gazda.<br />

#define __USE_BSD /* use bsd'ish ip header */<br />

#include /* these headers are for a Linux system, but */<br />

#include /* the names on other systems are easy to guess..<br />

*/<br />

#include <br />

#define __FAVOR_BSD /* use bsd'ish tcp header */<br />

#include <br />

#include <br />

#define P 25 /* lets flood the sendmail port */<br />

Tibor Asztalos<br />

135


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

unsigned short /* this function generates header checksums */<br />

csum (unsigned short *buf, int nwords)<br />

{<br />

}<br />

Tibor Asztalos<br />

unsigned long sum;<br />

for (sum = 0; nwords > 0; nwords--)<br />

sum += *buf++;<br />

sum = (sum >> 16) + (sum & 0xffff);<br />

sum += (sum >> 16);<br />

return ~sum;<br />

int<br />

main (void)<br />

{<br />

int s = socket (PF_INET, SOCK_RAW, IPPROTO_TCP); /* open raw socket<br />

*/<br />

char datagram[4096]; /* this buffer will contain ip header, tcp<br />

header,<br />

and payload. we'll point an ip header structure<br />

at its beginning, and a tcp header structure after<br />

that to write the header values into it */<br />

struct ip *iph = (struct ip *) datagram;<br />

struct tcphdr *tcph = (struct tcphdr *) datagram + sizeof (struct<br />

ip);<br />

struct sockaddr_in sin;<br />

/* the sockaddr_in containing the dest. address is used<br />

in sendto() to determine the datagrams path */<br />

sin.sin_family = AF_INET;<br />

sin.sin_port = htons (P);/* you byte-order >1byte header values to<br />

network byte order (not needed on big endian machines) */<br />

sin.sin_addr.s_addr = inet_addr ("127.0.0.1");<br />

memset (datagram, 0, 4096); /* zero out the buffer */<br />

/* we'll now fill in the ip/tcp header values, see above for<br />

explanations */<br />

iph->ip_hl = 5;<br />

iph->ip_v = 4;<br />

iph->ip_tos = 0;<br />

iph->ip_len = sizeof (struct ip) + sizeof (struct tcphdr); /* no<br />

payload */<br />

iph->ip_id = htonl (54321); /* the value doesn't matter here */<br />

iph->ip_off = 0;<br />

iph->ip_ttl = 255;<br />

iph->ip_p = 6;<br />

iph->ip_sum = 0; /* set it to 0 before computing the actual checksum<br />

later */<br />

iph->ip_src.s_addr = inet_addr ("1.2.3.4");/* SYN's can be blindly<br />

spoofed */<br />

iph->ip_dst.s_addr = sin.sin_addr.s_addr;<br />

tcph->th_sport = htons (1234); /* arbitrary port */<br />

tcph->th_dport = htons (P);<br />

tcph->th_seq = random ();/* in a SYN packet, the sequence is a<br />

random */<br />

tcph->th_ack = 0;/* number, and the ack sequence is 0 in the 1st<br />

packet */<br />

tcph->th_x2 = 0;<br />

tcph->th_off = 0; /* first and only tcp segment */<br />

tcph->th_flags = TH_SYN; /* initial connection request */<br />

tcph->th_win = htonl (65535); /* maximum allowed window size */<br />

tcph->th_sum = 0;/* if you set a checksum to zero, your kernel's IP<br />

stack should fill in the correct checksum during transmission */<br />

tcph->th_urp = 0;<br />

136


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

}<br />

Tibor Asztalos<br />

iph->ip_sum = csum ((unsigned short *) datagram, iph->ip_len >> 1);<br />

/* finally, it is very advisable to do a IP_HDRINCL call, to make<br />

sure that the kernel knows the header is included in the data, and<br />

doesn't insert its own header into the packet before our data */<br />

{ /* lets do it the ugly way.. */<br />

int one = 1;<br />

const int *val = &one;<br />

if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0)<br />

printf ("Warning: Cannot set HDRINCL!\n");<br />

}<br />

while (1)<br />

{<br />

if (sendto (s, /* our socket */<br />

datagram, /* the buffer containing headers and data */<br />

iph->ip_len, /* total length of our datagram */<br />

0, /* routing flags, normally always 0 */<br />

(struct sockaddr *) &sin, /* socket addr, just like in */<br />

sizeof (sin)) < 0) /* a normal send() */<br />

printf ("error\n");<br />

else<br />

printf (".");<br />

}<br />

return 0;<br />

5.7.1.3 Operatii de baza in nivelul de transport<br />

Pentru a ne putea folosi de pachetele raw, cunostinte de baza despre operatiile stivei IP sunt<br />

esentiale. Vom incerca sa dam o scurta introducere asupra celor mai importante operatii din stiva<br />

IP. Cel mai important protocol, este desigur TCP, asupra caruia ne vom concentra.<br />

Initializarea conexiunii: <strong>pentru</strong> a contacta un server udp sau tcp care asculta pe portul 1234, clientul<br />

apeleaza functia connect() cu structura sockaddr continand adresa si portul destinatie. Daca clientul<br />

nu atasaza cu bind() un port sursa, stiva IP a sistemului va selecta unul si il va atasa. Prin conectare,<br />

gazada trimite o datagrama continad urmatoarele informatii:<br />

• IP src: adresa clientului<br />

• IP dst: adresa serverului<br />

• TCP/UDP src: portul sursa al clientului<br />

• TCP/UDP dst: portul 1234. Daca un server este localizat pe portul 1234 pe masina<br />

destinatie, acesta va raspunde cu o datagrama continand: IP src, server IP dst, client srcport,<br />

server dstport. Daca nu exista nici un server pe acea masina, un mesaj ICMP de tip unreach<br />

este creat, subcod "Connection refused". Clientul apoi va termina. Daca masina destinatie<br />

este cazuta, ori un ruter va crea un mesaj ICMP unreach, ori clientul nu primeste nici un<br />

mesaj iar conexiunea se intrerupe datorita timpului de asteptare prea lung.<br />

Initierea TCP ("3-way handshake") si conectarea: clientul va face o initiere de conectare, cu flagul<br />

tcp SYN setat, un numar de secventa arbitrar, si fara numar de certificare. Serverul certifica mesajul<br />

SYN trimitand un pachet cu flagul SYN|ACK setat, alt numar de secventa aleator si ca numar de<br />

certificare secventa originala. In sfarsit, clientul raspunde cu o datagrama tcp cu flag-ul ACK setat,<br />

si cu secventa ack primita de la server incrementata cu unu. Odata ce conexiunea este stabilita,<br />

fiecare segment tcp va fi trimis fara nici un flag (PSH sau URG sunt optionale), numarul de<br />

secventa <strong>pentru</strong> fiecare pachet incrementat cu marimea segmentului tcp anterior. Dupa ce cantitatea<br />

de date specificata ca "window size" a fost transferata, perechea care trimite date va astepta <strong>pentru</strong><br />

137


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

certificare, un segment tcp cu flag-ul ACK setat, iar ca numar ack, unul din ultimile pachete de date<br />

care au putut fi primite. In acest mod, daca un segment se pierde, nu va fi certificat, si pot fi<br />

retransmise. Pentru a opri o conexiune, atat clientul cat si serverul trimit un pachet tcp cu numarul<br />

de secventa corect si flagul FIN setat, iar daca conexiunea se intampla vreodata sa se desincronizeze<br />

(abort, desynchronized, numar de secventa eronat etc.) perechea care observa eroarea va trimite un<br />

pachet RST cu numarul corect de secventa <strong>pentru</strong> a termina conexiunea.<br />

5.7.2. Opţiuni avansate de utilizare a socket-urilor<br />

Există trei căi posibile de manevrare a oţiunilor de utilizare a socket-urilor şi-anume:<br />

• funcţiile getsockopt() şi setsockopt()<br />

• funcţia fcntl()<br />

• funcţia ioctl()<br />

Funcţia getsockopt()<br />

Are sintaxa:<br />

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);<br />

returnează 0 în caz de succes, -1 în caz de eroare.<br />

• level indică dacă opţiunea de socket vizată este unul generic sau unul specific unui protocol anume.<br />

• optval este un pointer către o variabilă în care va fi citită valoarea curentă a opţiunii prin apelul<br />

funcţiei getsockopt().<br />

• optlen este dimensiunea variabilei optval.<br />

Funcţia setsockopt()<br />

Sintaxa:<br />

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);<br />

returnează 0 în caz de succes, -1 în caz de eroare.<br />

• level indică dacă opţiunea de socket vizată este unul generic sau unul specific unui protocol anume.<br />

• optval este un pointer către o variabilă din care va fi citită valoarea opţiunii <strong>pentru</strong> setare cu apelul<br />

funcţiei setsockopt().<br />

• optlen este dimensiunea variabilei optval.<br />

Există două tipuri de bază de opţiuni:<br />

- de tip flag - opţiune binară ce validează sau invalidează o anumită caracteristică.<br />

- de tip valoare - opţiune cu o anumită valoare.<br />

• Opţiunile Socket pot fi grupate în patru mari categorii:<br />

- opţiuni socket generice<br />

– SO_RCVBUF, SO_SNDBUF, SO_BROADCAST, etc.<br />

- opţiuni IPv4<br />

– IP_TOS, IP_MULTICAST_IF, etc.<br />

Tibor Asztalos<br />

138


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

- opţiuni IPv6<br />

– IPv6_HOPLIMIT, IPv6_NEXTHOP, etc.<br />

- opţiuni TCP<br />

– TCP_MAXSEG, TCP_KEEPALIVE, etc.<br />

Unele dintre opţiuni sunt legate de starea Socket-urilor, opţiuni ce sunt setate sau inspectate în funcţie<br />

de starea curentă a unui socket. Unele dintre aceste opţiuni sunt moştenite de la socket-ul server (în<br />

ascultare) de către socket-ul client (conectat de). Altele pot fi setate în mod independent atât de partea<br />

server cât şi de partea client. De exemplu SO_RCVBUF şi SO_SNDBUF sunt opţiuni ce sunt setate<br />

înaintea apelurilor listen() de partea server şi respectiv connect() de partea client.<br />

5.7.2.1 Opţiuni socket generice<br />

• sunt independente de protocol.<br />

• sunt gestionate în partea comună a codului de sistem ce implementează interfaţa socket<br />

• nu toate opţiunile generice sunt suportate de toate tipurile de socket. Anumite opţiuni sunt specifice<br />

doar anumitor tipuri de socket (SOCK_DGRAM, SOCK_STREAM).<br />

Câteva opţiuni generice: SO_BROADCAST, SO_DONTROUTE, SO_ERROR, SO_KEEPALIVE,<br />

SO_LINGER, SO_RCVBUF, SO_SNDBUF, SO_REUSEADDR<br />

• SO_BROADCAST<br />

- permite sau nu unui proces să trimită mesaje cu difuzare.<br />

- este suportat doar de către un socket de tip datagramă.<br />

- valoarea implicită este off.<br />

• SO_ERROR<br />

- Eroare în aşteptare - când apare o eroare pe un socket nucleul setează variabila so_error.<br />

- Procesul poate fi notoficat de această eroare în două moduri:<br />

– dacă procesul este blocat într-un appel select() fie <strong>pentru</strong> citire fie scriere, aceasta va returna<br />

cu specificarea codului de eroare apărut.<br />

– dacă procesul foloseşte mecanisme I/O orientat pe evenimente, semnalul SIGIO va fi generat<br />

şi livrat procesului.<br />

• SO_KEEPALIVE<br />

- scopul acestei opţiuni este detectarea căderilor sistemului pereche distant Astfel, această opţiune<br />

SO_KEEPALIVE va detecta conexiunile semideschise şi le termină.<br />

- dacă această opţiune este setată şi nu a avut loc un transfer de date timp de două ore (durată ce se<br />

poate stabili printr-o altă opţiune), entitatea TCP va declanşa o procedură de testare a conexiunii<br />

curente prin trimiterea unui pachet de control (numită "keepalive probe").<br />

– dacă perechea răspunde cu ACK, conexiunea se menţine deschisă şi numai după alte două ore<br />

se va verifica din nou activitatea pe aceea conexiune.<br />

– dacă perechea răspunde cu RST (sistemul a fost reboot-at probabil), se va seta o condiţie de<br />

eroare în variabila ECONNRESET iar socket-ul este închis.<br />

– dacă nu răspunde la mai mult de opt astfel de testări vor fi setate erorile socket în aşteptare fie<br />

ETIMEDOUT fie EHOSTUNREACH iar socket-ul închis.<br />

Tibor Asztalos<br />

139


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

• SO_LINGER<br />

- determină modul în care operează funcţia close() în cazul unui protocol ce oferă servicii orientate pe<br />

conexiune. Implicit, apelul revine imediat iar datele de transmis în aşteptare din buffer-ul de emisie sunt<br />

expediate entităţii de pereche.<br />

- implică un schimb de o structură de date formată dintr-un flag l_onoff şi o valoare de temporizare<br />

l_linger dintre procesul utilizator şi nucleu. Această structură este:<br />

struct linger {<br />

int l_onoff ;<br />

int l_linger ;<br />

}<br />

Scenarii posibile:<br />

– l_onoff este 0 - l_linger este ignorat.<br />

– l_onoff non-zero, l_linger este 0 - TCP abandonează conexiunea, descarcă toate datele din buffer-ul<br />

de emisie a socket-ului şi trimite un RST capătului celălalt.<br />

– l_onoff non-zero, l_linger non-zero - dacă există date de transmis în buffer-ul de emisie al socket-ului<br />

procesul va fi suspendat (stare de "somn") până la transmiterea şi confirmarea lor sau până la intrarea în<br />

time-out.<br />

• SO_RCVLOWAT şi SO_SNDLOWAT<br />

SO_RCVLOWAT - Receive Low Water Mark - cantitatea minimă de date în buffer-ul de recepţie a<br />

unui socket ca acesta să fie gata <strong>pentru</strong> citire.<br />

SO_SNDLOWAT - Send Low Water Mark - cantitatea minimă de spaţiu liber în buffer-ul de emisie a<br />

unui socket <strong>pentru</strong> ca acesta să poată fi scris.<br />

- aceste opţiuni sunt valabile doar <strong>pentru</strong> socket-uri de tip TCP (SOCK_STREAM) sau UDP<br />

(SOCK_DGRAM).<br />

• SO_RCVTIMEO şi SO_SNDTIMEO<br />

- aceste opţiuni permit specificarea time-out-urilor de emisie şi respectiv recepţie <strong>pentru</strong> un socket.<br />

Valorile de time-out trebuie să fie specificate într-o structură de tip timeval de forma:<br />

struct timeval {<br />

long tv_sec ;<br />

long tv_usec ;<br />

}<br />

-invalidarea unui time-out, se poate opţine prin valori nule a acestor variabile.<br />

• SO_REUSEADDR<br />

- permite unui server în ascultare să se reseteze şi să se reconecteze pe WKA (Well Known Address)<br />

anterior folosit chiar dacă conexiuni anterior stabilite mai există.<br />

- permite ca multiple instanţe ale aceluiaşi server să opereze pe un acelaş port, atâta timp cât fiecare se<br />

conectează (cu bind()) la o adresă IP locală diferită.<br />

- permite unui singur proces să conecteze un acelaş port la socket-uri multiple, atâta timp cât fiecare<br />

este conectat la o adresă IP locală diferită.<br />

- permite conexiuni total duplicate <strong>pentru</strong> socket-uri UDP (broadcast şi multicast).<br />

Tibor Asztalos<br />

140


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

• SO_DONTROUTE<br />

• opţiune booleană: permite invalidarea dirijării unei datagrame IP, foarte util în restricţionarea locală a<br />

transmisiilor IP.<br />

• este folosit, de regulă, de către procesele de dirijare din nodurile de comutare a pachetelor IP.<br />

5.7.2.2 Opţiuni specifice <strong>pentru</strong> socket-uri TCP sau UDP<br />

• TCP_KEEPALIVE<br />

- specifică timpul de aşteptare (în secunde) înainte ca entitatea TCP să declanşeze procedura de<br />

"keepalive probes".<br />

- este validă doar dacă opţiunea SO_KEEPALIVE a fost validată <strong>pentru</strong> acel socket.<br />

- o serie de implementări de SO menţin aceşti parametrii de temporizare la nivel nucleu şi nu la nivel<br />

socket, astfel orice modificare a lor <strong>pentru</strong> un socket va afecta implicit toate celelalte definite.<br />

• TCP_NODELAY<br />

- folosit <strong>pentru</strong> invalidarea algoritmului lui Nagle în funcţionarea entităţii TCP sau a întârzierii<br />

confirmărilor.<br />

Acest algoritm al lui Nagle prevede -<br />

TCP nu trimite pachete noi de date dacă cel anterior transmis nu a fost confirmat. La sosirea confirmării<br />

vor fi transmise toate pachetele de date cumulate între timp. Acest comportament este util în cazul<br />

transmiterii unor pachete de date mici deoarece cauzează emiterea lor grupate.<br />

Întârzierea confirmărilor -<br />

TCP va întârzia puţin transmiterea confirmărilor pachetelor de date sosite pe celălalt sens în speranţa că<br />

vor exista în curând date utile de transmis şi pe direcţia curentă a confirmărilor şi ca atare ele vor putea<br />

fi incluseîn antetul acestor pachete de date.<br />

• TCP_MAXSEG: setează dimensiunea maximă a unui segment TCP (TCP maximum segment size) de<br />

transmisie pe un socket.<br />

5.7.2.3 Opţiuni IPv4<br />

• IP_HDRINCL: folosit de un raw IP socket cănd se doreşte furnizarea de către aplicaţie şi a antetului<br />

IP.<br />

• IP_TOS: permite completarea câmpului TOS, “Type-of-service”, al antetului datagramelor IP.<br />

• IP_TTL: permite completarea câmpului TTL, “Time-to-live”, al antetului datagramelor IP.<br />

Exerciţii:<br />

1. Să se scrie cele două module ale unei aplicaţii de comunicare de tip client-server cu socket-uri de tip<br />

stream.<br />

2. Să se realizeze un program client şi unul server ce utilizează protocolul UDP şi permite realizarea<br />

unui “echo” (transmiterea unui mesaj către server şi aşteptarea, de către client a unui răspuns din<br />

partea acestuia la mesajul transmis).<br />

Tibor Asztalos<br />

141


<strong>Software</strong> <strong>pentru</strong> <strong>comunicaţii</strong><br />

3. În baza conceptelor teoretice prezentate şi pornind de la rutinele indicate, să se realizeze o<br />

aplicaţie client-server ce să realizeze transferul unui fişier de pe maşina pe care rulează serverul pe<br />

cea pe care rulează clientul. Fişierul se va afla în directorul /tmp al maşinii serverului. Clientul are<br />

un set minim de comenzi şi serverul un set minim de mesaje de eroare.<br />

4. Să se realizeze un program client şi unul server ce utilizează protocolul UDP şi permite obţinerea<br />

orei curente de pe o maşină distantă.<br />

Tibor Asztalos<br />

142


Anexa 1.<br />

VERSIUNI DE UNIX<br />

---------------------------------------------------------------<br />

|Nume Bazat pe Fabricant |<br />

---------------------------------------------------------------<br />

|AIX SV IBM |<br />

|A/UX SV Apple |<br />

|BOS SV Bull |<br />

|BSD BSD University of California at Berkeley (UCB)|<br />

|Chorus SV Chorus |<br />

|Coherent SV Mark Williams Company |<br />

|EP/IX POSIX Control Data Corporation |<br />

|FreeBSD BSD UCB |<br />

|GNU Hurd Mach Free <strong>Software</strong> Foundation |<br />

|HP-UX SV+BSD Hewlet Packard |<br />

|IRIX SV Silicon Graphics |<br />

|Linux --- Linus Torvalds + alţi colaboratori |<br />

|Minix clone SV Tanenbaum (Vrije Universitet Amsterdam) |<br />

|Net/2 BSD UCB |<br />

|NEWS-OS ? Sony |<br />

|QNX ? Quantum <strong>Software</strong> |<br />

|OSF/1 Mach DEC, Open <strong>Software</strong> Foundation |<br />

|Plan 9 - AT&T |<br />

|SCO Xenix SV SCO |<br />

|SCO Unix SV SCO |<br />

|Solaris SV Sun Microsystems |<br />

|SunOs BSD Sun Microsystems |<br />

|Ultrix BSD DEC |<br />

|UNICOS SV Cray |<br />

|UnixWare SV Novell |<br />

|UTEK ? Tektronix |<br />

|Xenix SV Microsoft |<br />

---------------------------------------------------------------<br />

STANDARDUL POSIX 1003<br />

1003.1 funcţii de bibliotecă, apeluri de sistem<br />

1003.2 shell-ul şi utilitarele<br />

1003.3 metode de testare<br />

1003.4 timp real<br />

1003.5 limbajul Ada <strong>pentru</strong> Unix<br />

1003.6 securitatea<br />

1003.7 administraţia sistemului<br />

1003.8 accesul la fişiere<br />

1003.9 Limbajul FORTRAN <strong>pentru</strong> Unix<br />

1003.10 super-calculatoare<br />

1003.12 interfeţele independente de protocol<br />

1003.13 real-time profiles<br />

1003.15 interfeţele <strong>pentru</strong> supercalculatoare -- batch processing<br />

1003.16 limbajul C<br />

1003.17 servicii de directoare<br />

1003.18 POSIX standardized profile<br />

1003.19 FORTRAN 90<br />

i


Anexa 2.<br />

CRONOLOGIE Sisteme de operare<br />

1968 Proiectul Multics<br />

1969 Unics <strong>pentru</strong> PDP-7, Bell Labs, AT&T<br />

1971 Unix versiunea 1, asamblor PDP11/20<br />

1974 Unix versiuna 4, C<br />

1975 Unix versiunea 6, foarte răspîndit<br />

1978 2.x Berkeley <strong>Software</strong> Distribution (BSD)<br />

1978 3BSD, memorie virtuală<br />

1979 Unix V7 ``ultimul Unix adevărat''<br />

1980 4.0BSD<br />

1982 System III, Unix comercial de la AT&T<br />

1982 Fondat Sun Microsystems<br />

1983 System V<br />

1983 4.2BSD, TCP/IP<br />

1984 System V Release (SVR)2; publicată SV Interface Definition SVID<br />

1984 X/Open, consorţiu de comercianţi Unix<br />

1985 SVID1, SVR2<br />

1986 SVR3, SVID2<br />

1986 4.3BSD<br />

1986 Mach<br />

1987 X/Open Portability Guide 2 publicat<br />

1987 Alianţă AT&T -- Sun<br />

1988 SVR4, amestecă SV, BSD, SunOS<br />

1988 Se fondează OSF<br />

1989 X/Open Portability Guide 3<br />

1991 OSF/1 bazat pe SVR2; micronucleu Mach2.5<br />

1992 Linux e creat<br />

1992 X/Open Portability Guide 4<br />

1992 Windows NT 1.0<br />

1993 4.4BSD; dezvoltarea Unix la Berkeley încetează<br />

1993 Novell cumpără Unix System Laboratories<br />

1993 Novell vinde dreptul <strong>pentru</strong> marca UNIX lui X/Open<br />

1995 Digital OSF/1<br />

1996 GNU Hurd alfa lansat<br />

1998 Solaris 2.6<br />

1998 Windows NT 5.0 (amânat)<br />

2000 Windows2000<br />

ii


Anexa 3.<br />

Apeluri de sistem folosite <strong>pentru</strong> lucrul cu fişiere şi directoare în Unix (programare):<br />

int open(const char *pathname, int oflag, ... /* mode_t mode */);<br />

int creat(const char *pathname, mode_t mode);<br />

- întorc descriptorul de fişier <strong>pentru</strong> execuţie corectă, altfel -1<br />

- se includ types.h, stat.h (din sys), si fcntl.h<br />

- oflag poate fi o combinaţie de: O_RDONLY, O_WRONLY, O_RDWR, O_APPEND,<br />

O_CREAT, O_EXCL, O_TRUNC, O_NOCTTY, O_NONBLOCK, O_SYNC.<br />

int close(int filedes);<br />

- se întoarce 0 <strong>pentru</strong> execuţie corectă, altfel -1<br />

ssize_t read(int filedes, void *buff, size_t nbytes);<br />

ssize_t write(int filedes, const void *buff, size_t nbytes);<br />

- se întorc numărul de octeţi scrişi/citiţi sau 0 <strong>pentru</strong> sfârşit de fişier în cazul execuţiei corecte,<br />

altfel, la eroare, întorc -1<br />

int dup(int filedes);<br />

int dup2(int filedes, int filedes2);<br />

- întorc descriptorul de fişier <strong>pentru</strong> execuţie corectă, altfel -1<br />

- se include <strong>pentru</strong> cele cinci funcţii fişierul antet unistd.h<br />

off_t lseek(int filedes, off_t offset, int whence);<br />

- se întoarce noul offset din fişier dacă s-a executat corect, altfel -1<br />

- se include unistd.h şi types.h (din sys)<br />

- whence ia valorile: SEEK_SET, SEEK_CUR, SEEK_END<br />

int fcntl(int filedes, int cmd, ... /* int arg */);<br />

- se includ types.h (din sys), unistd.h şi fcntl.h<br />

- cmd poate fi una din: F_DUPFD, F_GETFD, F_SETFD, F_GETFL, F_SETFL, F_GETOWN,<br />

F_SETOWN, F_GETLK, F_SETLK, F_SETLKW<br />

- la întoarcere depinde de cmd dacă se întoarce ceva corect, altfel <strong>pentru</strong> eroare se întoarce -1.<br />

int ioctl(int filedes, int request, ...);<br />

- se includ unistd.h şi ioctl.h (din sys)<br />

- întoarce -1 în caz de eroare şi orice altceva în caz de execuţie corectă<br />

int stat(const char *pathname, struct stat *buf);<br />

int fstat(int filedes, struct stat *buf);<br />

int lstat(const char *pathname, struct stat *buf)<br />

int chmod(const char *pathname, mode_t mode);<br />

int fchmod(int files, mode_t mode);<br />

int chown(const char *pathname, uid_t owner, gid_t group);<br />

iii


int fchown(int filedes, uid_t owner, gid_t group);<br />

int lchown(const char *pathname, uid_t owner, gid_t group);<br />

int mkdir(const char *pathname, mode_t name);<br />

- toate întorc 0 <strong>pentru</strong> execuţie corectă, iar la eroare -1<br />

- unde mode poate lua valorile: S_ISUID, S_ISGID, S_ISVTX, S_IRWXU, S_IRUSR,<br />

S_IWUSR, S_IXUSR, S_IRWXG, S_IRGRP, S_IWGRP, S_IXGRP, S_IRWXO,<br />

S_IROTH, S_IWOTH, S_IXOTH<br />

mode_t umask(mode_t cmask);<br />

- întoarce fosta mască de creare a fişierelor<br />

- <strong>pentru</strong> toate cele zece funcţii de mai sus se includ types.h şi stat.h (din sys)<br />

int access(const char *pathname, int mode);<br />

- se include unistd.h<br />

- întoarce 0 în caz de execuţie corectă, altfel -1<br />

int chdir (char *path);<br />

int access(const char *pathname, int mode);<br />

int unlink(const char *pathname);<br />

int link(const char *existingpath, const char *newpath);<br />

int symlink(const char *actualpath, const char *sympath);<br />

int rmdir(const char *pathname);<br />

int chdir(const char *pathname);<br />

int fchdir(int filedes); /* not POSIX.1 */<br />

void sync(void); /* not POSIX.1 */<br />

int fsync(int filedes); /* not POSIX.1 */<br />

- unde mode poate lua valorile: R_OK, W_OK, X_OK, F_OK<br />

- toate întorc 0 <strong>pentru</strong> execuţie reuşită, în caz contrar -1<br />

int readlink(const char *pathname, char *buf, int bufsize);<br />

- întoarce numărul de octeţi citiţi <strong>pentru</strong> execuţie corectă, altfel -1<br />

char *getcwd(char* buf, size_t size);<br />

- întoarce buf dacă s-a executat corect, altfel -1<br />

- se include unistd.h <strong>pentru</strong> toate cele unsprezece funcţii de mai sus<br />

DIR *opendir(const char *pathname);<br />

struct dirent *readdir(DIR *dp);<br />

- întorc un pointer <strong>pentru</strong> execuţie corectă, altfel NULL<br />

void rewinddir(DIR dp);<br />

int closedir(DIR *dp);<br />

iv


- întoarce 0 <strong>pentru</strong> execuţie corectă, şi -1 în caz de eroare<br />

- <strong>pentru</strong> ultimele patru funcţii se includ types.h (din sys) şi dirent.h<br />

int remove(const char *pathname);<br />

int rename(const char *oldname, const char *newname);<br />

- se include stdio.h<br />

- întorc 0 <strong>pentru</strong> execuţie corectă, altfel -1<br />

int truncate(const char *pathname, off_t length); /* not POSIX.1 */<br />

int ftruncate(int filedes, off_t length); /* not POSIX.1 */<br />

- se includ types.h (din sys) şi unistd.h<br />

- întorc 0 dacă se execută corect, altfel -1<br />

Apeluri de sistem folosite la manevrarea contextului unui proces UNIX:<br />

void _exit(status);<br />

- se include unistd.h<br />

void exit(int status);<br />

int atexit(void (*func) (void));<br />

int setenv(const char *name, const char *value, int rewrite);<br />

int putenv(const char *str);<br />

void unsetenv(const char *name);<br />

- întorc 0 la execuţie corectă, altfel o valoare diferită de zero<br />

void *malloc(size_t size);<br />

void *calloc(size_t nobj, size_t size);<br />

void *realloc(void *ptr, size_t newsize);<br />

- întorc un pointer nenul la execuţie corectă, altfel NULL<br />

void free (void *ptr);<br />

char *getenv(const char *name);<br />

- întoarce pointerul la valoarea asociata numelui primit ca argument, în caz de eroare întoarce<br />

NULL<br />

- <strong>pentru</strong> cele zece funcţii de mai sus se include stdlib.h<br />

int setjmp(jmp_buf env);<br />

- întoarce 0 dacă este apelată direct, şi o valoare nenegativă dacă se întoarce dintr-un apel la<br />

longjump<br />

void longjump(jmp_buf env, int val);<br />

- <strong>pentru</strong> cele două funcţii de mai sus se include setjmp.h<br />

v


int getrlimit(int resource, struct rlimit *rlptr); /* not POSIX.1 */<br />

int setrlimit(int resource, const struct rlimit *rlptr); /* not POSIX.1 */<br />

- întorc 0 la execuţie corectă, şi o valoare nonzero în caz de eroare<br />

- unde resource poate fi: RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA,<br />

RLIMIT_FSIZE, RLIMIT_MEMLOCK, RLIMIT_NOFILE, RLIMIT_NPROC,<br />

RLIMIT_OFILE, RLIMIT_RSS, RLIMIT_STACK, RLIMIT_VMEM<br />

- se includ time.h şi resource.h (amândouă din sys)<br />

Apeluri de sistem folosite la controlul proceselor şi a relaţiei dintre ele<br />

Următoarele funcţii sunt folosite la crearea şi controlul unui proces UNIX:<br />

pid_t getpid(void);<br />

- întoarce ID-ul procesului curent<br />

pid_t getppid(void);<br />

- întoarce ID-ul procesului părinte al procesului curent<br />

uid_t getuid(void);<br />

- întoarce ID-ul utilizatorului real al procesului curent<br />

uid_t geteuid(void);<br />

- întoarce ID-ul utilizatorului efectiv al procesului curent<br />

gid_t getgid(void);<br />

- întoarce ID-ul grupului real al procesului curent<br />

gid_t getegid(void);<br />

- întoarce ID-ul grupului efectiv al procesului curent<br />

pid_t fork(void);<br />

- întoarce 0 în procesul fiu, ID-ul procesului fiu în procesul părinte, şi -1 în caz de eroare<br />

- <strong>pentru</strong> functiile de mai sus se includ types.h (din sys) şi unistd.h<br />

pid_t wait(int *statloc);<br />

pid_t waitpid(pid_t pid, int *statloc, int options);<br />

- întorc ID-ul procesului la execuţie corectă, şi -1 la eroare<br />

- se includ types.h si wait.h (amândouă din sys)<br />

pid_t wait3(int *statloc, int options, struct rusage *rusage); /* not POSIX.1 */<br />

pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage); /* not POSIX.1 */<br />

vi


- întorc ID-ul procesului la execuţie corectă, şi 0 sau -1 în caz de eroare<br />

- se includ types.h, wait.h, time.h, şi resource.h (toate din sys)<br />

int execl(const char *pathname, const char *arg0, ... /* (char *)0 */);<br />

int execv(const char *pathname, char *const argv[]);<br />

int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);<br />

int execve(const char *pathname, char *const argv[], char *const envp[]);<br />

int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);<br />

int execvp(const char *filename, char *const argv[]);<br />

- toate cele şase funcţii întorc -1 în caz de eroare, şi nimic în caz de succes<br />

- se include unistd.h<br />

int setuid(uid_t uid);<br />

int setgid(gid_t gid);<br />

int setreuid(uid_t ruid, uid_t euid);<br />

int setregid(gid_t rgid, gid_t egid);<br />

int seteuid(uid_t uid);<br />

int setegid(gid_t gid);<br />

int setpgid(pid_t pid, pid_t pgid);<br />

int tcsetpgrp(int filedes, pid_t pgrpid);<br />

- întorc 0 la execuţie corectă, altfel -1<br />

pid_t getpgrp(void);<br />

- întoarce ID-ul grupului de procese din care face parte procesul apelant<br />

pid_t setsid(void);<br />

- întoarce ID-ul grupului de procese la execuţie corectă, altfel -1<br />

pid_t tcsetpgrp(int filedes);<br />

- întoarce ID-ul grupului de procese al grupului de procese ce rulează în foreground la execuţie<br />

corectă, altfel -1<br />

- <strong>pentru</strong> cele unsprezece funcţii de mai sus se includ types.h (din sys) şi unistd.h<br />

int system(const char *cmdstring);<br />

- întoarce diverse valori reflectând terminarea sau eşuarea programului lansat în execuţie de<br />

comanda dată ca argument.<br />

char *getlogin(void);<br />

- întoarce pointerul către numele de login la execuţie corectă, altfel NULL<br />

- se include <strong>pentru</strong> ultimele două funcţii fişierul stdlib.h<br />

clock_t times(struct tms *buf);<br />

- întoarce timpul trecut la execuţie corectă, altfel -1<br />

- se include times.h (din sys)<br />

vii


Functiile implicate în tratarea şi manevrarea semnalelor sunt:<br />

void (*signal(int signo, void (* func)(int))) (int);<br />

- unde signo poate fi unul din: SIGABRT, SIGALRM, SIGBUS, SIGCHLD,<br />

SIGCONT, SIGEMT, SIGFPE, SIGHUP, SIGILL, SIGINFO, SIGINT, SIGIO,<br />

SIGIOT, SIGKILL, SIGPIPE, SIGPOLL, SIGPROF, SIGPWR, SIGQUIT, SIGSEGV,<br />

SIGSTOP, SIGSYS, SIGTERM, SIGTRAP, SIGTSTP, SIGTTIN, SIGTTOU, SIGURG,<br />

SIGUSR1, SIGUSR2, SIGVTALRM, SIGWINCH, SIGXCPU, SIGXFSZ<br />

(semnificaţia semnalelor se extrage din /usr/include/signal.h)<br />

- întoarce starea precedenta a semnalului<br />

int sigemptyset(sigset_t *set);<br />

int sigfillset(sigset_t *set);<br />

int sigaddset(sigset_t *set, int signo);<br />

int sigdelset(sigset_t *set, int signo);<br />

int sigdelset(sigset_t *set, int signo);<br />

int sigpending(sigset_t *set);<br />

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);<br />

int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);<br />

- unde how ia una din valorile: SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK<br />

- intorc 0 la executie corecta, altfel -1<br />

int sigismember(const sigset_t *set, int signo);<br />

- întoarce 1 dacă condiţia este adevărată, altfel -1<br />

int sigsuspend(const sigset *sigmask);<br />

- întoarce -1 cu errno setat pe EINTR<br />

void psignal(int signo, const char *msg);<br />

- <strong>pentru</strong> toate funcţiile de mai sus se include signal.h<br />

int kill(pid_t pid, int signo);<br />

int raise(int signo);<br />

- întorc 0 la execuţie corectă altfel -1<br />

- se includ types (din sys) şi signal.h<br />

unsigned int alarm(unsigned int seconds);<br />

- întoarce 0 sau numărul de secunde până la, sau cel rămas până la următoarea alarmă<br />

int pause(void);<br />

- întoarce -1 cu errno setat la EINTR<br />

unsigned int sleep(unsigned int seconds);<br />

viii


- întoarce 0 sau numărul de secunde nedormite încă.<br />

- <strong>pentru</strong> ultimele trei funcţii se include unistd.h<br />

int sigsetjmp(sigjmp_buf env, int savemask);<br />

- întoarce 0 daca este apelată direct, şi o valoare nonzero dacă revine dintr-un apel la siglongjmp<br />

void siglongjmp(sigjmp_buf env, int val);<br />

- <strong>pentru</strong> ultimele două funcţii se include setjmp.h<br />

void abort(void);<br />

- aceasta funcţie nu revine niciodată<br />

- se include stdlib.h<br />

ix

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!