Documente Academic
Documente Profesional
Documente Cultură
SISTEME DE OPERARE
CHESTIUNI TEORETICE ŞI PRACTICE
U.T.PRES
CLUJ-NAPOCA, 2007
Coperta: Ovidiu Muraru
Cuprins
Prefaţa ........................................................................................................v
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
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
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.
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.
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.
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
Sistemul de fişiere în Linux
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
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
5
Sistemul de fişiere în Linux
6
Sisteme de operare. Chestiuni teoretice şi practice
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.
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
9
Sistemul de fişiere în Linux
ls [opţiuni] director
10
Sisteme de operare. Chestiuni teoretice şi practice
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
12
Sisteme de operare. Chestiuni teoretice şi practice
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).
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.
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
dir=/usr/include
cd $dir
num=3
k=${num}tmp # k=$numtmp ar fi fost
# interpretat ca variabila numtmp
echo $k # Se afiseaza 3tmp
15
Fişiere de comenzi în Linux
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ă.
17
Fişiere de comenzi în Linux
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.
nume_funcţie()
{ cmd1; ... cmd2; }
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.
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:
21
Fişiere de comenzi în Linux
22
Sisteme de operare. Chestiuni teoretice şi practice
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.
Comanda IF
Sintaxa comenzii este:
if cond1
then lista_cmd_1
[ elif cond2
then lista_cmd_2]
[ else lista_cmd_3]
fi
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
Comanda CASE
Sintaxa comenzii este:
case expresie in
sablon_1) lista_comenzi_1;;
sablon_2) lista_comenzi_2;;
...
esac
Comanda FOR
Sintaxa comenzii este:
25
Fişiere de comenzi în Linux
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
27
Fişiere de comenzi în Linux
read fname
valid $fname
if test -f invalid
then
echo Nume invalid
rm invalid
exit
fi
28
Sisteme de operare. Chestiuni teoretice şi practice
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
29
Fişiere de comenzi în Linux
30
Sisteme de operare. Chestiuni teoretice şi practice
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.
32
Sisteme de operare. Chestiuni teoretice şi practice
33
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *cale, mode_t permisiuni);
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t noct);
34
Sisteme de operare. Chestiuni teoretice şi practice
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t noct);
#include <unistd.h>
int close(int fd);
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);
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.
#include <unistd.h>
int link(const char* vecheaCale, const char* nouaCale);
#include <unistd.h>
int unlink(const char* cale);
36
Sisteme de operare. Chestiuni teoretice şi practice
#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);
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:
#include <unistd.h>
int access(const char* cale, int tipAcces);
38
Sisteme de operare. Chestiuni teoretice şi practice
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
#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
#include <sys/types.h>
#include <unistd.h>
int chown(const char* cale,
uid_t proprietar, gid_t grup);
40
Sisteme de operare. Chestiuni teoretice şi practice
#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);
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
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); }
}
43
Apeluri sistem pentru lucrul cu fişiere şi directoare în Linux
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
if (argc != 3) {
printf("Utilizare: %s fis_sursa fis_dest\n",
argv[0]);
exit(0);
}
if (nr < 0) {
perror("Eroare citire din fisier sursa");
exit(3);
}
close(from);
close(to);
}
44
Sisteme de operare. Chestiuni teoretice şi practice
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
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);
}
}
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
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.
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
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
50
Sisteme de operare. Chestiuni teoretice şi practice
Î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
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
52
Sisteme de operare. Chestiuni teoretice şi practice
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);
53
Sistemul de fişiere NTFS
54
Sisteme de operare. Chestiuni teoretice şi practice
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
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
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
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);
Funcţia SetFilePointer
Funcţia SetFilePointer deplasează pointerul unui fişier deschis.
DWORD SetFilePointer(
HANDLE hFile,
LONG lDistanceToMove,
PLONG lpDistanceToMoveHigh,
DWORD dwMoveMethod);
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);
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
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);
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
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ă:
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
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".
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;
/* Inchide fisierele */
CloseHandle(inhandle);
CloseHandle(outhandle);
}
#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() {
if (hSearch == INVALID_HANDLE_VALUE)
{
printf("Nu s-au gasit fisisere *.txt");
return;
}
if (!FindNextFile(hSearch, &FileData))
{
if (GetLastError() == ERROR_NO_MORE_FILES)
{
printf("Nu mai sunt fisiere *.TXT");
fFinished = TRUE;
}
else
{
printf("Eroare de cautare.");
return;
}
}
}
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
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();
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.
#include <unistd.h>
int setpgrp();
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();
68
Sisteme de operare. Chestiuni teoretice şi practice
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.
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[]);
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.
72
Sisteme de operare. Chestiuni teoretice şi practice
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.
74
Sisteme de operare. Chestiuni teoretice şi practice
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).
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
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.
80
Sisteme de operare. Chestiuni teoretice şi practice
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.
81
Thread-uri în Linux
#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal(pthread_t thread1,
pthread_t thread2);
#include <pthread.h>
void pthread_exit(void *retval);
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);
#include <pthread.h>
int pthread_setcancelstate(int val, int *vecheaVal);
#include <pthread.h>
int pthread_setcanceltype(int tip, int *vechiulTip);
83
Thread-uri în Linux
#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
#include <pthread.h>
void pthread_cleanup_push(
void (*functie)(void*),
void* arg);
void pthread_cleanup_pop(int executa);
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);
}
#include <pthread.h>
int pthread_join(pthread_t th, void **stareTerminare);
86
Sisteme de operare. Chestiuni teoretice şi practice
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.
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
87
Thread-uri în Linux
#include <pthread.h>
int pthread_attr_setdetachstate(
pthread_attr_t *atribute,
int stare);
int pthread_attr_getdetachstate(
const pthread_attr_t* atribute,
int *stare);
#include <pthread.h>
int pthread_detach(pthread_t th);
88
Sisteme de operare. Chestiuni teoretice şi practice
89
Thread-uri în Linux
#include <pthread.h>
pthread_atfork( void (*pregatire)(void),
void (*parinte)(void),
void (*fiu)(void));
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
92
Sisteme de operare. Chestiuni teoretice şi practice
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.
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.
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
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);
97
Procese şi thread-uri în Windows 2000
LPVOID CreateFiber(
SIZE_T dwStackSize,
LPFIBER_START_ROUTINE lpStartAddress,
LPVOID lpParameter);
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);
99
Procese şi thread-uri în Windows 2000
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.
void main(VOID)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
101
Procese şi thread-uri în Windows 2000
#include <windows.h>
#include <conio.h>
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
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
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.
105
Fişiere PIPE în Linux
106
Sisteme de operare. Chestiuni teoretice şi practice
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);
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
#include <unistd.h>
int pipe(int fd[2]);
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
Î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.
110
Sisteme de operare. Chestiuni teoretice şi practice
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];
112
Sisteme de operare. Chestiuni teoretice şi practice
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.
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
// 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;
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);
}
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
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
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.
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.
118
Sisteme de operare. Chestiuni teoretice şi practice
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.
120
Sisteme de operare. Chestiuni teoretice şi practice
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.
BOOL ConnectNamedPipe(
HANDLE hNamedPipe,
LPOVERLAPPED lpo);
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.
BOOL CreateChildProcess(VOID);
VOID WriteToPipe(VOID);
VOID ReadFromPipe(VOID);
VOID ErrorExit(LPTSTR);
VOID ErrMsg(LPTSTR, BOOL);
123
Fişiere PIPE în Windows
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;
VOID WriteToPipe(VOID)
{
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE];
125
Fişiere PIPE în Windows
VOID ReadFromPipe(VOID)
{
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE];
}
}
VOID main(VOID)
{
CHAR chBuf[BUFSIZE];
DWORD dwRead, dwWritten;
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;
}
}
VOID InstanceThread(LPVOID);
VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD);
int _tmain(VOID)
{
BOOL fConnected;
DWORD dwThreadId;
HANDLE hPipe, hThread;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mypipe");
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;
}
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
while (1)
{
// Citeste cererile clientului din pipe
fSuccess = ReadFile( hPipe,
chRequest, BUFSIZE*sizeof(TCHAR),
&cbBytesRead, NULL);
if (! fSuccess || cbBytesRead == 0)
break;
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);
}
129
Fişiere PIPE în Windows
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
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;
}
if (!fOk)
{
printf("Eroare WriteFile ");
return 0;
}
do
{
// Citeste din pipe
fOk = ReadFile( hPipe, chBuf,
BUFSIZE*sizeof(TCHAR), &cbRead, NULL);
} 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.
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.
134
Sisteme de operare. Chestiuni teoretice şi practice
135
Comunicarea prin semnale în Linux
#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);
136
Sisteme de operare. Chestiuni teoretice şi practice
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
};
137
Comunicarea prin semnale în Linux
138
Sisteme de operare. Chestiuni teoretice şi practice
#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);
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.
140
Sisteme de operare. Chestiuni teoretice şi practice
#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);
#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.
kill(getpid(), semnal);
142
Sisteme de operare. Chestiuni teoretice şi practice
Funcţia alarm
Sintaxa ei este următoarea:
#include <unistd.h>
unsigned int alarm(unsigned int secunde);
#include <sys/time.h>
struct itimerval {
struct timeval it_interval;
struct timeval it_value;
};
struct timeval {
long tv_sec; // secunde
long tv_usec; // microsecunde
};
143
Comunicarea prin semnale în Linux
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 siginterrupt
Sintaxa funcţiei este următoarea:
#include <signal.h>
int siginterrupt(int semnal, int intrerupere);
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
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
int terminare;
terminare = 0;
while (!terminare)
pause();
}
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;
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.
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.
150
Sisteme de operare. Chestiuni teoretice şi practice
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t cheie, int dimensiune, int optiuni);
151
Comunicarea prin memorie partajată şi cozi de mesaje în Linux
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).
# include <sys/types.h>
# include <sys/ipc.h>
key_t ftok(const char *caleFisier, int identificator);
152
Sisteme de operare. Chestiuni teoretice şi practice
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int id, const void *adresa, int optiuni);
int shmdt(const void *adresa);
153
Comunicarea prin memorie partajată şi cozi de mesaje în Linux
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.
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
};
154
Sisteme de operare. Chestiuni teoretice şi practice
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int id, int cmd, struct shmid_ds *buf);
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
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
156
Sisteme de operare. Chestiuni teoretice şi practice
157
Comunicarea prin memorie partajată şi cozi de mesaje în Linux
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t cheie, int optiuni);
158
Sisteme de operare. Chestiuni teoretice şi practice
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
#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);
160
Sisteme de operare. Chestiuni teoretice şi practice
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.
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
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int id, int cmd, struct msqid_ds *buf);
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
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.
ipcrm –q identificator
ipcrm –Q cheie
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
#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
#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
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.
170
Sisteme de operare. Chestiuni teoretice şi practice
171
Sincronizarea prin semafoare în Linux
172
Sisteme de operare. Chestiuni teoretice şi practice
173
Sincronizarea prin semafoare în Linux
174
Sisteme de operare. Chestiuni teoretice şi practice
#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
};
175
Sincronizarea prin semafoare în Linux
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.
176
Sisteme de operare. Chestiuni teoretice şi practice
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:
178
Sisteme de operare. Chestiuni teoretice şi practice
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
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
};
181
Sincronizarea prin semafoare în Linux
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.
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
185
Sincronizarea prin semafoare în Linux
186
Sisteme de operare. Chestiuni teoretice şi practice
#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
// initialiyare semafoare
semctl(semId, MUTEX, SETVAL, 1);
semctl(semId, SEM, SETVAL, PERMISIUNI);
188
Sisteme de operare. Chestiuni teoretice şi practice
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
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.
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.
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.
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
#include <pthread.h>
int pthread_mutex_init (
pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
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);
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.
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);
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ă.
197
Sincronizarea thread-urilor în Linux
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#include <pthread.h>
int pthread_cond_init (
pthread_cond_t *cond,
const pthread_condattr_t *attr);
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
198
Sisteme de operare. Chestiuni teoretice şi practice
#include <pthread.h>
int pthread_condattr_destroy (
pthread_condattr_t *attr);
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);
199
Sincronizarea thread-urilor în Linux
#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.
200
Sisteme de operare. Chestiuni teoretice şi practice
201
Sincronizarea thread-urilor în Linux
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));
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
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.
205
Planificarea thread-urilor în Linux
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
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);
207
Planificarea thread-urilor în Linux
#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);
208
Sisteme de operare. Chestiuni teoretice şi practice
#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);
#include <sched.h>
int sched_yield();
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);
#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
210
Sisteme de operare. Chestiuni teoretice şi practice
#include <unistd.h>
int nice(int inc);
211
Planificarea thread-urilor în Linux
#include <pthread.h>
int pthread_attr_setscope(
pthread_attr_t *attr, int scope);
int pthread_attr_getscope(
pthread_attr_t *attr, int *scope);
#include <pthread.h>
int pthread_attr_setinheritsched(
pthread_attr_t *attr, int inherit);
int pthread_attr_getinheritsched(
pthread_attr_t *attr, int *inherit);
212
Sisteme de operare. Chestiuni teoretice şi practice
#include <sched.h>
#include <stdlib.h>
sleep(2);
main()
{
pthread_t th1, th2, th3;
pthread_attr_t attr1, attr2, attr3;
struct sched_param schdPar1, schdPar2, schdPar3;
int id1, id2, id3;
213
Planificarea thread-urilor în Linux
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
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