Programare
de sistem
- Linux este compus din:
-
kernel, care se ocupa de abstractizarea hardware-ului (si care ofera
un API de system call-uri pentru programatori). Un system call este o
functie apelabila din C si este folosita la fel ca oricare alta.
Diferenta consta in ce se intampla in interiorul functiei:
implementarea acesteia face parte din kernel. Exemple: open, read,
write, fork, exit.
- librarii (libraria de baza este glibc),
construite be baza apelurilor. Exemple de functii de librarii:
printf, fopen, strlen, malloc.
- aplicatii, care pot utiliza
apeluri din librarii sau direct apeluri de sistem.
- tool-uri de
baza (bash, ls, ps, mv, cp, make, gcc etc)
- servere (http, ftp,
mail, ssh)
- editoare de text, de imagini, browsere,
samd
Man pages
Lista
de syscall-uri pentru arhitectura x86:
http://lxr.linux.no/linux+v2.6.29/arch/x86/include/asm/unistd_32.h
Pentru
a afla detalii despre un apel de sistem (de ex. read) se foloseste
comanda man 2 read. Numarul de dupa man indica categoria elementului
de cautat, pentru multiplexare in cazul in care comanda este
generica, ca in cazul nostru. Numarul 1 contine comenzi bash, numarul
2 apeluri de sistem, numarul 3 contine (printre altele) functii
standard (scanf, printf)
Apeluri de
sistem de IO
Un fisier deschis este reprezentat de un file
descriptor (fd). In general, un fd este obtinut printr-un apel open
sau creat. Un fisier este inchis cu close, se poate citi/scrie in
caracter cu read/write, iar cursorul fisierului poate fi mutat cu
apelul lseek.
Exemplu: io.c
Procese
Atunci
când executati un program în sistemul de operare Linux,
acesta creaza un context special pentru aceasta, context ce contine
tot ce îi trebuie sistemului de operare sa îl ruleze ca
si cum ar fi singurul program ce ruleaza în sistem.
Executia
programului se face în Linux la nivelul procesului. Procesul
este deci o instanta a unui program, este executia unei imagini
(ansamblul elementelor ce constituie contextul de executie al
procesului). Elementele contextului de executie sunt: codul sursa ,
memoria folosita de program, directorul de lucru, fisierele care sunt
deschise cât si pozitii la care se lucreaza în acestea,
limite ale resurselor, informatie legata de controlul accesului la
resurse si alte informatii de nivel inferior.
Pe sisteme de 32
de biti fiecare proces are un spatiu virtual de adrese de 4 GB (2^32
octeti) din care 3 GB sunt disponibili pentru alocare procesului,
ultimul GB fiind rezervat sistemului de operare (codul kernelului si
al driverelor, date, cache-uri, etc.). Asadar fiecare proces "vede"
sistemul de operare in spatiul sau de adrese insa nu poate accesa
zona respectiva decat prin intermediul apelurilor de sistem. Pe
sisteme de 64 de biti spatiul total de adrese este de 2^64 octeti (16
EB).
Linux creaza impresia rularii simultane a mai multor
programe prin comutarea rapida între procesele active din
tabela de proces. Pe un sistem uniprocesor, un singur proces va rula
la un moment dat, celelalte asteptandu-si randul la executie, sau
asteptand terminarea unor apeluri de durata (de ex. citirea datelor
de pe HDD). Pe un sistem cu 2 procesoare pot rula 2 procese in
acelasi timp, samd.
Sistemul de operare pune la dispozitie
apeluri pentru crearea unui proces (fork, exec), terminarea unui
proces (exit), asteptarea terminarii unui proces (wait, waitpid) si
alte apeluri utile (getpid, getppid, etc).
La fiecare rulare a
unui program sistemul UNIX face o operatie de fork, având ca
rezultat crearea unui context de proces si executia programului în
acel context. Nucleul sistemului de operare pastreaza evidenta
proceselor din sistem prin intermediul unei tabele a proceselor.
Aceast a tabela contine cîte o intrare pentru fiecare proces
existent în sistem, intrare ce contine o serie de informatii
despre acel proces: procesul parinte (ppid), utilizatorul care a
lansat procesul (uid), grupul care detine procesul (gid), etc.
În
continuare vom detalia pasii acestei proceduri:
1) Se aloca un
loc în tabela de proces (lista proceselor ce ruleaza în
sistem la un moment dat).
2) Sistemul îi asociaza procesului
un identificator sau PID(Process IDentifier). Acest identificator
poate fi folosit pentru a controla procesul mai târziu.
3)
Copiaza contextul “procesului Parinte”, procesul care a
solicitat aparitia noului proces.
4) Returneaza noul PID (al
“procesului Fiu”) “procesului Parinte”. Acest
lucru îi permite procesului parinte sa poata analiza si
controla direct procesele fii.
Dupa ce fork-ul este complet,
UNIX ruleaza programul. Una din diferentele fundamentale între
UNIX si multe alte sisteme de operare consta în procedeul în
doi pasi de rulare a unui program. În primul pas se creaza un
nou proces identic cu “procesul parinte”. În al
doilea se executa un program diferit. Aceasta procedura permite
câteva variatiuni interesante.
Procedurile prezentate
mai sus sunt realizate implicit de catre sistemul de operare UNIX, în
cele ce urmeaza vom prezenta câteva dintre cele mai utilizate
metode din limbajul C pentru crearea de “procese Fiu” si
realizarea comunicarii între acestea.
Singura
modalitate de creare a proceselor in UNIX/Linux este cu ajutorul
apelului sistem fork(). Prin acest apel se creeaza o copie a
procesului apelant, si ambele procese – cel nou creat si cel
apelant – isi vor continua executia cu urmatoarea instructiune
(din programul executabil) ce urmeazAa dupa apelul functiei fork.
Singura diferenta dintre procese va fi valoarea returnata de functia
fork, precum si, bineinteles, PID-urile proceselor. Procesul apelant
va fi parintele procesului nou creat, iar acesta va fi fiul
procesului apelant (mai exact, unul dintre procesele fii ai
acestuia).
Datorita acestei operatii de “clonare”,
imediat dupa apelul fork procesul fiu va avea aceleasi valori ale
variabilelor din program si aceleasi fisiere deschise ca si procesul
parinte. Mai departe insa, fiecare proces va lucra pe zona sa de
memorie. Deci, daca fiul modifica valoarea unei variabile, aceasta
modificare nu va fi vizibila si in procesul tata (si nici
invers).
pid_t fork() – este functia ce creaza un
“proces Fiu” când este executata în “procesul
Parinte”. Returneaza un întreg care are urmatoarele
semnificatii:
- în “Parinte” este PID–ul
“procesului Fiu”
- în “Fiu” este
0
Procesul nou creat poate afla PID-ul tatalui cu ajutorul
primitivei getppid, pe cind procesul tata nu poate afla PID-ul noului
proces creat, fiu al lui, prin alta maniera decit prin valoarea
returnata de apelul fork.
pid_t wait(int *state) – este
functia de asteptare a “proceselor Fiu”. Ea produce
suspendarea executiei procesului care o apeleaza pâna la
terminarea executiei unuia dintre fii sai. Este folosita pentru
evitarea cazurilor în care procesul parinte poate termina
executia si elibera memoria, înaintea proceselor fiu fapt ce ar
putea conduce la aparitia unor procese “zombie”(procese
ramase fara parinte).
pid_t waitpid(pid_t pid, int *status,
int options) – spre deosebire de functia anterioara adauga ca
parametru PID-ul procesului care este asteptat.
pid_t getpid()
– returneaza PID-ul procesului în care a fost
apelata.
pid_t getppid() – returneaza PID-ul procesului
parinte al procesului în care a fost apelata.
Thread-uri
(fire de executie)
Uneori se doreste ca o aplicatie sa
executa in paralel mai multe task-uri. De ex firefox downloadeaza un
fisier in timp ce utilizatorul navigheaza nestingherit. Acest lucru
se poate realiza lansand cate un proces pentru fiecare task, insa
acest lucru ne ingreuneaza sistemul. Thread-urile sunt mult mai
eficiente si mai usor de folosit in cazul in care se doreste o
executie a mai multor secvente de cod in acelasi timp.
Pentru
programarea cu threaduri POSIX (de departe cea mai raspandita solutie
de threading pe linux si alte sisteme de operare de tip UNIX) este
nevoie de urmatoarele:
- includerea unui header <pthread.h>
(#include <pthread.h>)
- compilarea cu flagul -lpthreads
pentru a linka libraria de thread-uri (gcc -lpthread thread_example.c
-o thread_example)
Functiile folosite in exemplu sunt:
-
pthread_create - creeaza un thread, oferindu-i un pointer catre o
functie pe care sa o ruleze, un parametru si eventual niste atribute
(dandu-se parametrul NULL in loc de acestea, se iau in considerare
atributele default)
- pthread_join - asteapta pana la terminarea
unui thread
- pthread_mutex_init - creeaza un mutex
-
pthread_mutex_destroy - distruge un mutex
- pthread_mutex_lock -
incearca sa acapareze un mutex, asteaptand eliberarea acestuia in
cazul in care acesta este acaparat de alt thread
-
pthread_mutex_unlock - elibereaza un mutex