Sunteți pe pagina 1din 227

ADRIAN COLEŞA IOSIF IGNAT ZOLTÁN SOMODI

SISTEME DE OPERARE
CHESTIUNI TEORETICE ŞI PRACTICE

U.T.PRES
CLUJ-NAPOCA, 2007
Coperta: Ovidiu Muraru
Cuprins
Prefaţa ........................................................................................................v

1. Sistemul de fişiere în Linux........................................................ 1


1.1. Structura sistemului de fişiere.............................................................1
1.2. Tipuri de fişiere ...................................................................................2
1.3. Structura unei partiţii Linux................................................................6
1.4. Localizarea datelor unui fişier.............................................................6
1.5. Drepturi de acces.................................................................................7
1.6. Comenzile ls şi chmod ......................................................................10
1.7. Probleme ...........................................................................................12

2. Fişiere de comenzi în Linux ..................................................... 14


2.1. Linia de comandă ..............................................................................14
2.2. Variabile............................................................................................15
2.3. Fişiere de comenzi.............................................................................18
2.4. Redirectarea fişierelor standard de intrare şi ieşire...........................27
2.5. Exemple de fişiere de comenzi .........................................................28
2.6. Probleme ...........................................................................................29

3. Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux


........................................................................................................ 32
3.1. Descriptori de fişier...........................................................................32
3.2. Apeluri sistem pentru lucrul cu fişiere ..............................................32
3.3. Funcţii pentru lucrul cu directoare .................................................... 42
3.4. Exemple.............................................................................................43
3.5. Probleme ...........................................................................................46

4. Sistemul de fişiere NTFS .......................................................... 48


4.1. Prezentare generală ...........................................................................48
4.2. Structura unei partiţii NTFS.............................................................48
4.3. Tipuri de fişiere şi drepturi de acces în NTFS ..................................51
4.4. Funcţii API Win32 pentru sistemul de fişiere NTFS ........................53
4.5. Fişiere cu seturi multiple (alternative) de date..................................61
4.6. Exemple.............................................................................................63
4.7. Probleme ...........................................................................................64

5. Apeluri sistem pentru lucrul cu procese în Linux ................. 66


5.1. Procese ..............................................................................................66
5.2. Grupuri de procese ............................................................................67

i
5.3. Programe şi procese .......................................................................... 68
5.4. Apelurile sistem fork şi exec ............................................................. 70
5.5. Apelurile sistem wait şi waitpid........................................................ 72
5.6. Apelul sistem exit.............................................................................. 74
5.7. Exemple ............................................................................................ 76
5.8. Probleme ........................................................................................... 77

6. Thread-uri în Linux .................................................................... 80


6.1. Specificaţia PTHREADS .................................................................. 80
6.2. Crearea unui thread ........................................................................... 80
6.3. Identitatea unui thread....................................................................... 82
6.4. Terminarea execuţiei unui thread...................................................... 82
6.5. Aşteptarea terminării unui thread...................................................... 86
6.6. Stabilirea atributelor unui thread ...................................................... 87
6.7. Relaţia dintre thread-uri şi procese ................................................... 89
6.8. Probleme ........................................................................................... 91

7. Procese şi thread-uri în Windows 2000 ................................... 94


7.1. Prezentare generală............................................................................... 94
7.2. Funcţii Win32 API pentru procese şi thread-uri ...................................... 95
7.3. Planificare şi priorităţi........................................................................... 99
7.4. Exemple .......................................................................................... 101
7.5. Probleme ......................................................................................... 103

8. Fişiere PIPE în Linux ............................................................... 105


8.1. Principiile comunicării prin fişiere pipe ......................................... 105
8.2. Fişiere pipe cu nume ....................................................................... 107
8.3. Fişiere pipe fără nume sau anonime................................................ 109
8.4. Comunicare unidirecţională şi bidirecţională ................................. 111
8.5. Redirectarea STDIN şi STDOUT spre fişiere pipe......................... 113
8.6. Probleme ......................................................................................... 117

9. Fişiere PIPE în Windows ......................................................... 118


9.1. Prezentare generală a fişierelor pipe în Windows........................... 118
9.2. Funcţii Win32 pentru utilizarea pipe-urilor anonime ..................... 118
9.3. Pipe-uri cu nume ............................................................................. 120
9.4. Exemple .......................................................................................... 123
9.5. Probleme ......................................................................................... 132

10. Comunicarea prin semnale în Linux .................................... 133


10.1. Funcţionalitatea semnalelor .......................................................... 133

ii
10.2. Tratarea semnalelor....................................................................... 135
10.3. Mascarea semnalelor..................................................................... 139
10.4. Trimiterea semnalelor ...................................................................141
10.5. Alte apeluri sistem legate de semnale ...........................................143
10.6. Exemple.........................................................................................145
10.7. Probleme .......................................................................................148

11. Comunicarea prin memorie partajată şi cozi de mesaje în


Linux............................................................................................. 149
11.1. Comunicarea între procese prin memorie partajată ......................149
11.2. Comunicarea între procese prin cozi de mesaje............................157
11.3. Exemple.........................................................................................164
11.4. Probleme .......................................................................................168

12. Sincronizarea prin semafoare în Linux ............................... 170


12.1. Sincronizarea cu semafoare........................................................... 170
12.2. Crearea semafoarelor ....................................................................173
12.3. Operaţii pe semafoare ...................................................................175
12.4. Controlul seturilor de semafoarelor ..............................................180
12.5. Comenzi shell pentru seturile de semafoare..................................183
12.6. Exemple.........................................................................................184
12.7. Probleme .......................................................................................189

13. Sincronizarea thread-urilor în Linux.................................... 192


13.1. Prezentare generală .......................................................................192
13.2. Lacăte ............................................................................................193
13.3. Variabile condiţionale ...................................................................197
13.4. Execuţia unei funcţii o singură dată ..............................................201
13.5. Probleme .......................................................................................203

14. Planificarea thread-urilor în Linux ....................................... 205


14.1. Planificarea thread-urilor ..............................................................205
14.2. Prioritatea şi politica de planificare ..............................................205
14.3. Domeniul de planificare şi domeniul de alocare...........................211
14.4. Proprietatea de moştenire..............................................................212
14.5. Exemplu de utilizare ..................................................................... 213
14.6. Probleme .......................................................................................215

Bibliografie .................................................................................. 217

iii
Prefaţa
Resursele unui sistem de prelucrare automată a datelor sunt:
a) resurse fizice: procesorul central, procesoarele de I/E, memoria
internă şi externă, dispozitivele periferice;
b) resursele logice: fişierele.

Un sistem de operare este o colecţie de programe de control rezidente în


memoria internă sau externă, care asigură:
a) gestionarea resurselor fizice şi logice ale sistemului de calcul;
b) o serie de funcţiuni de asistenţă,
având obiectivul de a asigura funcţionarea performantă a sistemului.

Programele de control sunt programe care conţin instrucţiuni privilegiate,


cum ar fi: mascarea întreruperilor, protecţia memoriei etc. Ele se execută în
mod nucleu (master, supervizor). Programele de control sunt organizate
într-o ierarhie cu două niveluri:
a) nivelul fizic, care primeşte controlul prin intermediul sistemului de
întreruperi instalat, având proceduri de intrare şi ieşire din sistemul
de operare, proceduri de tratare a întreruperilor etc.
b) nivelul logic, care cuprinde proceduri pentru alocarea resurselor la
procese, planificarea proceselor pentru execuţie, comunicarea şi
sincronizarea proceselor, etc.

Programele de serviciu nu conţin instrucţiuni privilegiate şi se execută în


mod aplicaţie (slave, utilizator). Ele conţin: editoare de text, compilatoare,
editorul de legături, depanatoare etc.

Sistemul de operare constituie interfaţa între utilizator şi hardware. Datorită


acestui lucru, interpretorul de comenzi, deşi este un program executat la fel
ca oricare program utilizator, este considerat ca făcând parte din sistemul
de operare.

Termenul de sistem de operare a început să fie folosit o dată cu crearea


unor programe de control, care permiteau înlănţuirea automată a lucrărilor,
multiprogramarea, prelucrarea conversaţională, etc. Sistemele de operare au
evoluat în timp, rolul lor crescând de la o generaţie la alta a sistemelor de
calcul.

v
Înţelegând prin proces un program secvenţial în execuţie, funcţiile generale
ale unui sistem de operare sunt:
a) Operaţii asupra proceselor: creare, execuţie, terminare, mutare dintr-
o zonă de memorie în alta;
b) Controlul progresului execuţiei proceselor, altfel spus, asigurarea că
fiecare proces acceptat logic se execută normal şi că nici un alt
proces să nu poată bloca la infinit progresul celorlalte;
c) Alocarea resurselor fizice diferitelor procese;
d) Accesul la resursele logice;
e) Rezolvarea unor probleme legate de apariţia unor erori hardware şi
software;
f) Protecţia, controlul accesului şi securitatea informaţiei;
g) Sincronizarea proceselor şi comunicarea între procese;
h) Asistarea operatorului, utilizatorului, personalului de exploatare şi
întreţinere.

Noţiunile fundamentale cu care lucrează un sistem de operare sunt procesele


şi fişierele. Un sistem de operare are în structura sa module de gestionare a
proceselor (implicit şi a procesorului), a memoriei, a fişierelor şi a
dispozitivelor periferice văzute ca fişiere speciale.

De la introducerea cursului de “Sisteme de operare” (anul 1981), în planul


de învăţământ al secţiei de Calculatoare, înfiinţată (1977) în cadrul
Facultăţii de Electrotehnică din U.T.C.N., la lucrările de laborator s-au
introdus noţiuni legate de principiile generale care au stat la baza proiectării
unui sistem de operare, cu exemplificare pe cele utilizate la noi în ţară:
SIRIS, SFDX-18, CP-M, DOS, RSX-11M, UNIX, Linux, Windows.

Lucrarea de faţă prezintă într-o ordine didactică noţiuni teoretice şi practice,


legate de concepte din teoria sistemelor de operare, cu exemplificare din
Linux şi Windows 2000. Astfel sunt tratate:
a) comenzile Linux şi fişierele de comenzi;
b) structura sistemelor de fişiere Linux şi Windows;
c) apeluri de sistem Linux şi Windows legate de gestionarea fişierelor;
d) apeluri de sistem Linux şi Windows legate de gestionarea proceselor
şi a threadurilor;
e) comunicarea între procese şi a mecanismelor de sincronizare între
procese şi threaduri.

vi
Lucrarea se adresează studenţilor din anul II, de la secţia de Calculatoare,
din cadrul Facultăţii de Automatică şi Calculatoare din U.T.C.N., ca
îndrumător de laborator, dar şi ca material didactic pentru unele capitole din
curs la disciplina de “Sisteme de operare”. Cei interesaţi, pot aprofunda
aceste noţiuni urmând cursul şi laboratorul disciplinei de Proiectarea
sistemelor de operare din anul IV.

Autorii aduc mulţumiri editurii U.T.Press, pentru multiplicarea lucrării într-


o formă grafică corespunzătoare.

18 ianuarie 2007 Autorii

vii
1. Sistemul de fişiere în Linux

Scopul lucrării
Lucrarea descrie modul de organizare a sistemului de fişiere şi
caracteristicile fişierelor în sistemul de operare Linux. De asemenea, ea
prezintă câteva dintre comenzile de manipulare a fişierelor.

1.1. Structura sistemului de fişiere


Sistemul de fişiere este o componentă importantă a unui sistem de operare. În
Linux, el este caracterizat prin trei aspecte: structura ierarhică, independenţa
faţă de hardware şi o mare flexibilitate. Structura ierarhică este organizată sub
forma unui arbore cu un director rădăcină (root). Directorul rădăcină poate
conţine fişiere ordinare, fişiere de tip legătură sau alte directoare, numite
subdirectoare. Subdirectoarele sunt noduri în arbore, iar fişierele sunt frunze.
Independenţa faţă de hardware rezultă din faptul că fişierele sunt vizibile şi
accesibile utilizatorului ca o succesiune de octeţi. Flexibilitatea se bazează pe
posibilitatea de a monta (insera) sau demonta, la orice nivel în ierarhia de
fişiere, noi structuri arborescente de directoare şi fişiere.

Structura standard a sistemului de fişiere Linux conţine, pe lângă alte directoare


şi următoarele: bin, boot, dev, etc, home, lib, proc, root, sbin, tmp, usr, var.

Figura 1. Structura arborescentă a sistemului de fişiere

1
Sistemul de fişiere în Linux

Structura arborescentă a sistemului de fişiere dă posibilitatea utilizatorilor să


creeze şi să gestioneze un număr mare de fişiere. Nu există limitare a
numărului de noduri, eventualele restricţii fiind impuse de hardware.
Referirea la un fişier se face prin specificarea numelui său precedat de un şir
de caractere numit cale, şir prin care se indică poziţia în arbore a fişierului
coborând pe nivelurile arborelui, fie pornind de la directorul rădăcină (cale
absolută sau completă), fie de la cel curent (cale relativă). Prin director
curent se înţelege directorul la care se raportează la un moment dat o
anumită aplicaţie. Calea unui fişier poate fi de orice lungime, caracterul '/'
fiind folosit pentru delimitarea numelor de directoare din cale. Caracterul '/',
pe lângă rolul de separator de directoare şi fişiere, identifică şi directorul
rădăcină. În ceea ce priveşte specificarea căii, precum şi unele caracteristici
ale unui fişier sau director, pot fi precizate câteva diferenţe faţă de sistemul
de operare Windows, precum:
• caracterul '\' este înlocuit în Linux cu '/';
• în Linux se face distincţie între litere mici şi litere mari;
• un fişier executabil în Linux este un fişier ce are setat dreptul de
execuţie (indiferent de extensia sa);
• modul de stabilire şi organizare a drepturilor de acces asupra unui
fişier diferă în cele două sisteme de operare, deşi interpretarea lor
este foarte asemănătoare;
• un fişier text în Linux are marcat sfârşitul de linie ('\n') printr-un
singur caracter (cel cu codul 10), în timp ce în Windows prin două
caractere (cu codurile 13 şi 10).

1.2. Tipuri de fişiere


În Linux se deosebesc patru tipuri de fişiere: ordinare (obişnuite), pipe,
speciale şi directoare. Din punct de vedere al sistemului de operare, un
fişier este un şir de octeţi de o anumită lungime. Accesul la datele din
fişiere se face în mod aleator, adică la orice poziţie (deplasament) la un
moment dat. Fişierele ordinare sunt păstrate pe disc.

Un fişier ordinar este creat de un proces. El poate fi text sau binar (de
exemplu un fişier executabil). Fiecare fişier are ataşat un număr, care este
interpretat ca index într-o listă de structuri (index node), păstrată într-o zonă
rezervată pe partiţia care conţine sistemul de fişiere. I-node-ul unui fişier
conţine toate informaţiile pe care sistemul de operare le foloseşte pentru
descrierea şi gestionarea fişierului, cu excepţia numelui său.

2
Sisteme de operare. Chestiuni teoretice şi practice

Fişierele pipe sunt fişiere folosite pentru comunicarea între procese, pe baza
principiului FIFO (First-In, First-Out).

Fişierele speciale sunt fişiere ataşate dispozitivelor de I/E (de tip bloc sau
caracter), rolul lor fiind acela de a oferi o interfaţă de acces la aceste
dispozitive similară cu cea de lucru cu fişierele ordinare. Fişierele speciale
sunt localizate de obicei în directorul /dev şi modelează dispozitive
precum: discuri, benzi magnetice, terminale, imprimante, mouse etc. De
exemplu, pentru fiecare partiţie a unui hard disc există câte un fişier special.
Un fişier special deţine un i-node, care însă nu referă blocuri de date pe
disc. În schimb, acest i-node conţine un număr de dispozitiv, care este
folosit ca index într-o tabelă internă sistemului de operare de proceduri
pentru dispozitive periferice. Pentru identificarea fiecărui dispozitiv se
folosesc două numere: major (identifică tipul dispozitivului) şi minor
(identifică numărul dispozitivului de tipul dat). Folosirea dispozitivelor în
această manieră conferă avantajul tratării uniforme. Din punct de vedere
utilizator nu există nici o diferenţă între lucrul cu fişiere ordinare şi cele
speciale. Exemplul de mai jos ilustrează comparativ acest lucru:
cp prg.c /home/student/prg1.c # copiere simpla
cp prg.c /dev/lp0 # listare la imprimanta

Un director face legătura între numele fişierelor şi locul unde acestea sunt
memorate pe disc. El nu conţine efectiv fişierele care îi aparţin, ci doar
referinţele la acestea, sub forma unei succesiuni de structuri numite intrări
în director. O intrare în director are cel puţin două câmpuri, şi anume:
numele fişierului şi numărul său de i-node. Orice director are în mod
implicit două intrări create automat odată cu crearea directorului. Acestea
poartă numele ’.’ şi ’..’ şi conţin referinţe spre i-nodul directorului curent şi,
respectiv spre părintele directorului curent. Un director ce conţine doar cele
două intrări amintite se consideră gol, putând fi şters cu ajutorul comenzilor
de ştergere.
Fiecare fişier are alocat un singur i-node care conţine, printre altele:
1. identificatorul utilizatorului care este proprietarul fişierului;
2. tipul fişierului (ordinar, director, pipe sau special);
3. drepturile utilizatorilor de acces la fişier;
4. data şi timpul ultimului acces şi al ultimei modificări a fişierului,
data şi timpul ultimei modificări efectuate asupra i-node-ului;
5. numărul de legături fizice ale fişierului (a se vedea comanda unlink);
6. adresele blocurilor de pe HDD ce conţin datele fişierului;
7. lungimea fişierului în octeţi şi în blocuri de octeţi alocate pe disc.

3
Sistemul de fişiere în Linux

O caracteristică importantă a sistemului de fişiere din Linux este noţiunea de


legătură (link). Scopul legării este acela de partajare a fişierelor (sau
directoarelor) între diferite directoare. Legarea unui fişier are ca efect
posibilitatea referirii acelui fişier prin căi şi, eventual, nume diferite în
arborele de fişiere şi directoare. În Linux există două tipuri de legături: fizică
(hard) şi simbolică. Pentru a înţelege modul de realizare şi funcţionare a lor,
trebuie să sintetizăm informaţiile prezentate până aici şi anume, faptul că
fiecare fişier are asociat un singur i-node, iar numele fişierului şi numărul său
de i-node sunt localizate într-o intrare din directorul în care a fost creat
fişierul. Numărul de i-node este un identificator unic al fişierului în cadrul
sistemului şi înscrierea sa în cadrul unei intrări dintr-un director poate fi
interpretată ca o referire sau o legătură spre fişier din acel director. Această
interpretare stă la baza înţelegerii legăturii fizice, care se realizează prin
alocarea unei noi intrări, fie în directorul iniţial, fie într-unul diferit, intrare în
director care va conţine numărul de i-node al fişierului legat. Numele intrării
respective poate fi identic sau diferit de cel al altei intrări care referă acelaşi
fişier. Evident, două intrări diferite din cadrul aceluiaşi director, care referă
acelaşi fişier (conţin acelaşi număr de i-node) trebuie să aibă nume diferite. În
urma stabilirii unei legături fizice spre un fişier, acesta va fi referit prin
numărul său de i-node din intrări de director diferite. Astfel, acelaşi fişier este
accesibil pe drumuri diferite şi, eventual, prin nume diferite în arborele de
fişiere şi directoare. Păstrarea numărului de legături fizice spre un fişier în i-
node-ul acestuia este necesară pentru a nu se şterge fizic fişierul şi i-node-ul
lui decât în momentul ajungerii acestui număr la zero, adică în momentul
ştergerii ultimei legături fizice spre acel fişier. În caz contrar sistemul de
fişiere ar fi adus într-o stare inconsistentă.
Dacă în cazul legăturii fizice, nu se creează un fişier nou, ci numai o intrare
nouă în director, în cazul legăturii simbolice ambele lucruri se întâmplă, adică
în directorul în care se creează legătura se alocă o intrare care va referi un i-
node nou, alocat unui nou fişier. Acest nou fişier este unul special, de tip
legătură simbolică şi este gestionat în mod automat şi transparent pentru
utilizator de către sistemul de operare. Conţinutul unui fişier legătură simbolică
este calea spre fişierul legat (referit) şi accesarea lui are ca efect accesarea
fişierului legat. Fişierul legătură simbolică nu conţine informaţii despre
localizarea fizică a fişierului legat (i-node-ul său), ci doar despre o cale spre
acel fişier, astfel încât ştergerea acelei căi face ca referinţa din fişierul legătură
simbolică să devină invalidă, fără a apărea însă inconsistenţe în sistemul de
fişiere. Avantajul legăturilor simbolice este acela că nu este necesară
memorarea numărului lor în i-node şi, în plus, conţinând o cale de tip şir de
caractere în arborele de fişiere şi directoare, pot referi fişiere aflate pe sisteme

4
Sisteme de operare. Chestiuni teoretice şi practice

de fişiere montate, fie ele chiar localizate pe o altă maşină, lucru care este
imposibil în cazul legăturilor fizice, la care un număr de i-node are semnificaţie
şi relevanţă doar în cadrul sistemului de fişiere de pe o aceeaşi partiţie.

Pentru stabilirea unei legături spre un fişier existent se poate folosi comanda
ln, a cărei sintaxă este următoarea, lipsa opţiunii -s indicând crearea unei
legături fizice, iar prezenţa ei indicând crearea unei legături simbolice:
ln [-s] nume_cale_veche nume_cale_noua

Nu este permisă efectuarea de legături fizice la directoare decât


administratorului sistemului şi aceasta doar în anumite cazuri în funcţie de
configuraţia sistemului. Ştergerea unei legături se poate realiza prin
comanda unlink.

O altă caracteristică importantă a sistemului Linux este posibilitatea de a


monta un întreg arbore de fişiere într-un director din ierarhia sistemului
(arborelui) de fişiere principal. Sistemul de operare Linux recunoaşte un
singur director rădăcină, dar este posibil ca fişierele să se găsească pe mai
multe suporturi fizice sau logice, fiecare volum având un sistem de fişiere
arborescent propriu. Este posibilă suprapunerea rădăcinii unui astfel de
sistem de fişiere peste un director al sistemului de fişiere recunoscut de
sistem. Această operaţie se numeşte montare şi poate fi realizată prin
comanda mount, dar în general doar de către superuser. Conţinutul
directorului în care se realizează montarea devine invizibil până la
efectuarea operaţiei de demontare, motiv pentru care se recomandă ca el să
fie gol. După montare, calea la punctul de montare prefixează orice acces la
un fişier sau director de pe sistem de fişiere montat. De exemplu:
mount /dev/hda1 /mnt/hdd -r
montează doar pentru citire (read-only) în directorul /mnt/hdd prima
partiţie a primului HDD al unui sistem.
Pentru a accesa o dischetă aflată în unitatea 0, comanda de montare poate
arăta:
mount /dev/fd/0 /mnt/fd0

Dacă montarea nu mai este necesară se poate proceda la operaţia de


demontare şi eliberarea punctului de montare prin comanda umount. De
exemplu, pentru demontarea dischetei comanda este:
umount /dev/fd/0

5
Sistemul de fişiere în Linux

1.3. Structura unei partiţii Linux


Orice partiţie care conţine un sistem de fişiere Linux are în principiu
următoarea structură:

Superbloc Lista de inode-uri Blocuri de date Zona de swapping


Figura 2. Dispunerea informaţiilor pe o partiţie Linux

Superblocul conţine, printre altele, următoarele informaţii:


• dimensiunea sistemului de fişiere;
• numărul blocurilor libere din sistemul de fişiere;
• tablou cu blocuri disponibile din sistemul de fişiere;
• indexul următorului bloc liber din tabloul blocurilor libere;
• dimensiunea listei de i-node-uri;
• numărul total de i-node-uri libere din sistemul de fişiere;
• tablou cu i-node-uri libere din sistemul de fişiere;
• indexul următorului i-node liber din tabloul i-node-urilor libere;
• indicator (un bit) al modificării superblocului.

Superblocul este încărcat în memorie la pornirea sistemului, sistemul de


operare asigurând pe durata funcţionării sale corespondenţa dintre superblocul
din memorie şi cel de pe HDD. Deoarece există intervale de timp în care
superblocul din memorie conţine modificări ce nu au fost încă scrise pe HDD,
se recomandă oprirea normală a sistemului. Distrugerea superblocului poate
cauza imposibilitatea accesului la sistemul de fişiere. Zona de swapping
serveşte pentru salvarea temporară a unor zone din memoria internă a
calculatorului pe HDD pentru crearea de spaţiu pentru aplicaţia activă.

1.4. Localizarea datelor unui fişier


Pentru a accesa un fişier din directorul curent, sistemul de operare citeşte
fiecare intrare din directorul respectiv şi compară numele fişierului cu
numele intrării, până când fişierul este găsit. Dacă fişierul este prezent,
sistemul extrage din intrarea din director numărul de i-node şi îl foloseşte ca
index în lista (tabela) i-node-urilor de pe disc, pentru a localiza structura
asociată fişierului şi a o aduce în memorie. În i-node se găsesc toate
informaţiile asociate fişierului, inclusiv adresele blocurilor alocate fişierului,
deci prin i-node-ul unui fişier sistemul are acces la datele fişierului. I-node-
ul este citit de pe HDD şi încărcat în tabela i-node-urilor din memorie, care

6
Sisteme de operare. Chestiuni teoretice şi practice

conţine toate i-node-urile fişierelor deschise. Tabela este gestionată intern


de sistemul de operare.

Localizarea unui fişier care nu e situat în directorul curent este un lucru


puţin mai dificil. De exemplu, pentru calea absolută /home/student/fis
sistemul încarcă iniţial directorul rădăcină, al cărei i-node este cunoscut,
fiind stabilit la formatarea partiţiei – de obicei i-node-ul cu numărul 2.
După aceasta, caută printre intrările directorului rădăcină cea cu numele
home şi găseşte i-node-ul asociat ei. I-node-ul este adus în memorie şi se
determină blocurile de pe disc care conţin directorul /home. Intrările acestui
director sunt citite şi comparate cu şirul student. O dată găsită intrarea, se
extrage i-node-ul pentru directorul /home/student şi se citesc de pe disc
blocurile sale de date. În final, se caută şirul fis printre intrările directorului
/home/student şi se determină i-node-ul corespunzător fişierului fis şi,
implicit, blocurile sale de date. În general, utilizarea căilor relative poate fi
mai convenabilă nu numai pentru utilizator, dar şi pentru sistem, pentru a se
reduce numărul operaţiile de căutare şi accesurile la HDD.

1.5. Drepturi de acces


Protecţia accesului la fişiere într-un sistem cu mai mulţi utilizatori este un
aspect important. Linux este un sistem de operare multiuser, ceea ce
înseamnă că face distincţie între utilizarea calculatorului de către diferiţi
utilizatori. Identificarea unui utilizator şi permiterea accesului la resursele
sistemului se face prin autentificare (procesul de login), pe baza unui nume
stabilit anterior în cadrul sistemului de operare şi a parolei asociate. Fiecărui
utilizator îi este asociat un număr unic de identificare în cadrul sistemului,
numit UID (User IDentifier). Utilizatorii pot fi grupaţi în grupuri, fiecare
grup având, de asemenea, asociat un identificator unic, numit GID (Group
IDentifier). Pentru vizualizarea identificatorilor de utilizator şi grup şi a
altor informaţii legate de aceştia se pot studia fişierele /etc/passwd şi,
respectiv /etc/groups.

În Linux fiecare fişier conţine în i-node-ul asociat identificatorul


utilizatorului (UID) căruia îi aparţine acel fişier şi identificatorul de grup
(GID) al proprietarului. În momentul în care un fişier este creat, el primeşte
ca semn de recunoaştere UID-ul celui care l-a creat, recunoscut ca
proprietar al fişierului. În i-node mai există un câmp care conţine trei seturi
de câte trei biţi. Biţii respectivi descriu permisiunile de acces la fişier ale
proprietarului, ale utilizatorilor din acelaşi grup principal din care face

7
Sistemul de fişiere în Linux

parte proprietarul şi respectiv, a celorlalţi utilizatori din sistem. Cei trei biţi
din fiecare set corespund drepturilor de citire (Read), de scriere (Write) şi de
execuţie (eXecute) şi indică dacă un proces (ce se execută în numele unui
anumit utilizator) poate efectua (bitul setat la 1) sau nu (bitul setat la zero)
operaţia corespunzătoare asupra fişierului respectiv. Procesele care se
execută în numele administratorului de sistem, utilizator cunoscut în Linux
sub numele de root, au acces nerestricţionat la toate fişierelor din sistem,
indiferent de drepturile de acces ale acestora.

Pentru fişiere ordinare semnificaţia drepturilor este evidentă. Pentru


directoare, dreptul de citire înseamnă drept de consultare (de listare) a
directorului (e permisă, de exemplu comanda ls). Dreptul de scriere
înseamnă că în director se pot crea noi fişiere, şterge fişiere, se poate monta
un sistem de fişiere, se pot adăuga sau şterge legături. Un director care are
drept de execuţie poate fi vizitat în timpul căutării unui fişier, adică se poate
trece prin el căutându-se una dintre componentele căii spre fişier. Pentru
fişiere speciale şi pipe dreptul de citire şi scriere semnifică capacitatea de a
executa apelurile sistem read şi respectiv, write. Dreptul de execuţie nu este
important în acest caz. Drepturile de acces ale unui fişier sunt păstrate pe 16
biţi cu următoarea semnificaţie:

Tabelul 1. Drepturi de acces


Bitul Semnificaţie
0-3 Tipul fişierului.
4 Setează ID-ul utilizator în timpul execuţiei (suid).
5 Setează ID-ul grupului în timpul execuţiei (sgid).
6 Bitul sticky. Era folosit pentru a indica păstrarea în memorie
a programului corespunzător fişierului executabil cu bitul
sticky setat şi după terminarea sa. Actualmente, este folosit în
Linux doar pentru directoare, permiţând doar operaţia de
append asupra lor.
7-9 Drept de citire, scriere, execuţie pentru proprietar.
10-12 Drept de citire, scriere, execuţie pentru grup.
13-15 Drept de citire, scriere, execuţie pentru alţii.

Bitul sticky
Dacă acest bit este poziţionat pentru un director, orice fişier sau subdirector
din acel director poate fi şters sau redenumit numai de proprietarul fişierului
sau al subdirectorului sau de root.

8
Sisteme de operare. Chestiuni teoretice şi practice

Biţii suid şi sgid


La intrarea în sesiune a unui utilizator, toate procesele (aplicaţiile) sale au
asociate, identificatorul său (UID-ul) şi cel al grupului principal (GID-ul)
din care utilizatorul face parte, ambele informaţii fiind preluate din fişierul
/etc/passwd. Cei doi identificatori menţionaţi se numesc UID real şi GID
real, deoarece sunt reprezentativi pentru utilizatorul real (persoana care a
deschis sesiunea). Fiecărui proces îi mai sunt asociaţi încă doi identificatori
numiţi UID efectiv şi GID efectiv. Identificatorii efectivi sunt folosiţi de
către sistemul de operare la verificarea drepturilor de acces. În mod normal,
identificatorii efectivi şi cei reali ai unui proces sunt identici, în afara
cazului în care procesul execută un fişier executabil asupra căruia are
drepturi de execuţie obişnuite, dar care are şi bitul suid sau sgid poziţionat.
În acest caz, procesul creat va avea identificatorul efectiv (UID sau GID,
corespunzător setării biţilor suid sau sgid) egal cu cel al proprietarului
fişierului executabil şi diferit de cel real, care a rămas nemodificat. Procesul
posedă, cât timp execută codul din fişierul executabil, aceleaşi drepturi ca şi
proprietarul fişierului. Dacă, de exemplu, acest proprietar este root-ul,
procesul posedă temporar toate drepturile asupra sistemului. Dar, singura
acţiune care o poate realiza este cea definită în programul executat şi
probabil determinată de proprietarul fişierului, în acest caz root-ul. Îndată ce
procesul termină de executat codul unui astfel de program, UID-ul efectiv
(sau GUID-ul efectiv), redevine egal cu cel real, iar procesul continuă să se
execute cu drepturile iniţiale.

Uneori se doreşte ca un utilizator comun să aibă, pentru un timp, privilegiile


altui utilizator. Fie o configuraţie ca in Figura 3, unde fişierul Fis are ca
proprietar pe utilizatorul U1 şi dreptul de scriere setat doar pentru el. Pentru
ca utilizatorul U2 să poată modifica datele din acest fişier, U1 furnizează un
program în fişierul executabil Prg, care are setat bitul suid, iar utilizatorul
U2 are dreptul de execuţie asupra lui. În această situaţie, în timpul execuţiei
lui Prg, U2 are UID-ul efectiv acelaşi cu al lui U1 şi astfel el poate modifica
datele din Fis, dar numai în cadrul oferit de programul Prg.

Drepturi Proprietar Grup Nume fişier


-rwx------ U1 G1 Fis
-rws--x--x U1 G1 Prg
Figura. 3. Accesul controlat la un fişier

9
Sistemul de fişiere în Linux

O exemplificare a situaţiei descrise mai sus este cea a schimbării propriei


parole de către un utilizator folosind comanda passwd (care aparţine root-
ului, are drepturi de execuţie pentru toată lumea şi bitul suid setat), comandă
cu care se modifică fişierul /etc/passwd (respectiv /etc/shadow, pe
unele sisteme), asupra căruia în mod normal nu pot fi operate modificări.
Bitul sgid este folosit în aceeaşi manieră pentru grup.
Un fişier care are aceşti biţi poziţionaţi afişează ca rezultat al comenzii
"ls -l" pe poziţia lui 'x' pentru proprietar litera 's'. Pentru setarea biţilor
suid şi sgid se foloseşte comanda chmod. Proprietarul fişierului executabil şi
superuserul pot modifica aceşti biţi. Se poate spune că 's' este o extensie a
permisiunii 'x' în contextul discutat.
Algoritmul folosit de sistem pentru a determina dacă un proces are sau nu
dreptul de a efectua o operaţie (citire, scriere sau execuţie) asupra unui fişier
dat este următorul:
1. Dacă UID-ul efectiv este 0, permisiunea este acceptată (utilizatorul
efectiv fiind root-ul);
2. Dacă UID-ul efectiv al procesului şi UID-ul proprietarului fişierului
se potrivesc, se decide permisiunea din biţii proprietarului;
3. Dacă GID-ul efectiv al procesului şi GID-ul proprietarului fişierului
se potrivesc, se decide permisiunea din biţii grupului;
4. Dacă nici UID şi nici GID nu se potrivesc atunci, se decide
permisiunea din ultimul set de trei biţi.

1.6. Comenzile ls şi chmod


Comanda ls afişează conţinutul unui director. Sintaxa ei este ilustrată mai jos:

ls [opţiuni] director

Opţiuni posibile ale comenzii sunt:


-d Afişează numai directoarele din directorul curent.
-l Afişează informaţii suplimentare precum: drepturile de acces,
numărul de legături, dimensiunea fişierului, data ultimei actualizări,
numele fişierului.
-i Afişează şi numărul i-node-ului fiecărui fişier.
-s Afişează numărul de blocuri pentru fiecare fişier.
-t Fişierele sunt sortate după data ultimei actualizări.

10
Sisteme de operare. Chestiuni teoretice şi practice

-u La afişare se consideră data ultimului acces în loc de data ultimei


actualizări pentru opţiunile -t sau -l.
-r Inversează ordinea de sortare.
-R Se face şi afişarea conţinutului subdirectoarelor şi a subdirectoarelor
acestora şi aşa mai departe.
-h Afişarea dimensiunii fişierelor se face într-un format mai uşor de
înţeles de către utilizator, ca de exemplu 1K, 2M, 3G etc. pentru
dimensiuni de KB, MB sau GB respectiv.
Un exemplu de utilizare al comenzii ls este:
ls -lsihR /

Comanda chmod schimbă drepturile de acces la un fişier ordinar sau


director. Sintaxa ei este:
chmod atr,[atr] fişier(e)
unde, atr se exprimă ca un număr octal din patru cifre sau printr-o
combinaţie de forma:
u|g|o|a +|-|= r|w|x|s|t
unde, '+' adaugă permisiune, '-' şterge permisiune, '=' atribuie permisiuni
pentru proprietar ('u'), grup ('g'), ceilalţi ('o') sau pentru toţi deodată ('a').
Permisiunile de acces se specifică prin 'r' pentru citire (Read), 'w' pentru
scriere (Write), 'x' pentru execuţie (eXecute), 't' pentru setarea bitului sticky,
's' pentru setarea bitului suid sau sgid.
Drepturile de acces la un fişier se păstrează într-un cuvânt, plasat în i-node-
ul fişierului. Se poate specifica direct valoarea acestui cuvânt, biţii având
semnificaţia:

Tabelul2. Specificarea sub forma unui număr în octal a drepturilor de acces


Drept Proprietar Grup Alţii
Citire 0400 0040 0004
Scriere 0200 0020 0002
Execuţie 0100 0010 0001

Exemplele de mai jos ilustrează modul de utilizare a comenzi.


chmod go-wx f1 # Sterge dreptul de scriere si
# executie pentru grup şi altii
# pentru fisierul f1
chmod 0764 f1 # f1 va avea permisiunile rwxrw-r--

11
Sistemul de fişiere în Linux

1.7. Probleme
1. Să se testeze comparativ pe Windows şi Linux următoarele comenzi de
manipulare a fişierelor şi directoarelor:
attrib chmod, ls -l
cd, chdir cd, pwd
comp cmp
copy cp, cat
del, erase rm, unlink
dir ls
fc cmp, diff
md, mkdir mkdir
ren mv
rd, rmdir rmdir
type cat

2. Să se parcurgă arborele sistemului de fişiere şi să se identifice în cadrul


structurii arborescente directoarele cu comenzi (fişiere executabile), cu
dispozitivele periferice (fişiere speciale), cu fişiere temporare etc.
3. Comanda Linux man permite obţinerea de informaţii despre comenzile
Linux, diferite apeluri de sistem, utilitare importante. Comanda se
apelează având ca argument numele comenzii despre care se solicită
informaţii. Utilizând comanda man obţineţi informaţii suplimentare
despre comenzile ls, echo, cat, chmod şi man.
4. Să se testeze şi să se explice efectul execuţiei următoarelor comenzi:
ls -l ? ls -la
ls –l * ls [a-z]*[!0-9]
ls a*b ls *[!o]
ls –li

5. Să se testeze folosirea comenzii ln pentru crearea legăturilor fizice şi


simbolice şi să se vizualizeze informaţiile acestor „fişiere legătură” cu
ajutorul comenzii ls.
6. Să se vizualizeze conţinutul unui director folosind comanda „ls -l”.
Să se identifice informaţiile din i-node. Utilizând comanda chmod, să se
modifice drepturile de acces ale unui fişier executabil şi ale unui director
arbitrar din structura sistemului de fişiere. Să se explice rezultatul
comenzilor.
7. Să se stabilească permisiunile de acces la directoarele dintr-un subarbore
de directoare ce aparţin unui utilizator U1, astfel încât un alt utilizator U2
să aibă acces la un anumit director (pe un nivel din adâncime) fără a

12
Sisteme de operare. Chestiuni teoretice şi practice

putea vedea conţinutul directoarelor prin care se trece pentru a ajunge la


acel director.
8. Să se testeze ce se poate face cu un fişier care are doar drepturi de citire
pentru toţi utilizatorii, iar directorul în care se găseşte drepturi de citire
scriere şi execuţie pentru toţi utilizatorii. Să se testeze cazul similar
pentru un fişier din directorul /tmp.
9. Să se testeze efectul diferitelor combinaţii a drepturilor de citire, scriere
şi execuţie ale unui director asupra fişierelor şi subdirectoarelor din acel
director.
10. Se presupune că execuţia programului Prg.exe are ca efect adăugarea
unei linii de text la sfârşitul unui fişier text, numele fişierului şi şirul de
caractere fiind transmise ca parametri ai programului în linia de
comandă. Cele două fişiere - executabil şi text - aparţin utilizatorului
User1. Să se stabilească, prin modificarea drepturilor de acces ale
fişierelor, contextul în care se permite execuţia cu succes a programului
Prg.exe de către un utilizator User2, care nu are drept de scriere asupra
fişierului text.
11. Să se modifice programul Prg.exe din problema anterioară astfel încât,
într-un context identic, utilizatorul User2 să-l poată executa cu succes,
pe când un alt utilizator User3 să nu poată face acest lucru. Se
recomandă folosirea funcţiei getuid în codul sursă al programului,
pentru obţinerea identificatorului utilizatorului real al aplicaţiei.
12. Dacă se copiază fişierul /bin/sh în directorul propriu, devenim
proprietarul copiei. Dacă se foloseşte comanda chmod se poziţionează
bitul suid, iar prin comanda chown se modifică proprietarul fişierului ca
fiind root-ul. Executând acum copia vom deţine privilegiul de a fi root.
Acest lucru nu se produce, deci care este greşeala în raţionament?

13
2. Fişiere de comenzi în Linux

Scopul lucrării
Lucrarea prezintă caracteristicile interpretorului de comenzi din Linux, o
serie de comenzi recunoscute de către acesta şi modul de scriere a fişierelor
de comenzi (script-uri).

2.1. Linia de comandă


Interpretorul de comenzi preia comenzile pe care trebuie să le execute sub
forma unui şir de caractere, numit linie de comandă, formatul unei comenzi
fiind următorul:

nume_comandă [optiuni] argumente

Orice comandă executată întoarce ca rezultat un număr, numit cod de retur,


care indică modul de terminare a comenzii, zero pentru o comandă
executată cu succes şi diferit de zero în caz de eşec.

Mai multe comenzi scrise pe o linie trebuie separate prin ';'. Comenzile pot
fi conectate prin pipe (simbolul '|'), astfel încât ieşirea unei comenzi
constituie intrare pentru a doua. Codul de retur este cel corespunzător
ultimei comenzi din pipe. De exemplu, comanda "ls -l | less" aplică
filtrul less pe rezultatul comenzii ls. Dacă linia de comandă este terminată
cu caracterul '&', ultima comandă a secvenţei de comenzi specificate în acea
linie se executată asincron (în background sau concurent) relativ la
interpretorul de comenzi, care va afişa identificatorul procesului lansat.
Continuarea unei comenzi pe linia următoare este posibilă dacă linia este
terminată cu caracterul '\'. Secvenţa de caracterele "&&" indică faptul că
execuţia comenzii de după ele se va face numai dacă precedenta comandă a
fost executată cu succes (funcţionalitate de tip AND). Pentru o
funcţionalitate de tip OR se poate folosi secvenţa "||".

În exemplele de mai jos este ilustrată folosirea câtorva din caracterele descrise.

who | grep "labso" > /dev/null && \


echo "labso is logged on"
ls -l file1 || ls -l file2

14
Sisteme de operare. Chestiuni teoretice şi practice

2.2. Variabile
Variabilele recunoscute de către interpretorul de comenzi pot fi: variabile
utilizator, parametri poziţionali şi variabile predefinite.

Variabile utilizator
Definirea variabilelor utilizator se face sub forma:

nume_var1=valoare

E important de precizat faptul că interpretorul de comenzi lucrează cu şiruri


de caractere, atât numele variabilei, cât şi valoarea ei fiind interpretate ca
şiruri de caractere. Prin urmare pentru a putea accesa conţinutul variabilei e
nevoie de o construcţie specială, lucru ce se face prin prefixarea numelui
variabilei cu caracterul '$', ca în exemplul de mai jos.

dir=/usr/include
cd $dir

Variabilele utilizator sunt evaluate la valoarea lor, în afara cazului în care


valoarea este delimitată de apostrofuri. Numele unei variabile nu poate fi
identic cu numele unei funcţii existente. Delimitarea numelui unei variabile,
în cazurile în care acesta este urmat de un alt şir de caractere – de exemplu
dacă se doreşte concatenarea valorii variabilei cu acel şir de caractere – se
face prin încadrarea lui între caracterele '{' şi '}'. Exemplu de mai jos
ilustrează acest lucru.

num=3
k=${num}tmp # k=$numtmp ar fi fost
# interpretat ca variabila numtmp
echo $k # Se afiseaza 3tmp

Interpretorul de comenzi oferă un mecanism de substituţie bazat pe


următoarele reguli:
• 'şir_de_caractere': caracterele situate între apostrofuri sunt
tratate ca şi caractere obişnuite făra a avea o semnificaţie specială;
• "şir_de_caractere": caracterele situate între ghilimele sunt tratate
ca şi caractere obişnuite fără a avea o semnificaţie specială, cu
excepţia caracterelor '$' şi '\';

15
Fişiere de comenzi în Linux

• \c: nu interpretează în mod special caracterul 'c'. În şirurile încadrate


între ghilimele, caracterul '\' este caracter de evitare a tratării
speciale de către interpretor a caracterelor din setul {$, `, " , \};
• var=`comandă`: are ca efect execuţia comenzii încadrate de
caracterele '`' (apostrof invers) şi atribuirea rezultatului ei variabilei. De
exemplu, rep=`pwd` atribuie variabilei rep rezultatul comenzii pwd.

În ceea ce priveşte variabilele definite în contextul unui interpretor de


comenzi trebuie ştiut faptul că acestea formează un set distinct pentru
fiecare instanţă (proces) a interpretorului, chiar dacă două variabile cu
acelaşi nume sunt folosite în contextul ambelor instanţe ale interpretorului.
Acest lucru este valabil atât în cazul a doi utilizatori, care în contextul
propriilor instanţe ale interpretorului îşi definesc variabile cu acelaşi nume,
cât şi în cazul în care un utilizator execută fişiere de comenzi în cadrul unui
interpretor, variabile cu acelaşi nume fiind folosite atât în cadrul
interpretorului, cât şi în fişierul de comenzi. Imaginea de mai jos, ilustrează
acest lucru prin secvenţa de comenzi introduse.

Se observă că în contextul interpretorului de comenzi se atribuie variabilei x


valoarea 100, valoare ce nu este modificată (la 50) prin execuţia fişierului
de comenzi. Explicaţia constă în faptul că execuţia comenzilor din cadrul
fişierului fis_cmd se face în contextul unei noi instanţe a interpretorului,
instanţă (proces) lansată de către interpretorul (procesul) iniţial, fiecare
proces având propriul set de variabile. Pentru a face cunoscută o variabilă
din cadrul unui interpretor unor alte instanţe lansate ulterior din cadrul
interpretorului se foloseşte comanda export. Exemplul de mai jos ilustrează
acest lucru.

16
Sisteme de operare. Chestiuni teoretice şi practice

Parametri poziţionali
Parametrii poziţionali notaţi $1, $2, $3, ... reprezintă modalitatea de a
accesa argumentele transmise unui fişier de comenzi în linia de comandă.
Variabila $0 este numele fişierului de comenzi ce se execută.

Variabile predefinite şi speciale


Există o serie de variabile predefinite, iniţializate la intrarea în sistem şi
utilizate de către interpretor sau alte aplicaţii. Câteva dintre acestea sunt:
$HOME
Desemnează directorul în care o instanţă a interpretorului, proprie
unui utilizator, este poziţionată în momentul intrării în sistem a
utilizatorului. De asemenea, valoarea variabilei este folosită ca
director implicit pentru comanda cd.
$PATH
Defineşte lista directoarelor parcurse de interpretor în căutarea unui
fişier executabil corespunzător comenzii introduse (directoarele sunt
separate prin caracterul ':').
$UID
Indică identificatorul utilizatorului în numele căruia se execută
interpretorul de comenzi.
$USER
Indică numele utilizatorului în numele căruia se execută
interpretorul de comenzi.
$SHELL
Indică numele interpretorului curent.

Variabilele speciale sunt descrise mai jos. Valoarea lor nu poate fi


modificată.
$# numărul argumentelor din linia de comandă (exclusiv $0).
$@,$* lista parametrilor poziţionali.

17
Fişiere de comenzi în Linux

$? starea de ieşire a ultimei comenzi executate.


$$ identificatorul de proces asociat interpretorului.
$! identificatorul ultimului proces lansat în background.

2.3. Fişiere de comenzi


Posibilitatea de a construi proceduri alcătuite din comenzi ale sistemului de
operare constituie una din principalele facilităţi puse la dispoziţie de către
interpretor. Acesta permite execuţia unor fişiere de comenzi tratate ca
proceduri. Apelul unei astfel de proceduri este identic cu al unei comenzi:
procedura arg1 arg2 ... argn

Procedura corespunde numelui unui fişier de comenzi, care trebuie să aibă


setat dreptul de execuţie, în caz contrar procedura şi parametrii ei trebuind
să fie specificaţi ca parametri ai unui alt interpretor, ca de exemplu:

/bin/bash procedura arg1 arg2 ... argn

Transmiterea parametrilor unei proceduri se face prin valoare. Reamintim


faptul că execuţia comenzilor din cadrul fişierului de comenzi se va face în
contextul unei noi instanţe a interpretorului, aceasta fiind un proces fiu al
interpretorului. Fişierele de comenzi pot fi apelate recursiv.

Comenzi Linux
Acest tip de comenzi sunt programe care apar sub forma unor fişiere
executabile şi sunt de regulă situate într-unul din directoarele /bin, /sbin,
/usr/bin, /usr/sbin sau altele, directoare ce sunt incluse de obicei în
cadrul valorii variabilei PATH. Descriem mai jos sumar câteva dintre
acestea.
man [secţiune_manual] nume_comandă
Afişează pagina de manual, care conţine informaţii despre comanda
specificată. Opţional, se poate indica şi secţiunea de manual (sub
forma unui număr) în care e situată comanda.
cp fişier_sursă fişier_destinaţie
cp listă_fişiere_sursă director_destinaţie
cp -R director_sursă director_destinaţie
Copiază un fişier sau un director într-un alt director, eventual sub un
alt nume, sau mai multe fişiere într-un anumit director.

18
Sisteme de operare. Chestiuni teoretice şi practice

mv fişier_sursă fişier_destinaţie
mv listă_fişiere_sursă director_destinaţie
Redenumeşte un fişier sau mută mai multe fişiere într-un director.
rm [-dR] fişier_sau_director
Şterge un fişier sau, pentru opţiunea -d, un director. Opţiunea -R
indică intrarea în adâncime (recursiv) în subdirectoare.
mkdir nume_director
Crează un director.
cat listă_fişiere
Afişează pe ecran conţinutul fişierelor specificate ca parametri.
test condiţie
Evaluează condiţia şi întoarce rezultatul evaluării. Această comandă
se regăseşte şi printre cele încorporate în codul încărcătorului şi este
folosită în cazul în care e necesară evaluarea unei condiţii (de
exemplu pentru comanda if). Condiţia poate să apară sub una din
formele de mai jos:
! condiţie
Neagă rezultatul evaluării expresiei.
cond1 –a cond2
Realizează o evaluare de tip ŞI (AND) logic.
cond1 –o cond2
Realizează o evaluare de tip SAU (OR) logic.
-n şir_de_caractere
Adevărat dacă şirul de caractere are lungime nenulă.
-z şir_de_caractere
Adevărat dacă şirul de caractere are lungimea zero.
şir_de_caractere1 = şir_de_caractere2
Adevărat dacă cele două şiruri sunt identice.
şir_de_caractere1 != şir_de_caractere2
Adevărat dacă cele două şiruri sunt diferite.
nr1 –eq nr2
Adevărat dacă cele două numere întregi sunt egale. Alte
opţiuni de comparare sunt: -lt (mai mic), -le (mai mic sau
egal), -gt (mai mare), -ge (mai mare sau egal).
-d nume_director
Adevărat dacă directorul specificat există.
-f fişier
Adevărat dacă fişierul specificat există şi e fişier ordinar.

19
Fişiere de comenzi în Linux

less fişier_text
Permite printre altele, afişarea pe ecran a unui fişier text, pagină cu
pagină. Permite de asemenea derularea înainte şi înapoi a
vizualizării.

uname -a
Afişează informaţii despre sistem.

pwd
Afişează directorul curent al instanţei interpretorului din care s-a
lansat comanda. Această comandă se regăseşte şi printre cele
incorporate în codul încărcătorului.

Funcţii şi comenzi încorporate în interpretor


În cadrul fişierelor de comenzi se pot defini funcţii. Formatul general pentru
definirea unei funcţii este:

nume_funcţie()
{ cmd1; ... cmd2; }

unde nume_funcţie este numele funcţiei, parantezele marchează definirea


funcţiei, iar între acolade este specificat corpul funcţiei. Se impune ca prima
comandă să fie separată de acoladă cu un spaţiu, iar ultima comandă să fie
terminată cu caracterul ';', dacă acolada se află pe aceeaşi linie cu comanda.
De regulă, dacă un utilizator şi-a definit mai multe funcţii într-un fişier, el
poate face cunoscut interpretorului curent aceste funcţii prin specificarea în
linia de comandă a numelui fişierului precedat de un punct şi un spaţiu, sub
forma:
. myfuncs

Execuţia unei funcţii este mai rapidă decât a unui fişier de comenzi echivalent,
deoarece interpretorul nu necesită căutarea fişierului pe disc, deschiderea lui
şi încărcarea conţinutului său în memorie. Ştergerea unei definiţii de funcţii
este similară cu ştergerea unei variabile. Se foloseşte comanda unset.

Comenzile încorporate în cadrul interpretorului pot fi apelate direct în fişierele


de comenzi. O parte dintre ele şi efectul lor este prezentat în cele ce urmează.
break [n]
Este comanda de părăsire a celei mai interioare bucle for, while sau
until. Dacă n este specificat se iese din n bucle. De exemplu:

20
Sisteme de operare. Chestiuni teoretice şi practice

while true
do
read cmd
if [ "$cmd" = quit ]
then break
else "$cmd"
fi
done
cd [dir]
Schimbă directorul curent la cel specificat. Directorul curent este
parte a contextului curent. Din acest motiv la execuţia unei comenzi
cd dintr-o subinstanţă a interpretorului doar directorul curent al
acesteia este modificat.
continue [n]
Este comanda care permite trecerea la o nouă iteraţie a buclei for,
while sau until. De exemplu:
for file
do
if [ ! -f "$file" ]
then
echo "$file not found"
continue
fi
# prelucrarea fisierului
done
echo [-n][arg]
Este comanda de afişare a argumentelor sale (care sunt cuvinte) la
ieşirea standard. Dacă opţiunea -n este specificată caracterul '\n' nu
este scris la ieşirea standard (nu se trece la linie nouă).
eval cmd
Evaluează o comandă şi o execută. De exemplu:

Se observă că eval parcurge lista de argumente de două ori: la


transmiterea argumentelor spre eval şi la execuţia comenzii. Lucrul
acesta este ilustrat şi de exemplul următor.

21
Fişiere de comenzi în Linux

Comanda eval este folosită în fişiere de comenzi care construiesc


linii de comandă din mai multe variabile. Comanda e utilă dacă
variabilele conţin caractere care trebuie să fie recunoscute de
interpretor nu ca rezultat al unei substituţii. Astfel de caractere sunt:
{;, |, &, < , >, "}.
exec prg
Execută programul prg specificat. Programul lansat în execuţie
înlocuieşte programul curent. Dacă exec are ca argument
redirectarea I/E, interpretorul va avea I/E redirectate. De exemplu:
file=$1 # contorizează numarul
count=0 # de linii dintr-un fisier
exec < $file
while read line
do
count=`expr $count + 1`
done
echo $count
exit [(n)]
Cauzează terminarea interpretorului curent cu cod de ieşire egal cu
n. Dacă n este omis, codul de ieşire este cel al ultimei comenzi
executate.
export [v...]
Marchează v ca nume de variabilă exportată pentru mediul
comenzilor executate ulterior. Dacă nu este precizat nici un
argument se afişează o listă cu toate numele exportate de către
interpretorul curent. Funcţiile nu pot fi exportate.
getopts opt v
Comanda este folosită la prelucrarea opţiunilor din linia de comandă.
Se execută, de regulă, în bucle. La fiecare iteraţie getopts
inspectează următorul argument din linia de comandă şi decide dacă
este o opţiune validă sau nu. Decizia impune ca orice opţiune să
înceapă cu caracterul '-' şi să fie urmată de o literă precizată în opt.
Dacă opţiunea există şi este corect precizată, ea este memorată în

22
Sisteme de operare. Chestiuni teoretice şi practice

variabila v şi comanda returnează zero. Dacă litera nu este printre


opţiunile precizate în opt, comanda memorează în v un semn de
întrebare şi returnează zero cu afişarea unui mesaj de eroare. În cazul
în care argumentele din linia de comandă au fost epuizate sau
următorul argument nu începe cu caracterul '-' comanda returnează o
valoare diferită de zero. De exemplu, pentru ca getopts să
recunoască opţiunile "-a" şi "-b" pentru o comandă oarecare cmd,
apelul este:
getopts ab var
Comanda cmd se poate apela:
cmd -a -b sau cmd -ab
În cazul în care opţiunea impune un argument suplimentar acesta
trebuie separat de opţiune printr-un spaţiu. Pentru a indica comenzii
getopts că urmează un argument după o opţiune, litera opţiunii
trebuie postfixată cu caracterul ':'. De exemplu, dacă opţiunea "-b",
din exemplul anterior, ar necesita un argument, atunci trebuie scris:
getopts ab: var

Dacă getopts nu găseşte după opţiunea "-b" argumentul, în


variabila var se memorează un semn de întrebare şi se va afişa un
mesaj de eroare. În caz că argumentul există, el este memorat într-o
variabilă specială OPTARG. O altă variabilă specială, OPTIND, este
folosită de comandă pentru a preciza numărul de argumente
prelucrate. Valoarea ei iniţială este 1.

read listă_nume_variabile
Se citeşte o linie din fişierul standard de intrare şi se atribuie
cuvintele citite variabilelor specificate. De exemplu:

23
Fişiere de comenzi în Linux

readonly [v...]
Identică cu read, dar valoarea variabilei v nu poate fi schimbată prin
atribuiri ulterioare. Dacă argumentul lipseşte, se afişează variabilele
read-only.
return [n]
Permite revenirea dintr-o funcţie cu valoarea n. Dacă n este omis,
codul returnat este cel al ultimei comenzi executate. Valoarea
returnată poate fi accesată prin variabila $? şi poate fi testată în
comenzile de control if, while şi until.
shift [n]
Deplasare spre stânga (cu n) a parametrilor din linia de comandă.
sleep n
Suspendă execuţia pentru n secunde.
[ condiţie ]
Comanda este echivalentă cu test condiţie.
type cmds
Furnizează informaţii despre comanda sau comenzile specificate.
Informaţia specifică dacă comanda este: o funcţie definită de
utilizator, una internă interpretorului sau o comandă Linux sub
forma unui executabil.
unset v
Permite ştergerea valorii unei variabile sau funcţii din mediul curent.

Comenzi de control a execuţiei


Comenzile ce permit controlul execuţiei ulterioare în funcţie de evaluarea
unei condiţii fac parte din comenzile implementate în cadrul interpretorului.
Ele sunt descrise mai jos.

Comanda IF
Sintaxa comenzii este:
if cond1
then lista_cmd_1
[ elif cond2
then lista_cmd_2]
[ else lista_cmd_3]
fi

Exemplul de mai jos ilustrează utilizarea comenzii if.

24
Sisteme de operare. Chestiuni teoretice şi practice

if test -f $1
then echo $1 este un fisier ordinar
elif test -d $1
then echo $1 este un director
else echo $1 nu e fisier ordinar sau director
fi

Este posibilă scrierea comenzii if pe o singură linie, dar în acest caz


condiţiile şi comenzile ce preced cuvintele cheie trebuie terminate cu
caracterul ';'. Astfel exemplul de mai sus se poate scrie:
if test -f $1; then echo $1 este un fisier ordinar
elif test -d $1; then echo $1 este un director
else echo $1 este necunoscut; fi

Comanda CASE
Sintaxa comenzii este:

case expresie in
sablon_1) lista_comenzi_1;;
sablon_2) lista_comenzi_2;;
...
esac

Se compară expresie cu fiecare din şabloanele prezente şi se execută lista


de comenzi unde se constată potrivirea. De exemplu, analiza unei opţiuni
din linia de comandă se poate face astfel:
case $1 in
-r) echo optiunea r;;
-m) echo optiunea m;;
*) ;;
esac

Comanda FOR
Sintaxa comenzii este:

for nume [in lista_cuvinte]


do
lista_comenzi
done

Variabila de buclă nume ia pe rând valorile din lista de cuvinte. Pentru


fiecare valoare se execută ciclul for. Dacă nu se specifică lista de cuvinte (şi
nici cuvântul cheie in), ciclul for se execută pentru fiecare parametru
transmis în linia de comandă. Condiţia poate fi specificată şi sub forma

25
Fişiere de comenzi în Linux

"for in şablon", unde şablon este un şir de caractere ce conţine


caracterul '*', caz în care variabila nume ia pe rând ca valoare numele
fişierelor şi a subdirectoarelor ce se potrivesc cu şablonul specificat.
Exemplul de mai jos este echivalent cu execuţia comenzii
"ls /home/student/*.c".
for fis in /home/student/*.c
do
echo $fis
done

Comanda WHILE
Sintaxa comenzii este:

while conditie
do
lista_comenzi
done

Lista de comenzi se execută atâta timp cât condiţia este îndeplinită, adică
atâta timp cât starea de ieşire a ultimei comenzi din condiţie este zero
(terminată cu succes). În caz contrar, bucla se termină. De exemplu, pentru a
testa periodic dacă utilizatorul user este în sesiune se poate folosi secvenţa
de mai jos:

while true
do
if who | grep "user" > /dev/null
then echo "user" este prezent
exit
else
sleep 120
done

Comanda UNTIL
Comanda e similară cu comanda while, dar lista comenzilor se execută atâta
timp cât codul de retur al ultimei comenzi din conditie este diferită de
zero (terminată fără succes), adică până când condiţia este îndeplinită.
Sintaxa comenzii este:

until conditie
do
lista_comenzi
done

26
Sisteme de operare. Chestiuni teoretice şi practice

2.4. Redirectarea fişierelor standard de intrare şi ieşire


Sistemul de operare deschide automat pentru fiecare proces nou creat trei
fişiere (având descriptori 0, 1, 2) corespunzător intrării, ieşirii şi ieşirii de
eroare standard (STDIN, STDOUT şi respectiv, STDERR). Interpretorul
permite redirectarea acestor descriptori spre alte dispozitive periferice sau
fişiere astfel:
comanda < nume_fisier
Descriptorul 0, care corespundea iniţial intrării standard, se asociază
fişierului nume_fişier, deschis pentru citire. Spunem că intrarea
standard a comenzii a fost redirectată. Prin urmare, toate citirile care
presupuneau introducerea de date de la tastatură se vor face din
fişierul nume_fişier.
comanda > nume_fisier
Descriptorul 1, care corespundea iniţial ieşirii standard, se asociază
fişierului nume_fişier, deschis pentru scriere. Spunem că ieşirea
standard a comenzii a fost redirectată. Prin urmare, toate scrierile
care presupuneau afişarea pe ecran se vor face în fişierul
nume_fişier.
comanda >> nume_fisier
Este similară construcţiei de mai sus, dar fişierul este folosit în
adăugare, deci nu se pierde conţinutul său anterior.
comanda > &nr
Indică faptul că descriptorul 1 se asociază fişierului deschis, indicat
de descriptorul nr. Presupune cunoaşterea descriptorului unui fişier
deschis şi, prin urmare, se foloseşte de obicei pentru a indica faptul
că ieşirea de eroare e redirectată spre acelaşi fişier ca şi cea standard,
sau invers. De exemplu:
ls >fis 2>&1

Exemplele de mai jos ilustrează modul de redirectare a ieşirii standard:


cat fis > /dev/lp
Listează fişierul la imprimantă.
cat f1 f2 > f3
Concatenează fişierele f1 şi f2 în f3. Dacă fişierul f3 există deja,
prin redirectare cu '>', vechiul său conţinut este pierdut.
cat f1 f2 >> f3
Dacă f3 există deja, la vechiul său conţinut se adaugă rezultatul
concatenării fişierului f1 şi f2.

27
Fişiere de comenzi în Linux

2.5. Exemple de fişiere de comenzi


Exemplul 1. Fişierul de comenzi de mai jos creează un fişier de articole, un
articol fiind constituit dintr-un nume de persoană. Se cere ca numele
fişierului să înceapă cu caracterul ‘f ’ şi să continue cu 4 cifre. Funcţia valid
verifică dacă această cerinţă este îndeplinită.

# verifica daca numele fisierului


# corespunde formatului dorit
valid()
{
case $1 in
f[3-5][1-6][1-6][1-6]);;
*) echo > invalid;;
esac
}

# inceputul programului - lista de comenzi


echo Creare fişier
echo Nume fisier sub forma fnnn:

read fname
valid $fname

if test -f invalid
then
echo Nume invalid
rm invalid
exit
fi

echo > $fname


aux=0

echo Introduceti articolele:


while read string
case $string in
[a-zA-Z]*);;
*) aux=1;;
esac
test $aux -eq 0
do
echo $string >> $fname
done

sort $fname -o $fname

echo Fisierul creat:


echo
cat $fname

28
Sisteme de operare. Chestiuni teoretice şi practice

Exemplul 2. Următorul fişier de comenzi afişează toate fişierele dintr-un


director transmis ca argument în linia de comandă, inclusiv din
subdirectoarele sale. Fişierul de comenzi se numeşte ls_rec.sh şi se apelează
astfel:
./ls_rec.sh /home/student
Conţinutul fişierului de comenzi ls_rec.sh este:

echo
echo Director $1

if test -d $1
then
for nume in $1/* $1/.[a-z,A-Z]*
do
if test -d $nume
then
./ls_rec.sh $nume
elif test –f $nume
then echo $nume
fi
done
fi

2.6. Probleme
1. Să se verifice ce afişează secvenţele de comenzi de mai jos:
a. eval echo \$$#
b. x=100
px=x
eval echo \$$px
eval $px=5
echo $x
c. ls –R / >fis 2>fis_err
d. exec ls
e. (în cadrul unui fişier de comenzi)
file=$1
count=0
while read line
do
count=`expr $count + 1`
done < $file
echo $count

Notă: Exemplul de la punctul e este conţinutul unui fişier de comenzi, în


care bucla while este executată într-o altă instanţă a interpretorului
deoarece intrarea ei este redirectată spre $file.

29
Fişiere de comenzi în Linux

2. Să se precizeze ce realizează comenzile:


who | wc -l > fis
ls *.c | wc -l >> fis
who | sort
3. Să se scrie un fişier de comenzi numit recho.sh, care îşi afişează
argumentele primite în linia de comandă în ordine inversă
a. pe aceeaşi linie sau
b. pe linii diferite (se poate utiliza comanda eval).
4. Fişierul de comenzi de mai jos decide dacă două directoare sunt identice
din punct de vedere al conţinutului fişierelor terminate în „.c” însă
conţine câteva erori. Care sunt ele?
Crtdir=`pwd`
if [ -d $1 ]
then
if [ -d $2 ]
then
cd $1
ls -R > $crtdir/f1
cd $crtdir
cd $2
ls -R > $crtdir/f2
cd $crtdir
grep ‚.c$’ f1 > f11 # raman doar fisierele
grep ‚.c$’ f2 > f22 # „.c” ordonat alfabetic
rm f1 f2
if cmp f11 f22
then
echo „Directoarele egale”
else
echo „Directoare diferite”
fi
rm f11 f22
else
echo „$2 nu e director”
fi
else
echo „$1 nu e director”
fi
5. Să se scrie un fişier de comenzi, care verifică dacă două directoare sunt
egale, fără a se folosi comanda ls. Numele celor două directoare se
transmit ca argumente în linia de comandă. Două directoare se consideră
că sunt egale dacă arborii ce le au ca rădăcină sunt identici din punct de
vedere al structurii, iar nodurile lor corespondente au acelaşi nume.

30
Sisteme de operare. Chestiuni teoretice şi practice

6. Să se scrie un fişier de comenzi care permite căutarea unui fişier în


întreaga structură a unui subdirector, fără a folosi comanda find sau alte
comenzi similare care fac acest lucru. Argumentele se precizează în linia
de comandă.
7. Să se scrie un fişier de comenzi care şterge toate sursele C dintr-un
director dacă ele se regăsesc ca şi nume în structura altui director.
Primul argument din linia de comandă este directorul în care se află
sursele C, iar al doilea este directorul de unde începe căutarea.
8. Să se scrie un fişier de comenzi care copiază întreaga structură a unui
director ca structură a unui alt director. Cele două directoare se transmit
ca argumente în linia de comandă.
9. Să se calculeze, folosind comanda ls, numărul de fişiere şi directoare
dintr-un director, luând în considerare întreaga structură a arborelui ce
are acel director ca rădăcină. Numele directorului se transmite ca
parametru în linia de comandă.
10. Să se scrie un fişier de comenzi care calculează şi afişează numărul de
fişiere, numărul de directoare şi numărul de legături simbolice dintr-un
director, luând în considerare întreaga structură a arborelui care are acel
director ca rădăcină. Se vor afişa aceleaşi informaţii pentru fiecare
subdirector parcurs. Numele directorului se transmite ca parametru în
linia de comandă.
11. Să se scrie un fişier de comenzi care creează un director a cărui cale
(numele) este specificată ca parametru în linia de comandă, iar în acel
director creează fişiere cu numele utilizatorilor conectaţi în acel
moment.
12. Să se scrie un fişier de comenzi care calculează numărul total de linii de
text şi cuvinte din toate fişierele dintr-un director, luând în considerare
întreaga structură a arborelui care are acel director ca rădăcină.
13. Să se scrie un fişier de comenzi care efectuează într-o buclă următorii
paşi: (1) citeşte de la tastatură două numere şi un operator +, -, * sau /;
(2) efectuează operaţia dorită şi (3) scrie rezultatul, pe o nouă linie, în
cadrul unui fişier sub forma:
nr_operatie: operand1 operator operand2 = rezultat

Din bucla respectivă se iese la introducerea caracterului x pe poziţia


operatorului. Înainte de terminare, se va scrie în fişier şi numărul de
operaţii efectuate. Numele fişierului în care se face scrierea se primeşte ca
parametru în linia de comandă.

31
3. Apeluri sistem pentru lucrul cu
fişiere şi directoare în Linux

Scopul lucrării
Lucrarea prezintă apelurile sistem uzuale folosite în operaţiile de
intrare/ieşire pe fişiere şi cele de manipulare a fişierelor şi directoarelor în
sistemul de operare Linux.

3.1. Descriptori de fişier


Sistemul de operare ataşează intern fiecărui fişier deschis un descriptor sau
identificator de fişier (în principiu, un număr întreg pozitiv). La deschiderea
unui fişier sau la crearea unui fişier nou, sistemul returnează un descriptor
de fişier procesului care a executat operaţia. Fiecare proces îşi are propriii
descriptori de fişier. Prin convenţie, primii trei descriptori de fişier ai
fiecărui proces sunt alocaţi automat la crearea lui. Descriptorul de fişier 0
este asociat intrării standard (tastatura), 1 ieşirii standard (ecranul), iar 2
ieşirii standard de eroare (ecranul). Ceilalţi descriptori sunt folosiţi de
proces pentru deschiderea de fişiere ordinare, pipe, speciale sau directoare.
Există cinci apeluri sistem care generează descriptori de fişiere: creat, open,
fcntl, dup şi pipe.

3.2. Apeluri sistem pentru lucrul cu fişiere


Apelul sistem OPEN
Deschiderea sau crearea unui fişier se poate face prin apelul sistem open.
Sintaxa acestui apel este:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *cale,
int optiuni, mode_t permisiuni);

Funcţia returnează un descriptor de fişier sau -1 în caz de eroare. La apelul


acestei funcţii se pot specifica două sau trei argumente, al treilea argument
fiind folosit doar la crearea de fişiere noi. Apelul cu două argumente este
folosit pentru deschiderea fişierelor existente. Funcţia returnează cel mai

32
Sisteme de operare. Chestiuni teoretice şi practice

mic descriptor de fişier disponibil. Acesta va fi utilizat în apelurile sistem


ulterioare: read, write, lseek şi close. Procesul care execută apelul sistem
open trebuie să aibă drepturi de citire şi/sau scriere asupra fişierului pe care
încearcă să-l deschidă, în funcţie de valoarea argumentului optiuni.
Pointerul din fişier (poziţia curentă relativ la care se vor efectua operaţiile
de citire şi scriere) este poziţionat pe primul octet din fişier. Argumentul
optiuni se formează printr-un SAU pe biţi între următoarele constante,
definite în fişierul fcntl.h:
O_RDONLY Fişierul este deschis doar pentru citire.
O_WRONLY Fişierul este deschis doar pentru scriere.
O_RDWR Fişierul este deschis pentru citire şi scriere.
O_APPEND Are efect doar dacă fişierul e deschis pentru scriere. În acest
caz, scrierile în fişier se fac întotdeauna la sfârşitul fişierului.
Acest lucru este asigurat automat de către sistemul de
operare, ca şi cum procesul ar poziţiona anterior scrierii,
pointerul în fişier la sfârşitul fişierului.
O_CREAT Dacă fişierul nu există, el este creat. Dacă există, este
trunchiat.
O_EXCL Dacă fişierul există şi este specificată şi opţiunea O_CREAT,
apelul open nu se execută cu succes.
O_NONBLOCK La fişiere pipe şi cele speciale pe bloc sau caracter cauzează
O_NDELAY trecerea în modul fără blocare atât pentru apelul open, cât şi
pentru operaţiile viitoare de I/E.
O_TRUNC Dacă fişierul există, i se şterge conţinutul.
O_SYNC Forţează scrierea efectivă pe disc prin write. Întârzie mult
întregul sistem, dar e eficace în cazuri critice.

Argumentul al treilea, permisiuni, poate fi o combinaţie de SAU pe biţi


între următoarele constante predefinite:
S_IRUSR, S_IWUSR, S_IXUSR Proprietar: read, write, execute.
S_IRGRP, S_IWGRP, S_IXGRP Group: read, write, execute.
S_IROTH, S_IWOTH, S_IXOTH Alţii: read, write, execute.
Aceste constante definesc drepturile de acces asupra unui fişier şi sunt
definite în fişierul sys/stat.h.

33
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux

Apelul sistem CREAT


Un fişier nou este creat cu ajutorul apelului sistem creat, a cărui sintaxă este:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *cale, mode_t permisiuni);

Funcţia creat returnează descriptorul de fişier sau -1 în caz de eroare.


Apelul funcţiei creat este echivalent cu apelul funcţiei open în forma:
open(cale, O_WRONLY | O_CREAT | O_TRUNC, mod);

Argumentul cale specifică calea şi numele fişierului, iar permisiuni


drepturile de acces. Dacă fişierul creat nu există, este alocat un nou i-node şi
o legătură spre el este plasată în directorul unde acesta a fost creat.
Proprietarul procesului (dat de UID-ul efectiv şi GUID-ul efectiv) care
execută acest apel trebuie să aibă permisiunea de scriere în directorul unde
se creează fişierul. Fişierul deschis va avea drepturile de acces specificate
de argumentul al doilea din apel (vezi şi umask). Apelul întoarce cel mai
mic descriptor de fişier disponibil. Fişierul este deschis în scriere, iar
dimensiunea sa iniţială este 0. Timpii de acces şi modificare din i-node sunt
actualizaţi. Dacă fişierul există (este nevoie de permisiunea de căutare în
director) conţinutul lui este şters, fişierul este deschis în scriere, dar nu se
modifică proprietarul sau drepturile de acces asupra lui. În acest ultim caz,
al doilea argument este ignorat.

Apelul sistem READ


Pentru a citi un anumit număr de octeţi dintr-un fişier de la poziţia curentă,
se foloseşte apelul sistem read. Sintaxa lui este:

#include <unistd.h>
ssize_t read(int fd, void* buf, size_t noct);

Funcţia returnează numărul de octeţi citiţi efectiv, 0 pentru sfârşit de fişier


(EOF) şi -1 în caz de eroare. Se încearcă citirea a noct octeţi din fişierul
deschis referit de descriptorul fd şi se depun la adresa de memorie indicată
de parametrul buf. Pointerul (poziţia curentă) în fişier este incrementat
automat după o operaţie de citire cu numărul de octeţi citiţi. Se revine din
funcţia read doar după ce datele citite de pe disc (din fişier) sunt transferate
în memorie. Acest tip de funcţionalitate se numeşte sincronă.

34
Sisteme de operare. Chestiuni teoretice şi practice

Apelul sistem WRITE


Pentru a scrie un anumit număr de octeţi într-un fişier la poziţia curentă, se
foloseşte apelul sistem write. Sintaxa lui este:

#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t noct);

Funcţia returnează numărul de octeţi scrişi si -1 în caz de eroare. Apelul


scrie noct octeţi preluaţi de la adresa de memorie indicată de parametrul
buf în fişierul al cărui descriptor este fd. Interesant de remarcat referitor la
acest apel este faptul că scrierea fizică pe disc este întârziată. Ea se
efectuează la iniţiativa sistemului de operare fără ca utilizatorul să fie
informat. Dacă procesul care a efectuat apelul sau un alt proces citeşte
datele care încă nu au fost scrise pe disc, sistemul le citeşte înapoi din
bufferele cache. Scrierea întârziată este mai rapidă, dar are trei dezavantaje:
1. eroare pe disc sau căderea sistemului duce la pierderea datelor;
2. un proces care a iniţiat o operaţie de scriere nu poate fi informat în
cazul apariţiei unei erori de scriere;
3. ordinea scrierilor fizice nu poate fi controlată.
Pentru a elimina aceste dezavantaje, în anumite cazuri se foloseşte opţiunea
O_SYNC specificată în momentul deschiderii fişierului. Dar cum aceasta
scade viteza sistemului şi având în vedere fiabilitatea sistemelor de astăzi,
se preferă mecanismul de lucru cu tampoane cache.

Apelul sistem CLOSE


Pentru închiderea unui fişier şi, implicit, eliberarea descriptorului ataşat, se
foloseşte apelul sistem close. Sintaxa lui este:

#include <unistd.h>
int close(int fd);

Funcţia returnează 0 în caz de succes şi -1 în caz de eroare. Un fişier


deschis este oricum închis automat la terminarea procesului.

Apelul sistem LSEEK


Pentru poziţionarea absolută sau relativă a indicatorului poziţiei curente
într-un fişier se foloseşte apelul sistem lseek. Următoarele operaţii de citire
din fişier şi scriere în fişier se vor efectua relativ la noua poziţie curentă în
fişier. Sintaxa funcţiei lseek este următoarea:

35
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t salt, int relativLa);

Funcţia returnează deplasamentul faţă de începutul fişierului al noii poziţii


curente din fişier sau -1 în caz de eroare. Nu se efectuează nici o operaţie
de I/O şi nu se trimite nici o comandă controlerului de disc. Saltul relativ la
punctul indicat de parametrul relativLa se face cu numărul de octeţi
indicaţi de parametrul salt. Parametrul relativLa poate avea doar una
dintre următoarele valori predefinite:
SEEK_SET repoziţionarea (saltul) se face relativ la începutul fişierului
(primul octet din fişier este la deplasament zero).
SEEK_CUR repoziţionarea se face relativ la poziţia curentă.
SEEK_END repoziţionarea se face relativ la sfârşitul fişierului.

Apelurile sistem open, creat, write şi read execută implicit lseek. Dacă un
fişier este deschis folosind constanta simbolică O_APPEND, se efectuează un
apel lseek la sfârşitul fişierului înaintea unei operaţii de scriere.

Apelul sistem LINK


Pentru a lega un fişier existent la un alt director (sau acelaşi) se foloseşte
apelul sistem link. Stabilirea unei legături înseamnă de fapt stabilirea unui
nou nume sau a unei noi căi de acces spre un fişier existent. Sintaxa acestui
apel sistem este:

#include <unistd.h>
int link(const char* vecheaCale, const char* nouaCale);

Funcţia returnează 0 în caz de succes şi -1 în caz contrar. Argumentul


vecheaCale trebuie să indice un fişier existent. Doar root-ul are dreptul de a
stabili legături spre un director.

Apelul sistem UNLINK


Pentru a şterge o legătură (cale) dintr-un director se foloseşte apelul sistem
unlink. Sintaxa lui este:

#include <unistd.h>
int unlink(const char* cale);

36
Sisteme de operare. Chestiuni teoretice şi practice

Funcţia returnează 0 în caz de succes şi -1 în caz contrar. Apelul unlink


decrementează contorul de legături fizice din i-node-ul fişierului specificat şi
şterge intrarea din director corespunzătoare fişierului şters. Dacă numărul de
legături ale unui fişier devine 0, spaţiul ocupat de fişierul în cauză şi i-node-ul
său este eliberat. Doar root-ul poate să şteargă un director folosind acest apel
sistem. Altfel, apelul sistem rmdir poate fi utilizat pentru a şterge un director.

Apelurile sistem STAT, LSTAT şi FSTAT


Pentru a obţine informaţii detaliate despre un fişier se pot folosi apelurile
sistem stat, lstat sau fstat.

#include <sys/types.h>
#include <sys/stat.h>
int stat(const char* cale, struct stat* buf);
int lstat(const char* cale, struct stat* buf);
int fstat(int fd, struct stat* buf);

Cele trei funcţii returnează 0 în caz de succes şi -1 în caz de eroare. Primele


două primesc ca parametru calea şi numele spre un fişier şi completează
structura de la adresa buf cu informaţii din i-node-ul fişierului. Apelul fstat e
similar, dar funcţionează pentru fişiere deschise cărora li se cunoaşte
descriptorul. Diferenţa între stat şi lstat apare doar în cazul unui fişier
legătură simbolică, caz în care stat returnează informaţii despre fişierul
referit (legat), pe când lstat returnează informaţii despre fişierul legătură.
Structura struct stat e definită în fişierul sys/stat.h şi conţine câmpurile:
struct stat {
mode_t st_mode; // tip fisier & drepturi
ino_t st_ino; // i-node
dev_t st_dev; // numar de dispozitiv (SF)
nlink_t st_nlink; // numarul de legaturi
uid_t st_uid; // ID proprietar
gid_t st_gid; // ID grup
off_t st_size; // dim. pt. fisiere ordinare
time_t st_atime; // timpul ultimului acces
time_t st_mtime; // timpul ultimei modificari
time_t st_ctime; // timpul schimbarii starii
dev_t st_rdev; // nr. dispozitiv
// pt. fisiere speciale
long st_blksize; // dimensiunea optima
// a blocului de I/E
long st_blocks; // numar de blocuri
// de 512 octeti alocate
};

37
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux

Comanda Linux care foloseşte cel mai des acest apel sistem este ls.
Declaraţiile de tipuri pentru membrii structurii se găsesc în fişierul
sys/types.h. Tipul fişierului este codificat, alături de drepturile de acces, în
câmpul st_mode şi poate fi determinat folosind următoarele macrouri:

Tabelul 1. Macrouri pentru obţinerea tipului unui fişier


Macro Semnificaţie
S_ISREG(st_mode) Fişier obişnuit
S_ISDIR(st_mode) Fişier director
S_ISCHR(st_mode) Dispozitiv special de tip caracter
S_ISBLK(st_mode) Dispozitiv special de tip bloc
S_ISFIFO(st_mode) Fişier pipe sau fifo
S_ISLNK(st_mode) Legătura simbolică

Decodificarea informaţiilor din st_mode poate fi făcută testând rezultatul


operaţiei de „ŞI pe biţi” (&) între câmpul st_mode şi una dintre constantele
(măşti de biţi): S_IFIFO, S_IFCHR, S_IFBLK, S_IFDIR, S_IFREG,
S_IFLNK, S_ISUID (setat bitul suid), S_ISGID (setat bitul sgid), S_ISVTX
(setat bitul sticky), S_IRUSR (drept de citire pentru proprietar), S_IWUSR
(drept de scriere pentru proprietar), S_IWUSR (drept de execuţie pentru
proprietar) etc.

Apelul sistem ACCESS


În momentul accesului unui fişier de către un proces, sistemul de operare
verifică permisiunea de acces la fişier a acelui proces în funcţie de UID-ul şi
GID-ul său efectiv. Există situaţii când este nevoie să se testeze drepturile de
acces bazându-se pe UID-ul şi GID-ul real. O situaţie în care acest lucru este
util este atunci când un proces se execută, datorită setării biţilor suid sau sgid
ai fişierului executabil, cu alte drepturi decât cele ale utilizatorului care a
lansat procesul, dar se doreşte să se verifice dacă utilizatorul real poate accesa
fişierul. Apelul sistem access permite testarea acestui lucru. Sintaxa lui este:

#include <unistd.h>
int access(const char* cale, int tipAcces);

Funcţia returnează 0 dacă există permisiunea şi -1 în caz contrar.


Parametrul tipAcces poate fi o combinaţie de tipul „ŞI pe biţi” între
următoarele constante predefinite: R_OK (dreptul de citire), W_OK (dreptul
de scriere), X_OK (dreptul de execuţie), F_OK (existenţa fişierului).

38
Sisteme de operare. Chestiuni teoretice şi practice

Apelul sistem UMASK


Pentru a îmbunătăţi securitatea sistemului de fişiere, sistemul de operare
Linux permite stabilirea pentru fiecare proces a unei măşti (filtru) de biţi ce
indică resetarea automată a unor drepturi de acces la crearea fişierelor.
Structura pe biţi a acestei măşti este similară cu structura câmpului din i-
node-ul fişierelor care codifică pe biţi permisiunile de acces. Biţii din mască
poziţionaţi pe 1 invalidează, la crearea unui fişier, biţii corespunzători din
argumentul care precizează drepturile de acces. Masca nu afectează apelul
sistem chmod, astfel încât procesele au posibilitatea de a-şi fixa explicit
drepturile de acces indiferent de valoarea măştii stabilite prin umask.
Sintaxa apelului este:

#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);

Funcţia returnează valoarea măştii anterioare. Efectul apelului este ilustrat


în exemplul de mai jos:
main()
{
int fd;
umask(022);
if ((fd=creat("temp", 0622))==-1)
{ perror("creat"); exit(0); }
system("ls -l temp");
}

Rezultatul afişat va fi de forma:


-rw------- temp
în care se observă resetarea automată a drepturilor de scriere pentru grup şi
alţi utilizatori decât proprietarul.

Apelul sistem CHMOD


Pentru a modifica drepturile de acces asupra unui fişier existent se poate
folosi apelul sistem chmod, a cărui sintaxă este:

#include <sys/types.h>
#include <sys/stat.h>
int chmod(const char* cale, mode_t permisiuni);

39
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux

Funcţia returnează 0 în caz de succes şi -1 în caz contrar. Funcţia chmod


modifică drepturile de acces ale fişierului specificat de parametrul cale în
conformitate cu valoarea argumentului permisiuni. Pentru a putea
modifica drepturile de acces, UID-ul efectiv al procesului trebuie să fie egal
cu cel al proprietarului fişierului sau procesul trebuie să aibă drepturi de
administrator (root).

Argumentul permisiuni poate fi specificat printr-una dintre constantele


descrise mai jos şi definite în sys/stat.h. Se poate obţine un efect cumulat al
lor folosind operatorul SAU pe biţi.

Tabelul 2. Măşti pe biţi pentru setarea drepturilor de acces la un fişier


Mod Descriere
S_ISUID Setează bitul suid
S_ISGID Setează bitul sgid
S_ISVTX Setează bitul sticky
S_IRWXU
Drept de citire, scriere şi execuţie pentru proprietar obţinut
din: S_IRUSR | S_IWUSR | S_IXUSR
S_IRWXG
Drept de citire, scriere şi execuţie pentru grup, obţinut din:
S_IRGRP | S_IWGRP | S_IXGRP
S_IRWXO Drept de citire, scriere şi execuţie pentru alţii, obţinut din:
S_IROTH | S_IWOTH | S_IXOTH

Apelul sistem CHOWN


Acest apel sistem este utilizat în scopul modificării proprietarului (UID) şi
al grupului (GID) căruia îi aparţine un fişier. Sintaxa funcţiei este:

#include <sys/types.h>
#include <unistd.h>
int chown(const char* cale,
uid_t proprietar, gid_t grup);

Funcţia returnează 0 în caz de succes şi -1 în caz de eroare. Apelul ei


schimbă proprietarul şi grupul fişierului precizat de parametrul cale, la noul
proprietar specificat de parametrul proprietar şi la noul grup specificat de
parametrul grup. În afară de root, un utilizator obişnuit nu poate schimba
proprietarul fişierelor altor utilizatori, dar poate schimba GID-ul pentru
fişierele proprii, dar numai pentru acele grupuri din care face parte.

40
Sisteme de operare. Chestiuni teoretice şi practice

Apelul sistem UTIME


În structura stat există trei membri care se referă la timp, conform tabelului
de mai jos.

Tabelul 3. Informaţii de timp asociate unui fişier


Câmp Descriere Operaţie
st_atime Ultimul acces la datele fişierului read
st_mtime Ultima modificare a datelor fişierului write
st_ctime Schimbarea stării i-node-ului chmod, chown

Diferenţa între timpul de modificare al fişierului şi cel de schimbare a stării


i-node-ului constă în faptul că primul se referă la momentul în care
conţinutul fişierului a fost modificat, iar cel de-al doilea la momentul în care
informaţia din i-node a fost modificată. Acest lucru se datorează faptului că
informaţia din i-node este memorată separat de conţinutul fişierului.
Apelurile sistem care modifică i-node-ul sunt cele care modifică drepturile
de acces asupra unui fişier, cele care schimbă UID-ul, numărul de legături
etc. Sistemul de operare nu reţine timpul ultimului acces la i-node. Acesta
este motivul pentru care apelurile sistem access şi stat nu schimbă nici unul
dintre aceşti timpi.
Timpii de acces şi de modificare ai unui fişier de orice tip pot fi schimbaţi
printr-unul dintre apelurile sistem de mai jos:

#include <sys/time.h>
int utimes(const char* cale,
const struct timeval* timpi);
int lutimes(const char* cale,
const struct timeval* timpi);
int futimes(int fd, const struct timeval* timpi);

Funcţiile returnează 0 în caz de succes şi -1 în caz contrar. Doar


proprietarul unui fişier sau root-ul pot modifica timpii asociaţi unui fişier.
Parametrul timpi reprezintă adresa (pointer) unui şir de două structuri
timeval, care corespund timpului de acces şi, respectiv, de modificare.
Câmpurile structurii timeval sunt descrise mai jos:

struct timeval {
long tv_sec; // sec. trecute din 1.01.1970
suseconds_t tv_usec; // microsecunde
};

41
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux

Pentru obţinerea timpului curent în forma cerută de structura timeval


poate fi folosită funcţia gettimeofday (a se vedea pagina de manual). Pentru
diferite conversii între forma normală a specificării unei date şi ore şi cea
specifică structurii timeval se poate folosi funcţia ctime sau o alta din
aceeaşi familie (a se vedea pagina de manual).

3.3. Funcţii pentru lucrul cu directoare


Conţinutul unui director poate fi obţinut de către procesele care au drept de
citire asupra directorului prin operaţii de citire similare cu cele asupra
fişierelor. Scrierea într-un director poate fi făcută doar de către sistemul de
operare. Structura unui director apare utilizatorului ca o succesiune de
structuri (elemente) numite intrări în director. O intrare în director conţine,
printre alte informaţii, numele fişierului şi i-node-ul acestuia. Pentru citirea
intrărilor unui director există următoarele funcţii:
#include <sys/types.h>
#include <dirent.h>
DIR* opendir(const char* cale);
struct dirent* readdir(DIR* dp);
void rewinddir(DIR* dp);
int closedir(DIR* dp);

Funcţia opendir are ca efect deschiderea directorului, adică pregătirea


pentru operaţiile ulterioare de citire a conţinutului lui. Ea returnează un
pointer valid dacă deschiderea a reuşit şi NULL în caz de eroare.
Funcţia readdir citeşte la fiecare nou apel al ei, în ordine secvenţială,
următoarea intrare din director: primul apel readdir citeşte prima intrare din
director, următorul apel citeşte următoarea intrare şi aşa mai departe.
Funcţia returnează un pointer valid spre o structură de tip dirent, dacă
citirea a reuşit şi NULL în caz contrar (sfârşitul directorului).
Funcţia rewinddir repoziţionează indicatorul din director spre prima intrare
din director (începutul directorului).
Funcţia closedir închide un director deschis anterior. Returnează -1 în caz
de eroare.
Structura dirent, definită în fişierul dirent.h, conţine cel puţin doi membri:
struct dirent {
ino_t d_fileno; // nr. i-node
char d_name[MAXNAMLEN + 1]; // nume fişier
}

42
Sisteme de operare. Chestiuni teoretice şi practice

3.4. Exemple
Exemplul 1. Programul de mai jos, numit CreareGauri.c, creează un fişier cu
două zone de 1M octeţi în care nu se scrie nimic. Astfel de fişiere se numesc
fişiere cu găuri. O gaură se obţine printr-un salt făcut cu funcţia lseek după
sfârşitul fişierului, operaţie urmată de o scriere la noul deplasament.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
char buf1[]="LAB ";
char buf2[]="OS ";
char buf3[]="Linux";

int main(void)
{
int fd;
if ((fd=creat("fisier.gol", 0644)) < 0)
{ perror("Eroare creare fisier"); exit (1); }
if (write(fd, buf1, strlen(buf1)) < 0)
{ perror("Eroare scriere"); exit(2); }
if (lseek(fd, 1024 * 1024, SEEK_SET) < 0)
{ perror("Eroare pozitionare"); exit(3); }
if (write(fd, buf2, strlen(buf2)) < 0)
{ perror("Eroare scriere"); exit(2); }
if (lseek(fd, 1024 * 1024, SEEK_SET) < 0)
{ perror("Eroare pozitionare"); exit(3); }
if (write(fd, buf3, strlen(buf3)) < 0)
{ perror("Eroare scriere"); exit(2); }
}

Urmăriţi efectul execuţiei programului cu ajutorul comenzilor:


rm -f fisier.gol
df -h
gcc -o CreareGauri.exe CreareGauri.c
./CreareGauri.exe
ls -l fisier.gol
stat fisier.gol
od -c fisier.gol
df -h

43
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux

Exemplul 2. Programul de mai jos copiază conţinutul unui fişier existent


într-un alt fişier. Numele celor două fişiere se citesc ca argumente din linia
de comandă. Se presupune că oricare dintre apelurile sistem read sau write
poate genera erori.

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define BUFSIZE 512

int main (int argc, char** argv)


{
int from, to, nr, nw;
char buf[BUFSIZE];

if (argc != 3) {
printf("Utilizare: %s fis_sursa fis_dest\n",
argv[0]);
exit(0);
}

if ((from = open(argv[1], O_RDONLY)) < 0) {


perror("Eroare deschidere fisier sursa");
exit(1);
}

if ((to = creat(argv[2], 0666)) < 0) {


perror("Eroare deschidere fisier dest.");
exit(2);
}

while((nr = read(from, buf, sizeof(buf))) != 0) {

if (nr < 0) {
perror("Eroare citire din fisier sursa");
exit(3);
}

if ((nw=write(to, &buf, nr)) < 0) {


perror("Eroare scriere in fisier dest.");
exit(4);
}

close(from);
close(to);
}

44
Sisteme de operare. Chestiuni teoretice şi practice

Exemplul 3. Programul de mai jos afişează conţinutul unui director,


specificând pentru fiecare element din director tipul său. Numele
directorului se transmite ca parametru în linia de comandă.

#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>

void listDir(char *dirName)


{
DIR* dir;
struct dirent *dirEntry;
struct stat inode;
char name[1000];

dir = opendir(dirName);
if (dir == 0) {
perror ("Eroare deschidere director");
exit(1);
}

while ((dirEntry=readdir(dir)) != 0) {

sprintf(name,"%s/%s",dirName,dirEntry->d_name);
lstat (name, &inode);

// test the type of file


if (S_ISDIR(inode.st_mode))
printf("dir ");
else if (S_ISREG(inode.st_mode))
printf ("fis ");
else
if (S_ISLNK(inode.st_mode))
printf ("lnk ");
else;
printf(" %s\n", dirEntry->d_name);

}
}

int main(int argc, char **argv)


{
if (argc != 2) {
printf ("UTILIZARE: %s nume_dir\n", argv[0]);
exit(0);
}

printf("Continutul directorului este:\n");


listDir(argv[1]);
}

45
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux

3.5. Probleme
1. Modificând Exemplul 1, să se verifice dacă în cazul creării unui fişier cu
găuri sistemul de operare alocă spaţiu pe HDD şi pentru găurile din
fişier. Pentru aceasta se va calcula, folosind apelul sistem lseek,
dimensiunea în octeţi şi în blocuri a unui fişier, iar apoi se vor compara
rezultatele obţinute cu valorile similare returnate de apelul sistem stat.
Se pot folosi, de asemenea comenzile stat şi df. Testele vor fi făcute pe
fişiere cu găuri foarte mari (sute de MB sau GB). Să se testeze, de
asemenea, ce returnează o operaţie de citire dintr-o gaură din fişier.
2. Să se scrie un program C care scrie în ordine inversă liniile unui fişier
text într-un alt fişier. Numele ambelor fişiere se specifică ca argumente
ale programului în linia de comandă.
3. Să se scrie un program C care citeşte dintr-un fişier octeţii de la
deplasamentele 0, 20, 40 etc. (până la sfârşitul fişierului) şi îi scrie la
sfârşitul aceluiaşi fişier. Să se afişeze dimensiunea fişierului înainte şi
după scrierea caracterelor.
4. Să se testeze dacă într-un fişier deschis în mod O_RDWR | O_APPEND, se
poate citi de la şi scrie la orice deplasament. Să se scrie apoi un program
C, care va fi lansat simultan de N ori. Programul scrie la sfârşitul unui
fişier binar identificatorul de proces, obţinut cu funcţia getpid. Nici unul
dintre cele N procese nu poate să-şi continue execuţia până ce toate
celelalte procese nu şi-au scris identificatorul propriu în fişier. În final,
fiecare proces afişează următorul identificator din fişier. Valoarea
constantei N se presupune cunoscută în momentul scrierii programului.
5. Să se scrie un program C care să permită inserarea unor şiruri de
caractere într-un fişier text, începând cu o anumită poziţie. Apelul
programului se face sub forma: ”insert fisier positie sir”.
6. Să se scrie un program care elimină tot al cincilea octet dintr-un fişier,
fără a se folosi un fişier temporar şi fără a citi în memorie întregul fişier.
Pentru ajustarea dimensiunii fişierului se poate folosi funcţia truncate.
7. Într-un fişier binar fis.bin sunt scrise numere întregi. Să se facă media
aritmetică a fiecărui grup de numere cuprinse între două zerouri. Să se
scrie valorile respective pe câte o linie distinctă în fişierul text medii.txt.
Începutul şi sfârşitul fişierului pot fi considerate ca zerouri.
8. Într-un fişier binar numit fis.bin sunt scrise numere şi caractere sub
forma: două numere întregi urmate de un caracter. Caracterul poate fi
‚+’, ‚-‚, ‚*’ sau ‚/’. Să se scrie un program C care citeşte un anumit grup

46
Sisteme de operare. Chestiuni teoretice şi practice

de numere şi operatorul ataşat, efectuează operaţia dintre cele două


numere şi apoi scrie într-un fişier text res.txt o linie de forma:
nr1 operator nr2 = rezultat
Linia de rezultat se va adăuga la sfârşitul fişierului. Numărul grupului
vizat din fişierul binar se specifică ca argument în linia de comandă.
9. Se consideră următorul program scris în fişierul sursă C prg.c:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void main( void) {
if (open(„temp”, O_RDWR) < 0)
{ perror(„Eroare open”); exit(1); }
if (unlink(„temp”) < 0)
{ perror(„Eroare unlink”); exit(2); }
printf(„Fisierul temp a fost sters.\n”);
sleep(30);
printf(„Terminare program.\n”);
}
Să se explice rezultatul următoarelor comenzi:
dd if=/dev/zero of=temp bs=1024 count=1024
ls -lh temp; df -h # înainte de execuţia progr.
Gcc prg.c -o prg.exe; prg.exe &
ls -lh temp; df -h # înainte de terminarea progr.
Ls -lh temp; df -h # după terminarea progr.
10. Să se scrie un program C care şterge un director cu tot ce conţine acesta.
Numele directorului se specifică în linia de comandă.
11. Să se scrie un program C care caută un fişier în întreaga structură a unui
arbore de fişiere şi directoare, care are ca rădăcină un director dat.
Numele fişierului şi al directorului de pornire se transmit ca argumente
ale programului în linia de comandă. Opţional, se poate considera cazul
indicării numelui de fişier sub forma unui şablon, folosind caracterul ‚*’.
12. Să se scrie un program C similar cu cel de la problema precedentă, dar
pentru căutarea unui şir de caractere în cadrul fişierelor.
13. Să se scrie un program C similar ca funcţionalitate comenzii Linux mv.
Programul se poate apela în formele:
move numeFisVechi numeFisNou
move numeFis numeDir
move numeDirVechi numeDirNou
14. Să se modifice Exemplul 3, astfel încât să se parcurgă întreaga structură
a directorului şi să se afişeze şi dimensiunea şi drepturile de acces. Se va
ţine cont de legăturile simbolice.

47
4. Sistemul de fişiere NTFS
Scopul lucrării
Această lucrare prezintă pe scurt câteva caracteristici ale NTFS, sistemul de
fişiere nativ al sistemului de operare Windows 2000, şi principalele funcţii
ale API-ului Win32 legate de gestiunea fişierelor şi directoarelor.

4.1. Prezentare generală


NTFS (New Technology File System) este un sistem de fişiere dezvoltat
special pentru Windows NT şi îmbunătăţit pentru Windows 2000. NTFS4 este
folosit la Windows NT, în timp ce sistemul de fişiere pentru Windows 2000
este NTFS5. Windows XP foloseşte o versiune uşor îmbunătăţită a NTFS5.
Facilităţile principale oferite de acest sistem de fişiere sunt următoarele:
• foloseşte adrese de disc de 64 de biţi şi poate suporta partiţii de
până la 264 bytes ;
• permite folosirea caracterelor Unicode în numele de fişiere;
• permite folosirea numelor de fişiere de până la 255 de caractere,
inclusiv spaţii şi puncte;
• permite indexarea fişierelor;
• oferă posibilitatea managementului dinamic al sectoarelor ;
• datorită compatibilităţii POSIX, permite crearea hard-link-uri,
face distincţie între litere mari şi mici în cadrul numelor de fişiere
şi păstrează informaţii de timp referitoare la fişier;
• permite utilizarea fişierelor cu seturi multiple de date.

4.2. Structura unei partiţii NTFS


La formatarea unei partiţii (volum) conform NTFS se creează o serie de
fişiere sistem, dintre care cel mai important este fişierul Master File Table
(MFT), care conţine informaţii despre toate fişierele şi directoarele de pe
volumul NTFS, fiind un fel de baza de date a sistemului.
Prima locaţie pe o partiţie NTFS este sectorul de boot, care este sectorul 0
al partiţiei şi conţine un program (cod executabil) de pornire a sistemului.
Alte informaţii necesare programului de boot-are (de exemplu informaţii
necesare accesării volumului) pot fi înscrise în sectoarele de la 1 la 16, care
sunt rezervate în acest scop. Figura 1 ilustrează structura unui volum NTFS
la terminarea formatării.

48
Sisteme de operare. Chestiuni teoretice şi practice

Primul fişier pe un volum NTFS este fişierul MFT. Pentru fiecare fişier de pe
un volum NTFS există cel puţin o intrare în MFT, inclusiv pentru MFT. Toate
informaţiile despre un fişier, incluzând numele, dimensiunea, informaţii de
timp referitoare la fişier, permisiuni şi datele efective sunt păstrate în MFT sau
în spaţiul situat în exteriorul MFT-ului care descrie intrări în MFT. Toate
aceste informaţii sunt considerate atribute ale fişierului, acesta fiind tratat ca o
colecţie de atribute. Un atribut este o secvenţă de octeţi organizaţi în două
componente: componenta de descriere a atributului (header) şi conţinutul său.
Atributele de fişier sunt păstrate în MFT, atunci când dimensiunea lor permite
să fie memorate în intrarea corespunzătoare din MFT sau în zone auxiliare de
pe HDD, exterioare fişierului MFT şi asociate intrării din MFT a fişierului.

Sectorul Fişiere
de boot Master File Table sistem Zona de fişiere

Figura 1. Structura unui volum NTFS

Tabelul de mai jos conţine toate tipurile de atribute definite în prezent de


sistemul de fişiere NTFS. Aceste tipuri de atribute sunt folosite intern de
către NTFS, utilizatorul neavând acces direct la atribute şi neputând defini
noi tipuri de atribute. Această listă este extensibilă, în sensul că în viitor se
vor putea defini şi alte atribute de fişier.

Tabelul 1. Tipuri de atribute ale fişierelor în NTFS


Tipul
Descriere
atributului
Standard
Include informaţii cum ar fi informaţii de timp şi numărul de legături.
information
Attribute Listează locaţiile tuturor înregistrărilor atributelor non-rezidente.
Lists
File Name Un atribut care se poate repeta atât pentru denumiri scurte, cât şi pentru
denumiri lungi de fişiere. Numele lung al fişierului poate fi de până la
255 de caractere Unicode. Numele scurt este în format 8.3. Nume
adiţionale sau hard link-uri, necesitate de POSIX, pot fi incluse ca
atribute de nume adiţionale ale fişierului.
Security
Denumeşte proprietarul fişierului şi utilizatorii care îl pot accesa.
Descriptor
Data Conţine datele din fişier. NTFS permite atribute multiple de date pentru
fiecare fişier. Fiecare fişier are întotdeauna un atribut implicit de date.
Object ID Un identificator unic în volum şi utilizat de facilitatea de regăsire a
legăturilor distribuite. Nu toate fişierele au identificatori de obiect.

49
Sistemul de fişiere NTFS

Logged Tool Similar unui flux de date, dar operaţiile sunt înscrise în fişierul log al
Stream NTFS întocmai ca şi modificările de metadate. Folosit de EFS.
Reparse Folosit pentru puncte de montare de pe disc şi de asemenea şi de drivere
Point de filtrare ale IFS (Installable File System) pentru a marca anumite fişiere
ca fiind speciale pentru acel driver.
Index Root Folosit pentru a implementa directoare şi alţi indecşi.
Index
Folosit pentru a implementa directoare şi alţi indecşi.
Allocation
Bitmap Folosit pentru a implementa directoare şi alţi indecşi (directoare f. mari)
Volume
Folosit doar de fişierul sistem $Volume. Conţine versiunea volumului.
Information
Volume Folosit doar de fişierul sistem $Volume. Conţine eticheta volumului.
Name

Fişierele metadata sunt structurile de date folosite de NTFS pentru accesul


şi managementul fişierelor. NTFS se bazează pe principiul „totul este
fişier”. Astfel, descriptorul de volum, informaţia de boot, înregistrări ale
sectoarelor defecte etc. sunt toate stocate în fişiere. Fişierele care stochează
informaţiile metadata ale NTFS sunt prezentate în tabelul de mai jos:

Tabela 2. Intrările din MFT corespunzătoare fişierelor metadata ale NTFS


Numele Inreg.
Descriere
fişierului MFT nr.
$MFT 0 MFT
$MFTmirr Fişier plasat în mijlocul discului, copie a primelor 16
1
înregistrări MFT.
$LogFile 2 Fişier de suport pentru jurnalizare.
$Volume Informaţii de gestiune – eticheta volumului, versiunea
3
sistemului de fişiere etc.
$AttrDef 4 Lista atributelor standard de fişiere pe volum.
$. 5 Directorul rădăcină.
$Bitmap 6 Harta de biţi a spaţiului liber pe volum.
$Boot 7 Sectorul de boot (partiţie boot-abilă).
$BadClus 8 Lista blocurilor defecte.
$Secure 9 Descriptori de securitate pentru toate fişierele.
$Upcase Fişier ce conţine tabelul de conformitate între majuscule şi
minuscule în numele de fişiere de pe volum. Acest fişier
este necesar pentru că numele de fişiere NTFS sunt
10
memorate în Unicode care are 65.000 de caractere diferite
şi nu este simplu să se caute echivalentul de majusculă,
respectiv minusculă.
$Quota Fişier în care sunt înregistrate drepturile utilizatorilor
11 asupra spaţiului de disc (a început să funcţioneze doar de la
NTFS5).

50
Sisteme de operare. Chestiuni teoretice şi practice

4.3. Tipuri de fişiere şi drepturi de acces în NTFS


În NTFS putem identifica următoarele tipuri de fişiere:
• fişiere sistem: sunt fişierele descrise în tabelul de mai sus şi
conţin informaţii ce sunt folosite numai de către sistemul de
operare (metadata).
• fişiere cu seturi multiple de date (Alternate Data Streams -
ADS): sunt fişiere care pe lângă setul de date principal
(implicit), mai conţin şi alte seturi distincte de date. Toate aceste
seturi de date sunt reprezentate prin atribute de tip Data. Modul
de creare şi utilizare, pentru un fişier, a seturilor de date
auxiliare celui principal, este descris mai jos.
• fişiere arhivate: NTFS poate arhiva şi dezarhiva fişierele „on-
the-fly”, adică în momentul efectuării operaţiilor de scriere şi
respectiv, citire a datelor din ele. Acest mecanism este invizibil
aplicaţiilor ce utilizează astfel de fişiere.
• fişiere criptate: EFS (Encrypted File System) oferă suport
pentru a stoca fişiere criptate pe un volum NTFS. Criptarea este
transparentă pentru utilizatorii care au cerut criptarea fişierului.
Accesul celorlalţi utilizatori nu este permis la aceste fişiere.
• fişiere „rare” (sparse files): sunt fişiere în care informaţia
scrisă nu se găseşte într-o singură zonă contiguă, ci zonele în
care s-au scris date alternează cu zone mari în care nu s-au scris
(„găuri”). NTFS permite setarea unui atribut special al acestor
fişiere, prin care se indică sistemului de I/E să aloce spaţiu pe
disc numai pentru zonele efectiv scrise din fişier.
• fişiere de tip „hard-link”: sunt fişiere speciale introduse de
NTFS5. Aceste fişiere permit ca un fişier să poate fi accesat prin
mai multe căi fără ca datele efective să fie duplicate. Dacă ştergem
un fişier la care există şi o altă legătură, datele nu vor fi şterse de
pe disc până când nu se şterg toate legăturile. Un fişier de tip hard-
link poate fi creat folosind funcţia CreateHardLink sau comanda
"fsutil hardlink create" (în Windows XP).

În ceea ce priveşte drepturile de acces în NTFS, ele sunt gestionate prin liste
de control al accesului (ACL). Aceste ACL-uri conţin informaţii care
definesc pentru fiecare utilizator sau grup de utilizatori drepturile pe care le
are asupra unui fişier. Drepturile de acces se numesc permisiuni.

51
Sistemul de fişiere NTFS

NTFS defineşte 6 astfel de permisiuni de bază, numite permisiuni speciale.


În Tabelul 3 sunt enumerate aceste permisiuni şi se explică ce efect are
fiecare asupra fişierelor, respectiv a directoarelor.

Tabelul 3. Permisiuni asupra fişierelor în NTFS


Permisiune Notaţie Drepturi acordate pt. fişiere Drepturi acordate pentru
directoare
Read R Citire conţinut fişier Citire conţinut director
Modificare conţinut director
Write W Modificare conţinut fişier (creare fişiere sau
subdirectoare)
X Traversare structură
Execute Executare (rulare) program
subdirectoare
Delete D Stergere fişier Ştergere director
Change P Schimbare drepturi de acces Schimbare drepturi de acces
Permissions pentru fişier pt. director
Take O Schimbare proprietar Schimbare proprietar
Ownership

Pentru a avea un control mai fin şi mai uşor asupra drepturilor de acces, s-au
introdus (începând cu Windows 2000) nişte grupuri de permisiuni, denumite
componente de permisiuni. Fiecare dintre ele grupează una sau mai multe
permisiuni speciale, după cum urmează:
Traverse Folder / Execute File setată pentru permisiunea X
List Folder / Read Data setată pentru permisiunea R
Read Attributes setată pentru permisiunea R + X
Read Extended Attributes setată pentru permisiunea R
Create Files / Write Data setată pentru permisiunea W
Create Folders / Append Data setată pentru permisiunea W
Write Attributes setată pentru permisiunea W
Write Extended Attributes setată pentru permisiunea W
Delete Subfolders and Files setată pentru permisiunea D
Delete setată pentru permisiunea D
Read Permissions setată pentru permisiunea R + W + X
Change Permissions setată pentru permisiunea P
Take Ownership setată pentru permisiunea O

Setarea acestor permisiuni poate fi făcută şi din interfaţa grafică în secţiunea


Security (Advanced...) din fereastra de proprietăţi (Properties) ale unui fişier.

52
Sisteme de operare. Chestiuni teoretice şi practice

4.4. Funcţii API Win32 pentru sistemul de fişiere NTFS


Toate resursele (fişiere, procese etc.) sistemelor de operare derivate din
Windows NT sunt identificate şi accesate prin intermediul unor structuri de
date numite handler-e. Orice proces care doreşte folosirea unei resurse
trebuie să obţină un handler pentru acea resursă. Handler-ul este similar
descriptorilor de fişier din sistemele Unix. Astfel, atunci când este creat sau
deschis un fişier, se returnează un handler şi fişierul poate fi accesat pentru
citire şi scriere folosind acest handler.

Funcţia CreateFile
Funcţia este folosită pentru a crea un fişier sau pentru a deschide un fişier
existent. Sintaxa funcţiei este următoarea:
HANDLE CreateFile(
LPCTSTR lpFileName, DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);

Semnificaţia parametrilor este următoarea:


lpFileName
Este un pointer către un şir de caractere terminat cu 0, care specifică
numele fişierului care se creează sau se deschide.
dwDesiredAccess
Specifică tipul de acces la fişier. O aplicaţie poate obţine acces doar
pentru citire, doar pentru scriere, pentru scriere şi citire sau acces de
interogare a dispozitivelor. Cele mai importante valori pentru acest
parametru sunt:
0 Obţinerea caracteristicilor dispozitivelor sistemului
şi a fişierelor, fără accesarea acestora. De exemplu
se poate verifica existenţa unui fişier, fără
deschiderea lui.
GENERIC_READ Dreptul de citire a fişierului. Datele se pot citi din
fişier şi pointerul de fişier poate fi deplasat.
GENERIC_WRITE Dreptul de scriere a fişierului. Datele pot fi scrise în
fişier şi pointerul fişierului poate fi deplasat. Combinat
cu GENERIC_READ indică dreptul de citire şi scriere.
DELETE Dreptul de a şterge fişierul.

53
Sistemul de fişiere NTFS

READ_CONTROL Dreptul de a citi informaţiile din descriptorul de


securitate al fişierului.
WRITE_OWNER Dreptul de a schimba proprietarul în descriptorul
de securitate al fişierului.
SYNCHRONIZE Dreptul de a folosi fişierul pentru sincronizare.
Acesta îi dă unui thread posibilitatea de a aştepta
până când fişierul este în starea marcată.
GENERIC_EXECUTE Dreptul de execuţie.
GENERIC_ALL Dreptul de citire, scriere şi execuţie.
dwShareMode
Specifică modul în care poate fi partajat fişierul între mai mulţi
utilizatori. Dacă dwShareMode este 0 şi CreateFile se încheie cu
succes, fişierul nu poate fi partajat şi nu poate fi deschis din nou
până când handler-ul nu este închis. Pentru a partaja fişierul, se
poate folosi o combinaţie a următoarelor valori:
FILE_SHARE_DELETE următoarele operaţii de deschidere a fişierului
vor reuşi numai dacă este solicitat accesul de
ştergere.
FILE_SHARE_READ următoarele operaţii de deschidere a fişierului
vor reuşi numai dacă este solicitat accesul de
citire.
FILE_SHARE_WRITE următoarele operaţii de deschidere a fişierului
vor reuşi numai dacă este solicitat accesul de
scriere.
lpSecurityAttributes
Este un pointer la o structură SECURITY_ATTRIBUTES care
determină dacă handler-ul poate fi moştenit de procesele fiu. Dacă
atributul lpSecurityAttributes este NULL, atunci handler-ul nu
poate fi moştenit.
dwCreationDisposition
Specifică acţiunea care se va efectua asupra fişierelor care există şi
ce acţiune să se efectueze dacă fişierul nu există. Acest parametru
trebuie să ia una dintre valorile următoare:
CREATE_NEW Creează un fişier nou. Funcţia eşuează dacă
fişierul există deja.
CREATE_ALWAYS Creează un fişier nou. Dacă fişierul există,
funcţia suprascrie fişierul, şterge atributele

54
Sisteme de operare. Chestiuni teoretice şi practice

existente şi combină atributele de fişier şi


opţiunile specificate de parametrul
dwFlagsAndAttributes cu opţiunea
FILE_ATTRIBUTE_ARCHIVE.
OPEN_EXISTING Deschide un fişier. Funcţia eşuează dacă
fişierul nu există.
OPEN_ALWAYS Deschide fişierul, dacă acesta există. Dacă
fişierul nu există, funcţia creează fişierul ca şi
cum parametrul dwCreationDisposition
ar fi CREATE_NEW.
TRUNCATE_EXISTING Deschide fişierul. O dată deschis, fişierul este
trunchiat astfel încât dimensiunea lui să fie de
0 octeţi. Procesul apelant trebuie să deschidă
fişierul cel puţin cu accesul GENERIC_WRITE.
Funcţia eşuează dacă fişierul nu există.
dwFlagsAndAttributes
Specifică atributele fişierului şi diferite opţiuni pentru fişier. Un
fişier poate avea următoarele atribute: archive, encrypted, hidden,
normal, not content indexed, offline, read-only, system, temporary şi
următoarele opţiuni: write through, overlapped, no buffering,
random access, sequential scan, delete on close, backup semantics,
POSIX semantics, open reparse point şi open no recall.
hTemplateFile
Specifică un handler cu acces GENERIC_READ la un fişier template.
Fişierul template furnizează atributele de fişier pentru fişierul ce se
creează.

Dacă funcţia are succes, valoarea returnată este un handler prin care se
accesează în continuare fişierul specificat. Dacă funcţia eşuează, valoarea
returnată este INVALID_HANDLE_VALUE. Pentru a obţine informaţii
detaliate despre eroarea apărută trebuie folosită funcţia GetLastError.

Funcţia DeleteFile
Funcţia şterge un fişier existent şi are următoarea sintaxă:
BOOL DeleteFile(
LPCTSTR lpFileName); // numele fişierului

Returnează o valoare nenulă în caz de succes şi 0 altfel.

55
Sistemul de fişiere NTFS

Funcţia CloseHandle
Funcţia închide un handler de fişier obţinut anterior cu funcţia CreateFile.

BOOL CloseHandle(
HANDLE hObject); //handler catre obiect

Returnează o valoare nenulă în caz de succes, 0 altfel.

Funcţia ReadFile
Funcţia citeşte date dintr-un fişier, începând de la poziţia indicată de către
pointerul fişierului. După ce operaţia de citire a fost finalizată, pointerul de
fişier este ajustat cu numărul de octeţi citiţi efectiv, mai puţin în cazul în
care handler-ul de fişier este creat cu atributul FILE_FLAG_OVERLAPPED.
Dacă handler-ul de fişier este creat pentru intrare-ieşire suprapusă (I/O),
aplicaţia trebuie să ajusteze poziţia pointerului de fişier după operaţia de
citire.

BOOL ReadFile(
HANDLE hFile, // handler către fisier
LPVOID lpBuffer, // buffer de date
DWORD nNumberOfBytesToRead, // nr octeti de citit
LPDWORD lpNumberOfBytesRead, // nr octeti cititi
LPOVERLAPPED lpOverlapped); // buffer suprapus

Semnificaţia parametrilor este următoarea:


hFile
Handler către fişierul de citit. Handler-ul de fişier trebuie să fi fost
creat cu accesul GENERIC_READ la fişier.
lpBuffer
Adresa de memorie unde se pun datele citite din fişier.
nNumberOfBytesToRead
Specifică numărul de octeţi care trebuie citiţi din fişier.
lpNumberOfBytesToRead
Pointer la variabila în care se scrie numărul de octeţi efectiv citiţi.
lpOverlapped
Pointer la o structură OVERLAPPED. Această structură este solicitată
dacă hFile a fost creat cu FILE_FLAG_OVERLAPPED.

Se revine din funcţia ReadFIle dacă numărul de octeţi cerut a fost citit sau
dacă a apărut o eroare. Dacă funcţia reuşeşte, valoarea returnată este nenulă.

56
Sisteme de operare. Chestiuni teoretice şi practice

Funcţia WriteFile
Această funcţie scrie date într-un fişier şi este destinată atât pentru operaţii
sincrone cât şi pentru operaţii asincrone. Funcţia începe să scrie datele în
fişier la poziţia indicată de pointerul de fişier. După ce operaţia de scriere a
fost terminată, pointerul de fişier este ajustat cu numărul de octeţi scrişi
efectiv, cu excepţia cazului în care fişierul este deschis cu
FILE_FLAG_OVERLAPPED.

BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped);

Semnificaţiile parametrilor sunt similare cu cele ale parametrilor funcţiei


ReadFile. Dacă funcţia se termină cu succes, valoarea returnată va fi nenulă.
Dacă funcţia eşuează, valoarea returnată este 0.

Funcţia SetFilePointer
Funcţia SetFilePointer deplasează pointerul unui fişier deschis.
DWORD SetFilePointer(
HANDLE hFile,
LONG lDistanceToMove,
PLONG lpDistanceToMoveHigh,
DWORD dwMoveMethod);

Semnificaţia parametrilor este următoarea:


hFile
Handler la fişierul al cărui pointer se va deplasa. Handlerul de fişier
trebuie să fi fost creat cu unul din următoarele două tipuri de acces la
fişier GENERIC_READ sau GENERIC_WRITE.
lDistanceToMove
Conţine cei mai puţin semnificativi 32 de biţi ai valorii cu care se va
deplasa pointerul fişierului. Pentru o valoare pozitivă pointerul va fi
mutat spre sfârşitul fişierului, iar pentru una negativă spre început.
lpDistanceToMoveHigh
Indică cei mai semnificativi 32 de biţi ai valorii cu care se va
deplasa pointerul fişierului. Dacă nu e nevoie de 64 de biţi, ci sunt
suficienţi 32, valoarea acestui parametru trebuie să fie NULL.

57
Sistemul de fişiere NTFS

dwMoveMethod
Poziţia relativ la care se va face deplasarea pointerului de fişier.
Acest parametru poate avea una din următoarele valori:
FILE_BEGIN Începutul fişierului.
FILE_CURRENT Actuala valoare a pointerului fişierului.
FILE_END Sfârşitul fişierului.
Dacă funcţia SetFilePointer se termină cu success şi
lpDistanceToMoveHigh este NULL, valoarea returnată este dublu-
cuvântul (32 de biţi) cel mai puţin semnificativ al noii poziţii a pointerului
de fişier. Dacă lpDistanceToMoveHigh nu este NULL, atunci funcţia
scrie la adresa indicată de acest parametru dublu-cuvântul cel mai
semnificativ al noii poziţii a pointerului de fişier. Dacă funcţia eşuează,
valoarea returnată este INVALID_SET_FILE_POINTER.

Funcţia GetFileAttributes
Această funcţie obţine setul de atribute specifice sistemului de fişiere de tip
FAT pentru un fişier sau un director specificat.
DWORD GetFileAttributes(
LPCTSTR lpFileName);

Dacă funcţia se termină cu succes, valoarea returnată va conţine codificat pe


biţi atributele fişierului sau directorului specificat. Atributele pot fi
identificate cu ajutorul următoarelor constante (măşti pe biţi):
• FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL
• FILE_ATTRIBUTE_SPARSE_FILE, FILE_ATTRIBUTE_REPARSE_POINT
• FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_HIDDEN
• FILE_ATTRIBUTE_ENCRYPTED, FILE_ATTRIBUTE_COMPRESSED,
• FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_SYSTEM etc.

Funcţia LockFile
Funcţia LockFile blochează o regiune dintr-un fişier deschis pentru a
asigura accesul în excludere mutuală la acea zonă, a procesului care o
blochează. Sintaxa funcţiei LockFile este:
BOOL LockFile(
HANDLE hFile,
DWORD dwFileOffsetLow,
DWORD dwFileOffsetHigh,
DWORD nNumberOfBytesToLockLow,
DWORD nNumberOfBytesToLockHigh);

58
Sisteme de operare. Chestiuni teoretice şi practice

Semnificaţia parametrilor este următoarea:


hFile
Handler la fişierul al cărei regiune se va bloca. Numele fişierului
trebuie să fi fost creat cu unul din următoarele tipuri de acces la
fişier: GENERIC_READ sau GENERIC_WRITE (sau ambele).
dwFileOffsetLow
Specifică cuvântul cel mai puţin semnificativ al deplasamentului în
fişier ce indică începutul zonei ce va fi blocată.
dwFileOffsetHigh
Specifică cuvântul cel mai semnificativ al deplasamentului în fişier
ce indică începutul zonei ce va fi blocată.
nNumberOfBytesToLockLow
Specifică cuvântul cel mai puţin semnificativ al dimensiunii zonei ce
va fi blocată.
nNumberOfBytesToLockHigh
Specifică cuvântul cel mai semnificativ al dimensiunii zonei ce va fi
blocată.
Dacă funcţia se termină cu succes, valoarea returnată este nenulă, altfel este 0.

Funcţia UnlockFile
Funcţia deblochează o regiune blocată anterior cu funcţia LockFile într-un
fişier deschis. Sintaxa acestei funcţii este similară cu cea a funcţiei
LockFile:
BOOL UnlockFile(
HANDLE hFile,
DWORD dwFileOffsetLow,
DWORD dwFileOffsetHigh,
DWORD nNumberOfBytesToUnlockLow,
DWORD nNumberOfBytesToUnlockHigh);

Funcţia CreateDirectory
Această funcţie creează un nou director. Dacă sistemul de fişiere existent
suportă opţiuni de securitate pentru directoare şi fişiere, funcţia va aplica
descriptorul de securitate specificat pentru noul director.
BOOL CreateDirectory(
LPCTSTR lpPathName,
LPSECURITY_ATTRIBUTES lpSecurityAttributes);

Dacă funcţia se termină cu succes, valoarea returnată este nenulă, altfel este 0.

59
Sistemul de fişiere NTFS

Funcţia RemoveDirectory
Funcţia RemoveDirectory şterge un director gol existent. Sintaxa funcţiei
este următoarea:

BOOL RemoveDirectory(
LPCTSTR lpPathName);

Dacă funcţia se termină cu succes, valoarea returnată este nenulă, altfel este 0.

Funcţia FindFirstFile
Această funcţie caută într-un director un fişier sau subdirector. Sintaxa
funcţiei este următoarea:

HANDLE FindFirstFile(
LPCTSTR lpFileName,
LPWIN32_FIND_DATA lpFindFileData);

Parametrul lpFileName indică calea spre fişierul sau subdirectorul căutat.


Numele fişierului poate să conţină şi caracterele '*' sau '?', caz în care este
interpretat ca un şablon căutându-se primul fişier sau director care se
potriveşte şablonului respectiv. Parametrul lpFindFileData este adresa
unei structuri de tipul WIN32_FIND_DATA, în care se vor depune informaţii
despre fişierul găsit (atribute, timpi etc.) Dacă funcţia se termină cu succes,
valoarea returnată este un handler de căutare care va putea fi folosit într-un
apel ulterior al funcţiei FindNextFile, pentru a se găsi următorul fişier care
se potriveşte şablonului specificat în apelul funcţiei FindFirstFile. Handler-
ul poate fi eliberat cu ajutorul funcţiei FindClose. Dacă funcţia
FindFirstFile eşuează, adică nici un fişier nu este găsit, valoarea returnată
este INVALID_HANDLE_VALUE.

Funcţia FindNextFile
Această funcţie continuă căutarea fişierelor sau directoarelor, care se
potrivesc şablonului specificat într-un apel anterior al funcţiei FindFirstFile.
Sintaxa funcţiei este:

BOOL FindNextFile(
HANDLE hFindFile, // handler de căutare
LPWIN32_FIND_DATA lpFindFileData);

Dacă funcţia se termină cu succes, valoarea returnată este nenulă, altfel este 0.

60
Sisteme de operare. Chestiuni teoretice şi practice

Funcţia MoveFile
Mută un fişier sau director existent. Operaţia poate fi văzută şi ca o
redenumire a fişierelor sau directoarelor. În cazul mutării unui director,
întregul arbore ce are ca rădăcină acel director este mutat în directorul
destinaţie. O restricţie a acestei funcţii este că nu permite mutarea unui
director între volume diferite.

BOOL MoveFile(
LPCTSTR lpExistingFileName, // numele vechi
LPCTSTR lpNewFileName); // noul nume

Dacă funcţia se termină cu succes, valoarea returnată este nenulă, altfel este 0.

Funcţia SetCurrentDirectory
Funcţia schimbă directorul curent pentru procesul care o apelează. Sintaxa
ei este următoarea:

BOOL SetCurrentDirectory(
LPCTSTR lpPathName); // numele noului director

Dacă funcţia se termină cu succes, valoarea returnată este nenulă, altfel 0.

4.5. Fişiere cu seturi multiple (alternative) de date


După cum am mai menţionat, sistemul NTFS permite ca unui fişier să-i fie
asociate mai multe atribute de tip Data, deci mai multe seturi de date.
Fiecare fişier are asociat un set de date principal, care nu are un nume
explicit, folosirea simplă a numelui fişierului indicând în mod implicit acest
set de date. Dacă este necesar, se mai pot asocia fişierului alte seturi
alternative cu nume explicite. Această facilitate permite ca unele date din
fişier să fie accesate ca o unitate separată. De exemplu, o aplicaţie grafică
poate să stocheze pentru un fişier imagine o versiune de calitate mai slabă,
dar de dimensiune mult mai mică a imaginii (thumbnail) într-un alt set de
date al fişierului, diferit de cel principal, care conţine imaginea propriu-zisă.
Fiecare set alternativ de date al unui fişier este tratat separat de NTFS, fiind
identificat în mod distinct printr-un nume de forma:
nume_fişier:nume_set_alternativ. Dimensiunile seturilor alternative de
date ale unui fişier nu sunt evidenţiate în dimensiunea fişierului, aceasta
fiind dată doar de dimensiunea setului principal. Toate seturile de date ale
unui fişier au aceleaşi permisiuni, şi anume cele ale fişierului.

61
Sistemul de fişiere NTFS

Un set de date alternativ poate fi creat prin apelul funcţiei CreateFile sau
din linia de comandă, specificând un nume de forma menţionată mai sus.
Exemplul de mai jos ilustrează acest lucru pentru linia de comandă:

echo "Setul principal de date" > Fisier.txt

Astfel, am creat fişierul cu numele Fisier.txt. Comanda următoare creează în


acest fişier un set alternativ cu numele ADS:

echo "Set alternativ de date" > Fisier.txt:ADS

Se poate observa că setul adăugat nu apare între fişierele din director şi nici
nu măreşte dimensiunea fişierului principal. Pentru a citi conţinutul setului
principal şi al celui alternativ se pot executa comenzile:
more < Fisier.txt
more < Fisier.txt:ADS

Pentru a deschide un set alternativ într-un editor de texte, de exemplu


Notepad, numele setului trebuie să aibă o extensie, de exemplu:
Fisier.txt:ADS.txt. În acest mod, el poate fi vizualizat şi editat în editor prin
comanda "notepad Fisier.txt:ADS.txt".

Seturile alternative de date pot conţine şi date binare, adică fişiere executabile.
Ele se pot executa cu comanda: "start .\Fisier.txt:fis.exe".

Sistemul de operare Windows foloseşte seturi alternative de date atunci


când specificăm date suplimentare pentru un fişier în secţiunea Summary a
paginii de proprietăţi (Properties) ale fişierului, pentru a stoca acele date.

Seturile alternative de date oferă, pe de altă parte, o bună posibilitate


viruşilor de a se ascunde, pentru că ele nu se văd în lista de fişiere şi nu
modifică dimensiunea şi marca de timp a fişierului principal. Sistemul de
operare Windows nu oferă în mod implicit programe utilitare care
detectează seturile alternative de date. Un astfel de utilitar poate fi însă găsit
la adresa http://www.microsoft.com/technet/sysinternals/default.mspx. El se
numeşte streams şi are sintaxa următoare:
streams [-s] [-d] <fisier sau director>
Opţiunea –s indică intrarea în adâncime în toate subdirectoarele directorului
în care se caută fişiere cu seturi multiple de date. Opţiunea –d indică
ştergerea setului de date specificat ca argument.

62
Sisteme de operare. Chestiuni teoretice şi practice

4.6. Exemple
Exemplul 1. Program de mai jos realizează copierea unui fişier existent
într-un alt fişier, folosind funcţiile API Win32 ale Windows 2000.

#include <windows.h>
#include <stdio.h>

#define BUF_SIZE 10

void main() {
HANDLE inhandle, outhandle;
char buffer[BUF_SIZE];
int count, s;
DWORD ocnt;

/* Deschide fisierele de intrare si de iesire */


inhandle = CreateFile("sursa.txt", GENERIC_READ,
0, NULL, OPEN_EXISTING,
0, NULL);

outhandle = CreateFile("dest.txt", GENERIC_WRITE, 0,


NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
/* Copieaza fisierul */
do {
s = ReadFile(inhandle, buffer,
BUF_SIZE, &count, NULL);
if (s && count > 0)
WriteFile(outhandle, buffer,
count, &ocnt, NULL);
} while (s>0 && count>0);

/* Inchide fisierele */
CloseHandle(inhandle);
CloseHandle(outhandle);
}

Exemplul 2. Programul următor, caută toate fişiere de tipul *.txt din


directorul curent şi setează atributul lor read-only.

#include <windows.h>
#include <stdio.h>

WIN32_FIND_DATA FileData;
HANDLE hSearch;
DWORD dwAttrs;
BOOL fFinished = FALSE;

63
Sistemul de fişiere NTFS

void main() {

/* Caută fisiere *.txt in directorul curent */


hSearch = FindFirstFile("*.txt", &FileData);

if (hSearch == INVALID_HANDLE_VALUE)
{
printf("Nu s-au gasit fisisere *.txt");
return;
}

// Seteaza fiecare fisier gasit la read-only


// daca nu are deja setat acel atribut
while (!fFinished)
{
dwAttrs = GetFileAttributes(FileData.cFileName);

if (!(dwAttrs & FILE_ATTRIBUTE_READONLY))


{
SetFileAttributes(FileData.cFileName,
dwAttrs | FILE_ATTRIBUTE_READONLY);
}

if (!FindNextFile(hSearch, &FileData))
{
if (GetLastError() == ERROR_NO_MORE_FILES)
{
printf("Nu mai sunt fisiere *.TXT");
fFinished = TRUE;
}
else
{
printf("Eroare de cautare.");
return;
}
}
}

// Închide handle-ul de căutare


FindClose(hSearch);
}

4.7. Probleme
1. Să se calculeze dimensiunea unui fişier în octeţi şi în număr de blocuri,
folosind funcţia SetFilePointer. Să se compare rezultatul obţinut cu
valorile afişate în pagina de proprietăţi a fişierului.
2. Să se scrie un program C care să afişeze conţinutul unui director dat,
indicând pentru fiecare element al directorului câteva proprietăţi (tipul,

64
Sisteme de operare. Chestiuni teoretice şi practice

dimensiunea, etc.). Programul poate primi opţiunea –R, caz în care va


afişa recursiv şi conţinutul subdirectoarelor (şi al subdirectoarelor lor şi
aşa mai departe) directorului iniţial. Numele directorului se precizează
ca argument în linia de comandă sau dacă lipseşte, se consideră
directorul curent.
3. Să se scrie un program C care pune în evidenţă modul de creare şi
manipulare a fişierelor de tip hard link (legătură fizică). Funcţia de
creare a unei legături fizice este CreateHardLink.
4. Să se creeze un fişier text căruia să i se asocieze un flux alternativ de
date, care să conţină un cod executabil, şi anume programul Calculator
(aflat în directorul %SystemRoot%\system32\calc.exe). Să se lanseze
apoi programul respectiv din cadrul fluxului alternativ al fişierului text.
5. Să se facă o comparaţie între modul de gestionare a fişierelor cu găuri
(sparse files) în sistemele de operare Linux şi Windows 2000 sau XP.
Acestea se creează făcând salt peste sfârşitul fişierului şi scriind ceva la
acel deplasament. O primă întrebare ar fi dacă se alocă spaţiu pe HDD
pentru gaură (zona nescrisă), iar o a doua întrebare ar fi ce returnează o
citire din acea gaură. Pe Windows tratarea unui fişier ca sparse file se
face cu ajutorul funcţiei DeviceIoControl, cu opţiunea
FSCTTL_SPARCE_FILE.
6. Să se scrie un program C care să pună în evidenţă proprietatea de
indexare (cu arbori B+) a directoarelor în NTFS. Pentru aceasta se vor
crea într-un director un număr foarte mare de fişiere (10.000 – 100.000)
şi se vor face apoi căutări atât ale tuturor fişierelor din director, cât şi ale
unor fişiere care nu există în director. Se va afişa timpul necesar creării
şi respectiv, căutării fişierelor, atât individual, pentru fiecare fişier în
parte, cât şi global pentru întregul set. Pentru măsurarea timpului se pot
folosi funcţiile GetTickCount sau clock sau QueryPerformanceCounter.
Testul descris să se facă pentru două directoare, unul având setată
opţiunea de indexare (în secţiunea GeneralÆ Advanced..., din pagina de
proprietăţi ale acelui director), celălalt neavând setată acea opţiune. Să
se realizeze testul şi pe sistemul de operare Linux şi să se compare
rezultatele obţinute.

65
5. Apeluri sistem pentru lucrul
cu procese în Linux

Scopul lucrării
În cadrul acestei lucrări sunt prezentate câteva aspecte legate de crearea şi
gestionarea proceselor în Linux şi apelurile sistem ce pot fi utilizate pentru
manipularea proceselor, cum ar fi cele pentru creare, terminare, aşteptare
după terminarea unui alt proces etc.

5.1. Procese
Un proces este entitatea ce reprezintă un program în execuţie, înţelegând
prin program codul şi datele aferente aflate într-un fişier executabil. Fiecare
proces are asociat un identificator unic numit identificator de proces,
prescurtat PID. PID-ul este un număr pozitiv atribuit de sistemul de operare
fiecărui proces nou creat. Cum PID-ul unui proces este unic, el nu poate fi
schimbat, dar numărul respectiv poate fi refolosit de către sistemul de
operare pentru identificarea unui nou proces, când procesul căruia i-a fost
atribuit anterior se termină. Un proces îşi poate obţine identificatorul
propriu prin apelul sistem getpid.
În Linux, orice proces nou este creat de către un proces deja existent, dând
naştere unei relaţii părinte-fiu. Excepţie face procesul cu PID-ul 0, care este
creat chiar de către sistemul de operare la pornirea sa. Un proces poate să
determine PID-ul părintelui său prin apelul sistem getppid. PID-ul procesului
părinte nu se poate modifica, adică un proces nu îşi poate schimba el însuşi
părintele. Acest lucru se poate întâmpla totuşi o singură dată, dar realizat în
mod automat de către sistemul de operare, atunci când un proces care are fii se
termină, moment în care toţi fiii săi devin fii ai procesului cu PID-ul 1
(procesul init). Sintaxa celor două apeluri sistem amintite este:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid();
pid_t getppid();

Sistemul de operare ţine evidenţa proceselor într-o structură de date internă


numită tabela proceselor. Fiecare proces din sistem are alocată o intrare în
această tabelă. Lista proceselor din tabela proceselor poate fi obţinută prin

66
Sisteme de operare. Chestiuni teoretice şi practice

comanda ps. O comandă similară este comanda top. În mod normal comanda
ps afişează doar procesele utilizatorului care a lansat-o şi doar acele procese
ataşate terminalului (indicat în coloana TTY a tabelei afişate) în care se
execută comanda. Dacă se doreşte şi afişarea proceselor altor utilizatori,
atunci se poate specifica opţiunea "-a", iar pentru afişarea proceselor care nu
sunt ataşate unui terminal, opţiunea "-x". Cu ajutorul opţiunii "-l" se
afişează mai multe informaţii despre un proces, ca de exemplul starea sa, PID-
ul părintelui său, UID-ul utilizatorului căruia îi aparţine acel proces. Modul de
afişare a acestor informaţii este ilustrat mai jos.

5.2. Grupuri de procese


Sistemul de operare permite constituirea unui set de procese ca şi grup
distinct pentru a facilita transmiterea de semnale în cadrul acelui grup. Pe
lângă PID-ul asociat, fiecare proces are şi un identificator de grup de
procese, prescurtat PGID, care permite identificarea unui grup de procese.
PGID-ul este moştenit de procesul fiu de la procesul părinte. Contrar PID-
ului, un proces poate să-şi modifice PGID-ul, dar numai prin crearea unui
nou grup având identificatorul egal cu PID-ul procesului. Acest lucru se
realizează prin apelul sistem setpgrp, cu următoarea sintaxă:

#include <unistd.h>
int setpgrp();

Funcţia setpgrp actualizează PGID-ul procesului apelant la valoarea PID-


ului său şi întoarce noul PGID. Procesul apelant părăseşte astfel vechiul
grup devenind liderul unui nou grup, urmând ca procesele fiu pe care
eventual le va crea să facă parte din acel grup. Deoarece procesul apelant
este primul membru al grupului şi numai descendenţii săi pot să aparţină
grupului (prin moştenirea PGID-ului), el este referit ca reprezentantul
(liderul) grupului. Deoarece doar descendenţii liderului pot fi membri ai
grupului, există o corelaţie între grupul de procese şi arborele proceselor.
Fiecare lider de grup este rădăcina unui subarbore, care după eliminarea
rădăcinii conţine doar procese ce aparţin grupului. Dacă nici un proces din

67
Apeluri sistem pentru lucrul cu procese în Linux

grup nu s-a terminat lăsând fii care au fost adoptaţi de procesul init, acest
subarbore conţine toate procesele din grup. Un proces îşi poate determina
PGID-ul său folosind apelul sistem getpgrp, cu următoarea sintaxă:

#include <unistd.h>
pid_t getpgrp();

Apelul întoarce PGID-ul procesului apelant. Deoarece PID-ul liderului este


acelaşi cu PGID-ul, getpgrp identifică liderul de grup.

Un proces poate fi asociat unui terminal, care este numit terminalul de


control asociat procesului. Acesta este moştenit de la procesul părinte la
crearea unui nou proces. Un proces este deconectat (eliberat) de terminalul
său de control la apelul setpgrp, devenind astfel un lider de grup de procese
(nu i se închide însă terminalul). Ca atare, numai liderul poate stabili un
terminal de control, devenind procesul de control pentru terminalul în cauză.
Un proces care nu este asociat unui terminal de control este numit daemon.
Spooler-ul de imprimantă este un exemplu de astfel de proces. Un proces
daemon este identificat în rezultul afişării comenzii ps prin simbolul ’?’
plasat în coloana TTY.

5.3. Programe şi procese


Un program este o colecţie de instrucţiuni şi date păstrate într-un fişier
executabil pe disc, având conţinutul organizat conform unui format bine
stabilit. Un program în Linux este format din mai multe segmente. În
segmentul de cod se găsesc instrucţiuni în format binar. În segmentul de
date se găsesc date predefinite (de exemplu, constante) şi date iniţializate.
Aceste două segmente, alături de segmentul de stivă, care conţine date
alocate dinamic la execuţia procesului, sunt părţi funcţionale ale unui proces
Linux. Pentru a executa un program, se creează un nou proces, care nu este
altceva decât un mediu în care se va executa programul. Programul este
folosit pentru a iniţializa primele două segmente, după care nu mai există
nici o legătură între proces şi programul pe care-l execută. Datele sistem ale
unui proces includ informaţii ca directorul curent, descriptori de fişiere
deschise, căi implicite, tipul terminalului, timp CPU consumat etc. Un
proces nu poate accesa sau modifica direct propriile date sistem, deoarece
acestea sunt în afara spaţiului său de adresare. Există însă multiple apeluri
sistem pentru a accesa sau modifica indirect aceste informaţii.

68
Sisteme de operare. Chestiuni teoretice şi practice

Toate procesele active la un moment dat în Linux sunt de fapt descendenţi


direcţi sau indirecţi ai unui singur proces, lansat la pornirea sistemului prin
comanda "/etc/init". La intrarea unui utilizator în sistem se lansează
automat un nou proces care este interpretorul de comenzi corespunzător
sesiunii acelui utilizator. Acest proces are menirea de a interpreta şi executa
comenzile introduse de la tastatură de către utilizator. Fiecare comandă
introdusă se execută în cadrul unui nou proces creat de către interpretorul de
comenzi. Crearea unui proces se realizează prin apelul sistem fork. La
fiecare execuţie a acestui apel, se creează un nou proces, distinct de
procesul care apelează fork, având propriul său PID. Cele două procese, cel
care a apelat funcţia fork şi cel nou creat, sunt concurente, adică
independente din punct de vedere al execuţiei. Totuşi ele sunt identice în
ceea ce priveşte codul şi datele. Apelul sistem fork realizează astfel o copie
a procesului iniţial şi, ca atare, imaginea proceselor în memorie este
identică. Procesul care a iniţiat apelul fork este identificat ca proces părinte,
iar procesul rezultat în urma apelului este identificat ca proces fiu.
Pentru a explica modul de lucru al interpretorului de comenzi considerăm
spre exemplu, execuţia comenzii "echo text". Interpretorul de comenzi
desparte comanda de argumente şi execută apoi apelul sistem fork, care are
ca efect crearea unui proces fiu. Procesul părinte (interpretorul), prin apelul
sistem wait, îşi suspendă execuţia şi aşteaptă terminarea procesului fiu.
Procesul fiu cere nucleului, prin apelul sistem exec, încărcarea şi pornirea
execuţiei unui nou program (cod şi date), respectiv cel memorat în fişierul
executabil "/bin/echo" şi comunică în acelaşi timp şi argumentele pentru
noul program. Sistemul suprascrie segmentele de cod şi date ale procesului
fiu cu conţinutul corespunzător acelor segmente citite din fişierul executabil
indicat de exec. În procesul fiu se va începe deci execuţia noului program,
care se va termina prin apelul sistem exit (apelat în mod explicit de către
proces sau implicit de către sistemul de operare la sfârşitul funcţiei main a
procesului). Apelul sistem exit are ca efect terminarea procesului curent
(fiul, în exemplul nostru), memorarea unui cod de terminare în tabela
proceselor, scoaterea procesului părinte din starea de aşteptare a fiului (stare
în care a intrat prin apelul lui wait) şi reluarea execuţiei sale. Funcţia wait
extrage din tabela proceselor, din intrarea asociată procesului fiu, codul de
terminare al acestuia, cod care este păstrat acolo până în momentul în care
procesul părinte apelează wait pentru acel fiu. Acest mod de funcţionare al
interpretorului corespunde unei execuţii sincrone şi creează iluzia că
interpretorul de comenzi este de fapt procesul care execută comanda. Este
posibilă însă şi execuţia în paralel (asincronă) a celor două procese, caz în
care procesul părinte (interpretorul) nu mai aşteaptă după terminarea fiului

69
Apeluri sistem pentru lucrul cu procese în Linux

său (procesul care execută comanda), ci îşi continuă imediat execuţia sa.
Acest lucru poate fi indicat în linia de comandă prin specificarea
caracterului '&' la sfârşitul liniei. În acest caz interpretorul afişează pe ecran
un număr, după care îşi continuă execuţia reafişând prompterul. Numărul
afişat reprezintă PID-ul procesului fiu, creat pentru a executa comanda.

5.4. Apelurile sistem fork şi exec


Apelul sistem fork este folosit pentru crearea unui nou proces şi are sintaxa:
#include <sys/types.h>
#include <unistd.h>
pid_t fork();

Procesul fiu, fiind o copie a procesului părinte, conţine acelaşi cod şi îşi
începe execuţia revenind din funcţia fork. Pentru a se face o distincţie între
revenirea din fork în procesul părinte şi în procesul fiu, funcţia returnează
PID-ul fiului în primul caz (în părinte) şi respectiv 0, în cel de-al doilea (în
fiu) sau -1 în caz de eroare. Codul de mai jos ilustrează acest lucru.
pid=fork();
... // cod executat de ambele procese
switch (pid) {
case -1:
/* Eroare! fork nereusit */
case 0 :
/* cod executat doar de fiu */
break;
default:
/* cod executat doar de părinte */
}
... // cod executat de ambele procese
Cazul de eroare poate să apară dacă s-a atins limita maximă de procese pe
care le poate lansa un utilizator sau dacă s-a atins limita maximă de procese
care se pot executa simultan în sistem.
În procesul fiu toate variabilele au iniţial valoarea moştenită din procesul
părinte, toţi descriptorii de fişier sunt aceiaşi ca în procesul părinte, se
moşteneşte acelaşi UID real şi GUID real, acelaşi PGID al grupului de
procese, aceleaşi variabile de mediu etc. Spaţiile de adrese ale celor două
procese şi resursele alocate de sistemul de operare sunt totuşi diferite,
însemnând ca cele două procese sunt distincte, fiul moştenind doar ca valoare
resursele părintelui său. Din momentul revenirii din apelul fork, procesele
părinte şi fiu se execută independent, concurând unul cu celalalt pentru

70
Sisteme de operare. Chestiuni teoretice şi practice

obţinerea procesorului şi a altor resurse ale sistemului. Procesul fiu îşi începe
execuţia din locul de unde şi-o continuă procesul părinte, adică următoarea
instrucţiune de după fork. Nu se poate preciza care dintre procese va porni
primul. Este posibilă doar, aşa cum a fost ilustrat mai sus, separarea execuţiei
în cele două procese prin testarea valorii întoarse de apelul fork.
Raţiunea creării unui proces fiu identic (ca şi conţinut) cu părintele său are
sens dacă se poate modifica segmentul de date şi cel de cod al procesului
rezultat, astfel încât să se poată încărca şi executa un nou program. Pentru
acest lucru este pusă la dispoziţie familia de funcţii exec (sub forma mai
multor variante ale sale: execl, execlp, execv şi execvp). Partea de sistem a
procesului nu se modifică în nici un fel prin apelul exec, deci nici PID-ul
procesului nu se schimbă. În acest caz procesul fiu va executa cu totul altceva
decât părintele său. După un apel exec reuşit nu se mai revine în vechiul cod.
Trebuie precizat totuşi că fişierele deschise ale tatălui se regăsesc deschise şi
în fiu (datorită copierii conţinutului tabelei de descriptori de fişier) şi rămân
aşa chiar şi după apelul exec. Închiderea automată a unui fişier deschis într-un
proces în urma apelului lui exec se face prin specificarea acestui lucru cu
ajutorul funcţiei fcntl, sub forma "fcntl(fd, F_SETFD, 1)". Un apel exec
nereuşit returnează valoarea -1, dar cum altă valoare nu se returnează, ea nu
trebuie testată. Insuccesul poate fi determinat prin specificarea unei căi greşite
spre fişierul executabil sau a unui fişier pentru care nu există drept de
execuţie. Diferitele variante de exec dau utilizatorului mai multă flexibilitate
la transmiterea parametrilor. Sintaxa lor este:
#include <unistd.h>
int execl(const char * cale,
const char * arg0, ..., NULL);
int execv(const char * cale, char * argv[]);
int execlp(const char * numefis,
const char * arg0, ..., NULL);
int execvp(const char * numefis, char * argv[]);

Toate funcţiile returnează -1 în caz de eroare şi nu se revine din ele în caz


de succes. Între primele două variante şi ultimele, deosebirea constă în
aceea că la ultimele două fişierul executabil specificat de parametrul
numefis se caută în directoarele din variabila PATH. Evident, în acest caz
nu are sens specificarea unei căi, adică parametrul numefis nu trebuie să
conţină caracterul '/', lucru care se cere în mod normal la primele două
variante de exec, prin parametrul cale. Dacă în numefis se precizează
caracterul '/', se presupune specificarea explicită a căii şi nu se face nici o

71
Apeluri sistem pentru lucrul cu procese în Linux

căutare. Calea indicată prin parametrul cale poate fi una absolută sau
relativă.
O altă deosebire există între apelurile de genul execl şi execv, deosebire care
se referă la modul de specificare a argumentelor fişierului executabil
(comenzii): ca listă, respectiv vector de şiruri de caractere. Indiferent de
modul de specificare a acestor argumente, trebuie reţinut faptul că ele descriu
linia de comandă, aşa cum ar fi introdusă ea de la tastatură, dacă programul
executabil ar fi lansat din interpretorul de comenzi. Linia de comandă începe
cu numele comenzii, adică al fişierului executabil, urmat de argumentele sale
(cuvinte separate prin spaţii) şi de şirul vid, marcat de apăsarea tastei ENTER.
În cazul specificării argumentelor ca listă, fiecare element al liniei de
comandă este precizat separat ca şir de caractere (cuvânt), iar sfârşitul listei
este marcat prin şirul vid (NULL). În cazul specificării argumentelor ca vector,
se indică doar adresa unui vector ce conţine elementele liniei de comandă.
Ultimul element al vectorului trebuie să fie, de asemenea, NULL. Funcţiile
execl şi execlp pot fi utilizate doar când se cunoaşte numele comenzii şi
argumentele sale în momentul scrierii codului (informaţi necesare la
compilare). Funcţiile execv şi execvp se pot folosi şi în cazul în care comanda
şi argumentele sale sunt precizate doar în timpul rulării programului. De
exemplu, este evident că interpretorul de comenzi foloseşte funcţia execvp
pentru a putea executa orice comenzi specificate în timpul rulării sale.
Se observă că fără fork, exec este limitat ca acţiune, iar fără exec, fork nu are
aplicabilitate practică. Deşi efectul lor conjugat este cel dorit, raţiunea
existenţei a două apeluri distincte va rezulta din parcurgerea lucrărilor
următoare.

5.5. Apelurile sistem wait şi waitpid


Cele două apeluri sistem pot fi folosite pentru sincronizarea execuţiei
proceselor părinte şi fiu, în sensul că procesul părinte aşteaptă terminarea
(normală sau cu eroare) procesului fiu folosind apelul sistem wait
sau waitpid. Sintaxa celor două apeluri sistem este:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* pstatus);
pid_t waitpid(pid_t pid, int* pstatus, int opt);

Ambele funcţii returnează în caz de succes PID-ul unui fiu terminat şi -1 în


caz de eroare. De exemplu, dacă procesul apelant nu are fii, cele două funcţii

72
Sisteme de operare. Chestiuni teoretice şi practice

întorc valoarea -1 şi poziţionează variabila sistem errno la valoarea


ECHILD. Funcţia waitpid returnează 0 dacă s-a precizat ca opţiune WNOHANG
şi nu există nici un proces fiu terminat. Argumentul pstatus este adresa la
care se va copia codul de terminare al procesului fiu al cărui PID este returnat.

Un proces ce apelează wait sau waitpid poate:


• fi blocat în apelul funcţiei, dacă toţi fiii săi sunt în execuţie,
reluându-şi execuţia în momentul terminării unuia dintre fiii săi;
• să primească imediat starea de terminare a fiului, dacă cel puţin
unul dintre fii s-a terminat înainte de apelul funcţiei wait;
• să primească o eroare, dacă nu are procese fiu.

Diferenţele între cele două apeluri sistem sunt:


• wait blochează procesul apelant până la terminarea unui fiu, în timp
ce waitpid poate avea specificată opţiunea WNOHANG, precizată prin
argumentul opt, care evită acest lucru;
• waitpid nu aşteaptă neapărat terminarea primului fiu, ci se poate
specifica prin argumentul pid procesul fiu aşteptat;
• waitpid permite controlul programelor prin argumentul opt.

Modul în care s-a terminat procesul fiu, normal sau cu eroare, este codificat
în octeţii de la adresa indicată de pstatus şi poate fi aflat cu ajutorul
macrourilor de mai jos:
WIFEXITED(*pstatus)
Întoarce TRUE dacă procesul fiu s-a terminat prin apelul explicit sau
implicit (la sfârşitul execuţiei sale) al lui exit sau prin apelul
instrucţiunii return la sfârşitul funcţiei main. Altfel, întoarce FALSE.
WEXITSTATUS(*pstatus)
Întoarce codul de terminare specificat în procesul fiu ca parametru al
funcţiei exit sau instrucţiunii return. Testarea acestei valori are sens
doar în cazul în care procesul s-a terminat prin exit.
WIFSIGNALED(*pstatus)
Întoarce TRUE dacă procesul fiu a fost terminat datorită recepţionării
unui semnal. Altfel, întoarce FALSE.
WTERMSIG(*pstatus)
Întoarce codul semnalului care a cauzat terminarea procesului fiu.
Testarea acestei valori are sens doar în cazul în care macroul
WIFSIGNALED, descris anterior, a întors rezultatul TRUE.

73
Apeluri sistem pentru lucrul cu procese în Linux

Există trei moduri de a termina un proces: (1) în mod voluntar, prin apelul
exit, (2) recepţionarea unui semnal de terminare sau a unui semnal netratat
de către proces şi (3) căderea sistemului. Codul de stare returnat prin
variabila indicată de parametrul pstatus indică, prin urmare, care dintre
primele două moduri a cauzat terminarea (în al treilea mod procesul părinte
şi sistemul de operare dispar, aşa încât starea fiului nu mai contează).
Argumentul opt al funcţiei waitpid poate fi 0, caz în care comportarea
funcţiei este similară cu cea a lui wait, sau una dintre constantele simbolice
WNOHANG şi WUNTRACED. Dintre acestea prezintă momentan interes doar
prima şi specificarea ei are ca efect revenirea imediată din waitpid, chiar şi în
cazul în care nici unul dintre procesele fii ale procesului apelant nu este
terminat.

In funcţie de valoarea parametrului pid, comportarea funcţiei waitpid este


următoarea:
pid == -1 Se aşteaptă după terminarea oricărui proces fiu (echivalent cu
wait).
pid > 0 Se aşteaptă terminarea procesului cu identificatorul specificat
de parametrul pid.
pid == 0 Se aşteaptă orice proces cu identificatorul de grup de procese
egal cu cel al apelantului.
pid < -1 Se aşteaptă orice proces cu identificatorul de grup de procese
egal în valoare absolută cu parametrul pid.
Funcţia waitpid returnează -1 dacă nu există proces sau grup de procese cu
PID-ul specificat sau PID-ul respectiv nu este al unui fiu de al său.

5.6. Apelul sistem exit


Acest apel sistem are ca efect terminarea procesului din care este apelat.
Sintaxa funcţiei este următoarea:
void exit(int* status);

Parametrul transmis funcţiei exit este interpretat ca şi cod de terminare şi


poate fi obţinut de un proces părinte pentru a verifica modul de terminare al
unui fiu de-al său. Prin convenţie, codul 0 semnifică terminarea normală a
procesului, iar un cod diferit de zero indică apariţia unei erori. Apelul lui
exit dintr-un proces mai are ca efect, pe lângă terminarea procesului apelant,

74
Sisteme de operare. Chestiuni teoretice şi practice

şi scoaterea părintelui acelui proces din starea de aşteptare a terminării unui


fiu, stare în care a intrat anterior prin apelul funcţiilor wait sau waitpid.
Mecanismul de scoatere a procesului părinte din starea de aşteptare se
bazează pe generarea semnalului SIGCHLD, semnal trimis unui proces
părinte de fiecare dată când unul dintre fiii săi se termină.
Trebuie remarcate următoarele trei situaţii, relativ la apelurile sistem wait şi
exit:
1. Procesul părinte se termină înaintea procesului fiu.
Procesul init (cu PID-ul 1) devine părintele oricărui proces al cărui
părinte iniţial s-a terminat. Sistemul de operare face astfel ca fiecare
proces să aibă întotdeauna un părinte.
2. Procesul fiu se termină înaintea procesului părinte.
Dacă procesul fiu se termină înaintea părintelui său, sistemul de
operare păstrează anumite informaţii (PID-ul, modul şi starea de
terminare, timp de utilizare a CPU etc.) şi după terminarea sa, restul
resurselor alocate procesului fiind eliberate. Aceste informaţii sunt
accesibile părintelui prin apelul funcţiei wait sau waitpid. În
terminologia specifică sistemelor de operare de tip Unix un proces
care s-a terminat şi pentru care părinte său nu a executat încă wait se
găseşte în starea zombie. În această stare, procesul nu mai are resurse
alocate (memoria alocată procesului este eliberată şi fişierele
deschise de el sunt închise), ci doar intrarea sa în tabela proceselor
este încă menţinută. Un proces zombie se poate observa prin
comanda ps -l, care afişează pe coloana corespunzătoare stării
procesului (notată cu litera 'S') litera 'Z'.
3. Procesul fiu, moştenit de procesul init, se termină.
Dacă un proces care are ca părinte pe procesul init se termină, acesta
nu devine zombie, deoarece procesul init apelează una dintre
funcţiile wait sau waitpid pentru fiii săi. Prin acest mecanism
procesul init evită încărcarea sistemului cu procese zombie.
Din cele prezentate mai sus, se poate observa că există o legătură strânsă
între apelurile sistem wait (waitpid) şi exit, funcţionalitatea lor completă
putând fi înţeleasă doar împreună. Remarcăm un dublu aspect al acestei
funcţionalităţi corelate, şi anume:
1. cel al sincronizării procesului părinte cu execuţia fiului, în sensul că
părintele este blocat până când fiul se termină;
2. cel de comunicare, fiul putând „transmite” informaţii despre modul
său de terminare părintelui.

75
Apeluri sistem pentru lucrul cu procese în Linux

5.7. Exemple
Exemplul 1. Programul de mai jos creează un proces fiu, aşteaptă
terminarea fiului şi afişează PID-ul acestuia şi starea sa de terminare (în
zecimal şi hexazecimal).

// parinte.c - codul parintelui


#include <sys/types.h>
#include <sys/wait.h>
main()
{
int pid, stare;
printf(" Parinte: inainte de fork()\n");
if ((pid=fork()) != 0)
wait(&stare);
else {
execl("./fiu", "fiu", 0);
perror("Eroare exec");
}
printf("Parinte: dupa fork()\n");
stare = WEXITSTATUS(stare);
printf("PID fiu=%d; terminat cu codul %d=%x\n",
pid, stare, stare);
}

// fiu.c - codul fiului.


// Obtinut prin comanda de compilare gcc fiu.c -o fiu
#include <sys/types.h>
#include <sys/wait.h>
main()
{
int pid;
printf("Fiul: incepe executia \n");
pid=getpid();
printf("Fiul: %d se termina\n", pid);
exit(10);
}

Exemplul 2. Programul de mai jos preia de la tastatură numele unor


comenzi Linux fără parametri şi le execută. Se foloseşte apelul sistem
execlp. La aşteptarea introducerii unei comenzi, programul afişează
prompterul ’>’.

76
Sisteme de operare. Chestiuni teoretice şi practice

#include <sys/types.h>
#include <sys/wait.h>
int main( void)
{
char buf[MAXLINE];
pid_t pid;
int status;
printf("> ");
while (fgets(buf, MAXLINE, stdin) != NULL) {
buf[strlen(buf)+1] = 0;
if ((pid=fork()) < 0) {
perror("Eroare fork");
exit(1);
}
else if (pid == 0) {
execlp(buf, buf, NULL);
perror("Eroare exec");
exit(2);
}
if ((pid=waitpid( pid, &status, 0)) < 0) {
perror("Eroare waitpid");
exit(2);
}
printf("> ");
}
exit(0);
}

5.8. Probleme
1. Să se vizualizeze efectul execuţiei programului de mai jos.
int main( void)
{
int pid, k=7;
pid=fork();
printf("Returnat %d\n", pid);
if (pid) k=2;
printf("k=%d\n", k);
}
2. Să se scrie un program C prin care să se pună în evidenţă faptul că fiii
unui proces părinte care se termină devin automat fiii procesului init.
3. Să se scrie un program C prin care să se creeze un proces aflat în starea
„zombie”.

77
Apeluri sistem pentru lucrul cu procese în Linux

4. Să se scrie un program C prin care să se pună în evidenţă faptul că două


procese aflate în relaţia părinte-fiu sunt concurente din punct de vedere
al execuţiei, că iniţial procesul fiu este o copie a părintelui, dar că
fiecare proces îşi are propriile date.
5. Să se execute programul de mai jos pe fişiere de test având dimensiunea
din ce în ce mai mare (până la câţiva MB) şi să se explice rezultatul.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int fdR, fdW;
char c;
rd_wr() {
for (;;) {
if (read( fdR, &c, 1) != 1) return;
write( fdW, &c, 1);
}
}
int main(int argc, char * argv[])
{
if (argc != 3) {
printf("Utilizare: %s sursa dest\n", argv[0]);
exit(1);
}
if ((fdR=open( argv[1], O_RDONLY)) < 0) {
perror("Eroare open");
exit(1);
}
if ((fdW=creat(argv[2], 0600)) < 0) {
perror("Eroare create");
exit(1);
}
fork();
rd_wr();
exit(0);
}
6. Să se scrie un program C care să testeze dacă există o limită impusă
numărului de procese pe care le poate crea simultan un utilizator. Pentru
a nu bloca sistemul, procesele create vor apela funcţia sleep pentru un
număr mare de secunde.
7. Să se scrie un program care să creeze mai mulţi fii. Unul dintre ei va citi
în mod continuu caractere de la tastatură şi le va reafişa pe ecran. Acest

78
Sisteme de operare. Chestiuni teoretice şi practice

proces este un proces interactiv. Ceilalţi fii vor executa o buclă infinită
în care vor genera primele N numere prime. Aceste procese sunt procese
intens consumatoare de procesor sau computaţionale. Să se testeze în ce
măsură timpul de reacţie al procesului interactiv este influenţat de
numărul de procese computaţionale.
8. Să se modifice Exemplul 2 astfel încât să se accepte introducerea unor
comenzi cu parametri şi să se implementeze funcţionalitatea
corespunzătoare specificării în linia de comandă a unui interpretor din
Linux a caracterelor ’&’, ’<’, ’>’ .
9. Să se testeze funcţionalitatea funcţiei system şi să se scrie apoi un
program C care să aibă funcţionalitatea similară acestei funcţii, folosind
apelurile sistem fork şi execvp.
10. Să se scrie două programe C, unul numit client.c, iar celalalt server.c.
Programul client va afişa pe ecran un prompter şi va citi de la tastatură
două numere întregi şi unul din caracterele ’+’ sau ’–’. Informaţiile citite
vor fi transmise, cu ajutorul apelului sistem execl unui proces fiu care va
executa codul serverului. Acesta va face operaţia corespunzătoare şi va
transmite rezultatul procesului părinte (client) cu ajutorul apelului
sistem exit. Procesul client va afişa apoi rezultatul şi va reafişa
prompterul pentru o nouă citire.
11. Să se testeze codul de mai jos:
for(i=1; i<=10; i++) {
fork();
printf("Procesul cu PID=%d\n", getpid());
}
Să se modifice apoi codul respectiv astfel încât la sfârşitul execuţiei
tuturor proceselor create, într-un fişier numit proc.txt să se găsească
scris numărul total de procese care au fost create, fără a ase folosi în
acest sens o formulă matematică de calcul a acestui număr, ci el să fie
obţinut prin comunicarea proceselor create.
12. Să se scrie un program C care să aibă o funcţionalitate asemănătoare cu
cea a comenzii ps. Programul va folosi informaţiile furnizate de sistemul
de operare în cadrul pseudo-sistemului de fişiere montat în directorul
/proc. În acest director, fiecărui proces din sistem îi corespunde un
director având numele identic cu identificatorul procesului. Pentru
detalii asupra structurii pseudo-sistemului proc şi a semnificaţiei
informaţiilor afişate în cadrul lui se poate studia pagina de manual
afişată de comanda man 5 proc.

79
6. Thread-uri în Linux

Scopul lucrării
Scopul acestei lucrări este de a prezenta câteva dintre funcţiile descrise de
specificaţia PTHREADS, în implementarea ei sub Linux, funcţii care oferă
posibilitatea creării şi gestionării thread-urilor multiple ale unui proces.

6.1. Specificaţia PTHREADS


PTHREADS este un model standardizat care oferă posibilitatea divizării
unui program în subprograme a căror execuţie poate fi concurentă. Aceste
subprograme identifică în cadrul unui proces execuţii independente şi vor fi
numite în cele ce urmează thread-uri. Litera P din numele modelului vine de
la POSIX (Portable Operating System Interface for UniX). Pachetul
PTHREADS este definit ca o colecţie de tipuri, structuri de date şi funcţii cu
o sintaxă specifică limbajului C. Lucrarea de faţă prezintă implementarea
modelului PTHREADS pentru sistemul de operare Linux, implementare
realizată în pachete precum LinuxThreads sau NGPT (Next Generation
Posix Threads).

Funcţiile existente în modelul PTHREADS corespund operaţiilor referitoare


la thread-uri, precum ar fi cele de creare, terminare, stabilire a atributelor şi
caracteristicilor thread-urilor, comunicare şi sincronizare între thread-uri,
stabilire a politicilor şi a mecanismelor de planificare. În cadrul acestei
prezentări sunt descrise funcţiile de creare şi terminare a thread-urilor unui
proces, precum şi alte câteva funcţii şi problemele legate de aceste două
momente ale execuţiei unui thread.

6.2. Crearea unui thread


Funcţia de creare a unui thread se numeşte pthread_create şi are următoarea
sintaxă:
#include <pthread.h>
int pthread_create (
pthread_t* idThread,
const pthread_attr_t* atribute,
void *(*functie)(void*),
void* arg);

80
Sisteme de operare. Chestiuni teoretice şi practice

Semnificaţia parametrilor funcţiei este următoarea:


idThread Este adresa unde va fi înscris identificatorul thread-ului nou
creat.
atribute Este adresa unei structuri care conţine atributele folosite la
crearea thread-ului. Dacă se specifică NULL pe poziţia
parametrului respectiv, atunci thread-ul este creat cu valorile
implicite ale atributelor sale.
functie Este adresa funcţiei pe care o va executa thread-ul.
arg Reprezintă adresa la care se găseşte argumentul transmis
funcţiei executate de thread. Argumentul poate avea astfel
orice structură dorită de utilizator.

Funcţia întoarce rezultatul zero în caz de succes şi o valoare pozitivă în caz


de eroare, valoare ce reprezintă codul erorii.

Thread-ul creat este executat în mod concurent cu thread-ul care a apelat


funcţia pthread_create şi cu alte eventuale thread-uri ale procesului. El îşi
începe execuţia apelând funcţia transmisă ca parametru şi îşi încheie
execuţia în mod implicit la sfârşitul funcţiei sau explicit apelând funcţia
pthread_exit descrisă mai jos.

Thread-urile create în cadrul unui proces sunt identice din punct de vedere al
relaţiei care există între ele, singurul diferit într-un anumit sens fiind cel
principal, creat odată cu crearea procesului şi care execută funcţia main.
Terminarea acestui thread duce la terminarea procesului şi, implicit, la
terminarea forţată a tuturor thread-urilor sale. Prin urmare, în mod normal, în
funcţia main, se aşteaptă terminarea celorlalte thread-uri ale procesului.

Exemplul următor ilustrează modul de creare a unui thread.


void* thFunction(void* arg) {
int* val = (int*) arg;
printf("Thread with argument %d\n", *val);
}
main() {
pthread_t th1;
int arg1 = 1;
pthread_create(&th1, NULL, thFunction, &arg1);
pthread_join(th1, NULL);
}

81
Thread-uri în Linux

Compilarea unui program C care foloseşte funcţiile din pachetul


PTHREADS trebuie făcută specificând biblioteca în care se găsesc acele
funcţii. Comanda de compilare are forma:

gcc progr.c –lpthread -o progr.exe

6.3. Identitatea unui thread


Fiecărui thread îi este asociat în momentul creării sale un identificator unic.
Acest identificator este obţinut de către thread-ul apelant al funcţiei
pthread_create. Identificatorul unui thread poate fi folosit, de exemplu, de
către un alt thread pentru ca acesta din urmă să aştepte după terminarea
execuţiei primului thread, apelând funcţia pthread_join. Un thread poate să-
şi obţină propriul identificator cu ajutorul funcţiei pthread_self. Pentru
compararea identităţii a două thread-uri este pusă la dispoziţie funcţia
pthread_equal. Sintaxa acestor două funcţii are următoarea formă:

#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal(pthread_t thread1,
pthread_t thread2);

6.4. Terminarea execuţiei unui thread


În mod normal, un thread îşi termină execuţia la sfârşitul funcţiei pe care o
execută. O modalitate explicită de terminare voluntară a unui thread este
prin apelarea de către acel thread a funcţiei pthread_exit, cu ajutorul căreia
se pot specifica şi informaţii legate de starea de terminare a thread-ului.

Sintaxa acestei funcţii este următoarea:

#include <pthread.h>
void pthread_exit(void *retval);

În cazul în care un thread se termină fără a apela în mod explicit funcţia


pthread_exit, informaţia de terminare referitoare la acel thread va
corespunde cu valoarea întoarsă ca rezultat de către funcţia executată de
thread. Prin urmare, informaţia despre starea de terminare a unui thread este
dată de valoarea parametrului retval al funcţiei pthread_exit, în cazul în care
aceasta este apelată, sau de rezultatul funcţiei executată de către thread, în
caz contrar.

82
Sisteme de operare. Chestiuni teoretice şi practice

Printre atributele unui thread există unul care indică dacă după terminare se
va păstra sau nu informaţia despre starea sa de terminare. Cele două
alternative corespund situaţiilor în care un alt thread poate aştepta (prin
apelul funcţiei pthread_join) după terminarea acelui thread pentru a obţine
informaţia care descrie starea sa de terminare, respectiv nu poate face acest
lucru. În cel de-al doilea caz, în momentul terminării thread-ului toate
resursele alocate lui sunt eliberate imediat. Modul în care se stabileşte
valoarea respectivului atribut este descris puţin mai jos.
O modalitate de terminare forţată, din exterior, a unui thread este prin apelul
funcţiei pthread_cancel de către un alt thread. Sintaxa funcţiei este:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

Un thread poate fi terminat doar de către un alt thread al aceluiaşi proces.


Posibilitatea ca un thread să poată fi terminat de către un alt thread este dată
de valoarea unui alt atribut al thread-ului, atribut care se numeşte stare de
terminare (cancelability state) a thread-ului respectiv. Setarea celor două
alternative în ceea ce priveşte terminarea unui thread de către alt thread se
poate face cu ajutorul funcţiei pthread_setcancelstate, a cărei sintaxă este:

#include <pthread.h>
int pthread_setcancelstate(int val, int *vecheaVal);

Valorile posibile pentru parametrului val sunt:


PTHREAD_CANCEL_ENABLE Thread-ul va putea fi oprit.
PTHREAD_CANCEL_DISABLE Thread-ul nu va putea fi oprit.

În momentul în care un thread, aflat în starea în care el poate fi oprit cu


pthread_cancel, primeşte din partea unui alt thread o cerere de terminare, el
poate să ia act de ea imediat terminându-şi execuţia sau poate amâna
momentul terminării sale până la întâlnirea unui aşa-numit punct de
terminare. Modul în care un thread se comportă la apariţia unei cereri de
terminare din exterior este dat de valoarea unui alt atribut al thread-ului,
atribut care se numeşte tip de terminare (cancelability type) şi care poate fi
setat cu ajutorul funcţiei pthread_setcanceltype, având următoarea sintaxă:

#include <pthread.h>
int pthread_setcanceltype(int tip, int *vechiulTip);

83
Thread-uri în Linux

Parametrul tip poate lua următoarele valori, corespunzătoare celor două


moduri de comportament descrise mai sus, şi anume:
PTHREAD_CANCEL_ASYNCHRONOUS Terminare imediată.
PTHREAD_CANCEL_DEFERRED Terminare amânată.

Punctele de terminare sunt acele puncte în execuţia thread-ului în care se


verifică sosirea unei cereri de terminare şi se execută operaţiile
corespunzătoare pentru terminarea thread-ului. Standardul POSIX defineşte
ca puncte de terminare următoarele funcţii: pthread_join, pthread_cond_wait,
pthread_cond_timedwait, pthread_testcancel, sem_wait, sigwait, sleep şi în
general, orice funcţie care pune thread-ul în aşteptare şi cedează procesorul
altui thread. Alte tipuri de funcţii POSIX sunt considerate a nu fi puncte de
terminare, adică în momentul apelului lor şi pe durata execuţiei lor, thread-ul
nu se va termina. Dintre funcţiile descrise mai sus a fi puncte de terminare,
funcţia pthread_testcancel reprezintă un punct de terminare stabilit de
programator, pe când celelalte sunt considerate a fi puncte de terminare
automate. Sintaxa funcţiei pthread_testcancel este:

#include <pthread.h>
void pthread_testcancel(void);

În mod implicit un thread este creat având următoarele valori ale celor două
atribute legate de terminarea sa din exterior, de către un alt thread:
• Starea de terminare: PTHREAD_CANCEL_ENABLE;
• Tipul de terminare: PTHREAD_CANCEL_DEFFERRED.

Terminarea unui thread de către un alt thread trebuie făcută cu mare atenţie
datorită problemelor destul de grave ce pot fi generate. Acest lucru este
necesar deoarece în momentul sosirii unei cereri de terminare thread-ul
respectiv s-ar putea să deţină date globale aflate într-o stare inconsistentă
sau să deţină resurse (lacăte, semafoare etc.) aşteptate şi de alte thread-uri şi
care trebuie neapărat eliberate înainte de terminarea thread-ului. Prin urmare
acele porţiuni de cod care sunt critice din acest punct de vedere ar trebui
protejate împotriva unei terminări necontrolate. Acest lucru se poate face
prin setarea tipul terminării thread-ului la PTHREAD_DEFERRED şi prin
specificarea unor funcţii care trebuie executate în momentul terminării
thread-ului, fie prin apelul lui pthread_exit, fie prin cel al lui
pthread_cancel de către un alt thread. Funcţiile ce vor fi executate la
terminarea unui thread sunt specifice fiecărui thread şi acţiunilor întreprinse

84
Sisteme de operare. Chestiuni teoretice şi practice

de către el şi trebuie să fie, în principiu, destinate eliberării resurselor


deţinute şi respectiv, aducerii datelor prelucrate de către thread într-o stare
consistentă.
Această pregătire a terminării controlate a thread-ului se poate face cu
ajutorul funcţiilor:
• pthread_cleanup_push, folosită pentru a se adăuga o nouă funcţie la
lista de funcţii care trebuie executate la terminarea thread-ului;
• pthread_cleanup_pop, folosită pentru extragerea din lista funcţiilor ce
trebuie executate la terminarea thread-ului a ultimei funcţii introduse
anterior.
Pentru un anumit thread pot fi setate mai multe funcţii care să se execute la
terminarea thread-ului respectiv, ordinea execuţiei acestora făcându-se pe
principiul de funcţionare al unei stive (LIFO). Sintaxa celor două funcţii
amintite mai sus este următoarea:

#include <pthread.h>
void pthread_cleanup_push(
void (*functie)(void*),
void* arg);
void pthread_cleanup_pop(int executa);

Semnificaţia parametrilor este următoarea:


functie Este funcţia care se va executa la terminarea thread-ului.
arg Este argumentul transmis funcţiei.
executa Indică dacă funcţia ce se scoate de pe stivă trebuie sau nu
executată.

Un amănunt practic, important de reţinut, este faptul că se impune folosirea


celor două funcţii ca perechi, deoarece sunt definite ca macrouri,
pthread_cleanup_push folosind acolada deschisă '{', iar pthread_cleanup_pop
pe cea închisă '}'. Nerespectarea acestei cerinţe generează la compilare erori
dificil de înţeles şi corectat. Din păcate acest mod de definire a celor două
macrouri impune anumite restricţii de utilizare, ele neputând face parte în mod
independent din blocuri diferite de instrucţiuni, de exemplu în corpul unor
instrucţiuni for diferite.

Exemplul de mai jos ilustrează folosirea acestor funcţii în situaţia în care un


thread alocă dinamic memorie pe care trebuie să o elibereze la terminarea sa.

85
Thread-uri în Linux

typedef struct m {
int size;
void* pMem;
} MEM;
void allocate_mem(void* arg) {
MEM* p = (MEM*) arg;
p->pMem = malloc(p->size);
}
void release_mem(void* arg) {
MEM* p = (MEM*) arg;
if (p->pMem)
free(p->pMem);
}
void* thFunction(void* arg){
int oldType;
MEM thMem;
pthread_setcanceltype(
PTHREAD_CANCEL_DEFERRED, &oldType);
thMem.size = 100;
thMem.pMem = NULL;
pthread_cleanup_push(
release_mem, (void *) &thMem);
allocate_mem(&thMem);
/* do some work with the memory*/
pthread_cleanup_pop(1);
pthread_setcanceltype(oldType, NULL);
}

6.5. Aşteptarea terminării unui thread


O metodă destul de grosieră de sincronizare a execuţiilor diferitelor thread-
uri este aceea ca un thread să aştepte terminarea unui alt thread. Acest lucru
se poate face cu ajutorul funcţiei pthread_join. Sintaxa funcţiei este:

#include <pthread.h>
int pthread_join(pthread_t th, void **stareTerminare);

Apelul funcţiei duce la blocarea thread-ului apelant până la terminarea


thread-ului indicat de parametrul th. De asemenea, se pot obţine informaţii
legate de terminarea thread-ului după care se aşteaptă, informaţii organizate
într-o structură a cărei adresă va fi înscrisă la adresa indicată de parametrul
stareTerminare, în caz că valoarea acestuia nu este NULL. Tipul

86
Sisteme de operare. Chestiuni teoretice şi practice

parametrului stareTerminare este dat de semnătura funcţiei executată de


un thread, funcţie care returnează un rezultat de tip void*. Dacă thread-ul
aşteptat a fost terminat forţat, atunci valoarea parametrului
stareTerminare este constanta predefinită PTHREAD_CANCELED.

Trebuie reţinut, aşa cum aminteam şi mai sus, că această funcţie poate fi
apelată doar pentru thread-uri pentru care se păstrează de către sistem
informaţii legate de terminarea lor.

Funcţia pthread_join poate fi apelată cu succes doar o singură dată pentru


un thread. Dacă un thread apelează funcţia pthread_join şi ulterior un alt
thread face acelaşi lucru, în cel de-al doilea caz funcţia nu va duce la
blocarea thread-ului apelant, ci va întoarce o valoare de eroare.

6.6. Stabilirea atributelor unui thread


Un thread are anumite proprietăţi pe care le numim atribute ale thread-ului
respectiv. Câteva dintre aceste atribute le-am amintit deja mai sus, cum ar fi
de exemplu modul de comportare a thread-ului în momentul apariţiei unei
cereri de terminare din exterior.

Alte atribute ale unui thread sunt:


• proprietatea de a se putea aştepta (prin apelul funcţiei pthread_join)
după terminarea respectivului thread;
• dimensiunea stivei;
• poziţionarea stivei în spaţiul de adrese al procesului;
• atribute legate de planificarea pentru execuţie a thread-ului.

Pentru a stabili valorile atributelor unui thread trebuie creată o structură de


tipul pthread_attr_t şi modificate apoi valorile implicite ale câmpurilor
acelei structuri, folosind diverse funcţii puse la dispoziţie în acest sens. Adresa
structurii de atribute trebuie transmisă funcţiei pthread_create. Dacă se
transmite NULL, atunci thread-ul este creat cu setul de parametri impliciţi.
Funcţiile de creare şi distrugere a unei structuri de atribute ale unui thread
sunt pthread_attr_init, respectiv pthread_attr_destroy, cu următoarea sintaxă:

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

87
Thread-uri în Linux

Momentan, singurul atribut care prezintă importanţă este cel legat de


posibilitatea de a apela pthread_join pentru un anumit thread. Setarea
valorii acestui atribut şi respectiv, obţinerea valorii sale curente se poate
face cu ajutorul funcţiilor de mai jos:

#include <pthread.h>
int pthread_attr_setdetachstate(
pthread_attr_t *atribute,
int stare);
int pthread_attr_getdetachstate(
const pthread_attr_t* atribute,
int *stare);

Valorile posibile ale parametrului stare sunt:


PTHREAD_CREATE_DETACHED
Nu se poate aştepta după terminarea thread-ului şi, implicit, nu sunt
păstrate informaţii despre modul de terminare a thread-ului.
PTHREAD_CREATE_JOINABLE
Se poate aştepta după terminarea thread-ului. Informaţiile despre
modul de terminare a thread-ului sunt păstrate până la obţinerea
acestora cu ajutorul funcţiei pthread_join. Aceasta este valoarea
implicită a atributului thread-ului.

Există funcţii cu nume similare pentru modificarea şi obţinerea celorlalţi


parametri ai unui thread, şi anume:
• pthread_attr_getstacksize şi pthread_attr_setstacksize;
• pthread_attr_getstackaddr şi pthread_attr_setstackaddr etc.

Dacă un thread este setat „detaşat”, atunci, în momentul terminării sale,


sistemul nu mai păstrează informaţii legate de starea în care s-a terminat,
toate resursele alocate respectivului thread fiind eliberate şi, prin urmare, nu
se poate apela cu succes funcţia pthread_join pentru acel thread. Un
comportament similar poate fi obţinut cu ajutorul funcţiei pthread_detach,
care poate fi apelată după crearea thread-ului. Apelarea acestei funcţii
pentru un thread pentru care există deja un alt thread care aşteaptă după
terminarea lui va fi fără efect, valoarea atributului rămânând neschimbată.
Sintaxa funcţiei este descrisă mai jos:

#include <pthread.h>
int pthread_detach(pthread_t th);

88
Sisteme de operare. Chestiuni teoretice şi practice

6.7. Relaţia dintre thread-uri şi procese


Comportamentul la semnale
Una dintre problemele care se pun referitor la procesele ce conţin mai multe
thread-uri este aceea a modului de utilizare a semnalelor. Este vorba atât
despre modul de transmitere a semnalelor către procese ce conţin mai multe
thread-uri, precum şi modalitatea de reacţie a acestora la semnalele trimise
lor de către alte procese.
Standardul PTHREADS prevede ca trimiterea unui semnal să se poată face
doar unui proces, şi nu unui thread din acel proces. Acest lucru este evident,
din moment ce din exterior nu se poate detecta, în mod normal, existenţa
thread-urilor unui proces, acestea ţinând de structura internă a procesului.
Modul de răspuns la primirea unui semnal, precum şi funcţia lui de tratare,
în cazul în care semnalul este preluat, se stabileşte, de asemenea, pentru
proces şi nu pentru un anumit thread din acel proces. Pe de altă parte,
definirea filtrului de blocare a unor semnale este proprie fiecărui thread,
putându-se defini câte un filtru pentru fiecare thread în parte. Definirea
respectivului filtru pentru un thread se face cu ajutorul funcţiei
pthread_sigmask, iar pentru proces, per ansamblu, cu ajutorul funcţiei
sigprocmask. Un semnal trimis unui proces este „recepţionat” (tratat) în
timpul execuţiei unuia dintre thread-urile care nu au blocat primirea acelui
semnal, fără a se şti care anume este acela.
Exemplul de mai jos ilustrează modul de stabilire a tratării unui semnal şi a
măştii de blocare a anumitor semnale.
void sigHandler(int sig) {
printf("Semnalul %d tratat in timpul executiei
thread-ului %d.\n", sig, pthread_self());
}
main() {
pthread_t th1;
signal(SIGUSR1, sigHandler);
pthread_create(&th1, NULL, thFunction, NULL);
pthread_join(th1, NULL);
}
void* thFunction(void* arg) {
sigset_t sigmask;
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGUSR1);
pthread_sigmask(SIG_SETMASK, &sigmask, NULL);
while(1);
}

89
Thread-uri în Linux

Efectul apelului funcţiei fork


O altă problemă care apare în cazul proceselor cu mai multe thread-uri este cea
corespunzătoare situaţiei în care unul din thread-urile procesului apelează
funcţia fork, de creare a unui nou proces. Întrebarea care se pune este dacă
thread-urile existente în procesul părinte în momentul apelului funcţiei fork vor
exista şi în procesul fiu sau nu. Standardul PTHREADS precizează că, deşi
procesul fiu este identic, ca şi conţinut, cu cel părinte, singurul thread activ în
procesul nou creat va fi doar acela care a apelat funcţia fork. Prin urmare,
execuţia procesului creat va corespunde cu execuţia thread-ului activ şi se va
termina o dată cu terminarea acestuia. Având în vedere modul de implementare
a thread-urilor în Linux cu ajutorul proceselor, acest mod de comportare este
evident, fiind creat un duplicat doar al procesului (thread-ului) care apelează
funcţia fork. Datorită acestui mod de funcţionare al funcţiei fork, în procesul fiu
pot să apară anumite probleme relativ la starea thread-urilor ce nu mai sunt
activate în fiu. Este posibil, spre exemplu, ca, în momentul creării noului
proces, unul din thread-urile inactive să fi blocat anterior, cu ajutorul unui
lacăt, accesul la o resursă partajată. Eliberarea lacătului şi, implicit, a accesului
la resursa partajată poate fi făcută doar de către thread-ul care a blocat lacătul.
El fiind, însă, inactiv în procesul fiu, este posibil ca thread-ul activ să rămână
blocat definitiv aşteptând deblocarea lacătului. Pentru a preveni astfel de
situaţii se poate folosi funcţia pthread_atfork, prin care se pot specifica funcţii
care să se execute, atât în procesul părinte, cât şi în cel fiu înainte şi după
efectuarea execuţia funcţiei fork. Sintaxa funcţiei este următoarea:

#include <pthread.h>
pthread_atfork( void (*pregatire)(void),
void (*parinte)(void),
void (*fiu)(void));

Semnificaţia parametrilor este următoarea:


pregatire Funcţia care se va executa în procesul părinte, înainte de
crearea noului proces.
parinte Funcţia care se va executa în procesul părinte, înainte de
revenirea din funcţia fork.
fiu Funcţia care se va executa în procesul fiu, înainte de
revenirea din funcţia fork.

Funcţia pthread_atfork poate fi apelată de mai multe ori, având ca efect


stabilirea unor liste de funcţii ce vor fi executate în ordinea LIFO.

90
Sisteme de operare. Chestiuni teoretice şi practice

6.8. Probleme
1. Să se scrie un program care testează dacă thread-urile unui proces se
execută în mod concurent sau nu. În acest scop, thread-urile create vor
executa o funcţie, în care, în cadrul unei bucle infinite, vor afişa
identificatorul de thread şi identificatorul procesului căruia îi aparţin.
2. Folosind ca punct de pornire programul de la Problema 1, să se testeze
cele două moduri de reacţie a unui thread la o cerere de terminare
transmisă prin apelul funcţiei pthread_cancel de către un alt thread.
3. Să se scrie un program care determină numărul maxim de thread-uri ce
pot fi active simultan în cadrul unui proces. Pentru a nu consuma
procesor, dar şi pentru a nu se termina thread-rile create, vor apela în
cadrul unei bucle infinite funcţia sleep. Pentru aflarea numărului dorit se
va testa valoarea întoarsă de funcţia pthread_create, pentru a detecta
momentul în care nu se mai pot crea alte thread-uri.
4. Să se scrie un program de tip server care creează în mod periodic thread-
uri care simulează deservirea unor cereri de la clienţi. Thread-urile de
deservire a cererilor afişează un mesaj, aşteaptă un anumit timp (cu
sleep) şi apoi se termină. În acelaşi timp, serverul acceptă comenzi
introduse de la tastatură. Să se implementeze funcţionalitatea comenzii
de oprire a serverului, adică la apăsarea unei anumite taste (de exemplu
'x') să nu se mai creeze noi thread-uri şi procesul se fie terminat.
Terminarea procesului trebuie făcută însă numai după terminarea thread-
urilor existente la acel moment.
5. Să se modifice problema anterioară astfel încât thread-urile ce simulează
deservirea cererilor de la clienţi să execute într-o buclă infinită operaţii
intens computaţionale (de exemplu calcularea primelor N numere
prime). La apăsarea unei taste, serverul va afişa pe ecran numărul de
thread-uri create până în acel moment. Să se urmărească modul în care
thread-urile computaţionale influenţează timpul de reacţie al thread-ului
ce citeşte de la tastatură.
6. Să se testeze comportamentul unor thread-uri diferite ale aceluiaşi
proces care apelează simultan funcţii de citire de la tastatură.
7. Să se testeze efectul apelului funcţiei pthread_join de către thread-ul
activ dintr-un proces fiu pentru aşteptarea terminării unui thread care
exista în procesul părinte în momentul apelului funcţiei fork, dar care nu
e activ în procesul fiu. Întrebarea care se pune este dacă thread-ul activ
va fi blocat în funcţia pthread_join sau nu, iar dacă nu, ce anume
returnează funcţia respectivă.

91
Thread-uri în Linux

8. Să se scrie un program C de copiere a conţinutului unui fişier într-un alt


fişier folosind mai multe thread-uri. Copierea se va face în zone de
anumite dimensiuni (de exemplu 512 octeţi, 1Kb, 2Kb, 4Kb etc.). Pentru a
evidenţia necesitatea thread-urilor într-o astfel de situaţie, precum şi
pentru a aprecia numărul de thread-uri pentru care operaţia este eficientă,
se va face o comparaţie între următoarele implementări ale problemei:
a. un singur thread;
b. N thread-uri create la începutul execuţiei programului, fiecare
copiind o anumită zonă a fişierului;
c. un thread existent va crea un nou thread doar înainte de a
apela funcţiile de acces la fişiere (read şi write), care e
posibil să-l pună în stare de aşteptare.
9. Să se efectueze aceleaşi teste ca în problema precedentă, dar pentru
cazul în care o zonă copiată trebuie prelucrată înainte de a fi scrisă în
fişierul destinaţie. Un exemplu de prelucrare ar putea fi scrierea în
ordine inversă a octeţilor.
10. Se presupune că într-un fişier text numbers.in, pe fiecare linie se găsesc
două numere întregi. Să se scrie programul C, care citeşte, pe rând, toate
liniile din fişier şi pentru fiecare linie citită creează un nou thread, căruia îi
transmite ca parametri (în cadrul unei structuri) cele două numere aflate pe
linia respectivă. Fiecare thread va prelua cei doi parametri, va face media
lor aritmetică, va scrie în fişierul result_threads.out, pe o linie, cele trei
numere şi identificatorul propriu de thread şi, în plus, numărul ce reprezintă
media aritmetică îl va transmite ca parametru funcţiei pthread_exit. După
terminarea citirii fişierului numbers.in, thread-ul main va prelua toate
rezultatele transmise de thread-urile create şi le va scrie în fişierul
results_main.out, fiecare rezultat alături de identificatorul thread-ului care
l-a produs. Să se compare, apoi conţinutul celor două fişiere.
11. Se presupune că thread-ul main al unui proces este blocat în aşteptarea
terminării unui alt thread al procesului. Se mai presupune referitor la
modalitatea de reacţie la apariţia semnalului SIGUSR1, că s-a stabilit
anterior o funcţie de tratare şi că thread-ul după care se aşteaptă a mascat
semnalul SIGUSR1 cu ajutorul funcţiei pthread_sigmask. Să se testeze
dacă prin trimiterea semnalului SIGUSR1 către proces, thread-ul main va
reacţiona sau nu şi, în caz afirmativ, dacă va rămâne în starea de aşteptare
a celuilalt thread sau nu.
12. Să se scrie două programe C corespunzătoare a două procese, fiecare cu
câte două thread-uri, astfel încât comunicarea prin semnalele SIGUSR1
şi SIGUSR2 dintre cele două procese să se facă la nivelul unei perechi de

92
Sisteme de operare. Chestiuni teoretice şi practice

thread-uri pentru fiecare semnal. Astfel prin semnalul SIGUSR1 vor


comunica doar cele două thread-uri care au fost create primele în cele
două procese, iar prin SIGUSR2 vor comunica doar celelalte două.
13. Se consideră că o anumită variabilă globală var dintr-un proces este
într-o stare consistentă doar dacă valoarea sa este una prestabilită (de
exemplu var=10). Presupunem că procesul va crea mai multe thread-uri
care apelează toate aceeaşi funcţie threadFunct, funcţie care modifică
valoarea variabilei var. Modificările valorii variabilei se pot face doar
prin intermediul funcţiilor aduna(int val) şi respectiv, scade(int
val). Să se scrie funcţia threadFunc ce va fi executată de către thread-
uri, astfel încât la sfârşitul execuţiei tuturor thread-urilor create valoarea
variabilei să rămână cea iniţială, considerată ca stare consistentă a
variabilei, indiferent de cazul în care thread-urile se termină în mod
normal sau sunt terminate de către thread-ul main prin apelul funcţiei
pthread_cancel.
14. În contextul descris de problema precedentă, să se asigure, folosind
funcţia pthread_atfork, că în procesul fiu valoarea variabilei var este cea
corespunzătoare stării consistente. Se consideră că este posibil ca şi
thread-ul care apelează funcţia fork să fi modificat valoarea variabilei
înainte de apelul lui fork.
15. Să se scrie un program care să genereze în mod continuu thread-uri. Se
presupune că alocarea structurii pthread_t asociată unui thread şi a
parametrilor funcţiei executate de thread se face în mod dinamic. Pentru
eliberarea memoriei alocată thread-urilor care s-au terminat se va crea
un alt thread, numit garbage collector.

93
7. Procese şi thread-uri în Windows 2000

Scopul lucrării
Lucrarea descrie structurile de date şi strategia folosită pentru gestionarea
proceselor şi thread-urilor în Windows 2000. Sunt prezentate, de asemenea,
câteva funcţii ale API-ului Win32 legate de procese şi thread-uri.

7.1. Prezentare generală


Ca şi în alte sisteme de operare, şi în Windows conceptul care
caracterizează execuţia unui cod executabil este procesul. Procesul
reprezintă cadrul asigurat de către sistemul de operare pentru execuţia unui
cod executabil, acest cadru cuprinzând starea maşinii şi toate resursele
alocate, necesare execuţiei respective. Conceptul care identifică în acest
cadru execuţia propriu-zisă este cel de thread. În Windows 2000, un proces
constă din unul sau mai multe thread-uri de execuţie împreună cu resursele
alocate acestora. Modelul proces din Windows 2000 include următoarele
elemente:
• codul şi datele programului executat;
• spaţiul virtual de adrese al procesului, distinct de cel al unui alt
proces;
• resurse de sistem (semafoare, filtre etc.) alocate de sistemul de
operare procesului, pe măsură ce thread-urile acestuia le solicită;
• identificatorul de proces (PID);
• cel puţin un thread.
Fiecare thread al unui proces are asociat un contor de program (numărător
de instrucţiuni) care ţine evidenţa instrucţiunilor care urmează să se execute.
În cazul Windows 2000, thread-ul este o entitate controlată de planificatorul
de execuţie. Thread-urile au următoarele componente:
• regiştri care descriu starea procesorului; unul dintre aceşti
regiştri este contorul de program;
• două stive: una pentru execuţia în mod nucleu, cealaltă pentru
execuţia în mod utilizator;
• zonă privată de memorie pentru dll-uri şi biblioteci run-time;
• identificatorul thread-ului (TID). Identificatorii de thread-uri şi
procese sunt alocaţi din acelaşi spaţiu valoric, astfel încât toate
procesele şi thread-urile vor avea identificatori unici.

94
Sisteme de operare. Chestiuni teoretice şi practice

Primele trei elemente din lista de mai sus poartă împreună denumirea de
contextul thread-ului. Planificarea thread-urilor este sarcina exclusivă a
nucleului sistemului de operare şi se bazează pe prioritatea thread-urilor.
Într-un proces, thread-urile sunt executate independent unul de altul şi nu
“se văd” reciproc.
API-ul Win32 defineşte conceptul de fibră, care poate fi asociată unui
thread. Fibrele sunt similare thread-urilor, dar sunt gestionate de către
utilizator şi nu de către sistemul de operare. Managementul fibrelor
presupune crearea, terminarea şi inclusiv planificarea lor, trecerea execuţiei
de la o fibră la alta făcându-se explicit la cererea utilizatorului. Fiecare
thread poate avea una sau mai multe fibre. Fibrele sunt cea mai mică entitate
executabilă care poate fi creată şi executată la nivel utilizator.
Fiecare resursă folosită de un proces (de exemplu thread-urile unui proces)
este reprezentată de un obiect. Procesul în sine este tratat ca un obiect. Un
obiect poate fi accesat printr-un handle obţinut pentru acel obiect. Pentru
securitate şi managementul resurselor, fiecare proces are asociată o structură
de date (un token de acces), care conţine identificatorul de securitate şi
drepturile de acces ale procesului.

7.2. Funcţii Win32 API pentru procese şi thread-uri


Crearea unui proces
Crearea proceselor se poate face folosind funcţia Win32 CreateProcess.
Această funcţie are 10 parametri, fiecare având la rândul lui mai multe opţiuni.
Nu vom face o prezentare completă a parametrilor acestei funcţii (şi nici pentru
celelalte funcţii), ci vom da numai o scurtă descriere a lor. Informaţii complete
asupra sintaxei şi comportamentului funcţiilor abordate se pot găsi în
documentaţia MSDN. Sintaxa funcţiei CreateProcess este următoarea:

BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation);

95
Procese şi thread-uri în Windows 2000

Semnificaţia parametrilor este următoarea:

lpApplicationName Calea spre fişierul executabil.

lpCommandLine Linia de comandă.

lpProcessAttributes Pointer la un descriptor de securitate pentru


proces. Pentru NULL se vor lua valorile
implicite.

lpThreadAttributes Pointer la un descriptor de securitate pentru


thread-ul iniţial.

bInheritHandles Un bit care spune dacă noul proces


moşteneşte handle-urile procesului creator.
dwCreationFlags Diferite flag-uri (de exemplu, modul de
eroare, prioritate, depanare, consola etc.).

lpEnvironment Pointer la şirul variabilelor de mediu. Dacă


este NULL, procesul fiu va primi valorile
variabilelor de mediu din procesul părinte.

lpCurrentDirectory Pointer la directorul de lucru al noului proces.

lpStartupInfo Pointer la o structură de date care descrie


fereastra iniţială de pe ecran (culoare, număr
de linii, titlu, icoana, forma cursorului etc.).

lpProcessInformation Pointer la o structură de date care returnează


informaţii despre procesul creat.

Următorii paşi sunt executaţi la un apel al funcţiei CreateProcess:


1. deschide fişierul executabil;
2. creează obiectul executiv de proces, EPROCESS;
3. creează thread-ul iniţial;
4. notifică subsistemul Win32 despre existenţa noului proces;
5. lansează în execuţie thread-ul iniţial;
6. iniţializează spaţiul de adrese al noului proces (de exemplu, se
încarcă DLL-urile necesare) şi se lansează în execuţie programul.

96
Sisteme de operare. Chestiuni teoretice şi practice

Crearea thread-urilor
Un thread poate fi creat într-un proces existent cu funcţia CreateThread,
funcţie care are sintaxa de mai jos:

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);

Semnificaţia parametrilor este următoarea:


lpThreadAttributes Pointer la un descriptor de securitate pentru
thread.
dwStackSize Dimensiunea iniţială în octeţi a stivei thread-
ului.
lpStartAddress Pointer la funcţia principală a thread-ului.
lpParameter Argumentul funcţiei principale.
dwCreationFlags Un thread poate fi suspendat sau poate fi
executat imediat după creare.
lpThreadId Pointer la identificatorul thread-ului.

Când funcţia CreateThread este apelată, următoarele operaţii sunt executate


de către sistem:
1. Creează o stivă utilizator pentru thread în spaţiul de adrese al
procesului.
2. Setează valorile iniţiale pentru hardware-ul legat de thread.
3. Apelează funcţia NTCreateThread care creează obiectul executiv
thread. Aceşti paşi sunt executaţi în mod kernel.
4. Notifică subsistemul Win32 despre noul thread, care îl forţează să
facă unele setări pentru thread.
5. Returnează handler-ul şi ID-ul thread-ului apelantului funcţiei
CreateThread.
6. Thread-ul este pus în starea „gata de rulare” (ready-to-run).

O variantă interesantă şi utilă a acestei funcţii este CreateRemoteThread,


care creează un thread care va rula în spaţiul de adrese al unui alt proces.

97
Procese şi thread-uri în Windows 2000

Sintaxa funcţiei este similară cu cea a funcţiei CreateThread cu diferenţa că


are un parametru adiţional, un handle la un proces în care thread-ul va fi
creat. Handle-ul trebuie să aibă setate următoarele drepturi de acces:
• PROCESS_CREATE_THREAD
• PROCESS_QUERY_INFORMATION
• PROCESS_VM_OPERATION
• PROCESS_VM_WRITE
• PROCESS_VM_READ.

Funcţia CreateRemoteThread are ca rezultat execuţia unui nou thread în


spaţiul de adrese al procesului dat. Thread-ul are acces la toate obiectele
deschise de procesul care l-a creat.

Crearea unei fibre


Pentru a crea o fibră, trebuie să apelăm funcţia CreateFiber. Ea alocă un
obiect fibră, îi asociază o stivă şi setează adresa specificată prin funcţia de
fibră ca început al execuţiei fibrei. Apelul funcţiei CreateFibre nu planifică
fibra pentru execuţie. Sintaxa funcţiei este următoarea:

LPVOID CreateFiber(
SIZE_T dwStackSize,
LPFIBER_START_ROUTINE lpStartAddress,
LPVOID lpParameter);

Semnificaţia parametrilor este următoarea:


dwStackSize Dimensiunea iniţială a stivei.
lpStartAddress Pointer la funcţia principală a fibrei.
lpParameter Argumentul funcţiei principale a fibrei.

Terminarea proceselor, thread-urilor şi a fibrelor


În condiţii normale, un proces îşi termină execuţia apelând funcţia
ExitProcess, prin care transmite şi starea de terminare. În cazul în care se
doreşte terminarea unui proces (împreună cu toate thread-urile lui) dintr-un
alt proces, se poate apela funcţia TerminateProcess, cu următoarea sintaxă:

BOOL TerminateProcess(
HANDLE hProcess,
UINT uExitCode);

98
Sisteme de operare. Chestiuni teoretice şi practice

După revenirea din funcţie, în uExitCode vom avea codul de ieşire pentru
procesul terminat ca urmare a apelului funcţiei.
Similar, funcţia TerminateThread este folosită pentru terminarea forţată a
unui thread. Când această funcţie este apelată, thread-ul nu are posibilitatea
să mai execute cod utilizator, stiva sa iniţială nu este dealocată, iar DLL-
urile ataşate thread-ului nu sunt anunţate de terminarea thread-ului. Sintaxa
funcţiei este următoarea:

BOOL TerminateThread(
HANDLE hThread,
DWORD dwExitCode);

Datorită faptului că apelul funcţiei TerminateThread poate duce la situaţii


neprevăzute, e bine să fie utilizată numai în cazuri extreme. Se recomandă să
se apeleze TerminateThread numai dacă se ştie exact ce face thread-ul ţintă.
Dacă thread-ul terminat este ultimul thread dintr-un proces, la apelul
funcţiei se va termina şi procesul thread-ului.
Pentru a termina o fibră, se apelează funcţia DeleteFiber care şterge
structurile de date alocate fibrei.

7.3. Planificare şi priorităţi


Funcţia SetPriorityClass setează clasa de prioritate pentru un proces
specificat. Această valoare împreună cu valoarea priorităţii fiecărui thread
din proces determină nivelul priorităţii de bază pentru acel thread. Sintaxa
funcţiei este următoarea:
BOOL SetPriorityClass(
HANDLE hProcess,
DWORD dwPriorityClass);

Parametrul dwPriorityClass poate avea una dintre următoarele valori:


• ABOVE_NORMAL_PRIORITY_CLASS
• BELOW_NORMAL_PRIORITY_CLASS
• HIGH_PRIORITY_CLASS
• IDLE_PRIORITY_CLASS
• NORMAL_PRIORITY_CLASS
• REALTIME_PRIORITY_CLASS.
Dacă funcţia se termină cu succes, ea returnează o valoare pozitivă. În caz
contrar, valoarea returnată este 0.

99
Procese şi thread-uri în Windows 2000

Funcţia SetThreadPriority setează valoarea priorităţii unui thread specificat.


Această valoare, împreună cu clasa de prioritate a procesului thread-ului
determină nivelul bază de prioritate pentru thread-ul respectiv.
BOOL SetThreadPriority(
HANDLE hThread,
int nPriority);

Parametrul nPriority poate avea următoarele valori:


• THREAD_PRIORITY_ABOVE_NORMAL
• THREAD_PRIORITY_BELOW_NORMAL
• THREAD_PRIORITY_HIGHEST
• THREAD_PRIORITY_IDLE
• THREAD_PRIORITY_LOWEST
• THREAD_PRIORITY_NORMAL
• THREAD_PRIORITY_TIME_ CRITICAL.
Dacă funcţia se termină cu succes, ea returnează o valoare pozitivă. În caz
contrar, valoarea returnată este 0.
Pentru a afla prioritatea unui thread, se poate apela funcţia GetThreadPriority.
Alte funcţii legate de planificarea thread-urilor şi a fibrelor sunt următoarele:
VOID Sleep(DWORD milsecunde);
Suspendă execuţia thread-ului curent pentru un interval specificat.
DWORD SuspendThread(HANDLE thread);
Suspendă execuţia thread-ului specificat.
BOOL SwitchToThread(void);
Thread-ul apelant va ceda execuţia unui alt thread pe procesorul
curent.
VOID SwitchToFiber(LPVOID fibra);
Planifică o fibră. Fibrele se creează cu funcţia CreateFiber. Înainte
de a putea planifica o fibră pentru execuţie, trebuie apelată funcţia
ConvertThreadToFiber pentru a iniţializa zona de memorie unde se
vor salva informaţiile de stare ale fibrei. Thread-ul devine fibra
executată. Funcţia SwitchToFiber salvează informaţiile de stare
pentru fibra curentă şi reîncarcă starea fibrei specificate. Se poate
apela SwitchToFiber şi cu adresa unei fibre create de un alt thread.
Pentru a face acest lucru, trebuie să avem o referinţă la adresa
returnată în celălalt thread când acesta a apelat CreateFiber şi
trebuie să folosim o sincronizare potrivită.

100
Sisteme de operare. Chestiuni teoretice şi practice

7.4. Exemple
Exemplul 1. Programul C de mai jos ilustrează modul de creare a unui
proces. Codul procesului fiu este ilustrat după cel al părintelui său.

// Codul procesului parinte: ProcesParinte.cpp


#include <windows.h>
#incude <stdio.h>

void main(VOID)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;

ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);

ZeroMemory(&pi, sizeof(pi));

// Porneste procesul fiu


if(! CreateProcess(
".\\ProcesFiu.exe", // calea spre executabil
"ProcesFiu arg1 arg2", // linia de comanda
NULL, // Handle-ul procesului nu poate
// fi mostenit
NULL, // Handle-ul thread-ului nu poate
// fi mostenit
FALSE, // Nu se mostenesc handle-urile
0, // Fara flag-uri de creare
NULL, // Foloseste variabilele de mediu
// ale parintelui
NULL, // Foloseste directorul curent al
// parintelui
&si, // Pointer la structura STARTUPINFO
&pi)) // Pointer la structura
// PROCESS_INFORMATION
{
printf("Eroare CreateProcess.\n");
exit(0);
}

// Asteapta terminarea fiului


WaitForSingleObject(pi.hProcess, INFINITE);

// Inchide handle-urile spre fiu


CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}

101
Procese şi thread-uri în Windows 2000

// Codul procesului fiu: ProcesFiu.cpp


int main(int argc, char **argv)
{
if (argc != 3) {
printf("Sunt necesari doi parametri!\n");
exit(1);
}

printf("Parametrii programului fiu: %s %s\n",


argv[1], argv[2]);
}

Exemplul 2. Exemplul următor prezintă modul cum se creează un nou


thread care execută o funcţia ThreadFunc definită local.

#include <windows.h>
#include <conio.h>

DWORD WINAPI ThreadFunc(LPVOID lpParam)


{
char szMsg[80];
wsprintf(szMsg, "Parametru=%d.",*(DWORD*)lpParam);
MessageBox(NULL, szMsg, "ThreadFunc", MB_OK);
return 0;
}

VOID main(VOID)
{
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread;
char szMsg[80];
hThread = CreateThread(
NULL, // atribute implicite
0, // dimens. implicita a stivei
ThreadFunc, // functia de executat
&dwThrdParam, // argumentele functiei
0, // flag-uri implicite de creare
&dwThreadId); // identificatorul thread-ului

// Verifica valoarea returnata


if (hThread == NULL) {
wsprintf(szMsg, "Eroare CreateThread.");
MessageBox(NULL, szMsg, "main", MB_OK);
} else {
_getch();
CloseHandle(hThread);
}
}

102
Sisteme de operare. Chestiuni teoretice şi practice

7.5. Probleme
1. Să se scrie un program C care demonstrează că execuţia mai multor
procese este concurentă.
2. Să se scrie un program C care demonstrează că execuţia mai multor
thread-uri este concurentă.
3. Să se scrie un program C care să impună o planificare a fibrelor unui
thread într-o ordine prestabilită. Să se verifice ce se întâmplă dacă una
dintre fibre intră într-o buclă infinită.
4. Să se scrie un program C care să verifice dacă există o limită a numărului
de procese ce pot fi create simultan în sistem de către un utilizator. Pentru
a nu bloca sistemul, procesele respective vor apela într-o buclă infinită
funcţia Sleep. Să se identifice apoi numărul de procese consumatoare de
procesor pentru care comportarea sistemului este rezonabilă. Acelaşi test
să se efectueze pentru cazurile în care prioritatea proceselor create este
una mai mică decât cea implicită.
5. Să se efectueze testele descrise în problema precedentă, dar în cazul
thread-urilor.
6. Să se testeze modul de planificare a proceselor şi a thread-urilor. În
acest scop vor fi create mai multe procese, respectiv thread-uri, fiecare
având una dintre priorităţile posibile. Procesele (thread-urile) vor afişa
pe ecran un mesaj.
7. Să se scrie codul C al unui proces de tip server care să creeze două
thread-uri. Primul thread simulează modul de deservire a clienţilor şi
creează la anumite intervale de timp câte un thread care ar deservi în
mod real o nouă cerere sosită de la un client. Thread-ul respectiv va fi
lăsat să ruleze într-o buclă infinită în care efectuează anumite operaţii
computaţionale. Opţional se poate impune o limită a numărului de
thread-uri astfel create. Cel de-al doilea thread al serverului va execută o
buclă infinită în care aşteaptă apăsarea unei taste şi afişează pe ecran
numărul de thread-uri create de primul thread al procesului server. Să se
testeze modul în care thread-urile computaţionale influenţează timpul de
răspuns (de reacţie) la apăsarea unei taste al celui de-al doilea thread al
serverului. Să se modifice apoi prioritatea thread-ului interactiv la valori
mai mari decât cea implicită, iar a celor computaţionale la valori mai
mici şi să se repete testele.
8. Să se scrie un program C care creează N thread-uri cu prioritatea
normală, numite thread-uri de lucru şi un thread cu prioritatea
THREAD_PRIORITY_IDLE, numit „garbage collector”. Thread-urile de
lucru execută o buclă infinită în care, la fiecare pas, trebuie să găsească

103
Procese şi thread-uri în Windows 2000

M elemente ale unui şir de întregi cu valoare 0 şi să le seteze la valoarea


1. Căutarea elementelor zero se va face aleator. După k încercări
nereuşite de găsire a unui element zero, un thread de lucru aşteaptă un
anumit timp, apelând Sleep, după care încearcă din nou. După ce a găsit
cele M elemente căutate, thread-ul de lucru afişează poziţia lor în şir şi
reia algoritmul de la început. Să se testeze modul de lucru al thread-ului
„garbage collector”, punându-se în evidenţă momentele la care el este
planificat. Să se modifice apoi priorităţile thread-urilor şi să se repete
testele.
9. Scrieţi un program care realizează adunarea în paralel a N numere
folosind thread-uri. Să se compare cu timpul de execuţie al adunării în
cazul efectuării operaţiei de către un singur thread.
10. Să se vizualizeze procesele active din sistem şi diferite caracteristici ale lor
cu ajutorul aplicaţiei Task Manager, lansată prin apăsarea combinaţiei de
taste Ctrl-Alt-Del. Opţiunea de meniu ViewÆSelect Columns... oferă
posibilitatea alegerii proprietăţilor ce vor fi afişate. Facilităţi asemănătoare
oferă aplicaţia Process Explorer disponibilă la adresa
http://www.microsoft.com/technet/sysinternals. Să se scrie apoi
un program C care să afişeze pe ecran toate procesele active şi câteva
dintre proprietăţile lor. În acest scop se vor studia şi folosi funcţiile:
a. CreateToolhelp32Snapshot, CloseToolhelp32Snapshot;
b. Process32First, Process32Next;
c. GetCurrentProcessId, GetCurrentThreadId.

104
8. Fişiere PIPE în Linux

Scopul lucrării
Lucrarea prezintă modalitatea de comunicare în Linux între procese aflate
pe acelaşi sistem folosind fişiere pipe cu nume şi fişiere pipe anonime sau
fără nume. Sunt descrise, de asemenea, principalele apeluri sistem de creare
şi utilizare a fişierelor pipe.

8.1. Principiile comunicării prin fişiere pipe


Fişierele pipe reprezintă în Linux un mecanism specializat de comunicare
între procese aflate pe acelaşi sistem. Fiind implementate ca fişiere,
utilizarea lor se face în mod similar cu utilizarea unui fişier obişnuit. Astfel,
ele trebuie create, deschise pentru a putea fi utilizate, iar pentru trimiterea
prin pipe şi recepţionarea din pipe a unei informaţii (mesaj), acea informaţie
trebuie scrisă (cu write) în, respectiv citită (cu read) din fişierul pipe.

Operaţiile pe pipe se fac la nivel de octeţi. Diferenţele utilizării fişierelor


pipe faţă de un fişier obişnuit apar sub forma unor restricţii impuse modului
în care se efectuează operaţiile de scriere şi citire din pipe. Aceste operaţii
se fac respectându-se principiul FIFO (First-In First-Out). Prin urmare, nu
se pot face modificări ale poziţiei curente în fişierul pipe folosind apelul
sistem lseek. Pe de altă parte, încercarea de citire dintr-un fişier pipe gol sau
cea de scriere într-un fişier pipe plin blochează procesul sau thread-ul care a
apelat funcţia read, respectiv write. Deblocarea proceselor se face în
momentul în care un alt proces scrie în pipe numărul de octeţi aşteptaţi de
read, în primul caz, respectiv citeşte din pipe un număr de octeţi mai mare
sau egal cu cel pe care doreşte să-i scrie funcţia write, în cel de-al doilea
caz. Trebuie remarcat astfel că nu se ajunge, în mod normal, la un sfârşit al
fişierului pipe, lucru care în cazul unui fişier obişnuit ar fi fost detectat prin
returnarea de către funcţia read a valorii zero, ci, în situaţia în care toţi
octeţii scrişi în pipe au fost citiţi (adică fişierul pipe e gol) şi se încearcă o
nouă citire, procesul ce face acea operaţie este blocat până când un alt
proces scrie în pipe. Acest mod de funcţionare a citirilor în şi scrierilor din
pipe asigură sincronizarea proceselor care comunică prin pipe.

În folosirea fişierelor pipe există două situaţii particulare, date de numărul


de procese care au deschis pipe-ul pentru citire (considerate posibili

105
Fişiere PIPE în Linux

cititori), respectiv de numărul proceselor care au deschis pipe-ul pentru


scriere (considerate posibili scriitori în pipe). Trebuie remarcat faptul că un
proces poate fi considerat în acelaşi timp atât cititor, cât şi scriitor, în funcţie
de modul în care a deschis fişierul pipe. În cazul în care un proces încearcă
să citească dintr-un fişier pipe gol pentru care nu mai există procese scriitor,
funcţia read nu se va bloca, ci va returna valoarea zero, adică similar cu
situaţia de detecţie a sfârşitului de fişier pentru fişierele normale. Această
situaţie corespunde detectării „sfârşitului” de fişier pipe, lucru care va fi
interpretat de către procesul care face citirea din pipe ca terminare a
comunicării prin pipe. În cazul în care un proces încearcă scrierea într-un
fişier pipe pentru care nu mai există procese cititor, sistemul de operare
generează o excepţie sub forma unui semnal (SIGPIPE), care, în mod
implicit, termină procesul care execută scrierea. Aceasta este o modalitate
de a evita blocarea definitivă a unui proces care scrie într-un pipe plin din
care nu mai citeşte nimeni. Dacă semnalul SIGPIPE transmis acelui proces
de sistemul de operare este captat de proces, el nu va mai fi oprit, ci se va
executa o rutină de tratare a mesajului, indicată de proces. Pentru a evita
cele două cazuri de excepţie descrise mai sus, sistemul de operare va bloca
un proces care încearcă să deschidă (cu open) un fişier pipe doar pentru
citire, dacă acel fişier nu este deja deschis de către un alt proces pentru
scriere, deblocarea procesului făcându-se în momentul deschiderii pipe-ului
pentru scriere. De asemenea, sistemul de operare va bloca un proces care
încearcă să deschidă doar pentru scriere un pipe care la acel moment nu mai
este deschis de către nici un alt proces pentru citire, deblocarea făcându-se
când pipe-ul este deschis de către un alt proces pentru citire. Pentru evitarea
acestei blocări funcţia open poate fi apelată cu opţiunea O_NDELAY sau
O_NONBLOCK pe poziţia celui de al doilea parametru, caz în care funcţia
open nu se blochează, dar returnează eroare. Evident, dacă un proces
deschide pipe-ul simultan atât pentru scriere, cât şi pentru citire el nu va fi
blocat, fiind considerat atât un posibil cititor, cât şi un posibil scriitor, dar
ambele operaţii se fac pe acelaşi descriptor, cel returnat de funcţia open.
Trebuie avut însă grijă pentru că într-o astfel de situaţie, dacă acel proces
încearcă citirea din pipe-ul gol, el va rămâne blocat, adică funcţia read nu
returnează imediat rezultatul 0, până când un alt proces nu va deschide pipe-
ul pentru scriere şi va scrie în pipe numărul de octeţi aşteptaţi de primul
proces. Dacă un astfel de proces nu apare, primul proces va rămâne definitiv
blocat, pentru că sistemul de operare îl consideră şi posibil scriitor, deşi el,
evident, nu poate să mai scrie ceva, fiind blocat în read.

106
Sisteme de operare. Chestiuni teoretice şi practice

8.2. Fişiere pipe cu nume


Fişierele pipe cu nume apar ca fişiere normale, având un nume, fiind
localizate într-un anumit director şi având anumite drepturi (dintre care,
evident, au sens doar cele de citire şi scriere). Orice proces care cunoaşte
numele fişierului pipe şi are acces la directorul în care e creat pipe-ul poate
deschide acel pipe pentru citire şi/sau scriere, în funcţie de drepturile pe care
le are asupra fişierului pipe. Fişierele pipe cu nume mai sunt cunoscute şi
sub numele de fişiere FIFO.

Crearea unui fişier pipe cu nume se poate face cu ajutorul funcţiilor mknod
sau mkfifo. Sintaxa celor două funcţii este:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int mknod(const char *nume_cale,
mode_t perm_acces_si_tip, dev_t disp);
int mkfifo(const char *nume_cale, mode_t perm_acces);

Ambele funcţii returnează 0 în caz de succes şi -1 în caz de eroare. Primul


parametru, nume_cale, reprezintă numele fişierului pipe, nume care poate
include şi calea spre directorul unde se doreşte crearea pipe-ului, în caz contrar
el fiind creat în directorul curent. Cel de-al doilea parametru, perm_acces,
reprezintă drepturile de acces la pipe, drepturi ce pot fi specificate sub forma
codificării în baza opt a biţilor corespunzători drepturilor de acces pentru
proprietar, grup şi alţi utilizatori. De exemplu o codificare de forma 0640
creează pipe-ul cu drepturi de scriere şi citire pentru proprietar, doar cu drept
de citire pentru utilizatorii din acelaşi grup cu proprietarul şi fără nici un drept
pentru restul utilizatorilor. În cazul funcţiei mknod, pe poziţia celui de-al doilea
parametru trebuie precizat şi tipul fişierului care se creează, adică FIFO, sub
forma unui SAU pe biţi între permisiunile de acces şi constanta S_IFIFO,
adică ceva de forma 0640 | S_IFIFO. Cel de-al treilea parametru al funcţiei
mknod are valoarea zero. Exemplele de mai jos ilustrează modul de creare a
unor fişiere pipe cu nume cu ajutorul celor două funcţii.
if (mkfifo("FIFO", 0600) < 0)
{ perror("Eroare creare FIFO"); exit(1); }

if (mknod("/tmp/FIFO", 0644 | S_IFIFO, 0) < 0)


{ perror("Eroare creare FIFO"); exit(1); }

107
Fişiere PIPE în Linux

Ştergerea unui fişier pipe se face la fel ca a unui fişier obişnuit, cu ajutorul
funcţiei unlink.
O dată creat, fişierul pipe cu nume poate fi deschis cu ajutorul funcţiei open
şi accesat în scriere sau citire cu funcţiile write, respectiv read. Reamintim
că este necesar ca fişierul pipe să fie deschis atât pentru scriere, cât şi pentru
citire pentru a putea fi folosit efectiv. În acest sens, un proces care încearcă
deschiderea pipe-ului doar pentru un anumit tip de operaţii (de exemplu,
doar pentru citire), va rămâne blocat în funcţia open, până când pipe-ul va fi
deschis şi pentru operaţii complementare (pentru scriere, în exemplul
nostru). Dacă se doreşte ca procesul să nu fie blocat, atunci funcţia open
trebuie apelată cu opţiunea O_NONBLOCK, dar trebuie avut grijă că în acest
caz principiile de sincronizare pe pipe nu mai funcţionează. Exemplul de
mai jos ilustrează modul de comunicare a două procese prin intermediul
unui fişier FIFO.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
// program1.c - codul C al primului proces
int main(int argc, char **argv)
{
int fd;
if (argc != 2) {
printf ("Utilizare: %s mesaj", argv[0]);
exit(0);
}
mkfifo("FIFO", 0600);
fd = open("FIFO", O_WRONLY);
write(fd, argv[1], strlen(argv[1]));
}
// program2.c - codul C al celui de-al doilea proces
int main(int argc, char **argv)
{
char buf[10];
int fd;
fd = open("FIFO", O_RDONLY);
if (fd < 0)
{ perror("Eroare deschidere pipe"); exit(1); }
n = read(fd, buf, 6); buf[n] = 0;
printf("S-a citit de pe pipe: %s\n", buf);
}

108
Sisteme de operare. Chestiuni teoretice şi practice

8.3. Fişiere pipe fără nume sau anonime


Fişierele pipe anonime se creează cu ajutorul apelului sistem pipe. Sintaxa
acestui apel este:

#include <unistd.h>
int pipe(int fd[2]);

Funcţia returnează 0 în caz de succes sau -1 în caz de eroare. Argumentul


fd al funcţiei reprezintă adresa unei zone de memorie (numele unui şir)
rezervată pentru două numere întregi. Efectul execuţiei apelului sistem pipe
este crearea fişierului pipe anonim, adică un fişier care nu poate fi accesibil
pe baza unui nume, fişierul fiind practic invizibil. Deoarece accesul la un
fişier presupune deschiderea fişierului, sistemul de operare, deschide în mod
automat fişierul creat în urma apelului funcţiei pipe, atât pentru citire, cât şi
pentru scriere şi memorează în cadrul şirului fd cei doi descriptori. Astfel,
în primul element al şirului – fd[0] – se va stoca descriptorul fişierului
pipe deschis pentru citire, iar în cel de-al doilea element al şirului – fd[1]
– se va stoca descriptorul fişierului pipe deschis pentru scriere.

Se pune acum întrebarea ce alt proces, în afara procesului care a creat fişierul
pipe, poate avea acces la acest fişier invizibil. Pipe-ul este accesibil procesului
care l-a creat doar prin intermediul celor doi descriptori stocaţi în cadrul
şirului fd. Prin urmare, singura modalitate ca un alt proces să aibă acces la
pipe ar fi prin intermediul celor două „deschideri” ale pipe-ului. Acestea sunt
însă nişte structuri interne ale sistemului de operare şi sunt referite în mod
indirect prin intermediul descriptorilor de fişier returnaţi de obicei de funcţia
open. Însă, fişierul pipe neavând nume, apelul funcţiei open nu este posibil.
Rezolvarea acestei probleme este apelul funcţiei fork de către procesul care a
creat pipe-ul, lucru care are ca efect crearea unui nou proces, care moşteneşte
de la procesul părinte, în copie, toate structurile de date. Printre acestea se
găseşte şi tabela descriptorilor fişierelor deschise, care va fi astfel identică în
procesul fiu ca şi în părinte şi, prin urmare, procesul fiu va avea acces la cele
două deschideri ale fişierului pipe. Figura 1 de mai jos ilustrează mecanismul
descris mai sus. Aici se pot observa două tipuri de tabele ce sunt folosite de
către sistemul de operare pentru gestionarea fişierelor deschise. Este vorba, pe
de o parte, de tabela fişierelor deschise, în care o intrare descrie un fişier
deschis într-un anumit mod, iar pe de altă parte, de tabela descriptorilor de
fişier, specifică fiecărui proces în parte şi care conţine referinţe spre intrările
din tabela fişierelor deschise.

109
Fişiere PIPE în Linux

Figura 1. Modul de accesare a pipe-lui anonim de către procese părinte-fiu

În concluzie, comunicarea prin fişiere pipe fără nume se poate face doar
între procese aflate în relaţia părinte-fiu sau între procese descendente din
acelaşi proces, cel care a creat pipe-ul. Avantajul utilizării unui astfel de
pipe, care precum se vede, are o utilitate limitată, este faptul că fişierul pipe
nu este vizibil şi accesibil altor procese, decât celui care a creat pipe-ul şi
descendenţilor săi, constituind astfel un fel de canal privat de comunicare
între aceste procese.

Codul de mai jos ilustrează modul de comunicare dintre două procese


printr-un fişier pipe fără nume.
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int fd[2], pid, n, buf[100];
int main(int argc, char *argv)
{
pipe(fd);
pid = fork();
if (pid == 0) { // proces fiu
n = read(fd[0], buf, 7);
buf[n] = 0;
printf("Fiul a receptionat: %s\n", buf);

110
Sisteme de operare. Chestiuni teoretice şi practice

n = write(fd[1], "FIU", 3);


printf("Fiul a transmis: FIU\n");
}
else { // poces parinte
write(fd[1], "PARINTE", 7);
printf("Parintele a transmis: PARINTE\n");
n = read(fd[0], buf, 3);
buf[n] = 0;
printf("Parintele a receptionat: %s\n", buf);
}
}

8.4. Comunicare unidirecţională şi bidirecţională


În exemplul anterior se poate observa o comunicare între cele două procese
– părinte şi fiu – care are loc în ambele direcţii, şi anume: de la părinte spre
fiu, şi de la fiu spre părinte. Numim o astfel de comunicare bidirecţională,
iar fişierul pipe bidirecţional. Trebuie însă precizat că acest tip de
comunicare şi denumirea pe care i-am dat-o este o convenţie pe care o
stabileşte utilizatorul şi este specifică unui anumit tip de aplicaţii (de genul
client-server), fără ca sistemul de operare să restricţioneze în vreun fel
comunicarea prin pipe-uri. Astfel, pe pipe ar putea comunica mai mult de
două procese, unele citind, altele preluând date (comunicare
multidirecţională) sau, într-un caz nerealist, dar teoretic posibil, un singur
proces ar putea să scrie şi să citească din pipe. Un alt caz particular de
comunicare pe care l-am putea evidenţia este cel în care comunicare prin
pipe se realizează între două procese, unul doar scriind date în pipe, iar
celălalt doar citind date din pipe. O astfel de comunicare o numim
unidirecţională.
În cazul comunicării bidirecţionale între două procese, este posibilă apariţia
următoarei situaţii nedorite şi, practic vorbind, eronate. În exemplul de mai
sus, se observă că procesul părinte efectuează o scriere (cu write) în pipe şi
imediat o citire (cu read) din pipe. Ne-am putea imagina că procesul
respectiv joacă rol de client al unui server – procesul fiu în exemplul nostru
– şi trimite o cerere la care aşteaptă un răspuns. Situaţia eronată care poate
să apară într-un astfel de scenariu este cea în care procesul părinte citeşte
din pipe informaţii pe care le-a scris chiar el puţin mai înainte. Evident,
sistemul de operare îi permite să facă acest lucru, neştiind care este intenţia
reală a procesului. Eroarea apare datorită faptului că nu există sincronizarea
dorită între execuţiile proceselor părinte (client) şi fiu (server). O astfel de
sincronizare nu este asigurată de către sistemul de operare şi trebuie
realizată de către utilizator. Există mecanisme speciale de sincronizare a

111
Fişiere PIPE în Linux

execuţiei proceselor. Aici vom indica o soluţie care necesită doar folosirea
fişierelor pipe. Pentru aceasta trebuie să ne reamintim că există o
sincronizare inclusă şi în cadrul funcţionalităţii fişierelor pipe, şi anume:
dacă un proces încearcă să citească dintr-un pipe gol, el este blocat (pus în
aşteptare) până când un alt proces scrie în pipe numărul de octeţi necesar.
Astfel, în contextul descris, un proces aşteaptă ca un alt proces să facă un
anumit lucru, aceasta şi însemnând, de altfel sincronizare. Folosindu-ne de
acest tip de sincronizare specifică comunicării prin fişiere pipe, vom evita
situaţia eronată descrisă mai sus utilizând pentru comunicarea între cele
două procese două fişiere pipe folosite în mod unidirecţional, adică: unul
pentru comunicarea dinspre procesul părinte (client) spre fiu (server), iar
celălalt pentru direcţia opusă. Codul corespunzător acestei soluţii de
comunicare bidirecţională este:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int fdSpreStanga[2], fdSpreDreapta[2];
int pid, n;
int buf[100];

int main(int argc, char **argv)


{
pipe(fdSpreDreapta); // pipe de la parinte spre fiu
pipe(fdSpreStanga); // pipe de la fiu spre parinte
pid = fork();
if (pid == 0) { // proces fiu
close(fdSpreStanga[0]);
close(fdSpreDreapta[1]);
n = read(fdSpreDreapta[0], buf, 7);
buf[n] = 0;
printf("Fiul a receptionat: %s\n", buf);
n = write(fdSpreStanga[1], "FIU", 3);
printf("Fiul a transmis: FIU\n");
}
else { // poces parinte
close(fdSpreStanga[1]);
close(fdSpreDreapta[0]);
write(fdSpreDreapta[1], "PARINTE", 7);
printf("Parintele a transmis: PARINTE\n");
n = read(fdSpreStanga[0], buf, 3);
buf[n] = 0;
printf("Parintele a receptionat: %s\n", buf);
}
}

112
Sisteme de operare. Chestiuni teoretice şi practice

Se observă acum că şi în cazul în care procesul părinte încearcă, imediat


după ce a scris în primul pipe, citirea răspunsului de la fiu, înainte ca
procesul fiu să fi reuşit citirea mesajului transmis de părinte şi scrierea
mesajului de răspuns, el va fi pus în aşteptare, pentru că cel de-al doilea
pipe, cel de pe care citeşte părintele, este gol. Procesul părinte va rămâne
blocat până când procesul fiu va scrie în cel de-al doilea pipe.

8.5. Redirectarea STDIN şi STDOUT spre fişiere pipe


În acest capitol dorim să discutăm o situaţie particulară de comunicare prin
fişiere pipe. Este vorba de „impunerea” din exterior, dar în mod transparent,
a comunicării între procese care nu comunică cu alte procese şi cu atât mai
puţin prin fişiere pipe, dar care preiau date de la intrarea standard şi afişează
rezultatele la ieşirea standard. Un exemplu de procese de acest fel sunt
majoritatea comenzilor recunoscute de către interpretorul de comenzi, a
căror cod executabil există, dar nu poate fi modificat. De exemplu, comanda
ls afişează pe ecran – adică la ieşirea standard – informaţii despre conţinutul
unui director. Comenzile sort sau cat, lansate fără parametri, aşteaptă datele
de intrare de la tastatură, adică de la intrarea standard. Interpretorul de
comenzi permite, pentru comenzi de genul celor amintite, specificarea unor
construcţii (comenzi compuse) de forma următoare:
ls | sort
cat fis1 fis2 | sort
Caracterul '|' din exemplele anterioare se numeşte „pipe” şi indică
interpretorului de comenzi că trebuie să stabilească condiţiile unei
comunicări între cele două comenzi, în sensul că ieşirea (rezultatul) primei
comenzi să devină intrare pentru cea de-a doua. Interpretorul de comenzi
permite specificarea mai multor comenzi separate de '|' în aceeaşi linie de
comandă, comunicarea prin fişiere pipe având loc simultan între fiecare
două procese ce corespund la două comenzi consecutive.

Vom încerca mai jos să ilustrăm modul în care interpretorul de comenzi


reuşeşte să stabilească o comunicare între două procese care nu ştiu unul de
celălalt şi care nu intenţionează să comunice unul cu celălalt. Pentru a putea
face acest lucru este nevoie de folosirea unui apel sistem special numit dup,
respectiv o versiune îmbunătăţită a sa, dup2, a căror sintaxă este următoarea:
#include <unistd.h>
int dup(int fdExistent);
int dup2(int fdExistent, int fdNou);

113
Fişiere PIPE în Linux

Efectul acestor două apeluri sistem este acela de creare a unui descriptor de
fişier duplicat pentru un descriptor deja existent, acest lucru însemnând că
acelaşi fişier deschis (aceeaşi „deschidere” a unui fişier) poate fi accesat
prin doi descriptori diferiţi, fdExistent şi fdNou. În tabelele din Figura 1,
acest lucru va apare ca două intrări diferite din tabela descriptorilor de
fişiere ai procesului care apelează funcţia dup sau dup2 – intrările cu
indecşii fdExistent şi fdNou – referind aceeaşi intrare din tabela
fişierelor deschise. Dacă se efectuează o operaţie de read, write sau lseek
folosind unul dintre descriptori, modificarea poziţiei curente din fişier este
vizibilă şi prin folosirea celuilalt descriptor.

Ambele apeluri sistem returnează valoarea noului descriptor alocat, în caz


de succes şi -1, în caz de eroare. Diferenţa dintre funcţiile dup şi dup2 este
aceea că la cea din urmă se indică şi valoare noului descriptor care se
doreşte alocat. Dacă acesta este deja alocat unei alte deschideri a unui fişier,
atunci se închide această deschidere şi se alocă descriptorul (intrarea din
tabela descriptorilor) pentru a realiza duplicarea. Reamintim faptul că tabela
descriptorilor de fişiere deschise este specifică fiecărui proces în parte şi,
prin urmare, apelurile dup şi dup2 afectează doar procesul care le apelează,
fără a avea vreo influenţă asupra altor procese. Mai trebuie menţionat faptul
că apelul sistem dup va aloca întotdeauna cel mai mic descriptor de fişier
disponibil, adică prima intrare disponibilă din tabela descriptorilor.

Folosind fişierele pipe fără nume şi apelul sistem dup2, descriem în codul
de mai jos modul de lucru al interpretorului de comenzi la introducerea în
linia de comandă a două comenzi separate prin caracterul '|'. Se presupune
introducerea unor comenzi fără parametri.

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fdPipe[2];
int pid1, pid2;
char cmd1[30], cmd2[30], cmd[30];
char *pos;
while (1) {
printf(">"); // afisare prompter
fgets(cmd, 30, stdin); // citire linie de cmd.
if (!strcmp(cmd, "exit\n"))
exit(0);

114
Sisteme de operare. Chestiuni teoretice şi practice

pos = index(cmd, '|');


if ((pos == NULL) || (pos == cmd)) {
printf("Introduceti 2 cmd. separate de |.\n");
continue;
}

// obtine comanda 1
if (*(pos-1) == ' ') {
strncpy(cmd1, cmd, pos - cmd - 1);
cmd1[pos-cmd-1] = 0;
} else {
strncpy(cmd1, cmd, pos - cmd);
cmd1[pos-cmd] = 0;
}

// obtine comanda 2
if (*(pos+1) == ' ')
strcpy(cmd2, pos+2);
else
strcpy(cmd2, pos+1);
if (cmd2[strlen(cmd2) - 1] == '\n')
cmd2[strlen(cmd2) - 1] = 0;

pipe(fdPipe); // creare pipe

pid1 = fork(); // creare fiu 1


if (pid1 < 0)
{ perror("Eroare creare fiu 1");exit(1); }

if (pid1 == 0) { // fiu 1
close(fdPipe[0]);
dup2(fdPipe[1], 1); // redirectare STDOUT
close(fdPipe[1]); // in pipe
execlp(cmd1, cmd1, NULL);
perror("Eroare executie comanda 1");
exit(0);
}

pid2 = fork(); // creare fiu 2


if (pid2 < 0)
{ perror("Eroare creare fiu 2"); exit(1); }

if (pid2 == 0) { // fiu 2
close(fdPipe[1]);
dup2(fdPipe[0], 0); // redirectare STDIN
close(fdPipe[0]); // din pipe
execlp(cmd2, cmd2, NULL);
perror("Eroare executie comanda 2");
exit(0);
}

115
Fişiere PIPE în Linux

close(fdPipe[0]);
close(fdPipe[1]);
waitpid(pid1, NULL, 0); // asteapta primul fiu
waitpid(pid2, NULL, 0); // asteapta al doilea fiu
} // de la while
} // de la main

Observăm în primul rând, în codul de mai sus, că interpretorul de comenzi


creează procese noi pentru fiecare comandă pe care o citeşte din linia de
comandă.

Observăm apoi, că pentru a realiza comunicarea prin pipe a proceselor care


execută cele două comenzi, se face redirectarea ieşirii standard spre pipe a
procesului care execută prima comandă şi respectiv, redirectarea intrării
standard dinspre acelaşi pipe a procesului care execută a doua comandă.
Pentru aceasta trebuie ştiut faptul că la crearea unui proces, în mod automat,
primele trei intrări din tabela descriptorilor de fişiere ai acelui proces, cu
alte cuvinte, primii trei descriptori – 0, 1 şi 2 – sunt automat asociate
fişierului standard de intrare (STDIN) – descriptorul 0 –, fişierului standard
de ieşire (STDOUT) – descriptorul 1 – şi fişierului standard de eroare
(STDERR) – descriptorul 2. Prin urmare, pentru redirectarea intrării
standard spre pipe, de exemplu, trebuie făcut în aşa fel ca descriptorul 0 să
indice fişierul pipe deschis pentru citire, adică să indice spre aceeaşi intrare
din tabela fişierelor deschise spre care indică şi descriptorul corespunzător
fişierului pipe deschis pentru citire. Aceasta înseamnă că descriptorul 0
trebuie să fie un duplicat al descriptorului fdPipe[0], lucru care se face
prin apelul funcţiei dup2.

Un alt lucru important care trebuie observat în codul de mai sus este
închiderea descriptorilor de citire şi scriere ai pipe-ului de către toate cele trei
procese (interpretorul şi cei doi fii). Pipe-ul va rămâne astfel accesibil doar
prin descriptorul 0 – pentru citire – de către al doilea proces şi prin
descriptorul 1 – pentru scriere – de către primul proces. Este esenţial în
această privinţă să se închidă toţi descriptorii de scriere pe pipe neutilizaţi. În
caz contrar, la sfârşitul comunicării dintre cele două procese, cel de-al doilea
proces – care citeşte din pipe, într-o buclă, până la detecţia sfârşitului de fişier,
când funcţia read întoarce 0 – va rămâne blocat în apelul funcţiei read
(apelată pentru descriptorul 0, redirectat la pipe). Acest lucru se întâmplă
pentru că sistemul de operare sesizează încă posibili scriitori în pipe, şi anume
procesul părinte şi/sau al doilea proces fiu, deşi singurul proces care practic ar
fi putut scrie era primul proces fiu, dar acesta s-a terminat.

116
Sisteme de operare. Chestiuni teoretice şi practice

8.6. Probleme
1. Să se determine dimensiunea maximă a unui fişier pipe, pentru ambele
tipuri de fişiere pipe.
2. Să se modifice codul interpretorului de comenzi dat ca exemplu în
cadrul lucrării pentru a accepta comunicarea prin fişiere pipe a unor
comenzi cărora li se specifică şi parametrii, sub forma:
cmd1 arg1 arg2 ... | cmd2 arg1 arg2

3. Se presupune că în două fişiere nume.txt şi prenume.txt sunt scrise pe câte


o linie numele şi respectiv, prenumele unor persoane, existând o
corespondenţă la nivel de număr de linie între cele două fişiere. Să se scrie
două programe C care citesc date din cele două fişiere, primul din fişierul
nume.txt, iar al doilea din fişierul prenume.txt şi scriu ceea ce au citit într-
un fişier numit persoane.txt. Cele două procese trebuie să-şi sincronizeze
execuţia folosind fişiere pipe astfel încât în fişierul perosoane.txt să apară
pe fiecare linie numele şi prenumele aceleiaşi persoane.
4. Să se scrie două programe C, unul numit server.c, iar al doilea anumit
client.c. Cele două procese nu sunt în relaţia părinte-fiu. Procesul client
citeşte de la tastatură două numere întregi şi unul din caracterele '+' sau
'-', pe care le trimite printr-un pipe cu nume procesului server. Acesta
efectuează operaţia corespunzătoare şi trimite înapoi prin pipe
procesului client rezultatul. Clientul va afişa rezultatul pe ecran, apoi
reia din nou întregul algoritm. Să se precizeze şi să se pună în evidenţă
problemele care pot apare în cadrul comunicaţiei dintre client şi server şi
să se modifice apoi codul celor două programe astfel încât problemele
respective să nu mai apară.
5. Să se scrie un program C numit server.c, care să accepte pe un fişier
pipe cu nume – numit PIPE_SERVER – cereri de conexiune din partea
unor procese clienţi, rezultate fiecare dintr-un program C numit client.c.
Mesajele de conectare trimise de clienţi pe pipe trebuie să conţină cel
puţin un câmp de identificare a cererii de conexiune (de exemplu tipul 0)
şi un câmp în care fiecare client să-şi transmită PID-ul propriu. Pentru
fiecare client conectat serverul va lansa câte un thread, care va răspunde
apoi clientului la cereri de genul: creare fişier, citire din fişier, scriere în
fişier, ştergere a unui fişier etc. Se presupune că fişierele sunt create şi
căutate în directorul curent al procesului server. Să se realizeze, folosind
fişiere pipe cu nume comunicarea între procesele client şi server, pe de o
parte şi între fiecare client şi thread-ul asociat, pe de altă parte.

117
9. Fişiere PIPE în Windows

Scopul lucrării
Această lucrare explică mecanismul de comunicare între procese în
Windows folosind fişiere pipe. Sunt prezentate principalele funcţii ale API-
ului Win32 legate de gestionarea fişierelor pipe cu nume şi anonime.

9.1. Prezentare generală a fişierelor pipe în Windows


Pipe-ul este un flux de octeţi care poate fi accesat prin interfaţa obişnuită de
lucru cu fişiere. Pipe-urile Windows oferă operaţii de I/E printr-un singur
handle. În Windows, se regăsesc atât pipe-urile cu nume, cât şi cele fără
nume sau anonime. Variantele fără nume sunt de fapt pipe-uri simple cu
nume la care nu li se face public numele. Sistemul de operare asigură
operaţii de I/E sincrone pe pipe-uri. Pentru accesul asincron la pipe-uri este
necesară folosirea unei interfeţe diferite.

Pipe-urile cu nume din Windows sunt de două tipuri: de tip octet (byte) şi de
tip mesaj. Tipul pipe-ului determină modul în care datele sunt scrise în pipe.
Pipe-urile de tip octet sunt similare pipe-urilor din Unix, datele fiind scrise
ca un şir de octeţi. În cazul pipe-urilor de tip mesaj, sistemul tratează octeţii
scrişi în pipe ca unităţi de mesaj. Tipul pipe-ului se specifică la creare
(funcţia CreateNamedPipe) prin constantele PIPE_TYPE_BYTE, respectiv
PIPE_TYPE_MESSAGE.

Procesul care creează un pipe se numeşte server de pipe. Procesul care se


conectează la un pipe deja creat se numeşte client de pipe. Unul dintre
procese scrie datele în pipe, apoi celălalt proces le citeşte din pipe. Pipe-
urile pot fi unidirecţionale (unul dintre procese numai scrie, celălalt numai
citeşte) sau bidirecţionale (ambele procese pot citi din pipe şi scrie în pipe).

9.2. Funcţii Win32 pentru utilizarea pipe-urilor anonime


Pipe-urile anonime sunt pipe-uri care se folosesc de obicei în mod
unidirecţional şi prin care se transferă, de regulă, date între un proces
părinte şi un proces fiu al acestuia. Pipe-urile anonime sunt întotdeauna
locale, ele nu pot fi utilizate pentru transferul datelor prin reţea.

118
Sisteme de operare. Chestiuni teoretice şi practice

Crearea pipe-urilor anonime


Funcţia CreatePipe creează un pipe anonim şi returnează două handle-uri:
un handle de citire din pipe şi unul de scriere în pipe. Pentru a realiza
comunicarea între două procese folosind un pipe anonim, serverul de pipe
trebuie să transmită unul dintre handle-uri unui alt proces. Aceasta se poate
face prin moştenire sau prin folosirea unui alt mod de comunicare
interproces.

Funcţia are următorul prototip:


BOOL CreatePipe(
PHANDLE phRead, PHANDLE phWrite,
LPSECURITY_ATTRIBUTES lpsa,
DWORD nSize);

Semnificaţia parametrilor este următoarea:


phRead
phWrite Indică adresele de memorie la care vor fi scrişi handle-urile
de citire din, respectiv de scriere în pipe.
lpsa Pointer la o structură SECURITY_ATTRIBUTES, care
specifică dacă handle-ul poate fi moştenit de procesele fiu.
nSize Dimensiunea, în octeţi, a pipe-ului.

Citirea din şi scrierea în pipe-urile anonime


Funcţiile standard de lucru cu fişiere, ReadFile şi WriteFile, sunt folosite
pentru citirea şi respectiv, scrierea pipe-ului. Pentru a citi dintr-un pipe,
procesul trebuie să apeleze funcţia ReadFile folosind handle-ul de citire al
pipe-ului. Se revine din funcţia ReadFile atunci când un alt proces scrie în
pipe-ul respectiv, dacă se închid toate handle-urile de scriere la pipe sau
dacă apare o eroare. Scrierea în pipe se realizează cu funcţia WriteFile, prin
handle-ul de scriere. Funcţia nu îşi termină execuţia până când nu se scriu
toţi octeţii în pipe sau dacă apare o eroare. Dacă pipe-ul este plin şi mai sunt
octeţi de scrişi, se aşteaptă până când un alt proces citeşte din pipe eliberând
suficient spaţiu liber din el.

Operaţiile de citire şi scriere asincrone nu sunt permise pentru pipe-urile


anonime. Din acest motiv funcţiile ReadFileEx şi WriteFileEx nu se pot
folosi cu pipe-uri fără nume.

119
Fişiere PIPE în Windows

Un pipe anonim există până când toate handle-urile (de citire şi de scriere)
vor fi închise. Un proces poate să-şi închidă un handle cu funcţia
CloseHandle.

Moştenirea handle-urilor de acces de către procesele fiu


Handle-ul returnat serverului de către funcţia CreatePipe poate fi moştenit
în trei moduri:
• Dacă câmpul bInheritHandle din structura
SECURITY_ATTRIBUTES este setat la TRUE, handle-urile create de
CreatePipe pot fi moştenite.
• Serverul de pipe poate folosi funcţia DuplicateHandle pentru a
schimba modalitatea de moştenire. Serverul poate crea un duplicat
care nu poate fi moştenit, chiar dacă handle-ul sursă putea fi
moştenit. Duplicarea poate schimba modul de moştenire şi în sens
invers. Funcţia are următorul prototip:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, // procesul sursă
HANDLE hSourceHandle, // handle-ul sursă
HANDLE hTargetProcessHandle, // procesul dest.
LPHANDLE lpTargetHandle, // handle-ul dest.
DWORD dwDesiredAccess, // permisiuni
BOOL bInheritHandle, // indic. moştenire
DWORD dwOptions); // opţiuni

• Prin funcţia CreateProcess, serverul poate specifica dacă procesul


fiu creat va primi sau nu handle-urile care pot fi moştenite.

9.3. Pipe-uri cu nume


Pipe-urile cu nume sunt pipe-uri unidirecţionale sau bidirecţionale, care pot
fi utilizate pentru comunicarea între serverul de pipe şi unul sau mai mulţi
clienţi de pipe. Toate instanţele unui pipe cu nume partajează acelaşi nume
de pipe, dar fiecare are propriul său buffer şi handle, deci apar ca şi canale
de comunicaţie distincte.

Orice proces poate accesa pipe-urile cu nume (în funcţie de drepturile de


acces), putând acţiona atât ca server, cât şi ca şi client. Astfel, comunicarea
între orice două procese devine foarte simplă.

Pipe-urile cu nume oferă posibilitatea de comunicare între procese aflate pe


aceeaşi maşină sau între procese aflate pe maşini diferite, legate în reţea.

120
Sisteme de operare. Chestiuni teoretice şi practice

Crearea pipe-urilor cu nume


Un pipe cu nume este creat de serverul de pipe prin apelul funcţiei
CreateNamedPipe. Sintaxa acestei funcţii este următoarea:
HANDLE CreateNamedPipe(
LPSTR pipeName,
DWORD dwOpenMode, DWORD dwPipeMode,
DWORD nMaxInstances, DWORD dwBufferOut,
DWORD dwBufferIn, DWORD timeOut,
LPSECURITY_ATTRIBUTES lpsa);

Semnificaţia parametrilor este următoarea:


pipeName
Este un şir de caractere, care indică numele pipe-ului. În Windows,
numele pipe-ului trebuie să respecte următoarea sintaxă:
\\ServerName\pipe\PipeName

ServerName este numele maşinii din reţea (specificat prin nume sau
prin adresă IP) unde rulează serverul de pipe sau caracterul '.', în
cazul în care serverul este pe aceeaşi maşină cu clientul. Cuvântul
pipe din nume este impus, adică trebuie să apară întotdeauna.
dwOpenMode
Specifică modul de acces la pipe şi poate avea valorile:
• PIPE_ACCESS_INBOUND : indică acces în citire;
• PIPE_ACCESS_OUTBOUND : indică acces în scriere;
• PIPE_ACCESS_DUPLEX : comunicare bidirecţională.
Prin acelaşi parametru se mai poate indica dacă operaţiile cu
pipe-ul vor fi sincrone sau asincrone (FILE_FLAG_OVERLAPPED) şi
dacă scrierile se realizează prin buffer sau fără
(FILE_FLAG_WRITE_THROUGH).
dwPipeMode
Indică tipul pipe-ului şi poate avea valorile:
• PIPE_TYPE_BYTE: pipe de tip octet pentru scriere;
• PIPE_READMODE_BYTE: pipe de tip octet pentru citire;
• PIPE_TYPE_MESSAGE: pipe de tip mesaj pentru scriere;
• PIPE_READMODE_MESSAGE: pipe de tip mesaj pentru citire.
Prin valorile PIPE_WAIT şi PIPE_NOWAIT se poate specifica dacă
operaţiile cu pipe-ul să fie sincrone (cu blocare) sau nu (asincrone).
nMaxInstaces
Numărul maxim de procese client care se pot conecta la pipe-ul creat.

121
Fişiere PIPE în Windows

dwBufferOut
dwBufferIn
Dimensiunea în octeţi a buffer-elor de ieşire, respectiv de intrare. Dacă
este necesar, sistemul poate modifica mărimea acestor buffer-e.
timeOut
Indică timpul maxim de aşteptare pentru terminarea unei operaţii de
I/E pe pipe.
lpsa
Indică adresa unei structuri de tipul SECURITY_ATTRIBUTES prin
care se specifică atribute de securitate.

Citirea din şi scrierea în pipe-urile cu nume


Pentru a putea face operaţii pe pipe, procesele trebuie să se conecteze la el.
Serverul de pipe, după crearea pipe-ului, trebuie să apeleze funcţia
ConnectNamedPipe. Sintaxa acestei funcţii este următoarea:

BOOL ConnectNamedPipe(
HANDLE hNamedPipe,
LPOVERLAPPED lpo);

Parametrul hNamedPipe este handle-ul returnat de funcţia CreateNamedPipe,


iar parametrul lpo este folosit doar dacă pipe-ul a fost deschis cu opţiunea
FILE_FLAG_OVERLAPPED. Altfel, valoarea lui trebuie să fie NULL. Funcţia
returnează o valoare pozitivă în caz de succes, iar în caz contrar 0.

Ca să se conecteze la un pipe cu nume creat de un alt proces, un proces


client trebuie să apeleze funcţia CreateFile, care va primi ca parametru
numele pipe-ului în formatul prezentat mai sus.

Comunicarea proceselor prin pipe-uri cu nume se face la fel ca şi la pipe-


urile anonime, adică prin funcţiile ReadFile şi WriteFile. Se pot folosi şi
funcţiile ReadFileEx şi WriteFileEx pentru comunicarea asincronă, caz în
care procesele nu sunt blocate când efectuează operaţii de citire sau scriere
pe pipe, care în mod normal s-ar bloca, dar se va executa o funcţie
specificată la terminarea operaţiei respective.

Folosind funcţia PeekNamedPipe se pot citi date din pipe fără a se şterge
octeţii citiţi din pipe. Funcţia TransactNamedPipe scrie un mesaj de cerere
într-un pipe bidirecţional de tip mesaj şi citeşte răspunsul într-o singură
operaţie (tranzacţie). Astfel se pot îmbunătăţii performanţele reţelei.

122
Sisteme de operare. Chestiuni teoretice şi practice

9.4. Exemple
Exemplul 1. Folosindu-se de două fişiere pipe anonime, un proces comunică
cu un fiu al său redirectându-i intrarea şi ieşirea standard spre cele două pipe-
uri. Fiul citeşte de la intrarea standard şi afişează ceea ce citeşte la ieşirea
standard, dar, având aceste fişiere redirectate, va citi de pe un pipe şi va scrie
pe celălalt, comunicând astfel cu părintele său. Procesul părinte primeşte în
linia de comandă numele unui fişier text pe care îl citeşte şi îl scrie pe primul
pipe şi afişează ceea ce fiul îi transmite pe cel de-al doilea pipe.

// Codul sursă al serverului


#include <stdio.h>
#include <windows.h>

#define BUFSIZE 4096

HANDLE hChildStdinRd, hChildStdinWr, hChildStdinWrDup;


HANDLE hChildStdoutRd, hChildStdoutWr;
HANDLE hChildStdoutRdDup, hInputFile, hStdout;

BOOL CreateChildProcess(VOID);
VOID WriteToPipe(VOID);
VOID ReadFromPipe(VOID);
VOID ErrorExit(LPTSTR);
VOID ErrMsg(LPTSTR, BOOL);

DWORD main(int argc, char *argv[])


{
SECURITY_ATTRIBUTES saAttr;
BOOL fSuccess;

// Asteapta numele unui fis. text in linia de cmd.


if (argc == 1)
ErrorExit("Please specify an input file");

// Setează campul bInheritHandle astfel incat


// handle-urile pot fi moştenite
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;

// Obtine handle-ul iesirii standard (STDOUT)


hStdout = GetStdHandle(STD_OUTPUT_HANDLE);

// Creeaza un pipe pentru iesirea standard


// a procesului fiu
if (! CreatePipe(&hChildStdoutRd, &hChildStdoutWr,
&saAttr, 0))
ErrorExit("Stdout pipe creation failed\n");

123
Fişiere PIPE în Windows

// Creeaza un handle de citire care nu poate fi


// mostenit si inchide handle-ul care poate
// fi mostenit pt. a se asigura ca fiul
// nu poate citi din acest pipe
ifSuccess = DuplicateHandle(GetCurrentProcess(),
hChildStdoutRd, GetCurrentProcess(),
&hChildStdoutRdDup , 0, FALSE,
DUPLICATE_SAME_ACCESS);
if( !fSuccess )
ErrorExit("DuplicateHandle failed");
CloseHandle(hChildStdoutRd);

// Creeaza un pipe pentru intrarea standard (STDIN)


// a procesului fiu
if (! CreatePipe(&hChildStdinRd, &hChildStdinWr,
&saAttr, 0))
ErrorExit("Stdin pipe creation failed\n");

// Duplica handle-ul de scriere a pipe-ului astfel


// incat sa nu poata fi mostenit
fSuccess = DuplicateHandle(GetCurrentProcess(),
hChildStdinWr, GetCurrentProcess(),
&hChildStdinWrDup, 0,FALSE,
DUPLICATE_SAME_ACCESS);
if (! fSuccess)
ErrorExit("DuplicateHandle failed");
CloseHandle(hChildStdinWr);

// Creează procesul fiu.


fSuccess = CreateChildProcess();
if (! fSuccess)
ErrorExit("Create process failed");

hInputFile = CreateFile(argv[1], GENERIC_READ, 0,


NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_READONLY, NULL);
if (hInputFile == INVALID_HANDLE_VALUE)
ErrorExit("CreateFile failed\n");

// Scrie in pipe-ul care este intrarea


// standard a fiului
WriteToPipe();

// Citeste din pipe-ul care este iesirea


// standard al fiului
ReadFromPipe();

return 0;
}

124
Sisteme de operare. Chestiuni teoretice şi practice

BOOL CreateChildProcess()
{
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
BOOL bFuncRetn = FALSE;

ZeroMemory(&piProcInfo,sizeof(PROCESS_INFORMATION));
ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
siStartInfo.cb = sizeof(STARTUPINFO);
siStartInfo.hStdError = hChildStdoutWr;
siStartInfo.hStdOutput = hChildStdoutWr;
siStartInfo.hStdInput = hChildStdinRd;
siStartInfo.dwFlags |= STARTF_USESTDHANDLES;

// Creează procesul fiu


bFuncRetn = CreateProcess(NULL, "child.exe",
NULL, NULL, TRUE, 0, NULL, NULL,
&siStartInfo, &piProcInfo);
if (bFuncRetn == 0)
ErrorExit("Eroare CreateProcess");
else
{
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
return bFuncRetn;
}
}

VOID WriteToPipe(VOID)
{
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE];

// CiteSte din fisierul text si scrie


// continutul lui intr-un pipe
for (;;)
{
if (! ReadFile(hInputFile, chBuf, BUFSIZE,
&dwRead, NULL) ||
dwRead == 0) break;
if (! WriteFile(hChildStdinWrDup, chBuf, dwRead,
&dwWritten, NULL)) break;
}

// Inchide handle-ul de scriere la pipe pentru ca


// procesul fiu termine citirea din pipe
if (! CloseHandle(hChildStdinWrDup))
ErrorExit("Close pipe failed");
}

125
Fişiere PIPE în Windows

VOID ReadFromPipe(VOID)
{
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE];

// Inchide hadle-ul de scriere în pipe inainte sa


// citească de din el
if (!CloseHandle(hChildStdoutWr))
ErrorExit("CloseHandle failed");

// Citeste iesirea procesului fiu si scrie la


// iesirea standard
for (;;)
{
if(!ReadFile(hChildStdoutRdDup, chBuf, BUFSIZE,
&dwRead, NULL) || (dwRead == 0))
break;

if (! WriteFile(hStdout, chBuf, dwRead,


&dwWritten, NULL))
break;

}
}

VOID ErrorExit (LPTSTR lpszMessage)


{
fprintf(stderr, "%s\n", lpszMessage);
ExitProcess(0);
}

// Codul sursă al procesului fiu este:


#include <windows.h>
#define BUFSIZE 4096

VOID main(VOID)
{
CHAR chBuf[BUFSIZE];
DWORD dwRead, dwWritten;

HANDLE hStdin, hStdout;


BOOL fSuccess;

hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
hStdin = GetStdHandle(STD_INPUT_HANDLE);

if ((hStdout == INVALID_HANDLE_VALUE) ||
(hStdin == INVALID_HANDLE_VALUE))
ExitProcess(1);

126
Sisteme de operare. Chestiuni teoretice şi practice

for (;;)
{
// Citeste de la STDIN
fSuccess = ReadFile(hStdin, chBuf, BUFSIZE,
&dwRead, NULL);

if (! fSuccess || dwRead == 0)
break;

// Scrie la STDOUT
fSuccess = WriteFile(hStdout, chBuf, dwRead,
&dwWritten, NULL);

if (! fSuccess)
break;
}
}

Exemplul 2. Folosind un pipe cu nume se implementează comunicarea între


un proces server şi mai multe procese client. Pentru fiecare client serverul
creează un nou thread care va deservi acel client pe o instanţă distinctă a
pipe-ului. Comunicarea pe instanţele pipe-ului este bidirecţională.

// Codul sursa al server-ului


#include <windows.h>
#include <stdio.h>
#include <tchar.h>

#define BUFSIZE 4096

VOID InstanceThread(LPVOID);
VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD);

int _tmain(VOID)
{
BOOL fConnected;
DWORD dwThreadId;
HANDLE hPipe, hThread;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mypipe");

/* Bucla principala: se creeaza o instanta pipe-ului


cu nume si apoi asteapta conectarea unui client.
Cand se conectează un client, se creeaza un thread
care va comunica cu clientul pe instanta creata
a pipe-ului
*/

127
Fişiere PIPE în Windows

for (;;)
{
hPipe = CreateNamedPipe(
lpszPipename, // numele pipe-ului
PIPE_ACCESS_DUPLEX, // acces citire/scriere
PIPE_TYPE_MESSAGE | // pipe de tip mesaj
PIPE_READMODE_MESSAGE | // mod citire-mesaj
PIPE_WAIT, // modul blocant
PIPE_UNLIMITED_INSTANCES,
BUFSIZE, // dim. buffer output
BUFSIZE, // dim. buffer input
NMPWAIT_USE_DEFAULT_WAIT,
NULL); // atribute de securitate implicite

if (hPipe == INVALID_HANDLE_VALUE)
{
printf("CreatePipe failed");
return 0;
}

/* Asteapta un client să se conecteze;


daca reuseste, functia returneaza a valoare
diferita de 0. Dacă functia returnează 0,
si GetLastError() returneaza valoarea
ERROR_PIPE_CONNECTED atunci clientul e deja
conectat; altfel nu se poate face conexiunea
*/

fConnected = ConnectNamedPipe(hPipe, NULL) ?


TRUE :
GetLastError() == ERROR_PIPE_CONNECTED);

if (fConnected)
{ // Creeaza un thread pentru acest client
hThread = CreateThread(
NULL, 0,
(LPTHREAD_START_ROUTINE) InstanceThread,
(LPVOID) hPipe, 0, &dwThreadId);

if (hThread == NULL)
{
printf("Eroare creare thread.");
return 0;
}
else CloseHandle(hThread);
}
else // Clientul nu se poate conecta
CloseHandle(hPipe); // inchide pipe-ul
}
return 1;
}

128
Sisteme de operare. Chestiuni teoretice şi practice

VOID InstanceThread(LPVOID lpvParam)


{
TCHAR chRequest[BUFSIZE];
TCHAR chReply[BUFSIZE];
DWORD cbBytesRead, cbReplyBytes, cbWritten;
BOOL fSuccess;
HANDLE hPipe;

// Parametrul thread-ului este un handle


// la o instanta de pipe
hPipe = (HANDLE) lpvParam;

while (1)
{
// Citeste cererile clientului din pipe
fSuccess = ReadFile( hPipe,
chRequest, BUFSIZE*sizeof(TCHAR),
&cbBytesRead, NULL);

if (! fSuccess || cbBytesRead == 0)
break;

HandleReq(chRequest, chReply, &cbReplyBytes);

// Scrie raspunsul in pipe


fSuccess = WrieFile( hPipe, chReply,
cbReplyBytes, &cbWritten, NULL);

if (! fSuccess || cbReplyBytes != cbWritten)


break;
}

/* Goleste pipe-ul pentru a permite clientului sa


citească continutul pipe-ului inainte de a se
deconecta. Apoi se deconecteaza pipe-ul si se
inchide handle-ul la el.
*/
FlushFileBuffers(hPipe);

DisconnectNamedPipe(hPipe);

CloseHandle(hPipe);
}

VOID HandleReq(LPTSTR chRequest,


LPTSTR chReply, LPDWORD pchBytes)
{
_tprintf( TEXT("%s\n"), chRequest );
lstrcpy(chReply, TEXT("Raspuns de la server"));
*pchBytes = (lstrlen(chReply)+1)*sizeof(TCHAR);
}

129
Fişiere PIPE în Windows

// Codul sursa al clientului


#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>

#define BUFSIZE 512

int _tmain(int argc, TCHAR *argv[])


{
HANDLE hPipe;
LPTSTR lpvMessage=TEXT("Raspuns de la server");
TCHAR chBuf[BUFSIZE];
BOOL fOk;
DWORD cbRead, cbWritten, dwMode;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mypipe");
if( argc > 1 )
lpvMessage = argv[1];
// Incearca sa deschida un pipe;
// asteapta daca este necesar
while (1)
{

hPipe = CreateFile(
lpszPipename, // numele pipe-ului
GENERIC_READ | // acces in citire si
GENERIC_WRITE, // scriere
0, NULL,
OPEN_EXISTING, // deschide un pipe existent
0, // atribute implicite
NULL); //

if (hPipe != INVALID_HANDLE_VALUE)
break;
// Iese dacă apare o eroare alta
// decat ERROR_PIPE_BUSY
if (GetLastError() != ERROR_PIPE_BUSY)
{
printf("Nu se poate deschide pipe-ul");
return 0;
}
// Daca toate instantele pipe-ului sunt ocupate,
// aşteaptă 20 de secunde
if (!WaitNamedPipe(lpszPipename, 20000))
{
printf("Nu se poate deschide pipe-il");
return 0;
}
}

130
Sisteme de operare. Chestiuni teoretice şi practice

// Pipe-ul s-a conectat;


// schimba tipul la modul citire-mesaj
dwMode = PIPE_READMODE_MESSAGE;

fOk = SetNamedPipeHandleState(
hPipe, // handle la pipe
&dwMode, // modul pipe nou
NULL, // nu se setează nr. maxim de octeti
NULL); // nu se setează time-out

if (!fOk)
{
printf("Eroare SetNamedPipeHandleState ");
return 0;
}

// Trimite mesaj la server-ul de pipe


fOk = WriteFile( hPipe, lpvMessage,
(lstrlen(lpvMessage)+1)*sizeof(TCHAR),
&cbWritten, NULL);

if (!fOk)
{
printf("Eroare WriteFile ");
return 0;
}

do
{
// Citeste din pipe
fOk = ReadFile( hPipe, chBuf,
BUFSIZE*sizeof(TCHAR), &cbRead, NULL);

if (!fOk && GetLastError() != ERROR_MORE_DATA)


break;

_tprintf( TEXT("%s\n"), chBuf );

} while (!fOk);

getch();

CloseHandle(hPipe);

return 0;
}

131
Fişiere PIPE în Windows

9.5. Probleme
1. Să se scrie codul C al unui proces care creează un pipe anonim şi apoi
un proces fiu. Procesul părinte scrie în pipe conţinutul unui fişier text, al
cărui nume îl primeşte în linia de comandă. Procesul fiu afişează
conţinutul pipe-ului executând comanda more.
2. Să se scrie, folosind pipe-uri, funcţii de sincronizare între două procese,
funcţii care să fie apelate la intrarea, respectiv ieşirea din regiunile
critice ale proceselor şi care să asigure o sincronizare de genul:
a. excludere mutuală;
b. execuţie alternată.
3. Folosind pipe-uri cu nume să se asigure o comunicare de tip inel a N
procese. În inelul respectiv va circula un mesaj de tip token, procesul
care deţine la un moment dat token-ul afişându-şi identificatorul şi a
câta oară a primit token-ul. Transferul token-ului în inel se face de un
număr de ori cunoscut de fiecare dintre procese. Construirea inelului şi
generarea token-ului se vor face de către un proces distinct de cele N,
care va porni şi cele N procese. Să se extindă apoi funcţionalitatea
inelului, astfel încât la un moment dat un proces să-şi poată anunţa
terminarea (ieşirea din inel), în timp ce celelalte să poată continua.
4. Să se scrie programul cu funcţionalitatea unui interpretor de comenzi
care acceptă introducerea în linia de comandă a unor comenzi compuse
de forma comanda1 | comanda2. Prima comandă afişează în mod
normal un rezultat de tip text pe ecran, iar cea de-a doua comandă îşi
citeşte datele de intrare de la tastatură. Efectul comenzii compuse este
acela de comunicare printr-un pipe anonim a celor două comenzi, în
sensul că ieşirea standard a primei comenzi este redirectată spre pipe, iar
intrarea standard a celei de-a doua este redirectată dinspre pipe.
5. Să se implementeze codul C al unui proces client şi al unui server
concurent multithread. Serverul furnizează clienţilor servicii de
efectuare a operaţiilor aritmetice cu două numere. Fiecare client este
deservit de câte un thread diferit al serverului, thread creat la conectarea
clientului. El recepţionează cereri de la procesele client şi transmite
rezultatul înapoi prin intermediul unui pipe cu nume. Structura unui
mesaj este:
typedef struct {
long idClient;
int x; int y; int operatie; int rez;
} Mesaj;

132
10. Comunicarea prin semnale în Linux

Scopul lucrării
Lucrarea prezintă aspecte legate de utilizarea semnalelor în Linux, privite
ca un mecanism de control a execuţiei proceselor şi de comunicare între
procese. Sunt descrise principalele apeluri sistem necesare generării şi
tratării semnalelor.

10.1. Funcţionalitatea semnalelor


Semnalele POSIX corespund unui mecanism special de control al execuţiei
proceselor. Acest mecanism constă în transmiterea unor mesaje speciale
proceselor de către sistemul de operare. Semnalele pot fi considerate un fel
de întreruperi software, deoarece mecanismul de generare şi tratare a lor
este asemănător cu cel specific întreruperilor. Semnalele sunt generate şi
trimise unui proces de către sistemul de operare ca urmare a apariţiei unor
situaţii speciale în execuţia acelui proces sau datorită unei cereri explicite în
acest sens a unui alt proces. Situaţiile speciale care duc la generarea unor
semnale sunt fie cele care reflectă excepţii generate de hardware
(instrucţiune ilegală, împărţire la zero etc.), fie cele constatate de către
sistemul de operare (scrierea într-un pipe închis, apăsarea unor combinaţii
speciale de taste etc.). Cererile explicite adresate de către un proces pentru
trimiterea unui semnal către un alt proces se fac prin apeluri sistem
specializate, puse la dispoziţie de către sistemul de operare. În acest ultim
caz, semnalele pot fi privite ca un mecanism de comunicare între procese,
însă unul nu foarte specializat pentru acest scop. Din acest motiv ele sunt
folosite nu atât pentru transmiterea de informaţii între procese, cât pentru
controlul şi sincronizarea execuţiei proceselor.

Pentru a se putea face distincţie între multiplele situaţii care pot duce la
generarea unui semnal, sistemul de operare clasifică semnalele în mai multe
clase sau tipuri. Fiecărui semnal îi este astfel asociat un identificator, care
indică tipul semnalului respectiv. Conform standardului POSIX, există două
tipuri de semnale: semnale standard şi semnale de tip real. Sistemul de
operare Linux suportă ambele tipuri de semnale, dar lucrarea de faţă face
referire doar la setul standard de semnale, ale căror nume, identificator de
tip, mod de tratare implicit şi semnificaţie sunt date în Tabelul 1. Deoarece
asocierea unui identificator pentru un anumit tip de semnal este dependentă

133
Comunicarea prin semnale în Linux

de arhitectură, în tabel sunt indicate, unde este cazul, toate cele trei variante
posibile pentru identificatorul de tip al unui semnal, în modul următor:
primul număr corespunde sistemelor cu arhitectură alpha şi sparc, numărul
din mijloc sistemelor cu arhitectură i386, ppc şi sh, iar cel de-al treilea
arhitecturii mips. În cazul specificării unui singur număr, acesta e valabil
pentru toate cele trei variante. În cazul tratării implicite a unor semnale care
au ca efect terminarea procesului căruia îi sunt trimise, sistemul de operare
poate salva în mod automat pe HDD imaginea din memorie a procesului
respectiv, sub forma unui aşa-numit fişier core, caz în care în tabel, în
coloana Tratare implicită, vom indica acest lucru prin termenul Core.

Tabelul 1. Tipurile standard de semnale din Linux


Semnal Identificator Tratare Semnificaţie
de tip implicită
Deconectarea terminalului de
SIGHUP 1 Terminare
care depinde procesul
Întrerupere de la tastatură
SIGINT 2 Terminare
(CTRL+C)
Terminare de la tastatură
SIGQUIT 3 Core
(CTRL+\)
SIGILL 4 Core Instrucţiune ilegală
Întrerupere la execuţia în mod
SIGTRAP 5 Core
de depanare
Generat de apelul funcţiei
SIGABRT 6 Core
abort
Specificarea unei adrese de
SIGBUS 10, 7, 10 Core
memorie invalidă
SIGFPE 8 Core Excepţie de virgulă mobilă
SIGKILL 9 Terminare Semnal de terminare forţată
Acces la adresă de memorie
SIGSEGV 11 Core
invalidă
Încercare de scriere într-un
SIGPIPE 13 Terminare
pipe închis (fără cititori)
Generat la expirarea timpului
SIGALRM 14 Terminare
stabilit de funcţia alarm
SIGTERM 15 Terminare Semnal de terminare
SIGUSR1 30, 10, 16 Terminare Rezervat utilizatorilor
SIGUSR2 31, 12, 17 Terminare Rezervat utilizatorilor

134
Sisteme de operare. Chestiuni teoretice şi practice

Terminarea sau oprirea unui


SIGCHLD 20, 17, 18 Ignorare
proces fiu
SIGCONT 19, 18, 25
Continuarea execuţiei
suspendată cu SIGSTOP
SIGSTOP 17, 19, 23 Suspendare Suspendarea execuţiei
Suspendarea execuţiei de la
SIGSTP 18, 20, 24 Suspendare
tastatură (CTRL+Z)
TTY input pentru proces ce
SIGTTIN 21, 21, 26 Suspendare
rulează în background
TTY output pentru proces ce
SIGTTOU 22, 22, 27 Suspendare
rulează în background
Căderea tensiunii de
SIGPWR 29, 30, 19 Terminare
alimentare
Generat la expirarea timpului
SIGPROF 27, 27, 29 Terminare
stabilit de funcţia setitimer
Generat la expirarea timpului
SIGVTALRM 26, 26, 28 Terminare
stabilit de funcţia setitimer

10.2. Tratarea semnalelor


Tratarea semnalelor se face în mod asincron, în sensul că nu există o funcţie
de recepţionare a unui semnal, funcţie pe care apelând-o un proces să
rămână blocat până la primirea acelui semnal. În mod normal procesul îşi
precizează modalitatea de reacţie la primirea unui semnal, apoi îşi execută
codul său. La primirea unui semnal execuţia procesului este suspendată
imediat şi se face salt la codul (utilizator sau sistem) prin care se
reacţionează la semnalul primit. Modul de reacţie la apariţia unui anumit
semnal poate fi stabilit de către fiecare proces în parte, existând trei astfel de
modalităţi:
• tratare implicită, stabilită în mod automat de către sistemul de
operare, având în mod normal ca efect terminarea execuţiei
procesului;
• ignorare, având ca efect continuarea execuţiei procesului ca şi când
semnalul nu ar fi apărut;
• tratare explicită, având ca efect execuţia unei funcţii specificată de
către utilizator.
Semnalele SIG_KILL şi SIG_STOP nu pot fi ignorate sau tratate în mod
explicit.

135
Comunicarea prin semnale în Linux

Pentru stabilirea modului de reacţie la apariţia unui semnal pot fi folosite


apelurile sistem signal sau sigaction, a căror sintaxă este prezentată mai jos:

#include <signal.h>
typedef void (*sighandler_t)(int);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sighandler_t signal(int semnal, sighandler_t functie);
int sigaction (int semnal,
const struct sigaction *noua_setare,
struct sigaction *vechea_setare);

În cazul ambelor funcţii, parametrul semnal reprezintă identificatorul


tipului de semnal pentru care se stabileşte modul de tratare.

În cazul funcţiei signal, parametrul functie descrie modul de tratare a


semnalului, putând avea ca valoare:
• adresa unei funcţii utilizator, dacă se doreşte tratarea explicită a
semnalului,
• SIG_IGN, pentru ignorarea semnalului sau
• SIG_DFL, pentru tratarea implicită de către sistemul de operare a
semnalului.
Funcţia utilizator de tratare a unui semnal primeşte ca parametru, în
momentul apelului ei de către sistemul de operare, ca urmare a apariţiei
semnalului, un întreg reprezentând identificatorul de tip al semnalului
apărut.

Funcţia signal returnează valoarea anterioară corespunzătoare tratării


semnalului sau SIG_ERR în caz de eroare.

Parametrul noua_setare al funcţiei sigaction reprezintă adresa unei


structuri struct sigaction, care descrie modul de tratare al semnalului.
Dacă valoarea parametrului vechea_setare este diferită de NULL, atunci
la adresa respectivă este salvată structura ce descrie modul de tratare
anterior al semnalului.
Funcţia sigaction returnează 0 în caz de succes şi -1 în caz de eroare.

136
Sisteme de operare. Chestiuni teoretice şi practice

În cadrul structurii struct sigaction câmpurile sa_handler şi


sa_sigaction trebuie completate în mod exclusiv, cu adresele unor
funcţii utilizator de tratare a semnalului. Primul câmp corespunde stabilirii
unei funcţii utilizator cu un singur parametru, similar apelului sistem signal,
pe când cel de-al doilea corespunde unei funcţii cu trei parametri, primul
parametru fiind identificatorul de tip al semnalului, cel de-al doilea adresa
unei structuri struct siginfo_t, iar cel de-al treilea adresa unei
structuri de tipul struct ucontext_t (definită în sys/ucontext.h),
care conţine contextul execuţiei procesului existent în momentul apariţiei
semnalului. Aceşti parametri sunt transmişi funcţiei utilizator în momentul
apelului ei de către sistemul de operare. Cele două structuri menţionate vor
fi alocate şi completate automat de către sistemul de operare pe stiva
procesului în momentul apelului funcţiei de tratare a semnalului. Structura
siginfo_t oferă informaţii suplimentare legate de generarea semnalului,
câmpurile ei fiind descrise mai jos.

struct siginfo_t {
int si_signo; // Numar semnal
int si_errno; // Cod de eroare
int si_code; // Cod semnal
pid_t si_pid; // ID proces
uid_t si_uid; // ID real utilizator
int si_status; // Valoare de terminare
clock_t si_utime; // Timp utilizator consumat
clock_t si_stime; // Timp sistem consumat
sigval_t si_value; // Valoare atasată semnalului
int si_int; // Semnal POSIX.1b
void * si_ptr; // Semnal POSIX.1b
void * si_addr; // Adresa memorie
int si_band;
int si_fd; // Descriptor fisier
};

Câmpurile si_signo, si_errno şi si_code sunt completate pentru toate


semnalele, dintre celelalte unele având semnificaţie doar pentru anumite
semnale. Pentru semnalele trimise cu ajutorul funcţiei kill (descrisă mai jos) şi
pentru semnalul SIGCHLD se completează şi câmpurile si_pid şi si_uid,
care reprezintă identificatorul procesului care a trimis semnalul şi respectiv,
identificatorul real al utilizatorului căruia îi aparţine respectivul proces.

Câmpul si_code indică motivul generării semnalului. Tabelul de mai jos


descrie câteva dintre posibilele valori ale acestui câmp.

137
Comunicarea prin semnale în Linux

Tabelul 2. Valori posibile ale câmpului si_code


Valoare Semnificaţie Observaţii
SI_USER Trimis cu kill sau Valabil pentru toate
raise semnalele
SI_KERNEL Generat de kernel Valabil pentru toate
semnalele
SI_QUEUE Trimis cu sigqueue Valabil pentru toate
semnalele
SI_TIMER Expirarea unui timer Valabil pentru toate
semnalele
CLD_EXITED Un proces fiu s-a Valabil semnalul
terminat SIGCHLD
CLD_KILLED Un proces fiu a fost Valabil semnalul
terminat forţat SIGCHLD
CLD_STOPED Un proces fiu a fost Valabil semnalul
oprit SIGCHLD
CLD_CONTINUED Un proces fiu oprit Valabil semnalul
anterior şi-a reluat SIGCHLD
execuţia
FPE_INTDIV Împărţire întreagă la Valabil semnalul
zero SIGFPE
FPE_FLTDIV Împărţire real la zero Valabil semnalul
SIGFPE
FPE_INTOVF Depăşire număr Valabil semnalul
întreg SIGFPE

Câmpul sa_mask al structurii struct sigaction indică tipurile de


mesaje ce vor fi mascate pe durata tratării semnalului indicat în apelul
funcţiei sigaction. Semnalul tratat este şi el mascat, cu excepţia cazurilor în
care se folosesc valorile SA_NODEFER sau SA_NOMASK pentru câmpul
sa_flags al structurii struct sigaction.

Valorile câmpului sa_flags modifică comportamentul funcţiei de tratare a


semnalului în modul următor:
SA_NOCLDSTOP
Dacă semnalul tratat este SIGCLD, nu se vor primi alte semnale de
acest tip când se termină procese fiu.

138
Sisteme de operare. Chestiuni teoretice şi practice

SA_ONESHOT sau SA_RESETHAND


Modul de tratare a semnalului devine cel implicit după terminarea
tratării semnalului.
SA_NODEFER sau SA_NOMASK
Nu blochează semnalul tratat, astfel încât el poate fi primit chiar în
timpul tratării sale, ceea ce înseamnă întreruperea funcţiei de tratare
a semnalului şi reapelarea ei în noul context.
SA_SIGINFO
Funcţia utilizator de tratare a semnalului trebuie să primească trei
argumente. În acest caz trebuie specificat cu o valoare nenulă câmpul
sa_sigaction, iar câmpul sa_handler trebuie setat la NULL.

10.3. Mascarea semnalelor


Aşa cum am menţionat şi mai sus la descrierea funcţiei sigaction, semnalele
pot fi mascate sau blocate pe durata tratării unui semnal, semnalul care
tocmai este tratat fiind mascat în mod implicit, iar celelalte în funcţie de
filtrul indicat în apelul funcţiei sigaction. Mascarea semnalelor se poate face
însă de către un proces şi în afara funcţiilor de tratare a semnalelor. Astfel,
pe lângă modalităţile de reacţie la semnale descrise mai sus, un proces poate
stabili un filtru pentru semnalele pe care doreşte să le blocheze. Mascarea
unui semnal este diferită de ignorarea semnalului. În primul caz sistemul de
operare menţine informaţia despre apariţia semnalului, dar amână tratarea
lui până în momentul deblocării lui, pe când în cel de-al doilea caz semnalul
este definitiv pierdut. Apeluri sistem utile pentru blocarea semnalelor sunt
cele descrise mai jos.

#include <signal.h>
int sigprocmask(int mod, const sigset_t *masca,
sigset_t *vechea_masca);
int sigpending(sigset_t *set_semnale);
int sigsuspend(const sigset_t *masca);

Funcţia sigprocmask are ca efect schimbarea filtrului (masca) ce indică


semnalele care vor fi mascate pe durata tratării unui semnal. Valoarea
parametrului mod indică modul de stabilire a noului filtru, astfel:
SIG_BLOCK
Setul semnalelor blocate este obţinut prin reuniunea setului curent cu
cel indicat de parametrul masca.

139
Comunicarea prin semnale în Linux

SIG_UNBLOCK
Setul semnalelor indicate în parametrul masca sunt demascate, adică
sunt eliminate din setul curent al semnalelor mascate.
SIG_SETMASK
Setul semnalelor blocate va fi setat la valoarea celui indicat de
parametrul masca.

Dacă pe poziţia celui de-al treilea parametru, vechea_masca, se specifică o


valoare nenulă, atunci la adresa respectivă va fi returnată valoarea actuală a
setului semnalelor mascate (cea de dinaintea apelului funcţiei sigprocmask).
Apariţia unui semnal mascat S1 în timpul tratării unui alt semnal S2 va fi
memorată, dar tratarea lui este amânată până la terminarea funcţiei curente
de tratare a semnalului S2. Dacă semnalul mascat apare de mai multe ori cât
timp este blocat, sistemul de operare memorează doar o singură apariţie a sa
şi nu mai multe.
Funcţia sigpending stochează la adresa indicată de parametrul
set_semnale setul semnalelor blocate, pentru care sistemul de operare
aşteaptă deblocarea pentru a fi tratate.
Funcţia sigsuspend setează filtrul de mascare a semnalelor unui proces la
valoarea parametrului masca, iar apoi suspendă execuţia procesului până la
apariţia unui semnal. Această funcţie poate fi utilă în cazul în care procesul
trebuie să îşi suspende execuţia până la apariţia unui anumit semnal, fără ca
această aşteptare să fie influenţată de apariţia altor semnale. Exemplul de
mai jos ilustrează modul de folosire a funcţiei într-o astfel de situaţie, când
se aşteaptă trimiterea semnalului SIGUSR1.
#include <stdio.h>
#include <signal.h>
void functie(int semnal)
{ printf("Tratare semnal %d\n", semnal); }
main()
{
sigset_t masca;
sigfillset(&masca);
sigdelset(&masca, SIGUSR1);
if (signal(SIGUSR1, functie) < 0)
{ perror("Eroare setare semnal 1"); exit(1); }
if (signal(SIGUSR2, functie) < 0)
{ perror("Eroare setare semnal 2"); exit(1); }
sigsuspend(&masca);
}

140
Sisteme de operare. Chestiuni teoretice şi practice

Pentru stabilirea filtrului de mascare a semnalelor pentru un proces, pot fi


utilizate următoarele funcţii:

#include <signal.h>
int sigemptyset(sigset_t* masca);
int sigfillset(sigset_t* masca);
int sigaddset(sigset_t* masca, int semnal);
int sigdelset(sigset_t* masca, int semnal);
int sigismember(const sigset_t* masca, int semnal);

Funcţia sigemptyset marchează toate semnalele posibile ca neaparţinând


filtrului de semnale masca. Funcţia sigfillset include toate semnalele
posibile în filtrul de semnale masca. Funcţiile sigaddset şi sigdelset adaugă
la filtrul de semnale indicat de parametrul masca, respectiv elimină din acel
filtru, semnalul specificat prin parametrul semnal. Funcţia sigismember
testează dacă semnalul indicat prin parametrul semnal aparţine filtrului de
semnale indicat de parametru masca.

10.4. Trimiterea semnalelor


Apelurile sistem care au ca efect trimiterea unui semnal către un proces sunt
cele descrise mai jos:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int semnal);
int raise(int semnal);
int sigqueue(pid_t pid, int semnal,
const union sigval valoare);
union sigval {
int sival_int;
void* sival_ptr;
};

Apelul sistem kill este folosit pentru trimiterea unui semnal de către un
proces către un alt proces. Atenţionăm asupra faptului că numele funcţiei
poate crea confuzie în ceea ce priveşte utilitatea sa. Apelul sistem kill are ca
efect trimiterea unui semnal, al cărui tip este specificat prin parametrul
semnal, către un proces, al cărui identificator este indicat prin parametrul
pid, şi nu „omorârea” (terminarea forţată) acelui proces, acesta fiind doar
cazul particular al trimiterii semnalului SIGKILL.

141
Comunicarea prin semnale în Linux

Pentru a putea trimite un semnal unui alt proces, procesul care apelează
funcţia kill trebuie să aibă drepturi de administrator sau să aibă acelaşi
identificator de utilizator real sau efectiv ca şi procesul căruia vrea să-i
trimită semnalul.

Dacă valoarea parametrului semnal este 0 (zero), atunci nu se trimite nici


un semnal, ci doar se verifică dacă procesul cu identificatorul pid există şi
dacă există drepturile necesare pentru trimiterea unui semnal spre procesul
indicat.

În funcţie de valoarea parametrul pid, semnalul trimis cu funcţia kill poate


ajunge la unul sau mai multe procese, astfel:
a. dacă pid > 0, atunci semnalul este trimis procesului cu
identificatorul pid;
b. dacă pid == 0, atunci semnalul este trimis tuturor proceselor
din acelaşi grup cu procesul curent;
c. dacă pid == -1, atunci semnalul este trimis tuturor proceselor,
cu excepţia procesului cu identificatorul 1 (procesul init);
d. dacă pid < -1, atunci semnalul este trimis tuturor proceselor
din grupul de procese cu identificatorul |pid|.

Apelul sistem raise are ca efect trimiterea semnalului indicat de parametrul


semnal către procesul apelant. Efectul este similar cu cel al funcţiei kill în
forma următoare:

kill(getpid(), semnal);

Apelul sistem sigqueue trimite semnalul indicat de parametrul semnal


procesului indicat prin parametrul pid. Funcţia permite transmiterea, o dată
cu semnalul, a unei informaţii auxiliare, indicată de parametrul valoare.
Această valoare poate fi interpretată ca un întreg sau ca un pointer.
Recepţionarea acestei informaţii de către procesul care primeşte semnalul
este posibilă dacă acesta şi-a stabilit modul de reacţie la primirea semnalului
cu funcţia sigaction cu valoarea câmpului sa_flags al structurii struct
sigaction setată la SA_SIGINFO, caz în care valoarea respectivă este
accesibilă prin câmpul si_value al structurii struct siginfo_t,
transmisă ca al doilea argument al funcţiei de tratare a semnalului. De
asemenea, câmpul si_code al respectivei structurii este setat la valoarea
SI_QUEUE.

142
Sisteme de operare. Chestiuni teoretice şi practice

10.5. Alte apeluri sistem legate de semnale


Există o serie de alte funcţii, pe lângă cele descrise mai sus, care pot fi
utilizate în cadrul comunicării proceselor prin semnale. Câteva dintre
acestea sunt descrise mai jos.

Funcţia alarm
Sintaxa ei este următoarea:

#include <unistd.h>
unsigned int alarm(unsigned int secunde);

Efectul apelului funcţiei alarm este acela că semnalul SIGALRM va fi


transmis procesului apelant după intervalul de timp indicat de parametrul
secunde. Dacă acest parametru are valoarea zero, atunci nu va fi generat
semnalul SIGALRM. Un apel al funcţiei anulează vechea setare relativă la
generarea semnalului SIGALRM stabilită printr-un apel anterior. Funcţia
returnează numărul de secunde rămase până la apariţia semnalului SIGALRM
programată printr-un apel anterior sau 0 (zero) în caz că nu a existat un
astfel de apel.

Funcţiile setitimer şi getitimer


Sintaxa celor două funcţii este următoarea:

#include <sys/time.h>
struct itimerval {
struct timeval it_interval;
struct timeval it_value;
};
struct timeval {
long tv_sec; // secunde
long tv_usec; // microsecunde
};

int getitimer(int contor, struct itimerval *valoare);


int setitimer(int contor,
const struct itimerval *valoareNoua,
struct itimerval *valoareVeche);

Funcţiile setitimer şi getitimer sunt folosite pentru a stabili şi respectiv, a


obţine valorile a trei contoare de timp pe care sistemul de operare le

143
Comunicarea prin semnale în Linux

asociază fiecărui proces. La expirarea fiecăruia dintre cele trei contoare


sistemul de operare trimite un anumit semnal procesului. Parametrul
tipContor indică contorul asupra căruia se efectuează operaţia şi poate
avea următoarele trei valori corespunzător celor trei tipuri de contor:

ITIMER_REAL Este decrementat în timp real, în funcţie de avansul


ceasului sistem, iar la expirarea sa este generat
semnalul SIGALRM.

ITIMER_VIRTUAL Este decrementat doar când procesul se află în


execuţie, iar la expirarea sa este generat semnalul
SIGVALRM.

ITIMER_PROF Este decrementat în timpul execuţiei procesului sau


când se execută cod sistem în contextul procesului, iar
la expirarea sa este generat semnalul SIGPROF. Acest
contor poate fi folosit în combinaţie cu contorul
ITIMER_VIRTUAL pentru a obţine informaţii despre
timpul în care execuţia unui proces are loc în spaţiul
utilizator sau în spaţiul nucleu.

Iniţializarea unui contor se face prin precizarea valorii sale (câmpul


it_value al structurii struct itimerval) şi a valorii implicite (câmpul
it_interval al structurii struct itimerval) pe care contorul o va
primi în momentul expirării sale (atingerea valorii zero). Dacă valoarea
contorului este 0 (zero), atunci contorul este anulat. Similar, dacă la
expirarea unui contor valoarea sa implicită este zero, contorul nu va mai fi
repornit. Dacă valoarea parametrului valoareVeche al funcţiei setitimer
este diferită de NULL, atunci la adresa respectivă se va înscrie vechea setare
a contorului.

Contoarele de timp nu expiră niciodată mai repede de timpul programat,


însă e posibil să expire puţin mai târziu, în funcţie de rezoluţia ceasului
sistem, care este de 10 ms. În momentul expirării contorul este resetat şi
semnalul corespunzător este trimis procesului. Dacă expirarea unui contor
are loc în timp ce procesul este în execuţie (lucru valabil întotdeauna pentru
contorul ITIMER_VIRT), semnalul generat va fi trimis imediat procesului.
Altfel, el este amânat până în momentul reluării execuţiei procesului.

144
Sisteme de operare. Chestiuni teoretice şi practice

Funcţia pause
Sintaxa funcţiei este următoarea:

#include <unistd.h>
int pause(void);

Funcţia pause are ca efect suspendarea execuţiei procesului apelant până în


momentul sosirii unui semnal. Revenirea din funcţia pause se face doar dacă
apare un semnal care nu este ignorat de către proces şi numai după execuţia
funcţiei de tratare a semnalului respectiv. Valoarea returnată este -1, iar
codul erorii memorat în variabila de sistem errno este EINTR.

Funcţia siginterrupt
Sintaxa funcţiei este următoarea:

#include <signal.h>
int siginterrupt(int semnal, int intrerupere);

Funcţia siginterrupt stabileşte comportamentul sistemului relativ la procesul


apelant în situaţia în care apariţia semnalului indicat de parametrul semnal
are ce efect întreruperea unui apel sistem. Dacă valoarea parametrului
intrerupere este 0 (zero), atunci apelul sistem întrerupt va fi reexecutat.
Acesta este comportamentul implicit al sistemului de operare Linux. Dacă
însă pentru semnalul respectiv s-a stabilit prin apelul sistem signal o funcţie
de tratare, atunci comportamentul implicit al sistemului este de a nu relua
execuţia apelului sistem întrerupt. Dacă valoarea parametrului
intrerupere este 1 şi nu s-au efectuat transferuri de date în cadrul
apelului sistem întrerupt până în momentul întreruperii lui, atunci
respectivul apel sistem nu va fi reluat, ci va returna imediat valoarea -1,
setându-se de asemenea, variabila errno la valoarea EINTR. Dacă valoarea
parametrului intrerupere este 1 şi s-au efectuat transferuri de date în
cadrul apelului sistem întrerupt până în momentul întreruperii lui, atunci
respectivul apel sistem nu va fi reluat, ci va returna imediat numărul de
octeţi transferat.

10.6. Exemple
Exemplul 1. Exemplul de mai jos ilustrează modalitatea de tratare a tuturor
semnalelor printr-o funcţie utilizator, cu excepţia semnalelor SIGKILL şi

145
Comunicarea prin semnale în Linux

SIGSTOP, care nu pot fi tratate în acest mod. Trimiterea de semnale spre


procesul care va executa codul din exemplu se poate face dintr-un alt proces
sau din linia de comandă cu ajutorul comenzii kill, identificatorul procesului
fiind obţinut cu ajutorul comenzii ps.

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

int terminare;

void functie(int sig, siginfo_t *siginfo, void* v)


{
printf("Tratare semnal %d\n", sig);

if (sig == SIGQUIT) // CTRL+\


terminare = 1;
}

int main(int argc, char **argv)


{
struct sigaction sigact;
int semnal;

// pregatirea strcuturii sigaction


sigact.sa_handler = NULL;
sigact.sa_sigaction = functie;
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = SA_SIGINFO;

for (semnal=1; semnal<32; semnal++)


if ((semnal != SIGKILL) && (semnal != SIGSTOP))
if (sigaction(semnal, &sigact, NULL) < 0) {
printf("Eroare tratare semnal %d:%s\n",
semnal, strerror(errno));
exit(1);
}

terminare = 0;
while (!terminare)
pause();
}

Exemplul 2. Exemplul de mai jos ilustrează modalitatea de sincronizare prin


semnale a execuţiei a două procese aflate în relaţia părinte-fiu. Cele două
procese trebuie să afişeze alternativ un mesaj, respectând o ordine bine
stabilită: mai întâi procesul părinte, apoi procesul fiu, apoi din nou părintele şi
aşa mai departe.

146
Sisteme de operare. Chestiuni teoretice şi practice

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int asteapta;
void functie(int semnal)
{
if (semnal == SIGUSR1){ asteapta = 1; }
}
int main(int argc, char **argv)
{
sigset_t masca;
int pidPereche, i;

// mascheaza toate semnalele


sigfillset(&masca);
sigprocmask(SIG_SETMASK, &masca, 0);

if (signal(SIGUSR1, functie) < 0) {


perror("Eroare setare SIGUSR1");
exit(1);
}

pidPereche = fork(); // creare fiu


if (pidPereche < 0){
perror("Eroare creare proces fiu");
exit(1);
}
if (pidPereche == 0) { // fiu
printf("Start: Proces fiu\n");
pidPereche = getppid();
asteapta = 1;
} else { // parinte
printf("Start: Proces parinte\n");
asteapta = 0;
}

for (i=1; i<100; i++) {


// isi asteapta randul
if (asteapta) { // fiul asteapta primul
sigdelset(&masca, SIGUSR1);
sigsuspend(&masca);
}
printf("Proces:%d, pas:%d\n", getpid(), i);
asteapta = 1;
// transfera randul procesului pereche
kill(pidPereche, SIGUSR1);
}
}

147
Comunicarea prin semnale în Linux

10.7. Probleme
1. Să se scrie un program C utilizat pentru copierea intrării standard într-un
fişier, specificat prin linia de comandă. Dacă în intervalul de 3N secunde nu
este introdus nici un caracter de la tastatură, programul este terminat. La
fiecare interval de N secunde neutilizate, utilizatorul va fi atenţionat.
Valoarea constantei N este specificată ca argument al programului în linia
de comandă.
2. Dintr-un fişier text fis.txt două procese, care nu sunt în relaţia părinte-
fiu, trebuie să citească pe rând (alternativ) câte un caracter. Pentru
sincronizarea execuţiei lor, cele două procese folosesc semnale.
Comunicarea reciprocă a PID-urilor se va face prin fişiere pipe cu nume.
Caracterele citite de fiecare proces sunt scrise în fişierele fis1.txt (primul
proces) şi fis2.txt (al doilea proces).
3. Un proces creează în mod continuu la anumite perioade timp câte un
proces fiu. Un proces fiu doarme pentru un număr aleator de secunde
(sau milisecunde) şi apoi se termină. La apăsarea combinaţiei de taste
CRL+C procesul va afişa câţi fii a creat şi câţi s-au terminat până în acel
moment. Să se scrie programul C corespunzător procesului părinte şi a
fiilor săi.
4. Se dau trei fişiere cu numele nume.txt, pren.txt şi nota.txt în care sunt
înregistrate, câte unul pe linie, numele, prenumele şi nota obţinută de mai
mulţi studenţi. Să se scrie un program C, prin lansarea căruia se generează
3 procese, fiecare proces citind datele dintr-un fişier dintre cele trei. Să se
sincronizeze funcţionarea celor trei procese astfel încât într-un fişier
tabel.txt să se scrie pe câte o linie informaţiile citite de ele de pe liniile cu
acelaşi număr de ordine sub forma: „NUME PRENUME NOTA”.
Sincronizarea se va face prin semnale.
5. Să se scrie un program C prin care să se testeze modalitatea de
funcţionare a apelurilor sistem ce au ca efect punerea procesului apelant
în stare de aşteptare în situaţia întreruperii lor de către semnale. Astfel
de apeluri sistem sunt read şi write.
6. Să se scrie un program C care să măsoare pentru o anumită perioadă,
timpul petrecut de acel proces în execuţie în mod nucleu, timpul petrecut
de acel proces în execuţie în mod utilizator şi timpul cât acel proces a
aşteptat după procesor.

148
11. Comunicarea prin memorie partajată şi
cozi de mesaje în Linux

Scopul lucrării
Lucrarea prezintă modalitatea de comunicare prin memorie partajată şi cozi
de mesaje în Linux între procese aflate pe acelaşi sistem. Sunt descrise atât
principiile care stau la baza celor două mecanisme de comunicare între
procese, cât şi apelurile sistem prin care ele sunt folosite şi gestionate de
către utilizator.

11.1. Comunicarea între procese prin memorie


partajată
Spaţiile de adrese virtuale ale proceselor sunt mapate în memoria fizică în
zone disjuncte pentru a asigura protecţia resurselor alocate unui proces de
accesul neautorizat al unui alt proces. Prin urmare, nu există, în mod
normal, o intersecţie, la nivelul memoriei fizice, între spaţiile de adrese ale
proceselor. O excepţie de la această regulă poate fi maparea segmentului de
cod al unor procese diferite, dar care rulează acelaşi cod executabil
(rezultate în urma lansării în execuţie a aceluiaşi program), în aceeaşi zonă
de memorie fizică pentru o utilizare mai eficientă a acesteia. O astfel de
intersectare a două spaţii de adrese ale proceselor formează ceea ce se
numeşte memorie partajată sau memorie folosită în comun. În cazul
segmentelor de cod această partajare are ca scop partajarea de către mai
multe procese a unei zone de memorie cu conţinut comun: segmentul de
cod. Acesta fiind accesat în mod normal doar în citire, partajarea lui nu are
ca efect influenţarea în vreun fel a execuţiei vreunui proces de către un alt
proces dintre cele implicate. Nu se poate vorbi aşadar de o comunicare între
procese în acest caz. Pe de altă parte însă, scrierea într-o zonă de memorie
partajată poate influenţa execuţia altor procese care citesc date din acea
zonă de memorie şi prin urmare această operaţie poate fi privită ca o
modalitate de transmitere de date între procese.
Partajarea unei zone de memorie între procese, cu posibilitatea de scriere şi
citire din acea zonă, este o tehnică cunoscută sub numele de comunicare
între procese prin memorie partajată. Menţionăm că acest mecanism de
comunicare poate fi folosit doar pentru procese care au acces la acelaşi
spaţiu de memorie fizică, fie ea memoria locală a unui sistem – caz în care

149
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

comunicarea se poate face doar între procesele ce rulează pe acel sistem, fie
o memorie distribuită – caz în care comunicarea poate avea loc şi între
procese ce rulează pe sisteme diferite, dar pentru care memoria apare ca
fiind locală. Modul în care sistemul de operare realizează partajarea unei
zone de memorie de către două procese este ilustrat în Figura 1.

Figura 1. Partajarea unei zone de memorie între două procese

Partajarea memoriei este o tehnică specifică sistemelor multi-thread, thread-


urile aceluiaşi proces partajând acelaşi spaţiu de adrese, cel al procesului
căruia îi aparţin. Accesul la zone de memorie comună prezintă avantajul
unei comunicări directe şi rapide între procese, fiind de fapt cel mai rapid
mecanism de comunicare între procese. Practic, comunicarea se produce în
mod automat şi transparent pentru procese, prin scrierea şi citirea din zona
de memorie partajată. În acelaşi timp, acest mod transparent de comunicare
prezintă şi pericolul unor accesări nesincronizate ale zonei de memorie de
către procese diferite, lucru ce poate duce la generarea unui conţinut
inconsistent al zonei de memorie şi, implicit, la comunicarea de date
incorecte între procese. Din acest motiv, accesul la zonele de memorie
partajată trebuie făcut în mod controlat, prin respectarea unor reguli de
acces, impuse, în general, prin folosirea unor mecanisme speciale de
sincronizare a execuţiei proceselor.

150
Sisteme de operare. Chestiuni teoretice şi practice

Utilizarea unei zone de memorie partajată în Linux necesită efectuarea


următorilor paşi:
• crearea ei de către un proces, lucru ce are ca efect alocarea ei în
memoria fizică;
• maparea unei zone din spaţiul virtual de adrese al procesului care
doreşte accesul la zona de memorie partajată peste memoria fizică
alocată acelei zone partajate; această operaţie se numeşte în
terminologia Linux ataşare a zonei partajate la spaţiul virtual de
adrese ale procesului;
• detaşarea memoriei partajate în momentul în care un proces nu mai
doreşte accesul le ea;
• ştergerea zonei, atunci când nu se mai doreşte utilizarea ei.

Crearea zonei de memorie partajată


Primul pas pe care trebuie să-l efectueze orice proces care doreşte să
acceseze o zonă de memorie partajată existentă este cel de obţinere a unui
identificator de acces la zona fizică de memorie partajată. Această operaţie
se face folosind acelaşi apel sistem ca şi cel de creare a zonei, şi anume
shmget, a cărui sintaxă şi funcţionalitate sunt prezentate mai jos:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t cheie, int dimensiune, int optiuni);

O zonă de memorie partajată fiind privită ca un „obiect” fizic al sistemului


de operare, identificarea sa se face pe baza unui identificator sau a unei chei
a cărei valoare este specificată de parametrul cheie. Toate procesele care
doresc accesul la o aceeaşi zonă de memorie partajată, deci care doresc să
folosească în comun acea zonă, trebuie să folosească aceeaşi cheie ca
parametru al funcţiei shmget. Dacă zona de memorie există deja, funcţia
shmget întoarce ca rezultat un identificator de acces la acea zonă (un
descriptor), descriptor care va fi folosit pentru ataşarea zonei de către
procese. Dacă zona de memorie nu există, ea poate fi creată, în funcţie de
valoarea parametrului optiuni. La crearea zonei se vor aloca numărul
minim de pagini de memorie necesare pentru a cuprinde numărul de octeţi
indicaţi prin parametrul dimensiune. Indiferent de valoarea acestui
parametru, dimensiunea unei zone de memorie partajată va fi un multiplu de
dimensiunea unei pagini. Pentru aflarea dimensiunii unei pagini se poate
folosi funcţia getpagesize. Valoarea celui de-al treilea parametru al funcţiei

151
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

poate fi exprimată ca o operaţie SAU pe biţi între următoarele constante


predefinite:

IPC_CREAT
Are ca efect crearea unei noi zone de memorie partajată. Dacă
această opţiune nu este folosită, funcţia shmget va verifica existenţa
zonei de memorie indicată de primul parametru şi permisiunea
procesului apelant de a accesa acea zonă. Dacă această opţiune este
folosită în cazul în care zona de memorie există deja, se va verifica
dacă noua dimensiune indicată de parametrul dimensiune este mai
mică sau egală cu actuala dimensiune a zonei şi dacă există drepturi
de acces la ea. În caz afirmativ se va întoarce ca rezultat un
descriptor de acces la acea zonă, iar în caz negativ funcţia se va
termina fără succes şi va întoarce ca rezultat valoarea -1.

IPC_EXCL
Indică faptul că zona de memorie nu trebuie creată dacă ea există şi
s-a folosit opţiunea IPC_CREAT, caz în care funcţia shmget nu va fi
executată cu succes şi va returna valoarea -1.

Drepturi de acces
Sunt reprezentarea în cifre octale a celor nouă biţi care indică
permisiunile de acces la zona de memorie, similar cu drepturile de
acces la fişiere, cu deosebirea că dreptul de execuţie este ignorat.

Deoarece orice proces poate folosi orice valoare ca şi cheie a unei zone de
memorie partajată, în cazul în care se doreşte crearea unei noi zone de
memorie partajată şi nu se cunoaşte în mod sigur valoarea unei chei
neutilizate, se poate folosi pe poziţia primului parametru al funcţie shmget
constanta predefinită IPC_PRIVATE. Evident, în acest caz zona de memorie
creată nu poate fi identificată printr-o anumită cheie şi prin urmare, nu va
putea fi folosită decât la comunicarea între procesul care a creat-o şi
descendenţi ai săi (procese create de el şi de fiii săi).

O funcţie utilă în încercarea de a asigura unicitatea unei chei într-un anumit


context este funcţia ftok, descrisă mai jos:

# include <sys/types.h>
# include <sys/ipc.h>
key_t ftok(const char *caleFisier, int identificator);

152
Sisteme de operare. Chestiuni teoretice şi practice

Funcţia ftok întoarce ca rezultat o cheie obţinută prin combinarea numărului


de i-node al fişierului indicat prin parametrul caleFisier şi cei mai puţin
semnificativi 8 biţi ai parametrului identificator. Cheia obţinută este
aceeaşi în cazul folosirii aceluiaşi fişier şi identificator, indiferent de calea
specificată pentru acel fişier. Deşi funcţia ftok încearcă obţinerea unei chei
unice pentru valori diferite ale celor doi parametri, ea nu prezintă totuşi
această garanţie.

Ataşarea şi detaşarea zonei de memorie partajată


Pentru accesarea unei zone de memorie partajată de către un proces, ea
trebuie ataşată spaţiului de adrese al acelui proces, lucru ce poate fi făcut
prin apelul sistem shmat. Detaşarea zonei de memorie partajată se face cu
ajutorul apelului sistem shmdt. Sintaxa celor două funcţii este următoarea:

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int id, const void *adresa, int optiuni);
int shmdt(const void *adresa);

Parametrul id al funcţiei shmat reprezintă descriptorul de acces la zona de


memorie partajată, descriptor întors ca rezultat de către shmget.
Parametrul adresa indică locaţia din spaţiul virtual de adrese al procesului
în care se doreşte ataşarea (maparea) memoriei partajate. Dacă valoarea lui
este NULL, atunci sistemul de operare va alege o adresă nefolosită din
spaţiul virtual de adrese, adresă care să fie multiplu de dimensiunea unei
pagini. Dacă valoarea lui este diferită de NULL şi opţiunea SHM_RND este
indicată în parametrul optiuni, atunci adresa la care se va ataşa memoria
partajată este cel mai mare multiplu de dimensiunea unei pagini (constanta
SHMLBA) mai mic decât valoarea parametrului adresa, iar dacă opţiunea
SHM_RND nu este folosită, atunci adresa indicată trebuie să fie aliniată la
adresă de pagină.
Parametrul optiuni poate avea o valoare rezultată dintr-un SAU pe biţi între
următoarele constante predefinite: SHM_RND, SHM_RDONLY (ataşarea doar
pentru citire a memoriei partajate), SHM_REMAP (maparea zonei de memorie
peste un spaţiu pe care deja a fost mapată o altă zonă de memorie partajată).

Funcţia shmat întoarce ca rezultat adresa de memorie – un pointer – la care


a fost ataşată (mapată) zona de memorie partajată. Accesul la zona de

153
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

memorie se va face prin pointer-ul respectiv, care poate fi interpretat ca


indicând elemente de un anumit tip. În caz de eroare funcţia întoarce -1.

Funcţia shmdt are ca efect detaşarea din spaţiul virtual de adrese al procesului
apelant a zonei de memorie partajată indicată de adresa specificată prin
parametrul adresa. Rezultatul întors este 0 (zero) în caz de succes, respectiv
-1 în caz de eroare. O zonă de memorie detaşată nu este ştearsă, acest lucru
făcându-se numai cu apelul sistem shmctl, descris mai jos.

Controlul zonei de memorie partajată


O dată creată o zonă de memorie partajată, sistemul de operare menţine
informaţii despre ea în cadrul unei structuri de date de tipul shmid_ds, ale
cărei câmpuri sunt descrise mai jos:

struct shmid_ds {
struct ipc_perm shm_perm; // permisiuni
int shm_segsz; // dim. in octeti
time_t shm_atime; // ultimul attach
time_t shm_dtime; // ultimul detach
time_t shm_ctime; // ultima modificare
// a structurii
unsigned short shm_cpid; // pid proces creator
unsigned short shm_lpid; // pid ultim proces
// ce a accesat zona
short shm_nattch; // nr. de procese ce
// au atasata zona
};

struct ipc_perm {
key_t key; // cheia zonei
ushort uid; // uid efectiv proprietar
ushort gid; // gid efectiv proprietar
ushort cuid; // uid efectiv utilizator creator
ushort cgid; // uid efectiv utilizator creator
ushort mode; // permisiuni
ushort seq; // numar de secventa
};

Valorile acestor câmpuri sunt modificate automat de către sistemul de operare


în timpul creării zonei şi pe măsură ce aceasta este ataşată şi accesată de
diferite procese. Există însă şi posibilitatea citirii acestor informaţii de către
utilizator sau chiar a modificării unora dintre ele cu ajutorul apelului sistem
shmctl.

154
Sisteme de operare. Chestiuni teoretice şi practice

Acelaşi apel sistem poate fi folosit şi pentru ştergerea zonei de memorie


partajată. Sintaxa lui este:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int id, int cmd, struct shmid_ds *buf);

Parametrul id este descriptorul zonei de memorie partajată obţinut de


procesul apelant prin apelul anterior al funcţiei shmget. Parametrul cmd indică
operaţia (comanda) care se va efectua asupra structurii shmid_ds asociată
zonei de memorie partajată, putând lua una dintre următoarele valori:

IPC_STAT
Indică operaţia de obţinere a informaţiilor despre zona de memorie
partajată, informaţii care vor fi stocate în câmpurile unei structuri
shmid_ds, a cărei adresă este indicată prin parametrul buf.
Procesul apelant trebuie să aibă drepturi de citire asupra zonei de
memorie partajată pentru a putea efectua această operaţie.

IPC_SET
Indică operaţia de modificare a unor informaţii ce descriu zona de
memorie partajată. Modificările sunt reflectate de valorile
câmpurilor structurii shmid_ds, a cărei adresă este indicată prin
parametrul buf. Câmpurile care pot fi modificate sunt:
shm_perm.uid (proprietar), shm_perm.gid (grup proprietar) şi
shm_perm.mode (permisiuni de acces). Câmpul shm_ctime
(timpul ultimei modificări a structurii shmid_ds a zonei de
memorie partajată) este automat actualizat la valoarea curentă a
ceasului sistem. Procesul apelant trebuie să aparţină utilizatorului
proprietar sau creator al zonei de memorie partajată sau să aibă
privilegii de administrator.

IPC_RMID
Indică operaţia de ştergere a zonei de memorie partajată. Ştergerea
efectivă a zonei se va face doar după ce ea nu va mai fi ataşată nici unui
proces, adică atunci când valoarea câmpului shm_nattch devine 0,
însă între timp ea nu va mai putea fi accesată şi ataşată de alte procese
în afara celor care deja o au ataşată. Procesul apelant trebuie să aparţină
utilizatorului creator sau proprietar al zonei de memorie partajată sau să
aibă privilegii de administrator. Valoarea parametrului buf nu este
folosită în cazul acestei operaţii şi de obicei ea este NULL.

155
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

Comenzi shell pentru memorie partajată


Zonele de memorie partajată create în sistem pot fi vizualizate cu ajutorul
comenzii ipcs. Numele comenzii este format din iniţialele cuvintelor Inter-
Process Communication Status şi ea are ca efect afişarea de informaţii
despre diferite resurse de tipul mecanisme de comunicare între procese,
printre care sunt şi zonele de memorie partajată. Opţiunea comenzii care are
ca efect afişarea doar a informaţiilor despre zonele de memorie partajată
este –m. Alte opţiuni utile, valabile pentru toate mecanismele de comunicare
între procese, deci şi pentru zonele de memorie partajată, sunt:
-t Afişarea informaţiilor legate de timpul creării, modificării, ultimului
acces.
-p Afişarea identificatorilor proceselor care au folosit mecanismele de
comunicare între procese şi modul în care le-au folosit.
-l Afişarea unor limite de dimensiune impuse mecanismelor de
comunicare între procese.
-u Afişarea unei scurte statistici legate de utilizarea mecanismelor de
comunicare între procese.
-i id
Afişarea de informaţii doar despre resursa al cărei descriptor este cel
dat de valoarea parametrului id. Această opţiune poate fi folosită în
combinaţie cu cele prezentate mai sus.

Comanda ipcs afişează atât cheia pe baza căreia a fost creată o resursă de
tip comunicare între procese, în cazul nostru zona de memorie partajată, cât
şi identificatorul returnat proceselor care obţin accesul la acea zonă prin
apelul funcţiei shmget.
O altă comandă utilă este cea de ştergere a resurselor create de sistemul de
operare pentru comunicarea între procese. Comanda se numeşte ipcrm (IPC
Remove) şi necesită specificarea cheii sau a identificatorului zonei de
memorie partajată, sub una din formele următoare:
ipcrm –m identificator
ipcrm –M cheie

Zona de memorie specificată va fi ştearsă doar după ce toate procesele care


o aveau ataşată în momentul apelului comenzii o detaşează de spaţiul lor de
adrese. Ştergerea unei zone de memorie partajată poate fi făcută doar de
către administratorul sistemului, utilizatorul proprietar sau cel creator al
respectivei zone de memorie.

156
Sisteme de operare. Chestiuni teoretice şi practice

11.2. Comunicarea între procese prin cozi de mesaje


Cozile de mesaje sunt un alt mecanism pe care sistemul de operare Linux îl
pune la dispoziţia proceselor de pe acelaşi sistem, care doresc să comunice,
adică să-şi transmită reciproc informaţii. Comunicarea folosind acest
mecanism se face la nivel de blocuri de octeţi, care formează structuri de
date interpretate şi manipulate ca mesaje, sistemul de operare făcând
distincţie între un mesaj şi un altul. Transmiterea informaţiei între procese
prin cozi de mesaje este similară celei folosite în cazul fişierelor pipe, coada
de mesaje putând fi privită ca un pipe specializat. Astfel, ordinea operaţiilor
de scriere în coada de mesaje şi de citire din ea este impusă de principiul
FIFO (First-In First-Out), iar comunicarea este – în mod implicit – una de
tip sincron, adică operaţia de citire a unui mesaj este sincronizată cu cea de
scriere, în sensul că se suspendă execuţia procesului care vrea să preia un
mesaj până în momentul în care cineva scrie mesajul aşteptat în coadă.
Mesajele care se transmit prin coada de mesaje sunt structurate sub forma a
două componente: o componentă de descriere a mesajului (header) şi una de
conţinut (corpul mesajului). Nu există o structură impusă la nivel de
conţinut al mesajelor, utilizatorul putându-şi defini propriile tipuri de
mesaje, cu structuri şi dimensiuni diferite. Din cadrul componentei de
descriere a mesajelor utilizatorul are acces doar la un câmp, care îi şi este,
de altfel, impus în structura mesajului. Acest câmp este interpretat ca
identificator al mesajului, identificator folosit de către sistemul de operare
pentru a face distincţie între diferite clase de mesaje. Este evident că există
şi alte informaţii incluse în cadrul header-ului unui mesaj – cum ar fi, de
exemplu, lungimea mesajului –, pe care însă utilizatorul nu le vede ca
aparţinând mesajului. Figura 2 ilustrează modul de transmitere a mesajelor
printr-o coadă de mesaje şi structura mesajelor.

Figura 2. Structura unei cozi de mesaje şi a mesajelor

157
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

Accesul la coada de mesaje este sincronizat în mod automat de către


sistemul de operare, în sensul că dacă mai multe procese efectuează
simultan operaţii de scriere şi citire asupra cozii de mesaje, conţinutul ei şi
al mesajelor manipulate rămâne consistent. În Linux există posibilitatea ca
un proces să ceară recepţionarea dintr-o coadă de mesaje a unui mesaj de un
anumit tip, care s-ar putea să nu fie mesajul care urma în mod normal să fie
citit, lucru care evident nu corespunde principiului FIFO ce stă la baza
comunicării prin cozile de mesaje. Totuşi, chiar şi în acest caz principiul
FIFO se aplică mesajelor din clasa cerută de procesul cititor.

Ca şi în cazul zonelor de memorie partajată, cozile de mesaje sunt create ca


resurse (obiecte) ale sistemului de operare, şi nu ca resurse ale proceselor.
Pentru a le putea folosi, procesele trebuie să obţină accesul la resursele
respective, într-un mod similar cu accesul la un fişier sau, mai exact, în mod
similar cu accesul la zonele de memorie partajată. Descriem mai jos
funcţiile pe care sistemul de operare Linux le pune la dispoziţia aplicaţiilor
utilizator pentru comunicarea prin cozi de mesaje.

Crearea cozii de mesaje


Funcţia de creare a unei cozi de mesaje este msgget. Obţinerea accesului la
o coadă de mesaje creată, operaţie pe care trebuie să o facă orice proces care
doreşte folosirea acelei cozi de mesaje, se face tot prin intermediul funcţiei
respective. Sintaxa ei este descrisă mai jos.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t cheie, int optiuni);

Parametrul cheie reprezintă identificatorul public al cozii de mesaje, cu alte


cuvinte un fel de nume al cozii de mesaje. Reamintim în acest sens faptul că
funcţia ftok poate fi folosită pentru a obţine dintr-o cale de fişier şi un număr
întreg o cheie, în încercarea de a folosi ca nume public al unei cozi de mesaje
un şir de caractere şi nu direct o cheie. Toate procesele care doresc să
comunice printr-o anumită coadă de mesaje, trebuie să obţină accesul la acea
coadă de mesaje folosind cheia sub care a fost creată coada respectivă.

Precizăm faptul că aceeaşi valoare poate fi folosită ca şi cheie, atât pentru o


coadă de mesaje, cât şi pentru o zonă de memorie partajată, fără a se realiza
interferenţe sau confuzii între ele, cele două categorii de resurse (şi altele,

158
Sisteme de operare. Chestiuni teoretice şi practice

cum ar fi semafoarele) fiind gestionate în mod independent de către sistemul


de operare.

Valoarea parametrului cheie poate fi constanta predefinită IPC_PRIVATE,


în cazul în care se doreşte crearea unei noi cozi de mesaje şi nu se cunoaşte
în mod sigur valoarea unei chei neutilizate. O astfel de coadă de mesaje va
putea fi folosită însă doar la comunicarea între procesul care a creat-o şi
descendenţi ai săi (procese create de el şi de fiii săi).

Parametrul optiuni are aceeaşi semnificaţie ca şi în cazul funcţiei shmget


de creare a zonelor de memorie partajată, având valoarea 0, în cazul în care
se doreşte numai obţinerea accesului la o coadă de mesaje deja creată sau o
combinaţie (SAU pe biţi) a următoarelor constante, în cazul în care se
doreşte crearea unei noi cozi de mesaje:

IPC_CREAT
Are ca efect crearea unei noi cozi de mesaje. Dacă această opţiune
nu este folosită, funcţia msgget va verifica existenţa cozii de mesaje
indicată de parametrul cheie şi permisiunea procesului apelant de a
accesa acea resursă. Dacă această opţiune este folosită în cazul în
care coada de mesaje există deja, se va verifica doar dacă procesul
apelant are drepturi de acces la ea. În caz afirmativ se va întoarce ca
rezultat un descriptor de acces la acea coadă de mesaje, iar în caz
negativ funcţia se va termina fără succes şi va întoarce ca rezultat
valoarea -1.

IPC_EXCL
Această opţiune se foloseşte doar în combinaţie cu IPC_CREAT şi
indică faptul că nu trebuie creată coada de mesaje dacă ea există
deja, caz în care funcţia msgget nu va fi executată cu succes şi va
returna valoarea -1.

Drepturi de acces
Sunt reprezentarea în cifre octale a celor nouă biţi care indică
permisiunile de acces la coada de mesaje, similar cu drepturile de
acces la fişiere, cu deosebirea că dreptul de execuţie este ignorat.
Dreptul de scriere este interpretat ca permisiune de trimitere a
mesajelor prin coada de mesaje, iar cel de citire ca permisiune de
preluare (recepţionare) a mesajelor.

159
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

Funcţia msgget întoarce ca rezultat, în cazul în care se execută cu succes, un


număr pozitiv, interpretat ca descriptor de acces la coada de mesaje indicată
sau -1, în caz de eroare.

Comunicarea prin coada de mesaje


Comunicarea prin cozi de mesaje înseamnă trimiterea, respectiv
recepţionarea de mesaje. Funcţiile prin care se realizează acest lucru sunt
msgsnd şi msgrcv, sintaxa lor fiind descrisă mai jos:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msgbuf {
long mtype; // tip mesaj
char mtext[1]; // continut mesaj
};
int msgsnd(int id, struct msgbuf *msg,
size_t dimMsg, int optiuni);
ssize_t msgrcv(int id, struct msgbuf *msg,
size_t dimMsg, long tip, int optiuni);

Ambele funcţii primesc ca prim parametru identificatorul de acces la coada


de mesaje, identificator returnat de funcţia msgget.

Al doilea parametru este adresa unei structuri de tipul struct msgbuf,


adresă de la care se preia mesajul ce trebuie trimis prin coada de mesaje, în
cazul funcţiei msgsnd, respectiv la care se depune mesajul preluat din coada de
mesaje, în cazul funcţiei msgrcv. Structura mesajului este stabilită de către
utilizator, singurul câmp impus fiind mtype, pe care sistemul de operare îl
foloseşte ca tip al mesajului. Câmpul mtext este folosit doar pentru a indica
începutul corpului mesajului (un şir de octeţi), al cărui conţinut (structură) este
definit de către utilizator. De fapt, utilizatorul poate să redefinească conţinutul
(şi chiar şi numele câmpurilor) structurii msgbuf sau poate să-şi definească
propria structură (cu alt nume), corespunzătoare mesajelor pe care doreşte să le
transmită prin coada de mesaje. De exemplu, structura mesajului poate fi
definită astfel:
struct msgbuf {
long tipMesaj;
char nume[100];
int varsta;
};

160
Sisteme de operare. Chestiuni teoretice şi practice

Parametrul dimMsg indică dimensiunea câmpurilor mesajului cu excepţia


câmpului care corespunde tipului mesajului, conform formulei:
dimMsg = sizeof(struct msgbuf) – sizeof(long);

Această valoare este folosită în cazul funcţiei msgsnd pentru a se cunoaşte


dimensiunea mesajului care se transmite prin coada de mesaje, iar în cazul
funcţiei msgrcv pentru a se cunoaşte limita maximă a mesajului care poate fi
preluat din coada de mesaje (sau cât spaţiu e rezervat pentru un mesaj la
adresa indicată de msg).

Parametrul tip al funcţiei msgrcv indică ce tip de mesaj se doreşte a fi


preluat din coada de mesaje. Se va prelua primul mesaj – în ordinea FIFO –
de acel tip din coadă. În funcţie de valoarea pe care o poate avea acest
parametru există următoarele cazuri:
• valoarea 0: se va prelua primul mesaj din coadă, indiferent de tipul său;
• o valoare pozitivă: se va prelua primul mesaj cu tipul egal cu acea valoare;
• o valoare negativă: se va prelua primul mesaj cu cel mai mic tip mai
mic sau egal decât valoarea absolută a parametrului tip.

Parametrul optiuni poate avea valoarea 0 sau poate fi obţinut printr-o


combinaţie (SAU pe biţi) a următoarelor constante predefinite:

IPC_NOWAIT
Indică faptul că procesul care apelează funcţiile msgsnd şi msgrcv nu
trebuie să fie pus în aşteptare în cazurile în care, în mod normal, apelul
lor ar avea acest efect. Într-o astfel de situaţie se revine imediat din cele
două funcţii, iar rezultatul întors de ele este -1 (eroare), variabila
errno fiind setată la valoarea EAGAIN, de către msgsnd şi respectiv,
ENOMSG de către msgrcv. Funcţia msgsnd blochează procesul apelant în
cazul în care nu mai este suficient spaţiu în coada de mesaje pentru
mesajul care trebuie transmis, până în momentul în care în coadă se
eliberează spaţiul necesar sau se şterge coada de mesaje. Funcţia
msgrcv blochează procesul apelant dacă nu există nici un mesaj de tipul
indicat, până la apariţia unui mesaj de acel tip sau ştergerea cozii de
mesaje.

MSG_EXCEPT
Această opţiune se utilizează doar pentru funcţia msgrcv şi doar în
cazul utilizării unei valori diferite de zero a parametrului tip.
Efectul ei este acela de a indica extragerea din coada de mesaje a
primului mesaj cu tipul diferit de cel indicat.

161
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

MSG_NOERROR
Această opţiune se utilizează doar pentru funcţia msgrcv şi indică
faptul că dacă lungimea mesajului care urmează a fi preluat din coada
de mesaje este mai mare decât dimensiunea maximă indicată de
parametrul dimMsg, atunci mesajul trebuie trunchiat la lungimea
maximă acceptată. Octeţii trunchiaţi ai mesajului sunt extraşi din coada
de mesaje, dar sunt definitiv pierduţi. Într-o aceeaşi situaţie, dar fără
specificarea opţiunii MSG_NOERROR, funcţia msgrcv eşuează şi întoarce
rezultatul -1, iar variabila errno este setată la valoarea E2BIG.

În cazul în care funcţiile msgsnd şi msgrcv sunt apelate pentru o coadă de


mesaje care este ştearsă, ele returnează valoarea -1, iar variabila errno este
setată la valoarea EIDRM.
În caz de succes, funcţia msgsnd întoarce 0, iar funcţia msgrcv numărul de
octeţi recepţionaţi din coada de mesaje. În caz de eşec, ambele funcţii
returnează -1.

Controlul cozii de mesaje


Pentru fiecare coadă de mesaje, sistemul de operare menţine o structură de
date de tipul struct msqid_ds, definită în fişierul <sys/msg.h>, ale cărei
câmpuri sunt următoarele:

struct msqid_ds {
struct ipc_perm msg_perm; // permisiuni de acces
ushort msg_qnum; // nr. mesaje din coada
ushort msg_qbytes; // nr. max. de octeti permisi
// pt. continutul total al msg
ushort msg_lspid; // pid-ul ultimului msgsnd
ushort msg_lrpid; // pid-ul ultimului msgrcv
time_t msg_stime; // momentul ultimului msgsnd
time_t msg_rtime; // momentul ultimului msgrcv
time_t msg_ctime; // momentul ultimei modificari
// a structurii msgid_ds
};
struct ipc_perm {
key_t key; // cheia cozii de mesaje
ushort uid; // uid efectiv proprietar
ushort gid; // gid efectiv proprietar
ushort cuid; // uid efectiv utilizator creator
ushort cgid; // uid efectiv utilizator creator
ushort mode; // permisiuni
ushort seq; // numar de secventa
};

162
Sisteme de operare. Chestiuni teoretice şi practice

Câmpurile structurilor descrise mai sus sunt în mod automat modificate


(actualizate) de către sistemul de operare în urma operaţiilor efectuate
asupra cozii de mesaje. Astfel, spre exemplu, câmpurile msg_lspid,
msg_qnum şi msg_stime sunt modificate în urma unui apel al funcţiei
msgsnd în felul următor: msg_lspid la valoarea identificatorului
procesului care a apelat funcţia, msg_qnum este incrementat cu 1 şi
msg_stime la valoarea timpul la care a fost efectuat apelul. Câmpurile
msg_lrpid, msg_qnum şi msg_rtime sunt modificate în mod similar în
urma unui apel al funcţiei msgrcv, cu deosebirea că msg_qnum este
decrementat cu 1. Există însă şi posibilitatea ca aplicaţiile utilizator să
acceseze în mod direct câmpurile structurii msqid_ds, fie pentru citirea
valorilor lor, fie chiar pentru modificarea unora dintre ele. Acest lucru este
posibil folosind funcţia msgctl. În mod similar cu shmctl, funcţia msgctl
poate fi folosită şi pentru ştergerea cozii de mesaje. Sintaxa ei este
următoarea:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int id, int cmd, struct msqid_ds *buf);

Parametrul id este descriptorul cozii de mesaje obţinut de procesul apelant


cu ajutorul funcţiei msgget. Parametrul cmd indică operaţia (comanda) care
se va efectua asupra structurii msqid_ds asociate cozii de mesaje, putând
lua una dintre următoarele valori:

IPC_STAT
Indică operaţia de obţinere a valorilor câmpurilor structurii
msqid_ds asociate cozii de mesaje, informaţii ce vor fi scrise în
memorie la adresa indicată de parametrul buf. Procesul apelant
trebuie să aibă drepturi de citire a cozii de mesaje pentru a putea
efectua această operaţie.

IPC_SET
Indică intenţia de modificare a unor câmpuri ale structuri shmid_ds
asociate cozii de mesaje. Valorile lor sunt luate de la adresa indicată
de parametrul buf. Câmpurile care pot fi modificate sunt:
msg_perm.uid (proprietar), msg_perm.gid (grup proprietar),
msg_perm.mode (permisiuni de acces) şi msg_qbytes (numărul
maxim de octeţi ai conţinutului mesajelor din coadă). Câmpul
shm_ctime (timpul ultimei modificări a structurii msqid_ds) este

163
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

automat actualizat la valoarea curentă a ceasului sistem. Procesul


apelant trebuie să aparţină utilizatorului proprietar sau creator al
cozii de mesaje sau să aibă privilegii de administrator.

IPC_RMID
Indică operaţia de ştergere a cozii de mesaje. Ştergerea se face
imediat, toate procesele care erau blocate în apeluri ale funcţiilor
msgsnd sau msgrcv fiind deblocate, funcţiile returnând -1 (eroare),
iar variabila de sistem errno fiind setată la EIDRM. Procesul apelant
trebuie să aparţină utilizatorului creator sau proprietar al cozii de
mesaje sau să aibă privilegii de administrator. Valoarea parametrului
buf nu este folosită în cazul acestei operaţii şi, de obicei, ea este
setată la NULL.

Comenzi shell pentru cozile de mesaje


Comenzile ipcs şi ipcrm descrise la capitolul despre zonele de memorie
partajată pot fi folosite şi în cazul cozilor de mesaje. Astfel comanda ipcs
afişează informaţii şi despre cozile de mesaje, iar ipcrm poate fi folosită şi
pentru ştergerea cozilor de mesaje. Opţiunea ce indică comenzii ipcs
afişarea doar a informaţiilor despre cozile de mesaje este -q. Pentru
ştergerea unei cozi de mesaje, comanda ipcrm trebuie executată în una
dintre următoarele forme, care cer specificarea fie a identificatorului asociat
cozii de mesaje, fie a cheii folosite la crearea ei:

ipcrm –q identificator
ipcrm –Q cheie

Ştergerea unei cozi de mesaje poate fi făcută doar de către administratorul


sistemului, utilizatorul proprietar sau creator al respectivei cozi de mesaje.

11.3. Exemple
Exemplul 1. Exemplul de mai jos ilustrează modul de utilizare alternativă a
unei zone de memorie partajată ca şi un şir de întregi, respectiv ca şir de
octeţi. Scopul programului este acela de a afişa reprezentarea din memorie,
la nivel de octet, a întregilor înscrişi în zona de memorie partajată.

#include <sys/types.h>
#include <sys/shm.h>

#define N 256

164
Sisteme de operare. Chestiuni teoretice şi practice

int main(int argc, char **argv)


{
int shmId, i, *pInt;
char *pChar;
// crearea zonei de memorie partajata
shmId = shmget(10000, N*sizeof(int), IPC_CREAT|0600);
if (shmId < 0) {
perror("Eroare creare mem. partajata");
exit(1);
}
// atasarea zonei si accesare ei ca int
pInt = (int*) shmat(shmId, 0, 0);
if (pInt == -1) {
perror("Eroare atasare mem. partajata");
exit(1);
}
// accesare zonei ca char*
pChar = (char*) pInt;
for (i=0; i<N; i++)
pInt[i] = i;
for (i=0; i<N*sizeof(int); i++) {
if ((i % sizeof(int)) == 0)
printf("%4d: ", pInt[i / sizeof(int)]);
printf("%2x ", (unsigned char) pChar[i]);
if ((i % sizeof(int)) == (sizeof(int) - 1))
printf("\n");
}
printf("\n");
shmdt(pInt); // detasarea zonei
shmctl(shmId, IPC_RMID, 0); // stergerea zonei
}

Exemplul 2. Exemplul de mai jos ilustrează modul de mapare (ataşare) a


unei zone de memorie partajată în spaţiul de adrese al unui proces la o
adresă indicată explicit de acel proces în apelul funcţiei shmat.

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#define SIZE 100

165
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

int main(int argc, char **argv)


{
int *s, *pInt, id, posElemPrim;
printf("SHMLBA=%d\n", SHMLBA); // dim. pagina
s = (int*) calloc(SIZE, sizeof(int));
printf("Memoria dinamica alocata la
adresa s=%x \n", s);
id = shmget(IPC_PRIVATE, SIZE*sizeof(int),
IPC_CREAT | 0644);
if (id < 0) {
perror("Eroare creare mem. partajata");
exit(1);
}
pInt=(int*) shmat(id, s,SHM_RND | SHM_REMAP);
if ((int)pInt == -1){
perror("Eroare atasare mem. partajata");
exit(1);
}
printf("Mememoria partajata atasata la
adresa pInt=%x \n", pInt);
posElemPrim = s - pInt;
printf("Positia primului element din s in pInt
este %d \n", posElemPrim);
s[0]=222;
printf("s[0]=%d este egal cu pInt[%d]=%d\n",
s[0], posElemPrim, pInt[posElemPrim]);
shmdt(pInt);
shmctl(id, IPC_RMID, 0);
}

Exemplul 3. Exemplul de mai jos ilustrează comunicarea bidirecţională


dintre două procese printr-o coadă de mesaje folosind mesaje cu tip, astfel
încât mesajele care sunt destinate unui anumit sens al comunicării nu pot
„circula” în sensul opus. Cu alte cuvinte nu există riscul ca un proces care
scrie în coadă un mesaj destinat celuilalt proces şi imediat citeşte din coadă
în aşteptarea unui mesaj de răspuns de la celălalt proces să îşi citească
propriul mesaj, lucru ce s-ar putea întâmpla dacă nu s-ar folosi mesaje cu tip
distinct pentru cele două sensuri ale comunicării.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

166
Sisteme de operare. Chestiuni teoretice şi practice

#define TYPE1 1
#define TYPE2 2
#define N 1000
#define STEPS 5
typedef struct msg {
long type;
int val;
} MSG;
int main(int argc, char **argv)
{
int id, msgSize, i, step, rcvType;
MSG m1, m2;
if ((argc != 2) ||
((strcmp(argv[1], "1")) &&
(strcmp(argv[1], "2")))) {
printf("Usage: %s 1|2"); exit(1);
}
if (!strcmp(argv[1], "1")) {
id = msgget(10000, IPC_CREAT | 0600);
m1.type = TYPE1; rcvType = TYPE2;
}
else {
id = msgget(10000, 0);
m1.type = TYPE2; rcvType = TYPE1;
}
if (id < 0) {
perror("Nu se poate accesa coada de msg.");
exit(2);
}
msgSize = sizeof(MSG) - sizeof(long);
for (step=1; step<=STEPS; step++) {
for (i=1; i<=N; i++) {
m1.val = i + (atoi(argv[1]) - 1) * N;
msgsnd(id, &m1, msgSize, 0);
printf("Msg. trimis: type=%d, val=%d\n",
m1.type, m1.val);
}
for (i=1; i<=N; i++) {
msgrcv(id, &m2, msgSize, rcvType, 0);
printf("Msg. primit: type=%d, val=%d\n",
m2.type, m2.val);
}
usleep(10000);
}
if (!strcmp(argv[1], "1"))
msgctl(id, IPC_RMID, 0);
}

167
Comunicarea prin memorie partajată şi cozi de mesaje în Linux

11.4. Probleme
1. Să se scrie un program C care să afişeze următoarele tipuri de adrese din
spaţiul de adrese al unui proces: adresa de alocare a segmentului de date
(adresa unde sunt alocate variabilele globale ale programului), adresa de
alocare a segmentului de stivă (adresa unde sunt alocaţi parametrii şi
variabilele funcţiei main), adresa de alocare a variabilelor dinamice,
adresa de mapare a zonelor de memorie partajată. Pe baza informaţiilor
afişate şi a rulării multiple a programului să se încerce ilustrarea
organizării spaţiului de adrese al unui proces.
2. Să se scrie două programe C, prin care se accesează o aceeaşi zonă de
memorie partajată. Procesele corespunzătoare celor două programe sunt
executate la terminale diferite şi trebuie să-şi ataşeze memoria partajată în
propriul spaţiu de adrese la adresa corespunzătoare unui şir de caractere
pe care îl alocă dinamic fiecare dintre procese. Unul dintre procese va citi
apoi de la tastatură în şirul propriu câte o linie, iar cel de-al doilea proces
trebuie să afişeze linia respectivă prin afişarea propriului şir de caractere.
3. Să se scrie codul C al două procese distincte, neaflate în relaţia părinte-
fiu, care îşi ataşează o aceeaşi zonă de memorie partajată în care este
stocat un număr întreg a cărei valoare iniţială este 0. Cele două procese
trebuie să incrementeze strict alternativ (primul proces, al doilea proces,
primul proces, al doilea proces şi aşa mai departe) contorul din zona de
memorie partajată şi să afişeze pe ecran noua sa valoare. Sincronizarea
execuţiei proceselor trebuie făcută cu ajutorul semnalelor. Deoarece
pentru acest lucru e necesar ca cele două procese să îşi cunoască
reciproc identificatorul de proces, ele vor schimba această informaţie
printr-o coadă de mesaje a cărei cheie o cunosc ambele.
4. Să se rezolve problema precedentă pentru situaţia în care sincronizarea
celor două procese se va face prin cozi de mesaje, caz în care nu mai
este nevoie ca ele să-şi cunoască (şi implicit să-şi interschimbe)
identificatorul de proces.
5. Să se implementeze problema precedentă pentru cazul generalizat al N
procese. Problema e similară cu transmiterea prin intermediul unui mesaj,
între procese aflate într-un inel, a unei permisiuni (un token) care dă la un
moment doar unui singur proces dreptul de a accesa contorul, şi anume
procesului care deţine permisiunea. După accesarea contorului permisiunea
este transmisă următorului proces din inel. Această strategie duce la
blocarea tuturor proceselor din inel în cazul opririi unuia dintre ele. Să se
testeze şi această situaţie şi să se propună o soluţie folosind semnale.

168
Sisteme de operare. Chestiuni teoretice şi practice

6. Să se scrie codul sursă C pentru două tipuri de procese: unul server şi


altul client. Serverul acceptă cereri de la clienţi pe o coadă de mesaje.
Cererile trimise de clienţi sunt cereri de creare/ştergere a unor fişiere în
cadrul unui director la care are acces procesul server, respectiv de citire
/scriere din/în fişierele create. Pentru fiecare cerere sosită, serverul
creează un thread, care va rezolva acea cerere. Thread-ul care deserveşte
o cerere dă răspuns la acea cerere folosind aceeaşi coadă de mesaje pe
care sosesc cererile. Procesul client afişează o interfaţă de tip linie de
comandă de la care citeşte comenzi de tipul:
create nume_fisier
read nume_fisier deplasament nr_octeti
remove nume_fisier
write nume_fisier deplasament sir_caractere
Corespunzător unei comenzi citite, formează un mesaj de cerere, pe care
îl scrie în coada de mesaje din care preia mesaje serverul. Va aştepta
apoi răspuns de la server pe aceeaşi coadă de mesaje şi va afişa
rezultatul cererii pe ecran.
7. Să se scrie codul C al trei procese distincte. Primul proces citeşte un şir
de numere întregi scrise fiecare pe câte o linie într-un fişier text nr.in,
numere pe care le transmite printr-o coadă de mesaje celui de-al doilea
proces, care le preia şi le foloseşte pentru generarea unui nou şir pe baza
formulei an = an-1 + an-2. Şirul generat este trimis apoi, prin aceeaşi
coadă de mesaje, celui de-al treilea proces, care îl va scrie în fişierul
nr.out, fiecare număr pe câte o linie. La terminarea fişierului nr.in,
primul proces trimite un mesaj de sfârşit celui de-al doilea proces, iar
acesta un mesaj similar celui de-al treilea proces.
8. Să se scrie două programe C, unul numit server, iar celălalt numit client.
Cele două procese comunică printr-o coadă de mesaje. Procesul client
citeşte din fişierul text nr.in de pe fiecare linie două numere întregi între
care se găseşte, separat prin spaţii, unul din caracterele '+', '-', '*',
'/', care indică operaţia ce trebuie efectuată cu cele două numere.
Clientul formează un mesaj pe care-l trimite serverului. Acesta preia
mesajul, îl decodifică, efectuează operaţia şi transmite clientului
rezultatul. Clientul scrie în fişierul text nr.out linia citită din fişierul de
intrare urmată de caracterul '=' şi de rezultatul primit de la server.
9. Să se folosească comanda ipcs cu opţiunea –l pentru a vizualiza
limitările impuse de sistemul de operare asupra mecanismelor de
comunicare între procese de tip memorie partajată şi cozi de mesaje.

169
12. Sincronizarea prin semafoare în Linux

Scopul lucrării
Lucrarea prezintă câteva aspecte legate de sincronizarea execuţiei
proceselor folosind semafoare şi apelurile sistem puse la dispoziţie de
sistemul de operare Linux pentru folosirea şi manipularea semafoarelor.

12.1. Sincronizarea cu semafoare


Semaforul este un mecanism de sincronizare a execuţiei proceselor care
acţionează în mod concurent asupra unor resurse partajate. Zonele de cod în
care un proces accesează şi, eventual, modifică resursele partajate cu alte
procese se numesc zone sau regiuni critice. Sincronizarea înseamnă, la modul
general, respectarea unor reguli de acces la resursele partajate. La modul
concret, înseamnă intrarea unui proces într-o zonă de cod critică doar dacă are
acordul comun sau permisiunea tuturor celorlalte procese implicate, în caz
contrar procesul fiind suspendat până în momentul în care o astfel de
permisiune e disponibilă, adică condiţiile sunt sigure pentru accesul la resursă.
Scopul sincronizării este evitarea situaţiilor care ar duce la o stare inconsistentă
şi nedeterminată a resurselor partajate. Cererea, respectiv acordarea unei
permisiuni presupune un schimb de informaţii între procesele concurente şi, în
acest sens, mecanismele de sincronizare sunt mecanisme de comunicare între
procese. Semaforul poate fi privit aşadar ca un mecanism de comunicare între
procese, scopul comunicării fiind sincronizarea execuţiei proceselor.
Semaforul constă, în esenţă, dintr-o valoare întreagă şi o coadă de aşteptare.
Mecanismul în sine asigură acordarea de permisiuni de intrare în zonele
critice, valoarea semaforului la un moment dat indicând numărul de
permisiuni disponibile la acel moment. Sincronizarea cu semafoare presupune
cererea unei permisiuni înainte de intrarea într-o zonă critică şi respectiv,
eliberarea permisiunii acordate la ieşirea din zona critică respectivă. Toate
procesele care doresc să-şi sincronizeze execuţia la o aceeaşi resursă partajată
trebuie să folosească în acest scop acelaşi semafor sau set de semafoare. În
urma acordării unei permisiuni unui proces, valoarea semaforului este
decrementată, iar în urma eliberării, ea este incrementată. Dacă în momentul
cererii unei permisiuni valoarea semaforului este zero, execuţia procesului
care a cerut permisiunea este suspendată şi procesul este pus în coada de
aşteptare a semaforului, procesorul fiind alocat altui proces. În momentul

170
Sisteme de operare. Chestiuni teoretice şi practice

eliberării unei permisiuni, unul dintre procesele care aşteptau în coada


semaforului este trezit şi primeşte permisiunea de acces. În modul clasic, un
proces cere şi eliberează doar o permisiune la un moment dat, dar o să vedem
că sistemul de operare Linux permite cererea şi eliberarea mai multor
permisiuni simultan. Figura 1 ilustrează modul de funcţionare a unui semafor
şi utilizarea lui de către un proces.

Figura 1. Interacţiunea unui proces cu un semafor

Operaţia de cerere a unei permisiuni poate fi, aşadar, văzută ca o operaţie de


scădere a valorii semaforului – notată de obicei cu P(), iar cea de eliberare a
unei permisiuni ca o operaţie de incrementare a lui – notată cu V(). Pentru a
funcţiona corect, valoarea unui semafor trebuie utilizată şi/sau modificată
doar prin intermediul operaţiilor P şi V şi nu în mod direct. Singura excepţie
de la această regulă o constituie iniţializarea semaforului, înainte de
folosirea sa de către procesele concurente. Operaţiile P şi V sunt speciale
prin faptul că sunt executate în mod atomic, adică toate sub-operaţiile lor
componente sunt executate neinteruptibil, ca şi cum ar fi o singură operaţie
ce nu mai poate fi descompusă, motiv pentru care ele sunt numite operaţii
primitive sau, mai scurt, primitivele semaforului. Efectul acestui mod de
implementare este faptul că semaforul asigură accesul la resursa partajată
doar în limita numărului de permisiuni disponibile. Chiar dacă la un
moment dat mai multe procese cer simultan o permisiune, numai un număr
de procese egal cu valoarea semaforului la acel moment vor primi

171
Sincronizarea prin semafoare în Linux

permisiune de trecere, celelalte fiind suspendate. Acelaşi lucru se întâmplă


şi dacă accesul la semafor se face în mod concurent atât pentru cererea de
permisiuni, cât şi pentru eliberarea de permisiuni. Cu alte cuvinte,
semaforul, deşi este la rândul lui o resursă partajată, nu devine un punct de
manifestare a nesincronizării proceselor.
Funcţionarea unui semafor depinde de valoarea lui iniţială şi de modul de
utilizare a primitivelor P şi V. Exemplul de mai jos ilustrează modalitatea
clasică de asigurare a sincronizării proceselor concurente pe regiunile lor
critice folosind un semafor cu o valoare iniţială pozitivă N. Toate procesele
trebuie să folosească semaforul în modul descris în exemplu, adică apelând
primitiva P înainte de a intra în regiunea critică, pentru a primi permisiunea
de acces şi respectiv V, la ieşirea din regiunea critică, pentru a elibera
permisiunea primită.
Semaphore s = N;
... // procesul e in afara zonei critice
s.P(); // cererea permisiunii de a intra
// in zona critica
... // procesul e in zona critica
// protejata de semafor
s.V(); // eliberarea permisunii primite
... // procesul e in afara zonei critice
Dacă valoarea iniţială a unui semafor este 1, atunci o sincronizare de forma
celei de mai sus asigură ceea ce se numeşte excludere mutuală, adică la un
moment dat doar un singur proces poate fi într-o regiune critică, adică un
singur proces poate la un moment dat folosi resursa partajată protejată de
semafor. Dacă valoarea semaforului este mai mare decât 1, atunci mai multe
procese, dar nu mai multe decât N, pot fi în regiunile lor critice simultan.
Dacă valoarea iniţială a semaforului este 0 (zero), atunci codul de mai sus ar
duce la blocarea definitivă a tuturor proceselor implicate în sincronizare,
ceea ce evident nu are nici un sens. Semafoarele se folosesc cu valoarea
iniţială 0 într-un alt context şi în alt mod. Un astfel de caz este cel în care
permisiunile sunt generate ca urmare a executării unor anumite procese,
care creează un context necesar execuţiei altor procese, semaforul jucând
rolul de contor al apariţiei unui anumit tip de evenimente. Exemplul clasic
este cel al proceselor producător şi consumator care comunică prin
intermediul unei resurse partajate (un buffer, un fişier etc.) unele depunând
mesaje în cadrul resursei, celelalte consumând mesajele produse. Evident
procesele consumator nu pot intra să acceseze resursa partajată până când
nu există cel puţin un mesaj depus în acea resursă de procesele producător.

172
Sisteme de operare. Chestiuni teoretice şi practice

Acest aspect al sincronizării proceselor producător/consumator este ilustrat


în exemplul de mai jos, rezolvarea completă a problemei fiind descrisă în
capitolul de exemple de la sfârşitul acestei lucrări.
Semaphore nrMsg = 0;
// Proces producător // Proces consumator
... ...
nrMsg.V(); nrMsg.P();
... ...

Observăm pe codul de mai sus că procesele consumator apelează doar


primitiva P a semaforului nrMsg, pe când procesele producător doar primitiva
V a aceluiaşi semafor, ceea ce are sens deoarece un producător produce un
mesaj şi implicit o permisiune pentru un consumator, pe când acesta din urmă
o dată ce consumă un mesaj, înseamnă că „consumă” şi o permisiune de acces
pentru ceilalţi consumatori. Ordinea cronologică a apelurilor primitivelor
semaforului din exemplul de mai sus, pentru ca aplicaţia per ansamblu să
evolueze este V şi P, invers faţă de exemplul anterior.

12.2. Crearea semafoarelor


Sistemul de operare Linux suportă mecanismul de sincronizare a proceselor
prin semafoare. În mod similar cu zonele de memorie partajată şi cu cozile de
mesaje, semafoarele sunt resurse ale sistemului şi nu ale unui anumit proces.
Prin urmare ele trebuie întâi create, iar pentru a putea fi folosite trebuie
obţinută o referinţă spre ele (un identificator de acces). Apelul sistem folosit
atât pentru crearea semafoarelor, cât şi pentru obţinerea identificatorului de
acces la ele se numeşte semget, având următoarea sintaxă:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t cheie, int nrSemafoare, int optiuni);

Funcţia semget creează un set de semafoare, care va conţine numărul de


semafoare indicat de parametrul nrSemafoare. Numărul de semafoare
dintr-un set este mai mare sau egal cu unu, existând o limită maximă a
acestui număr, limită impusă de sistem.
Parametrul cheie este un întreg, prin care se identifică în mod unic în
sistem un anumit set de semafoare. Comparativ cu fişierele, această cheie
joacă rol de nume al unui set de fişiere. Procesele care doresc folosirea
aceluiaşi set de semafoare trebuie să folosească aceeaşi valoare a cheii în

173
Sincronizarea prin semafoare în Linux

apelul funcţiei semget. Pentru a avea o identificare prin nume (şir de


caractere) şi în cazul semafoarelor, se poate folosi funcţia ftok, care
generează un număr întreg pe baza unei căi spre un fişier şi a unui alt număr
întreg. Apeluri diferite ale funcţiei ftok, folosind căi diferite spre un acelaşi
fişier, dar cu un acelaşi număr, are ca efect generarea aceleiaşi chei. Din
păcate funcţia ftok nu asigură faptul că pentru perechi diferite de cale şi
număr va genera întotdeauna o valoare unică a cheii.
Valoarea parametrului cheie poate fi constanta predefinită IPC_PRIVATE,
în cazul în care se doreşte crearea unui nou set de semafoare şi nu se
cunoaşte în mod sigur valoarea unei chei neutilizate. Un set de semafoare
creat astfel va putea fi folosit însă doar de către procesul care l-a creat şi de
descendenţi ai săi (procese create de el şi de fiii săi).
În funcţie de valorile parametrului optiuni, apelul funcţiei semget poate
avea ca efect crearea setului de semafoare şi obţinerea unui identificator de
acces la acel set sau doar obţinerea identificatorului de acces. În cazul în
care funcţia se execută cu succes, ea întoarce ca rezultat o valoare strict
pozitivă (identificatorul de acces), iar în caz de insucces întoarce -1, setând
variabila de sistem errno la o valoare ce corespunde erorii apărute.
Valoarea parametrului optiuni poate fi 0 (zero) sau diferită de zero. În
primul caz se doreşte doar obţinerea accesului la un set de semafoare deja
creat şi, în consecinţă, funcţia semget verifică existenţa setului de semafoare
corespunzător cheii indicate şi dreptul procesului apelant de a accesa acel
set de semafoare. Dacă aceste condiţii sunt verificate, funcţia întoarce
identificatorul de acces la setul de semafoare, altfel -1. În cazul în care
valoarea parametrului optiuni este nenulă, ea trebuie obţinută printr-o
combinaţie (SAU pe biţi) a următoarelor constante predefinite:
IPC_CREAT
Indică intenţia de creare a unui nou set de semafoare. Dacă această
opţiune nu este folosită, funcţia semget va verifica existenţa setului
de semafoare indicat de parametrul cheie şi permisiunea procesului
apelant de a accesa acea resursă. Dacă această opţiune este folosită
în cazul în care setul de semafoare există deja, se va verifica doar
dacă procesul apelant are drepturi de acces la el. În caz afirmativ, se
va întoarce ca rezultat un identificator de acces la acea zonă, iar în
caz negativ, funcţia se va termina fără succes.
IPC_EXCL
Această opţiune se foloseşte doar în combinaţie cu IPC_CREAT şi
indică faptul că nu trebuie creat setul de semafoare dacă el există

174
Sisteme de operare. Chestiuni teoretice şi practice

deja, caz în care funcţia semget nu va fi executată cu succes şi va


returna valoarea -1.
Drepturi de acces
Sunt reprezentarea în cifre octale a celor nouă biţi ce indică
permisiunile de acces la coada de mesaje, similar cu drepturile de
acces la fişiere, cu deosebirea că dreptul de execuţie este ignorat.
Dreptul de scriere este interpretat ca permisiune de modificare a
valorilor semafoarelor din set, iar cel de citire ca permisiune de citire
a valorilor lor.

12.3. Operaţii pe semafoare


Operaţiile pe semafoare corespund, în principiu, modificării valorii lor prin
decrementarea sau incrementarea ei cu un anumit număr. Cazul
decrementării corespunde cererii de permisiuni şi poate bloca procesul care
efectuează operaţia dacă valoarea curentă a semaforului este mai mică decât
numărul cu care se doreşte decrementarea. Incrementarea valorii unui
semafor corespunde eliberării (generării) de permisiuni pentru acel semafor.
Această operaţie nu va bloca niciodată procesul care o efectuează, ci
dimpotrivă ea poate avea ca efect deblocarea unor procese care aşteptau
pentru creşterea valorii semaforului. Funcţiile care permit efectuarea
operaţiilor menţionate mai sus sunt următoarele:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
struct sembuf
{
unsigned short int sem_num; // numar (id) semafor
short int sem_op; // operatia pe semafor
short int sem_flg; // optiuni operatie
};
struct timeval
{
time_t tv_sec; // secunde
long int tv_usec; // microsecunde
};

int semop(int id, struct sembuf *operatii,


unsigned nrOperatii);
int semtimedop(int id, struct sembuf *operatii,
unsigned nrOperatii, struct timespec *timeout);

175
Sincronizarea prin semafoare în Linux

Parametrul id reprezintă identificatorul setului de semafoare, identificator


returnat de funcţia semget. Parametrul operatii este adresa unui şir de
structuri de tipul struct sembuf, fiecare structură descriind o operaţie
asupra unui semafor din set. Parametrul nroperatii indică numărul de
structuri care trebuie folosite din şirul indicat de parametrul operatii.
Diferenţa între cele două funcţii se manifestă doar pentru cazurile în care
operaţiile pe setul de semafoare pot duce la blocarea procesului apelant.
Astfel, în cazul funcţiei semop, procesul rămâne blocat până în momentul în
care operaţiile pot fi efectuate, iar în cazul funcţiei semtimedop procesul
rămâne blocat cel mult un interval de timp indicat de parametrul timeout.

Descrierea unei operaţii pe unul din semafoarele din set necesită în primul
rând specificarea numărului de ordine al semaforului în cadrul setului –
câmpul sem_num al structurii struct sembuf, primul semafor având
numărul de ordine 0, al doilea 1 şi aşa mai departe. Se specifică apoi în
cadrul câmpului sem_op numărul cu care valoarea semaforului se modifică.
Există trei tipuri de valori ale acestui câmp:
• pozitive: valoarea semaforului este incrementată cu valoarea
câmpului sem_op. Această operaţie este întotdeauna executată,
fără a bloca procesul apelant al funcţiei semop.
• zero: se verifică dacă valoarea semaforului este zero. În caz
afirmativ, operaţia este efectuată cu succes, evident, valoarea
semaforului rămânând nemodificată. În caz contrar, procesul
apelant al funcţiei semop este blocat, până când valoarea
semaforului ajunge la zero.
• negative: valoarea semaforului este decrementată cu valoarea
absolută a câmpului sem_op. În cazul în care valoarea
semaforului este mai mică decât valoarea absolută a câmpului
sem_op, atunci procesul apelant al funcţiei semop este blocat
până în momentul în care este posibilă efectuarea operaţiei.

Câmpul sem_flg al structurii sembuf indică anumite opţiuni referitoare


la operaţia descrisă de respectiva structură. Valoarea acestui câmp poate fi
una dintre următoarele constante predefinite:
IPC_NOWAIT
Indică faptul că nu se doreşte blocarea procesului apelant al funcţiei
semop dacă operaţia pe semafor ar avea în mod normal acest efect.
Într-un astfel de caz, se revine imediat din funcţia semop, fără a se
efectua nici una dintre operaţiile indicate în şirul de structuri

176
Sisteme de operare. Chestiuni teoretice şi practice

operatii, funcţia întoarce rezultatul de eroare -1, iar variabila


sistem errno este setată la valoarea EAGAIN.
SEM_UNDO
Indică faptul că operaţia pe semafor trebuie făcută în sens opus
(decrementare în cazul incrementării şi incrementare în cazul
decrementării) şi cu aceeaşi valoare în momentul terminării procesului.
Acest mecanism poate fi folosit pentru a evita lăsarea într-o stare
inconsistentă a unui semafor în cazul în care un proces se termină
brusc, lucru care ar putea duce la o blocare a altor procese care
accesează semaforul respectiv. De exemplu, în cazul a două procese
care folosesc un semafor pentru a asigura accesul în regim de excludere
mutuală la o resursă partajată, dacă unul dintre ele decrementează
valoarea semaforului (ea ajunge la 0), iar apoi se termină brusc, celălalt
proces va aştepta la nesfârşit incrementarea valorii semaforului,
presupusă a fi făcută de acelaşi proces care a decrementat-o.
Mecanismul de undo (refacerea valorii semaforului) este asigurat prin
faptul că unui proces îi sunt în mod automat asociate de către sistemul
de operare anumite structuri de date, corespunzător fiecărui semafor pe
care îl accesează. În cadrul unei astfel de structuri există un contor la
care se adaugă, cu semn schimbat, numărul cu care se modifică
valoarea semaforului într-o operaţie care specifică opţiunea SEM_UNDO.
În momentul terminării procesului, sistemul de operare va efectua
asupra semaforului operaţia de adunare a valorii acestui contor la
valoarea semaforului, ceea ce, evident, poate însemna o incrementare
sau o decrementare, în funcţie de valoarea pozitivă sau negativă a
contorului. O problemă care se poate pune este ce se întâmplă în cazul
în care semaforul trebuie decrementat cu un număr (valoarea contorului
de undo) care este mai mare decât valoarea curentă a semaforului. În
mod normal o astfel de operaţie ar bloca procesul, până când s-ar putea
efectua operaţia. Procesul fiind însă în faza de terminare, s-ar putea ca
acest lucru să nu fie acceptabil. Soluţia sistemului de operare Linux
pentru această situaţie este de a decrementa semaforul cu valoarea
maximă posibilă – aducerea valorii semaforului la 0 – şi de a termina
imediat procesul.

O proprietate foarte importantă a funcţiilor semop şi semtimedop este aceea că


setul de operaţii indicate în apelul lor se execută în mod atomic, adică sau sunt
efectuate simultan toate, dacă acest lucru este posibil sau nu se efectuează nici
una dintre ele şi procesul este blocat. Bineînţeles, dacă pentru operaţiile care
ar avea ca efect blocarea procesului se specifică opţiunea IPC_NOWAIT,

177
Sincronizarea prin semafoare în Linux

procesul nu este blocat şi se revine imediat din apelul funcţiilor, dar cu cod de
eroare. Acest tip de funcţionalitate al celor două funcţii poate fi folosit pentru
a evita interblocarea proceselor. Trebuie remarcat, încă o dată, că un apel al
funcţiei semop cu mai multe operaţii este diferit, datorită acestei execuţii
atomice a lor, de apelul repetat al funcţiei semop pentru fiecare operaţie în
parte. Exemplul următor ilustrează această diferenţă şi pune în evidenţă
evitarea interblocării a două procese prin efectuarea atomică a operaţiilor pe
mai multe semafoare simultan. Se presupune că două procese doresc
decrementarea a două semafoare diferite, care au valoarea iniţială 1. Dacă un
proces decrementează semafoarele într-o anumită ordine, prin apeluri diferite
ale funcţiei semop, iar celălalt proces în ordine inversă, atunci e posibil să se
ajungă la o blocare reciprocă a proceselor în următorul scenariu: primul
proces decrementează primul semafor, apoi, înainte de a decrementa pe cel
de-al doilea, este suspendat şi se începe execuţia celuilalt proces. Acesta
decrementează mai întâi cel de-al doilea semafor şi încercând să decrementeze
apoi şi primul semafor va fi blocat, valoarea semaforului fiind 0. La reluarea
execuţiei, primul proces va încerca decrementarea celui de-al doilea semafor,
dar va fi şi el blocat pentru că semaforul are deja valoarea 0. Codul de mai jos
ilustrează acest posibil scenariu, folosind apeluri ale funcţiei sleep pentru a
surprinde în mod sigur situaţiile de suspendare a proceselor corespunzătoare
scenariului descris mai sus:

// Primul procesul // Al doilea proces

int id; int id;


struct sembuf op={0,0,0}; struct sembuf op={0,0,0};
id = semget(10000, 0, 0); id = semget(10000, 0, 0);
semctl(id, 0, SETVAL, 1); // "asteapta" procesul 1
semctl(id, 1, SETVAL, 1); // sa decr. semaforul 1
sleep(1); // suspendat
op.sem_num = 0;
op.sem_op = -1; op.sem_num = 1;
semop(id, &op, 1); op.sem_op = -1;
semop(id, &op, 1);
// "asteapta" ca procesul 2
// sa decr. semaforul 2 op.sem_num = 0;
sleep(1); // suspendat op.sem_op = -1;
semop(id, &op, 1);
op.sem_num = 1;
op.sem_op = -1;
semop(id, &op, 1);

Implementarea prin care se evită interblocarea proceselor specifică ambele


operaţii într-un singur apel al funcţiei semop, caz în care nu contează

178
Sisteme de operare. Chestiuni teoretice şi practice

ordinea specificării lor, pentru că oricum întregul set de operaţii se execută


atomic. Codul de mai jos ilustrează acest mod de implementare:

// Primul procesul // Al doilea proces


int id; int id;
struct sembuf op[2]; struct sembuf op[2];
int pid; int pid;
id = semget(10000, 0, 0); id = semget(10000, 0, 0);
semctl(id, 0, SETVAL, 1);
semctl(id, 1, SETVAL, 1);
op[0].sem_num = 0; op[0].sem_num = 1;
op[0].sem_op = -1; op[0].sem_op = -1;
op[0].sem_flg = 0; op[0].sem_flg = 0;
op[1].sem_num = 1; op[1].sem_num = 0;
op[1].sem_op = -1; op[1].sem_op = -1;
op[1].sem_flg = 0; op[1].sem_flg = 0;
semop(id, op, 2); semop(id, op, 2);

Alte câteva observaţii care trebuie făcute referitor la cele două funcţii de
efectuare a operaţiilor pe semafoare sunt legate de situaţiile în care un
proces care le apelează este blocat. Aşa cum am menţionat deja, acest lucru
se întâmplă deoarece cel puţin unul dintre semafoarele indicate în setul de
operaţii nu poate fi decrementat cu valoarea indicată sau se aşteaptă
atingerea valorii zero a unuia dintre semafoare. Dacă mai multe procese
apelează succesiv una dintre cele două funcţii (pe acelaşi set de semafoare)
şi sunt blocate, ele sunt puse într-o listă de aşteptare asociată setului de
semafoare, listă ce funcţionează după principiul FIFO. Acest principiu este
aplicat însă doar în cazul în care pentru mai multe procese din cele din listă
sunt îndeplinite la un moment dat condiţiile de deblocare. Într-o astfel de
situaţie procesele sunt „trezite” în ordinea în care ele au fost introduse în
lista de aşteptare. Însă, în cazul în care doar pentru un anumit proces sunt
îndeplinite condiţiile de deblocare, atunci, indiferent de ordinea în listă a
acelui proces, el va fi deblocat imediat, procesele din faţa lui din listă
rămânând în continuare blocate. Cu alte cuvinte, putem spune că efectuarea
operaţiilor pe semafoare nu se bazează pe o strategie de rezervare a
semafoarelor. De exemplu, dacă un proces P1 vrea la un moment dat să
decrementeze un semafor cu valoarea 3, dar valoarea semaforului este doar
2, atunci procesul P1 este blocat, dar cele două permisiuni ale semaforului
nu sunt considerate rezervate procesului, ci dacă un alt proces P2 vrea la un
moment ulterior să decrementeze valoarea semaforului cu 1 sau cu 2, atunci

179
Sincronizarea prin semafoare în Linux

i se permite să facă acest lucru imediat, fără a se ţine cont de intenţia


procesului P1. Evident, într-o astfel de situaţie există riscul ca procesul P1 să
rămână definitiv blocat, dacă, să zicem, procese de genul lui P2 continuă să
apară în mod continuu şi valoarea semaforului nu ajunge niciodată la 3.
Funcţiile semop şi semtimedop returnează 0 în caz de succes şi -1 în caz de
eroare, înscriind în variabila de sistem errno codul corespunzător situaţiei
de eroare. Câteva posibile coduri de eroare sunt: E2BIG (argumentul
nrOperatii este mai mare decât limita SEMOPM admisă de sistem),
EACCES (procesul apelant nu are dreptul să execute cel puţin una din
operaţiile indicate pe setul de semafoare), EAGAIN (cel puţin una dintre
operaţiile specificate ar fi blocat procesul, dar a fost specificată şi opţiunea
IPC_NOWAIT), EFBIG (pentru cel puţin o operaţie valoarea câmpului
sem_num nu se încadrează în intervalul corespunzător setului de semafoare
– 0 primul semafor etc.), EIDRM (setul de semafoare a fost şters, sau înainte
de apelul funcţiei, sau în timp ce procesul aştepta după o anumită valoare a
unui semafor din set), EINVAL (identificatorul setului de semafoare este
invalid sau parametrul nrOperatii este negativ).
Pe lângă operaţiile descrise mai sus, de modificare a valorii unui semafor prin
incrementare sau decrementare, există şi posibilitatea stabilirii în mod direct a
unei noi valori pentru semafor. Această situaţie corespunde momentului
iniţializării unui semafor, deci înainte de a fi el folosit de procesele concurente
pentru sincronizare. Funcţia care permite acest lucru se numeşte semctl. Ea
este folosită şi pentru alte operaţii de control legate de setul de semafoare,
motiv pentru care este descrisă în secţiunea următoare.

12.4. Controlul seturilor de semafoarelor


Sistemul de operare asociază fiecărui set de semafoare o structură de date
care conţine informaţii necesare gestionării acelui set de semafoare. Această
structură de date este de tipul struct semid_ds şi conţine, printre altele,
următoarele câmpuri:

struct semid_ds
{
struct ipc_perm sem_perm; // permisiuni
time_t sem_otime; // timpul ultimului apel
// al functiei semop
time_t sem_ctime; // timpul ultimei modif.
// a structurii semid_ds
unsigned long int sem_nsems; // nr. de sem. din set
};

180
Sisteme de operare. Chestiuni teoretice şi practice

struct ipc_perm {
key_t key; // cheia setului de semafoare
ushort uid; // uid efectiv proprietar
ushort gid; // gid efectiv proprietar
ushort cuid; // uid efectiv utilizator creator
ushort cgid; // uid efectiv utilizator creator
ushort mode; // permisiuni
ushort seq; // numar de secventa
};

De asemenea, fiecărui semafor din cadrul unui set de semafoare îi este


asociată o structură de date care descrie acel semafor. O astfel de structură
este de tipul struct sem şi conţine câmpurile următoare:
struct sem {
ushort semval; // valoarea semaforului
short sempid; // pid ultima operatie
ushort semncnt; // nr. de procese ce asteapta ca
// val. semaforului sa creasca
ushort semzcnt; // nr. de procese ce asteapta ca
// val. semaforului sa devina 0
};
Funcţia ce permite accesul aplicaţiilor utilizator la aceste structuri de date,
atât pentru citirea lor, cât şi pentru modificarea unora dintre câmpurile lor,
se numeşte semctl şi are următoarea sintaxă:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun
{
int val; // pt. SETVAL
struct semid_ds *buf; // IPC_STAT & IPC_SET
unsigned short int *array; // pt. GETALL & SETALL
};
int semctl(int id, int nrSem,
int cmd, union semun parametru);

Parametrul id este identificatorul setului de semafoare, identificator


returnat de funcţia semget. Parametrul nrSem indică unul dintre semafoarele
din set, numărătoarea începând cu 0 pentru primul semafor şi aşa mai
departe până la ultimul semafor. Acest parametru este folosit în relaţie cu
valoarea celui de-al treilea parametru al funcţiei, cmd, care indică operaţia
ce trebuie efectuată, sau asupra unui semafor din set – caz în care valoarea
parametrului nrSem este luată în considerare – sau asupra întregului set de
semafoare sau a structurii asociate lui – caz în care parametrul nrSem este

181
Sincronizarea prin semafoare în Linux

ignorat. În mod similar, interpretarea parametrului parametru este


dependentă de valoarea lui cmd, aceasta putând fi una dintre următoarele
constante predefinite:
IPC_STAT
Indică citirea structurii asociată setului de semafoare şi, în mod
corespunzător, al patrulea parametru al funcţiei semctl este
interpretat ca indicând spre o structură de tipul struct semid_ds.
Parametrul nrSem este ignorat.
IPC_SET
Indică modificarea unora dintre câmpurile structurii asociate setului
de semafoare şi, în mod corespunzător, al patrulea parametru al
funcţiei semctl este interpretat ca indicând spre o structură de tipul
struct semid_ds. Câmpurile care pot fi modificate de către
aplicaţiile utilizator sunt: sem_perm.uid, sem_perm.gid şi
sem_perm.mode. Parametrul nrSem este ignorat.
IPC_RMID
Indică intenţia de ştergere a setului de semafoare specificat prin
parametrul id. Operaţia poate fi efectuată doar de către procese
aparţinând administratorului sau utilizatorului creator sau proprietar al
setului de semafoare. Parametrii nrSem şi parametru sunt ignoraţi.
GETVAL
Indică citirea valorii unuia dintre semafoarele din set, şi anume, cel
indicat prin parametrul nrSem. Al patrulea parametru al funcţiei este
ignorat, iar funcţia semctl întoarce ca rezultat valoarea semaforului
sau -1 în caz de eroare.
SETVAL
Indică modificarea valorii unuia dintre semafoarele din set, şi
anume, cel indicat prin parametrul nrSem. Al patrulea parametru al
funcţiei este interpretat ca un număr întreg, indicând noua valoare a
semaforului.
GETALL
Indică citirea valorii tuturor semafoarelor din set. Parametrul nrSem
este ignorat. Al patrulea parametru al funcţiei indică spre un şir de
întregi în care se vor depune valorile curente ale semafoarelor.
SETALL
Indică modificarea valorii tuturor semafoarelor din set. Parametrul
nrSem este ignorat. Al patrulea parametru al funcţiei indică spre un
şir de întregi din care se vor lua noile valori ale semafoarelor.

182
Sisteme de operare. Chestiuni teoretice şi practice

GETNCNT
Indică obţinerea numărului de procese care aşteaptă ca valoarea
semaforului să crească, număr întors ca rezultat de către funcţia
semctl. Parametrul nrSem este ignorat.
GETZCNT
Indică obţinerea numărului de procese care aşteaptă ca valoarea
semaforului să devină zero, număr întors ca rezultat de către funcţia
semctl. Parametrul nrSem este ignorat.
GETPID
Indică obţinerea identificatorului procesului care a efectuat ultimul
apel al funcţiei semop – câmpul sempid al structurii struct sem
– pe semaforul indicat de parametrul nrSem. Identificatorul
procesului respectiv este întors ca rezultat de către funcţia semctl.

În cazurile în care funcţia semctl nu se poate executa cu succes, ea va


returna -1, setând variabila de sistem errno la un anumit cod de eroare. În
caz de succes, ea va returna sau o valoare pozitivă, atunci când prin
parametrul cmd se cere acest lucru – conform cazurilor descrise mai sus –
sau 0, în celelalte situaţii.

12.5. Comenzi shell pentru seturile de semafoare


O altă modalitate de a obţine informaţii despre seturile de semafoare sau de
a le şterge este oferită de comenzile shell ipcs şi ipcrm.

Comanda ipcs afişează informaţii despre toate resursele sistemului create


pentru comunicarea între procese: cozi de mesaje, zone de memorie
partajată şi seturile de semafoare. Pentru a se afişa doar informaţiile
despre semafoare, comanda trebuie lansată cu opţiunea –s. Informaţiile
afişate cuprind cheia sub care a fost creat setul de semafoare,
identificatorul asociat setului şi returnat de apelul funcţiei semget,
drepturile de acces, numărul de semafoare din set şi altele.

Ştergerea unui set de semafoare, în cazul în care există drepturile necesare,


se poate face cu ajutorul comenzii ipcrm, sub una din următoarele forme:
ipcrm –s identificator
ipcrm –S cheie

Ştergerea unui set de semafoare poate fi făcută doar de către administratorul


sistemului, utilizatorul proprietar sau creator al respectivului set.

183
Sincronizarea prin semafoare în Linux

12.6. Exemple
Exemplul 1. Două procese părinte şi fiu copiază în mod concurent un fişier
sursă într-un fişier destinaţie folosind aceleaşi fişiere deschise, moştenite de fiu
de la părinte. Folosind semafoare, se impune accesul în regim de excludere
mutuală a celor două procese la zona de cod unde se realizează copierea.
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <fcntl.h>
void P(int semId, int semNr)
{
struct sembuf op = {semNr, -1, 0};
semop(semId, &op, 1);
}
void V(int semId, int semNr)
{
struct sembuf op = {semNr, 1, 0};
semop(semId, &op, 1);
}
void copy(int fdSursa, int fdDest, int semId)
{
char c;
int nr, term = 0;
while (! term)
{
P(semId, 0); // cerere unica permisiune
if ((nr=read(fdSursa, &c, 1)) != 1)
{ perror("Eroare citire"); term = 1; }
if (!term && (write(fdDest, &c, nr) != nr))
{ perror("Eroare scriere"); term = 1; }
V(semId, 0); // eliberare permisune
}
}
int main(int argc, char **argv)
{
int id, pid, fdSursa, fdDest;
if (argc != 3) {
printf("Utilizare: %s sursa dest\n", argv[0]);
exit(1);
}
id = semget(30000, 1, IPC_CREAT | 0600);
if (id < 0)
{ perror("Eroare creare semafor"); exit(2); }

184
Sisteme de operare. Chestiuni teoretice şi practice

if (semctl(id, 0, SETVAL, 1) < 0)


{ perror("Eroare setare val. sem."); exit(3); }
if ((fdSursa = open(argv[1], O_RDONLY)) < 0)
{ perror("Eroare deschidere fisier"); exit(4); }
if ((fdDest = creat(argv[2], 0644)) < 0)
{ perror("Eroare creare fisier"); exit(5); }
pid = fork();
copy(fdSursa, fdDest, id);
if (pid) {
waitpid(pid, 0);
semctl(id, 0, IPC_RMID, 0);
}
}

Exemplul 2. Problema producători / consumatori. Să se sincronizeze


execuţia proceselor de tip producător şi consumator, care sunt generate în
mod continuu şi acţionează asupra unui buffer de mesaje circular, astfel
încât să se respecte principiul de comunicare FIFO, să nu se suprascrie
mesaje nepreluate şi să nu se preia acelaşi mesaj de mai multe ori.
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <fcntl.h>
#define N 100
#define MUTEX 0
#define SPATII 1
#define MESAJE 2
int *buffer, *prodMsg, *consMsg;
void P(int semId, int semNr)
{
struct sembuf op = {semNr, -1, 0};
semop(semId, &op, 1);
}
void V(int semId, int semNr)
{
struct sembuf op = {semNr, +1, 0};
semop(semId, &op, 1);
}
void producator(int idProd, int msg, int semId)
{
P(semId, SPATII);
P(semId, MUTEX);
buffer[*prodMsg] = msg;
*prodMsg = (*prodMsg + 1) % N;

185
Sincronizarea prin semafoare în Linux

printf("Prod.%d msg: %d\n", idProd, msg);


V(semId, MUTEX);
V(semId, MESAJE);
}
void consumator(int idCons, int *msg, int semId)
{
P(semId, MESAJE);
P(semId, MUTEX);
*msg = buffer[*consMsg];
*consMsg = (*consMsg + 1) % N;
printf("Cons.%d msg: %d\n", idCons, *msg);
V(semId, MUTEX);
V(semId, SPATII);
}
int main(int argc, char **argv)
{
int semId, shmId, i, pid, msg;
shmId = shmget(IPC_PRIVATE, (N+2) * sizeof(int),
IPC_CREAT | 0600);
if (shmId < 0)
{ perror("Eroare creare shm"); exit(2); }
buffer = (int*) shmat(shmId, 0, 0);
prodMsg = &buffer[N];
consMsg = &buffer[N+1];
semId = semget(IPC_PRIVATE, 3, IPC_CREAT | 0600);
if (semId < 0)
{ perror("Eroare creare sem"); exit(2); }
semctl(semId, MUTEX, SETVAL, 1); // lacat
semctl(semId, SPATII, SETVAL, N); // pt. prod.
semctl(semId, MESAJE, SETVAL, 0); // pt. cons.
if ((pid = fork()) == 0) // fiu creeaza prod.
for (i=1; i<=10*N; i++)
if (fork() == 0)
{ producator(i, i, semId); exit(0); }
else // parinte creeaza cons.
for (i=1; i<=10*N; i++)
if (fork() == 0)
{ consumator(i, &msg, semId); exit(0); }
}

Exemplul 3. Problema scriitori / cititori. Să se sincronizeze execuţia a


două tipuri de procese care accesează aceeaşi resursă partajată, în cazul de
faţă un şir de întregi: unele procese doar citesc din şir – procese cititor, iar
altele doar scriu în şir – procese scriitor. Regulile de sincronizare sunt:
1. mai mulţi cititori pot accesa simultan şirul, dar nu în acelaşi timp cu
scriitorii;

186
Sisteme de operare. Chestiuni teoretice şi practice

2. scriitori accesează şirul în regim de excludere mutuală, adică atunci


când un scriitor accesează şirul nici un alt proces, fie el scriitor sau
cititor nu poate accesa şirul.
Soluţia din exemplu foloseşte un semafor cu o valoare iniţială mare (mai
mare decât numărul posibil de cititori care pot fi activi simultan la un
moment dat), semafor pe care scriitorii îl decrementează cu un număr egal
cu valoarea iniţială, adică cer toate permisiunile pentru a bloca accesul
oricărui alt proces, iar cititorii îl decrementează cu 1, pentru a bloca doar
scriitori, dar fără a bloca alţi cititori.

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <fcntl.h>
#define N 10
#define MUTEX 0
#define SEM 1
#define PERMISIUNI 100
#define CITITORI 100
#define SCRIITORI 10
int *buffer;
void down(int semId, int semNr, int val)
{
struct sembuf op = {semNr, val<0?val:-val, 0};
semop(semId, &op, 1);
}
void up(int semId, int semNr, int val)
{
struct sembuf op = {semNr, val>0?val:-val, 0};
semop(semId, &op, 1);
}
void scriitor(int id, int semId)
{
int i;
down(semId, SEM, PERMISIUNI);
printf("SCRIITOR: %d\n", id);
for (i=0; i<=N; i++) {
buffer[i] = id * N + i;
usleep(100000);
}
up(semId, SEM, PERMISIUNI);
}

187
Sincronizarea prin semafoare în Linux

void cititor(int id, int semId)


{
int localBuf[N], i;
down(semId, SEM, 1);
printf("CITITOR: %d - GET\n", id);
for (i=0; i<=N; i++) {
localBuf[i] = buffer[i];
usleep(100000);
}
up(semId, SEM, 1);
// scrierea pe ecran in regim de execludere mutuala
down(semId, MUTEX, 1); // blocare lacat
printf("CITITOR: %d - PRINT\n", id);
for (i=0; i<=N; i++)
printf("%d ", localBuf[i]);
printf("\n");
up(semId, MUTEX, 1); // eliberare lacat
}
int main(int argc, char **argv)
{
int semId, shmId, i, pid, msg;
shmId = shmget(IPC_PRIVATE, N * sizeof(int),
IPC_CREAT | 0600);
if (shmId < 0)
{perror("Eroare creare shm"); exit(2); }

buffer = (int*) shmat(shmId, 0, 0);

semId = semget(IPC_PRIVATE, 2, IPC_CREAT | 0600);


if (semId < 0)
{ perror("Eroare creare sem"); exit(2); }

// initialiyare semafoare
semctl(semId, MUTEX, SETVAL, 1);
semctl(semId, SEM, SETVAL, PERMISIUNI);

// creare procese cititori si scriitori


if ((pid = fork()) == 0){ // fiul creeaza scriitori
for (i=0; i<SCRIITORI; i++) {
if (fork() == 0) {
scriitor(i, semId);
exit(0);
}
if ((i % (SCRIITORI/3)) == 0)
sleep(3);
}

188
Sisteme de operare. Chestiuni teoretice şi practice

for (i=0; i<SCRIITORI; i++)


wait(0);
}
else { // parintele creeaza scriitori
for (i=0; i<CITITORI; i++) {
if (fork() == 0) {
cititor(i, semId);
exit(0);
}
if ((i % (CITITORI/5)) == 0)
sleep(3);
}
for (i=0; i<CITITORI; i++)
wait(0);
waitpid(pid, 0);
shmctl(shmId, IPC_RMID, 0);
semctl(semId, IPC_RMID, 0);
}
}

12.7. Probleme
1. Să se sincronizeze două procese folosind semafoare, astfel încât ele să
incrementeze strict alternativ de un anumit număr de ori (acelaşi pentru
ambele procese) un contor aflat într-o zonă de memorie partajată.
2. Pentru Exemplul 1 din cadrul lucrării să se testeze ce se întâmplă dacă
unul dintre procese se termină în regiunea sa critică fără să elibereze
semaforul. Să se modifice modul de efectuare al operaţiilor pe
semafoare prin folosirea opţiunii SEM_UNDO şi să se verifice din nou
funcţionarea aplicaţiei în contextul precizat.
3. Să se modifice Exemplul 3 din cadrul lucrării astfel încât să se asigure o
prioritate de acces la şirul partajat pentru următoarele tipuri de procese:
a. cititori
b. scriitori
4. Să se scrie, folosind semafoare Linux, codul C al două tipuri de procese,
care joacă rolul unor maşini care circulă în direcţii opuse peste un pod.
Presupunând că podul este în lucru, se impune ca la un moment dat pe pod
să poată fi maximum MAX maşini, iar circulaţia pe pod să se desfăşoare
într-o singură direcţie. Pentru sincronizarea circulaţiei pe pod, se poate
presupune existenţa a câte unui semafor la fiecare capăt al podului,
semafoare care nu pot indica simultan aceeaşi culoare (roşu sau verde).
Schimbarea simultană a culorilor semafoarelor poate fi făcută periodic sau

189
Sincronizarea prin semafoare în Linux

pe baza unor alte criterii, cu acest lucru putându-se ocupa un alt proces.
Trebuie să se ţină cont de faptul că în momentul schimbării direcţiei de
circulaţie, pe pod pot fi în traversare maşini, iar maşinile din sensul opus
trebuie să aştepte eliberarea podului înainte de a putea intra pe pod.
5. Se consideră două străzi care se intersectează. Maşinile circulă pe cele
două străzi într-un singur sens. În intersecţie există două semafoare, câte
unul pe fiecare stradă, corespunzător direcţiei de circulare pe strada
respectivă. Să se scrie, folosind semafoare Linux, codul proceselor care
joacă rolul maşinilor ce circulă pe prima şi respectiv, pe cea de-a doua
stradă. De asemenea, să se scrie codul procesului care controlează
semafoarele fizice din intersecţie, schimbând periodic culorile lor.
6. Să se scrie un program C, care creează în mod continuu procese care
incrementează un contor aflat într-o zonă de memorie partajată, aşteaptă
o perioadă de 1 secundă şi apoi se termină. Un alt proces citeşte periodic
într-o buclă infinită valoarea contorului din zona de memorie partajată şi
o afişează pe ecran. Să se scrie codul celor două tipuri de procese
folosind semafoare, astfel încât procesul care citeşte valoarea contorului
să fie prioritar în accesul la zona de memorie partajată faţă de procesele
care incrementează contorul.
7. Se presupune că N filozofi doresc să servească cina într-o încăpere în
care există o masă cu N farfurii cu spagheti şi N furculiţe. Pentru a putea
mânca, un filozof are nevoie de două furculiţe: a lui şi cea a vecinului
din stânga. Fiecare filozof are un număr de identificare unic şi este
reprezentat printr-un proces, care va apela într-o buclă infinită două
funcţii: think(int idFilozof) şi eat(int idFilozof). Să se scrie
codul unui proces filozof folosind semafoare, astfel încât servirea mesei
să se desfăşoare fără probleme. Situaţiile care trebuie evitate sunt cele
de interblocare şi de aşteptare la infinit a unui filozof pentru a intra la
masă.
8. Se presupune că într-o frizerie există M frizeri şi N scaune de aşteptare.
Prin urmare, M clienţi pot fi serviţi la un moment dat şi alţi maximum N
clienţi pot aştepta în frizerie. Restul trebuie să aştepte afară din frizerie.
Rolul frizerilor şi al clienţilor este jucat de procese, câte un proces
pentru fiecare persoană. Să se scrie codul proceselor frizer şi client
astfel încât să fie respectate regulile de mai sus şi frizerii să nu stea în
cazul în care sunt clienţi care aşteaptă. De asemenea, trebuie evitată
situaţia ca un client care aşteaptă afară din frizerie să fie servit înaintea
unuia care aşteaptă în frizerie. Se poate lua eventual în considerare şi
situaţia respectării ordinii de sosire în frizerie.

190
Sisteme de operare. Chestiuni teoretice şi practice

9. Un proces generează aleator două tipuri de procese: producător şi


consumator. Un producător trebuie să transmită un mesaj (un număr întreg)
unui consumator. Se presupune că nu există un loc (buffer) de stocare a
mesajelor şi, prin urmare, un producător trebuie să aştepte sosirea unui
consumator căruia să-i transmită mesajul în mod direct. Transmiterea
mesajului se va face printr-o coadă de mesaje pe care o creează fiecare
consumator şi a cărei cheie o cunoaşte şi producătorul. Evident, şi un
consumator trebuie să aştepte sosirea unui producător pentru a avea ce
mesaj să preia. Se cere să se scrie codul corespunzător celor două tipuri de
procese, execuţia lor sincronizându-se prin semafoare.
10. Se presupune că într-o sală de aşteptare, pentru a se face economie de
curent pe timpul nopţii, s-a instalat un senzor optic, astfel încât lumina
să fie aprinsă doar atâta timp cât există persoane în acea sală. Se cere să
se implementeze, folosind pentru sincronizare semafoarele Linux, codul
C al următoarelor tipuri de procese:
a. persoana, care joacă rolul unei persoane care doreşte să intre
în sală, iar după ce o face, stă un timp acolo, apoi iese;
b. controler, care controlează senzorul optic şi care într-o buclă
infinită detectează apariţia primei persoane ce intră în sală,
moment în care comandă aprinderea luminii, respectiv
detectează ieşirea ultimei persoane din sală, moment în care
comandă stingerea luminii;
Se va ţine cont de următoarele observaţii:
• nu se va folosi tehnica „busy waiting” de verificare în mod
continuu într-o buclă a unei condiţii, ci se vor folosi
operaţiile pe semafoare pentru a pune un proces în aşteptare;
• după momentul în care sala devine goală şi lumina se stinge,
prima persoană care apare, poate să intre numai după ce
lumina s-a aprins.
11. Să se implementeze funcţiile atomNa() şi atomCl() necesare simulării
unui proces chimic virtual de formare a sării de bucătărie (NaCl). Pentru
simulare se vor crea un număr aleator de threaduri, unele având rol de
atomi de Na, executând funcţia atomNa, iar celelalte având rol de atomi
de Cl, executând funcţia atomCl. Se cere folosirea semafoarelor pentru a
se realiza sincronizarea threadurilor, în scopul formării moleculelor de
sare din atomii de Na şi Cl. Un thread oarecare trebuie să aştepte doar
până în momentul în care este posibilă formarea unei molecule. Se cere ca
intrarea atomilor în reacţie să se producă în ordinea în care ei au apărut. Să
se rezolve apoi problema similară pentru formarea apei H2O.

191
13. Sincronizarea thread-urilor în Linux

Scopul lucrării
Lucrarea prezintă mecanismele de sincronizare între thread-uri puse la
dispoziţie în Linux prin implementarea specificaţiei PTHREADS. Sunt
descrise funcţiile care permit accesul la aceste mecanisme, precum şi câteva
modalităţi de utilizare a lor.

13.1. Prezentare generală


Mecanismele de sincronizare existente în Linux ca urmare a implementării
specificaţiei PTHREADS pot fi utilizate pentru sincronizarea execuţiei
thread-urilor unui proces, în condiţiile în care acestea accesează în mod
concurent resurse comune. Având în vedere faptul că thread-urile unui
proces folosesc în comun toate resursele alocate de sistemul de operare
acelui proces, sincronizarea este o problemă implicită folosirii thread-urilor
multiple. Aşadar, mecanismele de sincronizare sunt inerente utilizării
thread-urilor.

Mecanismele de sincronizare prezentate în cadrul acestei lucrări sunt


lacătele, variabilele condiţionale şi mecanismul care asigură execuţia o
singură dată a unor funcţii.

Fiecare proces îşi poate crea, sub forma unor variabile globale, propriile
mecanisme de sincronizare de tipul celor precizate mai sus. Fiind declarate
ca variabile globale, aceste mecanisme de sincronizare sunt vizibile tuturor
thread-urilor acelui proces, însă ele nu sunt accesibile thread-urilor unui alt
proces şi, prin urmare, nu pot fi folosite pentru sincronizarea execuţiei
proceselor.

Lacătele sunt folosite pentru asigurarea accesului în regim de excludere


mutuală a unor thread-uri la o anumită resursă. Variabilele condiţionale
reprezintă un mecanism specializat de aşteptare a unui eveniment în cadrul
unei regiuni de excludere mutuală. Mecanismul de execuţie o singură dată a
unor funcţii este folosit în cazul operaţiilor de iniţializare, care trebuie
efectuate o singură dată. Prezentăm în cele ce urmează funcţiile de creare şi
de folosire a acestor mecanisme de sincronizare.

192
Sisteme de operare. Chestiuni teoretice şi practice

13.2. Lacăte
Variabilele de tip lacăt din specificaţia PTHREADS sunt de tipul
phtread_mutex_t. Ele sunt folosite pentru asigurarea accesului în regim
de excludere mutuală a unei resurse. Înainte de a putea fi utilizat, un lacăt
trebuie iniţializat, fie în mod implicit, fie în mod explicit.

Iniţializarea şi ştergerea lacătului


În momentul iniţializării unui lacăt, se stabilesc valorile atributelor care
descriu modul de funcţionare al lacătului respectiv. Accesul la atributele
unui thread se poate face prin intermediul unei structuri de tipul
pthread_mutex_attr_t. Valorile atributelor unui thread pot fi setate
automat la valorile lor implicite sau pot primi valori explicite specificate de
către utilizator.

Iniţializarea implicită a unui lacăt se face utilizând constanta predefinită


PTHREAD_MUTEX_INITIALIZER. Efectul acestui tip de iniţializare este
crearea unui lacăt cu valorile implicite ale atributelor sale. Acest tip de
iniţializare se face în următorul mod:

#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

Iniţializarea explicită necesită folosirea funcţiei pthread_mutex_init. Sintaxa


funcţiei este următoarea:

#include <pthread.h>
int pthread_mutex_init (
pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);

Primul parametru este setat de către sistem şi joacă rol de identificator al


lacătului. Cel de-al doilea parametru reprezintă adresa unei structuri care
conţine atributele lacătului şi care trebuie alocată de către utilizator anterior
apelului. Modul de creare şi iniţializare a unei astfel de structuri este descris
puţin mai jos. Dacă valoarea celui de-al doilea parametru este NULL, lacătul
va fi creat cu valori implicite ale atributelor sale.

În momentul în care nu mai este nevoie de un lacăt el poate fi şters cu


ajutorul funcţiei pthread_mutex_destroy, a cărei sintaxă este următoarea:

193
Sincronizarea thread-urilor în Linux

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

Atributele lacătului
Crearea unei structuri care să conţină atributele unui lacăt se face prin
declararea unei variabile de tipul pthread_mutexattr_t. Înainte de a
putea fi folosită, ea trebuie însă iniţializată, lucru care se face cu ajutorul
funcţiei pthread_mutexattr_init, care are următoarea sintaxă:

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

Efectul apelului acestei funcţii este iniţializarea valorilor atributelor din cadrul
structurii de la adresa attr la valorile implicite. Este important de observat
faptul că funcţia pthread_mutexattr_init nu alocă memorie pentru structura de
atribute, ci doar iniţializează valorile atributelor conţinute de acea structură.
Distrugerea structurii de atribute ale unui lacăt se face cu următoarea funcţie:

#include <pthread.h>
int pthread_mutexattr_destroy (
pthread_mutexattr_t *attr);

În implementarea din Linux a specificaţiei PTHREADS, există posibilitatea


stabilirii valorii unui singur atribut al unui lacăt. Acest atribut descrie
efectul apelării repetate a primitivei de blocare a lacătului de către thread-ul
care a blocat deja lacătul. Funcţiile de setare, respectiv obţinere a valorii
acestui atribut sunt:
#include <pthread.h>
int pthread_mutexattr_settype (
pthread_mutexattr_t *attr,
int kind);
int pthread_mutexattr_gettype (
pthread_mutexattr_t *attr,
int *kind);

Valorile posibile ale atributului de tip sunt:


PTHREAD_MUTEX_FAST_NP
Dacă thread-ul care a blocat anterior lacătul apelează
funcţia de blocare din nou, atunci el este suspendat în
mod definitiv.

194
Sisteme de operare. Chestiuni teoretice şi practice

PTHREAD_MUTEX_RECURSIVE_NP
Thread-ul care a blocat anterior lacătul nu este suspendat
în momentul apelării repetate a funcţiei de blocare, dar
pentru eliberarea lacătului, funcţia de deblocare trebuie
apelată de acelaşi număr de ori ca şi cea de blocare.
PTHREAD_MUTEX_ERRORCHECK_NP
În momentul apelării funcţiei de blocare a lacătului se verifică dacă
thread-ul care face acest lucru este chiar cel care a blocat anterior
lacătul şi, în caz afirmativ, funcţia întoarce ca rezultat o eroare.
Codul de eroare este EDEADLK.

Terminaţia _NP (Non-Portable) a constantelor de mai sus, indică faptul că


acest atribut este specific numai implementării sub Linux a standardului
PTHREADS, fiind posibil ca el să nu fie regăsit în alte implementări. Prin
urmare, nu este recomandată utilizarea lui în aplicaţii portabile. Valoarea
implicită a atributului care determină tipul unui thread este
PTHREAD_MUTEX_FAST_NP.

Stabilirea tipului unui lacăt se poate face şi în momentul declarării sale prin
atribuirea unor valori predefinite, în felul următor:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex =
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t mutex =
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

Blocarea lacătului
Operaţia prin care se realizează accesul în regim de excludere mutuală la o
resursă partajată utilizând un lacăt se numeşte operaţia de blocare a
lacătului. Această operaţie se termină cu succes doar pentru un singur thread
la un moment dat. Celelalte thread-uri sunt puse în stare de aşteptare într-o
coadă asociată lacătului, până în momentul eliberării lui de către thread-ul
care l-a blocat. Funcţia de blocare a unui lacăt se numeşte
pthread_mutex_lock şi are următoarea sintaxă:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);

195
Sincronizarea thread-urilor în Linux

O altă funcţie ce poate fi utilizată pentru a încerca blocarea unui lacăt este
pthread_mutex_trylock. Comportamentul acestei funcţii este similar cu cel
al funcţiei pthread_mutex_lock, cu diferenţa că nu are ca efect suspendarea
thread-ului care o apelează dacă lacătul nu poate fi blocat, ci se revine din
funcţie imediat, dar cu o valoare de eroare. Sintaxa funcţiei este următoarea:

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

Utilizarea funcţiei pthread_mutex_trylock este aparent trivială şi poate părea


chiar utilă, însă lucrurile nu stau chiar aşa. În primul rând funcţia reprezintă
o abatere de la regula generală de sincronizare, care presupune blocarea
thread-urilor în momentul în care nu au acces la resursa partajată. Dacă acel
thread poate face altceva în timp ce resursa este ocupată, aceasta înseamnă
că el nu are cu adevărat nevoie de acea resursă. Se pune atunci întrebarea de
ce nu s-a creat un alt thread care să se ocupe de ceea ce thread-ul în discuţie
poate face în timp ce resursa e ocupată, noul thread nefiind astfel implicat în
protocolul de sincronizare. Pe de altă parte, situaţia folosirii funcţiei
pthread_mutex_trylock este aceea în care thread-ul verifică periodic
îndeplinirea anumitor condiţii (tehnica de spooling). S-ar putea însă, ca în
cazul în care lacătul este intens solicitat şi de către alte thread-uri, să se
ajungă la situaţia ca thread-ul care apelează pthread_mutex_trylock să nu
reuşească practic niciodată să blocheze lacătul. În plus, verificarea periodică
are ca rezultat încărcarea procesorului.

Situaţiile în care folosirea funcţiei pthread_mutex_trylock este utilă sunt


cele de programare în timp-real, când thread-ul trebuie să poată reacţiona
rapid la apariţia anumitor evenimente şi situaţiile în care se încearcă
detectarea şi evitarea interblocărilor prin folosirea de lacăte organizate sub
forma unei ierarhii.

Eliberarea lacătului
Eliberarea lacătului este operaţia opusă celei de blocare. Aceste două
operaţii sunt întotdeauna folosite în pereche pentru a asigura faptul că unele
thread-uri nu vor aştepta la nesfârşit pentru un anumit lacăt. Operaţia de
eliberare se realizează prin apelul funcţiei pthread_mutex_unlock, cu
următoarea sintaxă:
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);

196
Sisteme de operare. Chestiuni teoretice şi practice

Exemplu de utilizare
Secvenţa de cod următoare ilustrează modul de creare şi respectiv, de
utilizare a lacătelor pentru asigurarea accesului în regim de excludere
mutuală la o variabilă.

/* Functia de creare a unui lacat */


void createMutex(pthread_mutex_t *mutex)
{
pthread_mutex_init(mutex, NULL);
}

/* Functia de modificare a unei variabile */


/* Accesul la variabila e in excludere mutuala */
void increment(pthread *mutex, int *variable)
{
pthread_mutex_lock(mutex); // blocarea lacatului
(*variable)++; // acces exclusiv
pthread_mutex_unlock(mutex); // deblocarea lacatului
}

/* Functia de distrugere a unui lacat */


void destroyMutex(pthread_mutex_t *mutex)
{
pthread_mutex_destroy(mutex);
}

13.3. Variabile condiţionale


Variabila condiţională este un mecanism care poate fi utilizat de thread-uri
pentru a aştepta îndeplinirea unei condiţii, condiţie care caracterizează
starea resurselor partajate. În cazul în care respectiva condiţie nu este
realizată, thread-ul este pus într-o coadă de aşteptare asociată variabilei prin
apelul unei funcţii speciale, coadă din care poate va fi ulterior trezit de către
un alt thread (prin apelul altei funcţii speciale), în momentul în care acesta
din urmă constată îndeplinirea condiţiei. Deoarece condiţiile pentru care se
aşteaptă utilizând variabile condiţionale sunt dependente de valorile unor
variabile care descriu starea resursele partajate, variabile care la rândul lor
sunt partajate şi accesate în mod concurent, testarea condiţiilor trebuie să se
facă în regim de excludere mutuală. Acesta este motivul pentru care
variabilele condiţionale sunt întotdeauna utilizate în combinaţie cu un lacăt.
Se va observa, în acest sens, în cele de mai jos că funcţiile cu ajutorul cărora
sunt folosite variabilele condiţionale sunt apelate întotdeauna doar în cadrul
unei zone de excludere mutuală.

197
Sincronizarea thread-urilor în Linux

Variabilele condiţionale sunt de tipul pthread_cond_t şi trebuie


iniţializate înainte de a fi folosite. Similar cu lacătele, iniţializarea lor poate
fi implicită sau explicită.

Iniţializarea şi ştergerea variabilelor condiţionale


Iniţializarea implicită presupune declararea variabilei şi atribuirea
constantei predefinite PTHREAD_COND_INITIALIZER, sub forma următoare:

#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

Iniţializarea explicită se face cu ajutorul funcţiei pthread_cond_init, cu


următoarea sintaxă:

#include <pthread.h>
int pthread_cond_init (
pthread_cond_t *cond,
const pthread_condattr_t *attr);

Parametrul cond este setat de către sistem şi reprezintă identificatorul


variabilei condiţionale, iar parametrul attr reprezintă adresa unei structuri
care conţine atributele variabilei condiţionale.

Dacă se doreşte ştergerea unei variabile condiţionale, acest lucru se poate


face cu ajutorul funcţiei de mai jos:

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);

Atributele variabilelor condiţionale


O structură care conţine atributele unei variabile condiţionale este de tipul
pthread_condattr_t. Funcţia de iniţializare a unei astfel de structuri este:

#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);

Distrugerea unei structuri de atribute ale unei variabile condiţionale se face


cu funcţia următoare:

198
Sisteme de operare. Chestiuni teoretice şi practice

#include <pthread.h>
int pthread_condattr_destroy (
pthread_condattr_t *attr);

Implementarea din Linux a specificaţiei PTHREADS nu oferă posibilitatea


stabilirii valorii nici unui atribut a variabilelor condiţionale.

Funcţia de aşteptare
Intrarea unui thread în starea de aşteptare a îndeplinirii unei anumite condiţii
se face cu ajutorul uneia dintre funcţiile pthread_cond_wait şi
pthread_cond_timedwait. Thread-ul va fi scos din starea de aşteptare prin
apelul funcţiei pthread_cond_signal de către un alt thread, care constată
îndeplinirea condiţiei după care se aşteaptă. Cele două funcţii de aşteptare
au următoarea sintaxă:

#include <pthread.h>
int pthread_cond_wait (
pthread_cond_t *cond,
pthread_mutex_t *mutex);
int pthread_cond_timedwait (
pthread_cond_t *cond,
pthread_mutex_t *mutex,
const struct timespec *abstime);

În ambele cazuri thread-ul este blocat şi pus într-o coadă de aşteptare.


Diferenţa între cele două funcţii este aceea că prima duce la blocarea thread-
ului pe o durată nelimitată, până la apelul funcţiei care indică îndeplinirea
condiţiei, pe când cea de-a doua funcţie precizează un interval de timp după
care thread-ul este automat scos din coada de aşteptare şi îşi reia execuţia, chiar
dacă condiţia nu este încă îndeplinită.

Se observă că ambele funcţii au ca parametru un lacăt. Aceasta se datorează


faptului că întotdeauna variabilele condiţionale sunt folosite, aşa cum am
precizat, în cadrul unei regiuni de cod protejate de un lacăt. În momentul în
care se intră în aşteptare este necesar ca lacătul să fie eliberat, pentru ca un alt
thread să poată intra în propria sa regiune critică, să modifice starea resurselor
partajate, să verifice realizarea condiţiei şi să semnalizeze acest lucru thread-
urilor blocate. Acesta este motivul transmiterii adresei lacătului blocat anterior
de către thread în momentul apelului uneia dintre funcţiile de aşteptare.

199
Sincronizarea thread-urilor în Linux

Funcţia de semnalizare sau trezire


Există două astfel de primitive, prima – funcţia pthread_cond_signal, pentru
scoaterea unui singur thread din lista celor care aşteaptă, cea de-a doua –
funcţia pthread_cond_broadcast, pentru scoaterea tuturor thread-urilor din
coada de aşteptare a variabilei condiţionale. Sintaxa acestor funcţii este
următoarea:

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

Exemplu de utilizare
Codul de mai jos reprezintă implementarea funcţiilor specifice problemei
cunoscută sub numele de producător-consumator. Scenariul problemei
presupune că mai multe thread-uri comunică prin intermediul unei zone de
memorie de tip buffer circular de mesaje. Unele thread-uri, numite
producători, adaugă mesaje în buffer, iar alte thread-uri, numite
consumatori, preiau mesajele din buffer, în ordinea în care acestea au fost
adăugate. Pentru evitarea situaţiilor de aducere a buffer-ului într-o stare
inconsistentă sau de obţinere a unor rezultate eronate, se permite accesul la
buffer doar în regim de excludere mutuală, atât a thread-urilor producător,
cât şi a celor consumator. Pentru rezolvarea eficientă a situaţiilor în care
thread-urile producător trebuie să aştepte eliberarea de spaţiu în cadrul
buffer-ului plin şi a celor în care thread-urile consumator trebuie să aştepte
adăugarea de noi mesaje în buffer-ul gol, se folosesc variabile condiţionale.

#define DIMBUFFER 100


int buffer[DIMBUFFER];
int msgNo = 0; // nr. de mesaje nepreluate
int indexProd = 0; // index de adaugare mesaj
int indexCons = 0; // index de preluare mesaj
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t full = PTHREAD_COND_INITIALIZER;
/* Functia apelata de producatori */
/* Acces in excludere mutuala */
void produce(int item)
{
pthread_mutex_lock(&mutex); // blocare lacat
while (msgNo == DIMBUFFER) // test bufer plin
pthread_cond_wait(&full, &mutex);

200
Sisteme de operare. Chestiuni teoretice şi practice

buffer[indexProd] = item; // adaugare mesaj


// reactualizare index producatori
indexProd = (indexProd + 1) % DIMBUFFER;
// incrementare nr. mesaje nepreluate
msgNo++;

// trezirea unui consumator


pthread_cond_signal(&empty);

pthread_mutex_unlock(&mutex); // debloare lacat


}

/* Functia apelata de consumatori */


/* Acces in excludere mutuala */
void consumer(int *item)
{
pthread_mutex_lock(&mutex); // blocare lacat

while (msgNo == 0) // test bufer gol


pthread_cond_wait(&empty, &mutex);

*item = buffer[indexCons]; // preluare mesaj


// reactualizare index consumatori
indexCons = (indexCons + 1) % DIMBUFFER;
// decrementare nr. mesaje nepreluate
msgNo--;

// trezirea unui producator


pthread_cond_signal(&full);

pthread_mutex_unlock(&mutex); // debloare lacat


}

13.4. Execuţia unei funcţii o singură dată


De multe ori este nevoie, în aplicaţiile care presupun existenţa mai multor
thread-uri, de funcţii care sunt apelate în faza de iniţializare. O astfel de
funcţie poate, spre exemplu, să deschidă un fişier sau să iniţializeze o
variabilă sau un lacăt. Aceste proceduri ar trebui în mod normal executate o
singură dată. În situaţia în care toate thread-urile sunt identice din punct de
vedere funcţional, apelul unei astfel de funcţii de iniţializare apare în toate
thread-urile, însă ea ar trebui executată doar o singură dată.

Standardul PTHREADS specifică un mecanism util în situaţii ca cea descrisă


mai sus, prin care utilizatorul poate fi sigur că o anumită funcţie va fi

201
Sincronizarea thread-urilor în Linux

executată doar o singură dată, indiferent de numărul de apeluri ale ei din


diferite thread-uri. Vom numi în cele ce urmează acest mecanism
pthread_once. Mecanismul pthread_once este un mecanism de sincronizare
între thread-uri în faza de iniţializare a acestora.

În situaţia în care apelăm o anumită funcţie doar prin mecanismul


pthread_once, putem face următoarele presupuneri:
1. Indiferent de numărul de apeluri ale funcţiei, dintr-unul sau mai
multe thread-uri, ea va fi executată doar o singură dată de către
primul thread care o apelează.
2. Nici un thread care apelează funcţia prin mecanismul pthread_once
nu poate trece de ea (nu se revine din funcţie) până când primul apel
al ei nu se termină cu succes. Acest lucru ne asigură că thread-urile
nu pot trece de punctul de iniţializare, până când iniţializarea nu este
finalizată.

Pentru a putea apela o funcţie prin intermediul mecanismului pthread_once,


trebuie mai întâi declarată o variabilă de tip pthread_once_t, care trebuie
apoi iniţializată cu valoarea PTHREAD_ONCE_INIT.
#include <pthread.h>
pthread_once_t once_block = PTHREAD_ONCE_INIT;

Acestei variabile i se poate asocia o singură funcţie, ale cărei apeluri se vor
face doar prin intermediul mecanismului pthread_once. Apelul funcţiei
asociate variabilei se va face cu ajutorul funcţiei pthread_once, cu
următoarea sintaxă:

#include <pthread.h>
int pthread_once (
pthread_once_t *once_block,
void (*init_routine) (void));

Primul parametru reprezintă variabila de tip pthread_once_t, iar cel de-


al doilea este funcţia asociată ei. De reţinut că o funcţie apelată prin
mecanismul pthread_once, nu ar trebui să mai fie apelată de nicăieri decât
prin mecanismul respectiv, deoarece în caz contrar nu se mai poate respecta
principiul de sincronizare impus.

202
Sisteme de operare. Chestiuni teoretice şi practice

13.5. Probleme
1. Să se scrie programele C care să testeze efectul următoarelor operaţii:
a. blocarea unui lacăt neiniţializat;
b. blocarea unui lacăt de către un thread care deja a blocat lacătul;
c. eliberarea de către un thread a unui lacăt blocat de un alt thread.
Să se identifice eventualele tipuri de erori care rezultă în cazul
folosirii fiecărui tip dintre cele trei tipuri de lacăt prezentate.
2. Să se implementeze problema producător-consumator cu buffer circular
folosind funcţiile descrise mai sus. Se va crea un număr aleator de
thread-uri de tip producător şi consumator şi fiecare va efectua un număr
aleator de paşi. Să se afişeze, pe parcursul execuţiei aplicaţiei numărul
de thread-uri producător şi consumator care sunt în aşteptare.
3. Să se implementeze un protocol de traversare a unui pod aflat în
reparaţii, pe care circulaţia maşinilor se poate face la un moment dat
doar într-un singur sens. Se presupune că podul suportă greutatea a
maximum N maşini. În momentul sosirii la pod, un thread asociat unei
maşini va executa următoarea funcţie:
void CirculaPePod(int dir)
{
IntraPePod(dir);
TraverseazaPod(dir);
IeseDePePod(dir);
}
Se cere implementarea funcţiilor de mai sus, astfel încât circulaţia să se
desfăşoare în siguranţă.
4. Să se testeze comportamentul unei variabile de tipul pthread_once_t
în cazul asocierii ei cu două sau mai multe funcţii diferite.
5. Să se scrie un program C prin care se creează mai multe thread-uri,
fiecare thread iniţializând variabila globală var cu valoarea iniţială 1 şi
incrementând-o apoi cu 1. Thread-ul principal va afişa valoarea finală a
variabilei. Să se testeze efectul execuţiei programului atât în cazul
folosirii mecanismului pthread_once pentru iniţializarea variabilei, cât şi
în cazul nefolosirii lui.
6. Să se implementeze, folosind lacăte, o clasă prin care să se realizeze
comportamentul mecanismului pthread_once.
7. Problema filozofilor. Se presupune ca N filozofi doresc să servească
cina într-o încăpere în care există o masă rotundă cu N farfurii cu
spagheti şi N furculiţe. Pentru a putea mânca, un filozof are nevoie de
două furculiţe: a lui şi cea a vecinului din stânga. Fiecare filozof are

203
Sincronizarea thread-urilor în Linux

un număr de identificare unic şi este reprezentat printr-un thread, care


va apela într-o buclă infinită două funcţii: think(int idFilozof) şi
eat(int idFilozof). Să se scrie programul C care generează thread-
urile corespunzătoare filozofilor şi codul celor două funcţii, astfel
încât servirea mesei să se desfăşoare fără probleme. Situaţiile care
trebuie evitate sunt cele de inter-blocare şi de aşteptare la infinit a unui
filozof pentru a intra la masă.
8. Se presupune că într-o frizerie există un singur frizer şi N scaune de
aşteptare. Un singur client poate fi servit la un moment dat şi alţi
maximum N clienţi pot aştepta în frizerie. Restul clienţilor trebuie să
aştepte afară din frizerie. Atât frizerul, cât şi clienţii săi sunt reprezentaţi
cu ajutorul thread-urilor. Să se scrie funcţiile care vor fi executate de
cele două tipuri de thread-uri, astfel încât să fie respectate regulile de
mai sus şi frizerul să nu stea, în cazul în care are clienţi care aşteaptă. De
asemenea trebuie evitată situaţia ca un client care aşteaptă afară din
frizerie să fie servit înaintea unuia care aşteaptă în frizerie. Se poate lua
eventual în considerare şi situaţia respectării ordinii de sosire în frizerie.
9. Se presupune că la un moment dat, în cadrul unui proces, se execută
concurent mai multe thread-uri, fiecare reprezentând fie un atom de H,
fie unul de O. Să se scrie codul funcţiilor executate de cele două tipuri de
thread-uri astfel încât, întotdeauna când e posibil, doi atomi de H şi cu
unul de O să se cupleze formând o moleculă de apă (H2O). În cazul în
care formarea moleculei nu este posibilă, atomii prezenţi trebuie să
aştepte sosirea atomilor necesari. Fiecare atom va intra în componenţa
unei singure molecule de apă. Presupunând că fiecare atom are un
identificator unic, să se scrie într-un fişier, o singură dată, componenţa
fiecărei molecule de apă.
10. Pe malul unui râu se găsesc misionari şi canibali. Ei doresc să traverseze
râul având la dispoziţie o barcă. În barcă încap trei persoane şi
traversarea se face doar cu barca plină. Pentru a nu fi mâncaţi în timpul
traversării, misionarii trebuie să fie în barcă mai mulţi decât canibali. Să
se scrie codul unor funcţii executate de threaduri care joacă rol de
misionari şi canibali, astfel încât aceste threaduri să-şi sincronizeze
execuţia în încercarea lor de a traversa râul. De asemenea, se cere ca în
cazul în care există numărul de persoane suficient pentru a se face un
transport, acestea să nu fie întârziate.

204
14. Planificarea thread-urilor în Linux

Scopul lucrării
Lucrarea prezintă funcţiile care pot fi utilizate de către utilizator pentru a
stabili modul de planificare a unui thread în Linux, în contextul execuţiei
concurente a mai multor thread-uri.

14.1. Planificarea thread-urilor


Planificarea thread-urilor constă în strategia folosită de către sistemul de
operare pentru a decide la un moment dat care thread trebuie executat şi
pentru cât timp. Componenta sistemului de operare care realizează
planificarea se numeşte planificatorul de thread-uri.

Sistemul de operare Linux foloseşte un mecanism de planificare bazat pe


priorităţi. Fiecărui thread îi este asociată o prioritate, fiind ales întotdeauna
pentru execuţie thread-ul cu prioritatea cea mai mare. Corespunzător
fiecărei priorităţi, se menţine o listă de thread-uri care candidează pentru
obţinerea procesorului. Rezultatul deciziei de planificare este controlat prin
implementarea a trei strategii diferite de inserare a thread-urilor în listele de
priorităţi. Dintre cele trei strategii, numite politici de planificare, două sunt
destinate planificării thread-urilor de prioritate ridicată, care aparţin
aplicaţiilor de timp real, iar cea de-a treia este destinată planificării thread-
urilor aplicaţiilor obişnuite, de prioritate mică.

În cele ce urmează vom descrie cele trei politici de planificare şi vom


prezenta setul de funcţii puse la dispoziţie în Linux pentru stabilirea şi
modificarea parametrilor care influenţează planificarea unui thread, mai
exact prioritatea şi politica de planificare.

14.2. Prioritatea şi politica de planificare


Există două atribute ale unui thread care fac ca respectivul thread să fie
tratat într-un mod special de către planificatorul de thread-uri. Aceste
atribute sunt prioritatea şi politica de planificare.
Pe baza priorităţii se face diferenţierea între thread-uri în ceea ce priveşte
planificarea lor pentru execuţie. Politica de planificare este strategia care

205
Planificarea thread-urilor în Linux

defineşte modul în care thread-urile cu aceeaşi prioritate sunt executate pe


procesoarele disponibile. În Linux există posibilitatea stabilirii a trei politici
de planificare, una pentru thread-urile aplicaţiilor obişnuite şi celelalte două
pentru aplicaţiile de timp real. Fiecărui thread i se asociază o prioritate statică,
având valoarea cuprinsă între 0 şi 99, valoare care poate fi modificată doar cu
ajutorul unor anumite funcţii. Corespunzător fiecărei priorităţi posibile,
planificatorul de thread-uri din Linux menţine câte o listă a thread-urilor
active care au acea valoare a priorităţii statice. Planificatorul va alege
întotdeauna pentru execuţie un thread din lista corespunzătoare celei mai mari
priorităţi pentru care există thread-uri active. Politica de planificare determină
pentru fiecare thread modul în care el este inserat şi avansează în lista
corespunzătoare priorităţii statice pe care o are asociată.

Politica de planificare
Acestui atribut al thread-ului i se poate atribui o anumită valoare întreagă, sub
forma unor constante predefinite, corespunzător uneia dintre cele trei politici
de planificare posibile. Numele constantelor, precum şi caracteristicile fiecărei
politici de planificare sunt:

SCHED_FIFO
Este o politică disponibilă pentru thread-urile aplicaţiilor de timp
real şi funcţionează pe baza principiului „primul sosit, primul servit”
– FIFO. O dată ales un thread pentru execuţie, acesta nu poate fi
întrerupt, decât dacă devine activ un alt thread cu prioritatea mai
mare decât a lui, dacă execută o instrucţiune care îl pune în stare de
aşteptare (de exemplu o instrucţiune de I/O) sau dacă în mod
voluntar cedează procesorul prin apelul funcţiei sched_yield. În
primul caz, thread-ul este pus la începutul listei ataşate priorităţii pe
care o are, iar în celelalte două la sfârşitul listei.

SCHED_RR
Funcţionează similar cu politica SCHED_FIFO, dar unui thread
având această politică de planificare i se poate aloca procesorul
pentru execuţie doar pe durata unei cuante de timp fixate. În
momentul expirării cuantei, thread-ul este întrerupt şi pus la
sfârşitul listei ataşate priorităţii pe care o are thread-ul.

SCHED_OTHER
Reprezintă politica de planificare a thread-urilor obişnuite. Thread-
urile având asociată această politică de planificare pot avea doar

206
Sisteme de operare. Chestiuni teoretice şi practice

valoarea 0 a priorităţii statice, fiind păstrate într-o singură listă, din


care sunt alese pentru execuţie pe baza principiului de time-sharing
şi prin calcularea dinamică a unor priorităţi specifice doar thread-
urilor din cadrul listei respective.

Valoarea implicită a parametrului ce descrie politica de planificare a unui


thread este SCHED_OTHER.

Toate politicile de planificare prezentate sunt preemtive, adică dacă la un


moment dat devine activ (e inserat într-una din listele menţinute de
planificator) un thread mai prioritar decât cel curent, acesta din urmă este
întrerupt şi procesorul este alocat thread-ului cu prioritatea mai mare.

Prioritatea de planificare
Valoarea pe care o poate primi prioritatea statică a unui thread depinde de
politica de planificare stabilită pentru acel thread. Acest atribut este văzut ca
un parametru al politicii de planificare. Intervalul în care se situează
prioritatea unui thread este definit de o valoare minimă, respectiv maximă.
În general, aceste limite sunt 1 şi, respectiv 99, însă valoarea lor efectivă
pentru o anumită politică de planificare se poate obţine cu ajutorul funcţiilor
de mai jos:

#include <sched.h>
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);

Thread-urile programate pentru o planificare cu politica SCHED_OTHER pot


avea doar prioritatea statică 0, fiind considerate thread-uri de prioritatea cea
mai mică. Pentru celelalte două politici de planificare, priorităţile pot fi
cuprinse între 1 şi 99. În Linux, setarea pentru un thread a unei priorităţi
mai mari decât 0 şi prin urmare a unei politici de planificare alta decât
SCHED_OTHER este posibilă doar pentru thread-urile cu privilegii de
administrator.

Funcţii de stabilire a priorităţii şi politicii de planificare


Există două modalităţi de stabilire a politicii de planificare şi respectiv, a
priorităţii unui thread. Prima modalitate este folosită în procesul de creare a
thread-ului şi presupune folosirea unei structuri de atribute de tipul

207
Planificarea thread-urilor în Linux

pthread_attr_t, în cadrul căreia trebuie stabilite, anterior momentului


creării thread-ului, valorile dorite ale celor două atribute. Cea de-a doua
modalitate permite schimbarea valorilor atributelor thread-ului în mod
dinamic, de către el însuşi, pe durata execuţiei sale. Pentru ambele
modalităţi sunt puse la dispoziţie funcţii specifice, care vor fi prezentate în
cele ce urmează.

În cazul primei modalităţi de lucru, funcţiile care pot fi utilizate pentru


stabilirea, respectiv obţinerea valorii curente a politicii de planificare şi a
priorităţii unui thread sunt cele prezentate mai jos, cu următoarea sintaxă:

#include <pthread.h>
#include <sched.h>
int pthread_attr_setschedpolicy(
pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(
pthread_attr_t *attr, int *policy);
int pthread_attr_setschedparam(
pthread_attr_t *attr,
const struct sched_param *param);
int pthread_attr_getschedparam(
pthread_attr_t *attr,
struct sched_param *param);

Semnificaţia parametrilor funcţiei este următoarea:


attr Este adresa structurii de atribute ale unui thread, structură
care trebuie iniţializată anterior prin apelul funcţiei
pthread_attr_init. Structura va fi apoi transmisă funcţiei
pthread_create, având ca efect crearea unui thread cu
valorile atributelor stabilite în cadrul respectivei structuri de
atribute.
policy Reprezintă valoarea care se stabileşte pentru politica de
planificare, în cazul funcţiei pthread_attr_setschedpolicy,
respectiv adresa variabilei în care se obţine valoarea curentă
a politicii de planificare, în cazul funcţiei
pthread_attr_getschedpolicy. Valoarea acestui parametru
poate fi una dintre constantele amintite mai sus:
SCHED_OTHER, SCHED_FIFO, SCHED_RR.

208
Sisteme de operare. Chestiuni teoretice şi practice

param Este o structură prin care se specifică (funcţia


pthread_attr_setschedparam) sau în care se obţin (funcţia
pthread_attr_getschedparam) parametrii politicii de
planificare. În forma actuală, structura conţine un singur câmp,
care este prioritatea thread-ului, aşa cum este ilustrat şi mai jos:
struct sched_param{
int sched_priority;
};

Setarea dinamică a celor două atribute legate de planificarea thread-urilor se


poate face cu ajutorul funcţiei pthread_setschedparam, iar obţinerea
valorilor lor cu funcţia pthread_getschedparam, cu următoarea sintaxă:

#include <pthread.h>
int pthread_setschedparam(
pthread_t th, int policy,
const struct sched_param *param);
int pthread_getschedparam(
pthread_t th, int *policy,
struct sched_param *param);

În urma apelului funcţiei pthread_setschedparam, thread-ul pentru care se


stabilesc noile valori ale parametrilor de planificare este mutat la începutul
listei asociate noii priorităţi a thread-ului şi el poate întrerupe thread-ul
curent, în caz că are prioritatea mai mare decât acesta.

Thread-ul curent poate ceda la un moment dat procesorul prin apelul


funcţiei pthread_yield, caz în care el este pus la sfârşitul listei
corespunzătoare priorităţii statice a thread-ului şi thread-ul din capul listei
respective va fi ales pentru execuţie. În cazul în care thread-ul care apelează
funcţia sched_yield este singurul cu acea prioritate, el îşi va continua
execuţia. Sintaxa funcţiei sched_yield este următoarea:

#include <sched.h>
int sched_yield();

Având în vedere că implementarea specificaţiei PTHREADS sub Linux se


bazează pe utilizarea proceselor (lightweight processes), considerăm utilă
precizarea câtorva funcţii referitoare la planificarea proceselor, caz în care
chestiunile descrise mai sus rămân în totalitate valabile, dar raportate la
procese. Funcţiile respective şi sintaxa lor sunt descrise mai jos:

209
Planificarea thread-urilor în Linux

#include <sched.h>
int sched_setscheduler(
pid_t pid, int policy,
const struct sched_param *p);
int sched_getscheduler(pid_t pid);
int sched_setparam(
pid_t pid, const struct sched_param *p);
int sched_getparam(
pid_t pid, struct sched_param *p);

În cazul politicii de planificare SCHED_OTHER, calculul priorităţii dinamice


a thread-urilor din această categorie poate fi influenţată prin stabilirea unei
priorităţi de bază a thread-ului. Stabilirea unei astfel de priorităţi şi
obţinerea valorii ei curente se poate face cu ajutorul următoarelor funcţii:

#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);

Semnificaţia şi valorile parametrilor funcţiilor este:


which Indică pentru cine se doreşte setarea sau obţinerea valorii priorităţii
dinamice de bază. Valorile posibile sunt PRIO_PROCESS,
PRIO_PGRP sau PRIO_USER pentru cazul unui proces, grup de
procese şi respectiv, utilizator.
who Este interpretat în funcţie de valoarea parametrului which şi poate fi
identificatorul unui proces, al unui grup de procese sau al unui
utilizator. Valoarea 0 indică procesul din care se apelează funcţia.
prio Este valoarea priorităţii dinamice de bază şi poate avea o valoarea
cuprinsă între –20 şi +20. Valorile mai mici indică o prioritate mai
mare. Stabilirea unei valori mai mici decât cea curentă (adică,
creşterea priorităţii) poate fi făcută doar de către administratorul de
sistem. Valoarea implicită este 0. Funcţia getpriority returnează o
valoare cuprinsă între 1 şi 40 (reprezentând rezultatul expresiei
20 - prio), deoarece valorile negative sunt rezervate de obicei
cazurilor de eroare.

210
Sisteme de operare. Chestiuni teoretice şi practice

Modificarea valorii priorităţii dinamice de bază poate fi făcută cu ajutorul


funcţiei nice descrisă mai jos.

#include <unistd.h>
int nice(int inc);

Funcţia are ca efect adunarea valorii inc la valoarea curentă a priorităţii


dinamice de bază a procesului. Valori negative, având ca efect creşterea
priorităţii procesului (thread-ului) pot fi specificate doar de către
administratorul de sistem, ceea ce înseamnă că un proces obişnuit nu poate
fi decât „politicos” (nice) în sensul scăderii propriei priorităţi în favoarea
altor procese. Prin urmare, funcţia nu prezintă un grad mare de utilitate.

14.3. Domeniul de planificare şi domeniul de alocare


Domeniul de planificare a thread-urilor determină mulţimea thread-urilor
care concurează la un moment dat pentru obţinerea unui procesor din cele
disponibile. Se definesc două posibilităţi de specificare a unui asemenea
domeniu, şi anume:
• domeniu de proces: când un thread concurează pentru obţinerea unui
procesor doar cu thread-uri aparţinând aceluiaşi proces;
• domeniu sistem: când thread-urile tuturor proceselor din sistem sunt
luate în considerarea în momentul alocării unui procesor unui thread.

O altă problemă care se pune în cazul sistemelor multiprocesor este aceea a


determinării setului de procesoare pe care un thread poate fi executat. Acest
set se numeşte domeniu de alocare. În cazul cel mai simplu, toate
procesoarele din sistem pot fi incluse în acelaşi domeniu de alocare, având ca
efect executarea tuturor thread-urilor din sistem pe oricare dintre ele. În
cazurile mai speciale, din motive de eficienţă sau atunci când anumite thread-
uri necesită un regim preferenţial, se pot defini mai multe domenii de alocare,
fiecare domeniu fiind destinat execuţiei unui anumit grup de thread-uri.

Dezavantajul utilizării unui domeniu de proces pentru planificarea thread-


urilor este acela că la un moment dat thread-uri ale unui proces pot să
aştepte după eliberarea unui procesor, chiar în situaţiile în care thread-uri cu
prioritate mai mică ale altor procese sunt în execuţie pe unele procesoare.
Aceasta deoarece dintre thread-urile respectivului proces doar unul este ales
la un moment dat pentru execuţie. În cazul domeniului sistem, thread-urile
aceluiaşi proces pot fi simultan în execuţie, atât datorită disponibilităţii unor

211
Planificarea thread-urilor în Linux

procesoare, cât şi datorită faptului că au prioritate mai mare faţă de thread-


urile altor procese.

Funcţiile de mai jos pot fi folosite pentru stabilirea şi obţinerea valorii


atributului care determină pentru un thread domeniul de planificare.

#include <pthread.h>
int pthread_attr_setscope(
pthread_attr_t *attr, int scope);
int pthread_attr_getscope(
pthread_attr_t *attr, int *scope);

Valorile permise pentru atributul scope sunt:


• PTHREAD_SCOPE_SYSTEM şi
• PTHREAD_SCOPE_PROCESS.

14.4. Proprietatea de moştenire


Există un atribut care oferă posibilitatea ca un thread să moştenească
atributele de planificare ale thread-ului care l-a creat. Funcţia prin care se
setează acest atribut se numeşte pthread_attr_setinheritsched, iar cea prin
care se obţine el este pthread_attr_getinheritsched, cu următoarea sintaxă:

#include <pthread.h>
int pthread_attr_setinheritsched(
pthread_attr_t *attr, int inherit);
int pthread_attr_getinheritsched(
pthread_attr_t *attr, int *inherit);

Parametrul inherit poate lua următoarele două valori predefinite:


PTHREAD_EXPLICIT_SCHED
Valorile atributelor care identifică politica şi prioritatea de
planificare nu se moştenesc, ci trebuie specificate explicit.
Evident, dacă nu sunt specificate, ele iau valorile implicite setate
de către sistem, dar nu le moştenesc pe cele ale thread-ului creator.
PTHREAD_INHERIT_SCHED
Politica şi prioritatea de planificare se moştenesc de la thread-ul
creator.

212
Sisteme de operare. Chestiuni teoretice şi practice

14.5. Exemplu de utilizare


Codul de mai jos oferă un model de utilizare şi testare a principalelor funcţii
descrise mai sus pentru stabilirea politicii de planificare şi a priorităţii unui
thread.

#include <sched.h>
#include <stdlib.h>

void* fcTh(void* arg)


{
int i, policy;
struct sched_param schdPar;
int id = *(int*)arg;

sleep(2);

for (i=1; i<10000; i++) {


pthread_getschedparam(pthread_self(),
&policy, &schdPar);

printf("Thread %d has priority %d\n",


id, schdPar.sched_priority);

printf("Thread %d has policy: ", id);


switch (policy){
case SCHED_OTHER:
printf("SCHED_OTHER\n");
break;
case SCHED_FIFO:
printf("SCHED_FIFO\n");
break;
case SCHED_RR:
printf("SCHED_RR\n");
break;
} // end switch
} // end for
} // end fcTh()

main()
{
pthread_t th1, th2, th3;
pthread_attr_t attr1, attr2, attr3;
struct sched_param schdPar1, schdPar2, schdPar3;
int id1, id2, id3;

printf("The SCHED_FIFO min priority is: %d\n",


sched_get_priority_min(SCHED_FIFO));

213
Planificarea thread-urilor în Linux

printf("The SCHED_FIFO max priority is: %d\n",


sched_get_priority_max(SCHED_FIFO));

printf("The SCHED_RR min priority is: %d\n",


sched_get_priority_min(SCHED_RR));

printf("The SCHED_RR max priority is: %d\n",


sched_get_priority_max(SCHED_RR));

printf("The SCHED_OTHER min priority is: %d\n",


sched_get_priority_min(SCHED_OTHER));

printf("The SCHED_OTHER max priority is: %d\n",


sched_get_priority_max(SCHED_OTHER));

id1 = 1;
pthread_attr_init(&attr1);
pthread_attr_setschedpolicy(&attr1, SCHED_RR);
schdPar1.sched_priority = 10;
pthread_attr_setschedparam(&attr1, &schdPar1);
pthread_create(&th1, &attr1, fcTh, &id1);

id2 = 2;
pthread_attr_init(&attr2);
pthread_attr_setschedpolicy(&attr2, SCHED_RR);
schdPar2.sched_priority = 10;
pthread_attr_setschedparam(&attr2, &schdPar2);
pthread_create(&th2, &attr2, fcTh, &id2);

id3 = 3;
pthread_attr_init(&attr3);
pthread_attr_setschedpolicy(&attr3, SCHED_RR);
schdPar3.sched_priority = 12;
pthread_attr_setschedparam(&attr3, &schdPar3);
pthread_create(&th3, &attr3, fcTh, &id3);

pthread_join(th1, NULL);
pthread_join(th2, NULL);
pthread_join(th3, NULL);
}

214
Sisteme de operare. Chestiuni teoretice şi practice

14.6. Probleme
1. Să se testeze funcţiile descrise în cadrul lucrării. Se va folosi ca model
programul C de mai sus. Să se stabilească alternativ diferite politici de
planificare şi priorităţi ale celor trei thread-uri.
2. În cadrul problemei producător-consumator, se cere introducerea unui nou
thread numit garbage-collector, care are rolul de eliberare a spaţiilor din
buffer conţinând mesaje preluate, dar neeliminate de către thread-urile
consumator. Se presupune că thread-urile producător şi consumator sunt
programate pentru planificare utilizând politica SCHED_RR, iar pentru
thread-ul garbage_collector se foloseşte politica SCHED_OTHER.
3. Se consideră un pod pe care se poate circula doar într-un singur sens la
un moment dat. În plus, pe pod se pot afla simultan doar MAX_MASINI
maşini. O maşină este reprezentată de un thread, care va executa
procedura Circula, având următoarea formă:
Circula(int directie)
{
IntraPePod(directie);
TraverseazaPod(directie);
IeseDePePod(directie);
}
(a) Se cere să se implementeze procedurile de mai sus, folosind lacăte şi
variabile condiţionale, astfel încât să fie respectate regulile de
traversare a podului amintite mai sus.
(b) Datorită faptului că respectarea regulilor de mai sus poate duce la
apariţia cazului când maşinile dintr-o anumită parte a podului pot să
aştepte un timp nedeterminat, atunci când din sens contrar vin
încontinuu maşini, se cere introducerea unui thread cu rol de control
a circulaţiei (ceea ce în realitate este realizat cu două semafoare la
fiecare intrare pe pod, care indică pe rând culoarea verde). Acest
thread va aloca un interval de traversare pentru fiecare direcţie, la
expirarea acestui timp, schimbând sensul de circulaţie. Opţional se
poate introduce un anumit grad de inteligenţă controlorului de trafic,
care să ţină cont de fluxul de maşini din ambele direcţii. Acest
controlor inteligent va acorda un timp de traversare mai mare pentru
direcţia din care vin mai multe maşini, sau dacă dintr-o direcţie nu
vin maşini, atunci nu va schimba sensul de circulaţie.
(c) Să se introducă thread-uri care să joace rolul maşinilor de poliţie sau
salvare, adică vor avea o prioritate mai mare decât a thread-urilor
reprezentând maşini obişnuite. Acestea nu vor fi afectate de direcţia

215
Planificarea thread-urilor în Linux

de circulaţie impusă de controlorul de trafic, dar evident vor trebui


să ţină cont de regulile enunţate iniţial, adică să aştepte după
maşinile din sens contrar care sunt pe pod şi să nu se depăşească
numărul maxim de maşini de pe pod.
4. Inversarea priorităţilor. Să se testeze funcţionarea unui proces cu trei
thread-uri T1, T2 şi T3, având fiecare trei priorităţi diferite
0 < p1 < p2 < p3, în următorul context: primul thread blochează un
lacăt L pentru a modifica o variabilă partajată V. Între timp porneşte cel
de-al doilea thread, care va executa o buclă infinită, fără a încerca însă
blocarea lacătului L. Cel de-al treilea thread va încerca ulterior şi el
blocarea lacătului. Să se urmărească şi să se comenteze rezultatul
execuţiei celor trei thread-uri.

216
Bibliografie
1. Andrew Tanenbaum, Modern Operating Systems, 2nd Edition, Prentice
Hall, 2001.
2. Daniel Bovet, Marco Cesati, Understanding the Linux Kernel, 2nd
Edition, O’Reilly, 2002.
3. Mark Mitchell, Jeffrey Oldham, Alex Samuel, Advanced Linux
Programming, CodeSourcery LLC, New Riders Publishing, First
Edition, June 2001 (disponibilă în format pdf la adresa
www.advancedlinuxprogramming.com)
4. Bradford Nichols, Dick Buttlar, Jacqueline Proulx Farrell, PThreads
Programming. A POSIX Standard for Better Multiprocessing, first
edition, O’Reilly, 1996.
5. ***, Paginile de manual din Linux, disponibile şi la adresa
www.linuxmanpages.com

217

S-ar putea să vă placă și