Pentru parcurgerea demo-urilor, folosim arhiva aferentă. Demo-urile rulează pe Linux. Descărcăm arhiva folosind comanda
wget http://elf.cs.pub.ro/so/res/cursuri/curs-03-demo.zip
și apoi decomprimăm arhiva
unzip curs-03-demo.zip
și accesăm directorul rezultat în urma decomprimării
cd curs-03-demo/
Acum putem parcurge secțiunile cu demo-uri de mai jos.
Pentru a afișa informații despre procesul shell curent, folosim comenzile de mai jos:
ps -f $$ ps -F $$ lsof -p $$ pmap $$ ls /proc/$$/ cat /proc/$$/status
Aceste comenzi ne afișează infomații precum PID-ul procesului, comanda de la care a pornit, PID-ul procesului părinte, timpul de rulare, fișierele deschise (lsof
), spațiul de adresă al procesului (pmap
).
Dorim să aflăm informații complete despre un proces creat. Pentru aceea pornim în shell o comandă (find
) care să creeze un proces:
/usr/bin/time -v find /usr/share > /dev/null
Comanda time
trebuie dată în cale completă (/usr/bin/time
) pentru a nu rula comanda time
internă shell-ului.
Comanda permite rularea altei comenzi și afișarea de informații despre procesul creat de aceasta. Astfel de informații sunt cel de timp:
Procesul rulează pe procesor timp de User time + System time. Observăm că Elapsed time este mai mare decât timpul efectiv de rulare a procesului pe procesor. Acest lucru se întâmplă întrucât în acest timp au mai rulat și alte procese procesor, timp în care procesul curent a așteptat.
Pentru a afișa ierarhia de procese a sistemului folosim comenzile de mai jos:
pstree ps -H
Rădăcina ierarhiei este procesul init
(PID-ul 1), procesul părinte al sistemului.
Vrem să vizualizăm evoluția arborelui de procese. Vom vizualiza subarborele unui proces shell.
Pentru început, aflăm PID-ul procesului shell curent:
echo $$
Fie $PID
valorea afișată de comanda de mai sus. Deschidem un shell nou și rulăm comanda:
watch -n 1 pstree -a -p $PID
Comanda de mai sus afișează, cu refresh de o secundă, ierarhia de procese începând cu shell-ul inițial.
Pentru a altera ierarhia, rulăm comenzile de mai jos:
sleep 20 & bash sleep 30 # asteptati terminarea comenzii exit
Observăm evoluția ierarhiei în al doilea shell:
sleep 20
și procesul bash
nou creat.sleep
și așteaptă încheierea sa. În acest moment shell-ul inițial are un proces copil sleep
, un proces copil bash
și un proces “nepot” sleep
.
Pentru a urmări numărul de schimbări de context ale unui proces, consultăm fișierul /proc/$PID/status
. Ne interesează câmpurile voluntary_ctxt_switches
și nonvoluntary_ctxt_switches
. Pentru a urmări aceste valori pentru shell-ul curent folosim comanda:
cat /proc/$$/status
Câmpul voluntary_ctxt_switches
(schimbări de context voluntare) se referă la schimbările în care procesul lasă de bună voie procesorul, de obicei acțiuni blocante de intrare/ieșire. Câmpul nonvoluntary_ctxt_switcher
(schimbări de context nevoluntar) se referă la schimbările în care procesul este dat la o parte de pe procesor; de obicei acest lucru înseamnă expirarea cuantei curente de rulare; poate fi vorba și de apariția unui proces de prioritate mai bună.
Dacă rulăm de multe ori comanda de mai sus (cat /proc/$$/status
) vom observa alterarea celor două câmpuri. Se alterează preponderent câmpul voluntary_ctxt_switches
întrucât procesul curent, shell-ul, așteaptă în general intrare de la utilizator. Făcând în majoritate acțiuni de intrare/ieșire (blocante), cele mai dese schimbări de context sunt cele voluntare.
Vrem să observăm diferența tipurilor de schimbări de context pentru procese I/O bound și procese CPU bound. Vom folosi directorul ctxt-switch/
din arhiva cu demo-uri a cursului.
Parcurgem fișierele cpu.c
și io.c
. Observăm că cpu.c
face multe acțiuni (consumă procesor), în timp ce io.c
face operații blocante. Compilăm cele două programe folosind comanda
make
și obținem executabilele cpu
și io
.
Întâi rulăm executabilul cpu
:
./cpu
Într-o altă consolă urmărim numărul de schimbări de context realizate de procesul nou creat:
cat /proc/$(pidof cpu)/status
Observăm că se modifică preponderent valoarea câmpului nonvoluntary_ctxt_switches
. Acest lucru se întâmplă pentru că avem un proces CPU-bound. Acesta va consuma timp de procesor ori de câte ori prinde ocazia și va fi dat afară în majoritar la expirarea cuantei (nu face operații blocante). Un astfel de proces poartă numele de CPU hog.
Apoi rulăm executabilul io
:
./io
Într-o altă consolă urmăriți numărul de schimbări de context realizate de procesul nou creat:
cat /proc/$(pidof io)/status
Observăm că se modifică preponderent valoarea câmpului voluntary_ctxt_switches
. Acest lucru se întâmplă pentru că avem un proces IO-bound. Acesta va executa multe operații blocante în cadrul cărora va ceda de bună voie procesorul.
La o privire mai atentă la câmpul voluntary_ctxt_switches
pentru procesul creat din executabilul io
, observăm că acesta se modifică doar la apelul sleep
nu și la apelul write
, deși write
este un apel I/O cu probabilitate de a se bloca. În realitate însă un apel write
nu va copia date pe disc (sau la alt dispozitiv I/O) ci într-un buffer intern al nucleului. După copierea datelor în buffer-ul intern al nucleului, apelul write
se întoarce, neexistând partea de blocare.
Pentru a urmări comportamentului cursorului de fișier (file pointerului) vom folosi directorul fork-file-pointer/
din arhiva cu demo-uri a cursului.
Vom parcurge fișierul fork-file-pointer.c
. Observăm că același descriptor de fișier este folosit și de procesul copil și de procesul părinte. Vrem să vedem dacă aceste două procese partajează cursorul de fișier.
Pentru început compilăm programul folosind comanda:
make
Obținem executabilul fork-file-pointer
. Rulăm executabilul:
./fork-file-pointer
Într-o altă consolă urmărim evoluția cursorului de fișier pentru procesul creat (procesul părinte) folosind comanda:
lsof -a -o -d 0-1023 -p $(pidof fork-file-pointer | cut -d ' ' -f 1)
Observăm modificarea cursorului de fișier de la valoarea 0
la valoarea 10
apoi la valoarea 20
apoi la valoarea 30
.
OFFSET
.
f.txt
:
cat f.txt
Observăm că fiecare proces (părinte sau copil) a scris în continuarea celuilalt. Adică, în urma fork()
, se partajează cursorul de fișier al fișierelor deschise înainte de fork()
. Procesul părinte partajează cursorul de fișier cu procesele copil.
Vrem să urmărim apariția proceselor orfane și a proceselor zombie și să le investigăm. Pentru aceasta vom folosi directorul orphan-zombie/
din arhiva cu demo-uri a cursului.
Parcurgem fișierele orphan.c
și zombie.c
. Primul program creează un proces orfan, adică își încheie execuția procesul părinte, dar procesul copil continuă execuția. Al doilea program creează un proces zombie, adică procesul copil își încheie execuția, dar procesul părinte continuă execuția fără a aștepta procesul copil.
Compilăm cele două programe folosind comanda
make
și obținem executabilele orphan
și zombie
.
Rulăm executabilul orphan
:
./orphan
Într-o altă consolă urmărim PID-urile și PPID-urile celor două procese (părinte și copil):
watch -n 1 ps -f -C orphan
Urmărim mesajele afișate de procesul copil. Mesajele vor fi afișate și după terminarea procesului părinte. Observăm actualizarea PID-ului procesului părinte (PPID) pentru procesul copil: după ce a rămas orfan, procesul copil a fost adoptat de init
(procesul cu PID-ul 1
).
Rulăm executabilul zombie
:
./zombie
Într-o altă consolă urmărim cele două procese (părinte și copil):
watch -n 1 ps -f -C zombie
Observăm trecerea procesului copil în starea zombie; în ieșirea comenzii ps
apare șirul <defunct>
. Observăm eliminarea procesului zombie după așteptarea sa de procesul părinte.
Un proces este zombie pentru că reține niște informație reziduală legată de modul în care și-a încheiat execuția; această informație reziduală este utilă procesului părinte. În momentul în care procesul părinte citește această informație reziduală folosind un apel din familia wait
, procesul copil, acum zombie, își încheie complet execuția și dispare din sistem.