Un proces este un program în execuție. Procesele sunt unitatea primitivă prin care sistemul de operare alocă resurse utilizatorilor. Orice proces are un spațiu de adrese și unul sau mai multe fire de execuție. Putem avea mai multe procese ce execută același program, dar oricare două procese sunt complet independente.
Informațiile despre procese sunt ținute într-o structură numită Process Control Block
(PCB), câte una pentru fiecare proces existent în sistem. Printre cele mai importante informații conținute de PCB regăsim:
PC
(contor program), SP
(indicator stivă)În momentul lansării în execuție a unui program, în sistemul de operare se va crea un proces pentru alocarea resurselor necesare rulării programului respectiv. Fiecare sistem de operare pune la dispoziție apeluri de sistem pentru lucrul cu procese: creare, terminare, așteptarea terminării. Totodată există apeluri pentru duplicarea descriptorilor de resurse între procese, ori închiderea acestor descriptori.
Procesele pot avea o organizare:
init
(pid = 1).
În general, un proces rulează într-un mediu specificat printr-un set de variabile de mediu. O variabilă de mediu este o pereche NUME = valoare
. Un proces poate să verifice sau să seteze valoarea unei variabile de mediu printr-o serie de apeluri de bibliotecă (Linux, Windows).
Pipe-urile (canalele de comunicație) sunt mecanisme primitive de comunicare între procese. Un pipe poate conține o cantitate limitată de date. Accesul la aceste date este de tip FIFO (datele se scriu la un capăt al pipe-ului pentru a fi citite de la celălalt capăt). Sistemul de operare garantează sincronizarea între operațiile de citire și scriere la cele două capete (Linux, Windows).
Există două tipuri de pipe-uri:
Lansarea în execuție a unui program presupune următorii pași:
fork
- procesul copil are o copie a resurselor procesului părinte. exec*
.În UNIX un proces se creează folosind apelul de sistem fork:
os.fork()
Efectul este crearea unui nou proces (procesul copil), copie a celui care a apelat fork
(procesul părinte). Procesul copil primește un nou process id (PID
) de la sistemul de operare.
Pentru aflarea PID
-ului procesului curent și al procesului părinte se vor apela funcțiile de mai jos.
Funcția getpid întoarce PID
-ul procesului apelant:
os.getpid()
Funcția getppid întoarce PID
-ul procesului părinte al procesului apelant:
os.getppid()
Familia de funcții exec va executa un nou program, înlocuind imaginea procesului curent, cu cea dintr-un fișier (executabil). Acest lucru înseamnă:
PC
(contorul program), SP
(indicatorul stivă) și registrele generale vor fi reinițializate. PID
-ul și descriptorii de fișier care nu au setat flag-ul CLOSE_ON_EXEC
rămân neschimbați (implicit, flag-ul CLOSE_ON_EXEC
nu este setat).os.execl(path, arg1, arg2, ...); os.execv(path, args); os.execlp(file, arg1, arg2, ...);
Exemplu de folosire a funcțiilor de mai sus:
os.execl("/bin/ls", "ls", "-la") args = ["ls", "-la"] os.execv("/bin/ls", args) os.execlp("ls", "ls", "-la")
execl
și execv
nu caută programul dat ca parametru în PATH
, astfel că acesta trebuie însoțit de calea completă. Versiunile execlp
și execvp
caută programul și în PATH
.
Toate funcțiile exec*
sunt implementate prin apelul de sistem execve.
Familia de funcții wait suspendă execuția procesului apelant până când procesul (procesele) specificat în argumente fie s-a terminat, fie a fost oprit (SIGSTOP
).
os.waitpid(pid, options)
Starea procesului interogat se poate afla examinând status
cu macrodefiniții precum WEXITSTATUS, care întoarce codul de eroare cu care s-a încheiat procesul așteptat, evaluând cei mai nesemnificativi 8 biți.
Există o variantă simplificată, care așteaptă orice proces copil să se termine. Următoarele secvențe de cod sunt echivalente:
(pid, status, info) = wait3(0) | status = waitpid(-1,0)
Pentru terminarea procesului curent, Linux pune la dispoziție apelul de sistem sys.exit.
import sys sys.exit(status)
Un proces al cărui părinte s-a terminat poartă numele de proces orfan. Acest proces este adoptat automat de către procesul init
, dar poartă denumirea de orfan în continuare deoarece procesul care l-a creat inițial nu mai există.
Un proces finalizat al cărui părinte nu a citit (încă) statusul terminării acestuia poartă numele de proces zombie. Procesul intră într-o stare de terminare, iar informația continuă să existe în tabela de procese astfel încât să ofere părintelui posibilitatea de a verifica codul cu care s-a finalizat procesul. În momentul în care părintele apelează funcția wait
, informația despre proces dispare. Orice proces copil o să treacă prin starea de proces zombie la terminare.
Pentru terminarea unui alt proces din sistem, se va trimite un semnal către procesul respectiv prin intermediul apelului de sistem kill. Mai multe detalii despre kill
și semnale în laboratorul de semnale.
import os def my_system(command): try: pid = os.fork() if pid == -1: # error forking return os.EXIT_FAILURE elif pid == 0: # child process os.execvp(command, args) # only if exec failed */ os._exit(os.EXIT_FAILURE) else: # parent process pass #only parent process gets here status = os.waitpid(pid, 0) if (os.WIFEXITED(status)): print("Child {} terminated normally, with code {}".format(pid, os.WEXITSTATUS(status))) return status except Exception as e: print ("Error: {}".format (e)) my_system("ls")
dup duplică descriptorul de fișier oldfd
și întoarce noul descriptor de fișier, sau -1
în caz de eroare:
os.dup(oldfd)
dup2 duplică descriptorul de fișier oldfd
în descriptorul de fișier newfd
; dacă newfd
există, mai întâi va fi închis. Întoarce noul descriptor de fișier, sau -1
în caz de eroare:
os.dup2(oldfd, newfd)
Descriptorii de fișier sunt, de fapt, indecși în tabela de fișiere deschise. Tabela este populată cu pointeri către structuri cu informațiile despre fișiere. Duplicarea unui descriptor de fișier înseamnă duplicarea intrării din tabela de fișiere deschise (adică 2 pointeri de la poziții diferite din tabelă vor indica spre aceeași structură din sistem, asociată fișierului). Din acest motiv, toate informațiile asociate unui fișier (lock-uri, cursor, flag-uri) sunt partajate de cei doi file descriptori. Aceasta înseamnă că operațiile ce modifică aceste informații pe unul dintre file descriptori (de ex. lseek
) sunt vizibile și pentru celălalt file descriptor (duplicat).
os.CLOSE_ON_EXEC
nu este partajat (acest flag nu este ținut în structura menționată mai sus).
Descriptorii de fișier ai procesului părinte se moștenesc în procesul copil în urma apelului fork
. După un apel exec
, descriptorii de fișier sunt păstrați, excepție făcând cei care au flag-ul CLOSE_ON_EXEC
setat.
În cadrul unui program se pot accesa variabilele de mediu, prin accesarea structurii os.environ care contine toate variabilele sub forma cheie,valoare, unde cheia e numele variabilei.
getenv întoarce valoarea variabilei de mediu denumite name
, sau o valoare default dacă nu există o variabilă de mediu denumită astfel. Daca valoarea predefinita nu se precizeaza, se va intoarce None:
os.getenv(key, defaultValue)
Pentru a seta valoarea unei variabile de mediu, se foloseste structura environ:
os.environ['API_USER'] = 'username'
unsetenv șterge din mediu variabila denumită name
:
os.unsetenv(name)
Pentru rezolvarea laboratorului, va rugam sa clonati repository-ul. daca il aveti deja, va rugam sa rulati git pull
.
Intrați în directorul 1-system
.
Programul my_system.py
execută o comandă transmisă ca parametru, folosind funcția de bibliotecă system. Modul de funcționare al system este următorul:
sh
cu argumentele -c “comanda”, timp în care procesul părinte așteaptă terminarea procesului copil.Rulați programul dând ca parametru o comandă.
python3 my_system.py pwd
Cum procedați pentru a trimite mai mulți parametri unei comenzi? (ex: ls -la
)
Pentru a vedea câte apeluri de sistem execve se realizează, rulați:
strace -e execve,clone -ff -o output ./my_system ls
execve,clone
-ff
însoțit de -o output
generează câte un fișier de output pentru fiecare proces.Revedeți secțiunea Înlocuirea imaginii unui proces în Linux și pagina de manual pentru execve .
Inlocuiti functia system
cu execvp
. Functia primeste ca parametru o comanda si lista de argumente (despartite de virgule).
Urmariti linia cu TODO 1.
Intrați în directorul 2-parameters
.
Rulati programul parameters.py folosind comanda python3 parameters.py
. Ce face programul parameters.py?
Rezolvati exercitiul in fisierul program.py.
Folositi functia system
pentru a rula programul parameters cu cativa parametrii.
Inlocuiti functia system cu execlp. De ce nu se afiseaza textul prin print-ul de dupa execlp?
Urmariti liniile cu TODO 2.
Intrați în directorul 3-run
.
Folsiti functiile fork si execl pentru a rula comanda ls -l
din programul run.py.
Urmariti liniile cu TODO 1.
Asigurati-va ca textul “ls was run” este afisat dupa inchiderea programului programului ls. (Hint: waitpid)
Urmariti liniile cu TODO 2.
Mutati si modificati linia cu TODO 3 astfel incat sa se afiseaza codul de iesire (exit code) al programului ls. (Hint: WEXITSTATUS)
Rulati programul exitcode.py si afisati codul de iesire. Modificati programul exitcode.py astfel incat sa intoarca alt cod de iesire.
Urmariti liniile cu TODO 4.
Intrați în directorul 4-orphan
și inspectați sursa orphan.py
.
Dati drepturi de executie pe fisier (chmod u+x
) si rulați programul folosind comanda:
./orphan.py
which python3
si inlocuiti calea din prima linie a fisierului cu cea afisata in terminal.
Deschideți alt terminal și rulați comanda:
watch -d '(ps -al | grep -e orphan -e PID)'
Observați că pentru procesul indicat de executabilul orphan
(coloana CMD
), pid-ul procesului părinte (coloana PPID
) devine 1, întrucât procesul este adoptat de init
după terminarea procesului său părinte. De ce sunt doua procese orphan
?
Intrați în directorul 5-zombie
și inspectați sursa zombie.py
.
Dati drepturi de executie pe fisier (chmod u+x
) si rulați programul folosind comanda:
./zombie.py
which python3
si inlocuiti calea din prima linie a fisierului cu cea afisata in terminal.
Deschideți alt terminal și rulați comanda:
watch -d '(ps -al | grep -e zombie -e PID)'
Observați că pentru procesul indicat de executabilul zombie
coloana CMD
devine zombie
<defunct>. Ce se intampla de fapt?
Modificati fisierul zombie.py astfel incat procesul sa nu mai devina un zombie (Hint: waitpid).
Urmariti liniile cu TODO 1.
Intrați în directorul 6-tiny
.
Următoarele subpuncte au ca scop implementarea unui shell minimal, care oferă suport pentru execuția unei singure comenzi externe cu argumente multiple și redirectări. Shell-ul trebuie să ofere suport pentru folosirea și setarea variabilelor de mediu.
Observație: Pentru a ieși din tiny shell folosiți exit
sau CTRL+D
.
Creați un nou proces care să execute o comandă simplă.
Funcția simple_cmd
primește ca argument un vector de șiruri ce conține comanda și parametrii acesteia.
Citiți exemplul my_system și urmăriți în cod comentariile cu TODO 1
.
Pentru testare puteți folosi comenzile:
./tiny > pwd > ls -al > exit
Trebuie să completați funcțiile set_var
și expand
; acestea sunt apelate deja atunci când se face parsarea liniei de comandă. Verificarea erorilor trebuie făcută în aceaste funcții.
TODO 2
../tiny > echo $HOME > name=Makefile > echo $name
Completați funcția do_redirect
astfel încât tiny-shell trebuie să suporte redirectarea output-ului unei comenzi (stdout) într-un fișier.
Dacă fișierul indicat de filename
nu există, va fi creat. Dacă există, trebuie trunchiat.
Citiți secțiunea Copierea descriptorilor de fișier și urmăriți în cod comentariile cu TODO 3
.
Pentru testare puteți folosi comenzile:
./tiny > ls -al > out > cat out > pwd > out > cat out