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 (#include ) - 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