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