Razvan Deaconescu
20 septembrie 2005
Un program este un fisier executabil care rezida pe disc. Un program poate fi incarcat in memorie si executat de kernel. O instanta executabila a unui program se numeste proces. Un proces este, asadar, un program aflat in executie. Unele sisteme de operare (printre care si Linux) folosesc intern denumirea de task.
Un program C incepe cu executia unei functii rezervate (main). Prototipul functiei este
int main (int argc, char **argv);
unde argc este numarul de argumente, iar argv este un vector de pointeri la argumente.
Cand un program este rulat de catre kernel (in cadrul unui proces, prin intermediul unor functii rezervate), o rutina speciala este invocata inainte de functia main. Aceasta rutina ia valori din kernel (vectorul de argumente si variabilele de mediu) si creeaza cadrul necesar pentru ca functia main sa fie invocata.
Un program executabil are urmatoarele zone de memorie componente:
int my_init_var = 100;
aparand in afara oricarei functii, impune ca variabila my_init_var sa fie memorata in segmentul de date;
int my_not_init_var;
aparand in afara oricarei functii, impune ca variabila my_not_init_var sa fie memorata in segmentul de date neinitializate;
Operatiile I/O intensive sunt acelea in care mult timp este petrecut in citirea/scrierea de informatii in fisierele de pe harddisk sau comunicatia cu placa de retea sau alte dispozitive periferice. Operatiile CPU intensive sunt cele care consuma mult timp folosind procesorul. Un server FTP isi petrece majoritatea timpului servind cereri sosite din retea si citind informatii de pe harddisk si deci executa operatii I/O intensive. De partea cealalta, un program care realizeaza prelucrari de semnale (transformate Fourier, de pilda) va executa operatii CPU intensive. Un program mixt este un compilator, care isi petrece o parte din timp scanand fisierele ce contin cod sursa sau cod obiect si alta parte din timp construind diverse structuri de date pentru realizarea de optimizari.
In lumea Linux/Unix un proces este identificat unic de un numar intreg pozitiv. In mod normal acesta are o valoare cuprinsa intre 0 si 65535; limita superioara poate fi modificata daca o situatie necesita acest lucru.
In momentul crearii unui proces, acestuia ii este atribuita o valoare unica din intervalul specificat, prima gasita libera dupa pid-ul ultimului proces creat. Cand se atinge valoarea de 65535, valorile de atribuit se reiau de la 0. Acest identificator unic poarta numele de process ID (pid).
Procesul cu pid-ul 0 se numeste swapper si se ocupa cu planificarea proceselor (scheduling). Acesta nu corespunde unui program de pe disc; este parte a kernel-ului.
Procesul cu pid-ul 1 se numeste init. Pornirea procesului init este ultima etapa a procesului de bootstrap (boot) a sistemului de operare. init este responsabil cu pornirea tuturor celorlalte procese importante si, astfel, a incarcarii complete a sistemului de operare.
O categorie speciala de procese sunt procesele daemon. Un daemon este un proces care ruleaza in background (cu alte cuvinte, nu are un terminal de control si, de obicei, are inchisi descriptorii de intrare, de iesire si de eroare standard). Comunicatia cu utilizatorul se realizeaza prin folosirea de semnale, fisiere de configuratatie sau mecanisme de comunicare interproces prin intermediul unui proces care nu este daemon.
Pentru crearea unui daemon se poate folosi functia daemon. Sintaxa de apel este:
#include <unistd.h> int daemon (int nochdir, int noclose);
Functia intoarce 0 in caz de succes si -1 in caz de eroare. Daca argumentul nochdir este 0 atunci se va schimba directorul curent la directorul radacina (``/''). Acest lucru este necesar pentru a preveni situatia in care directorul curent este un director de pe un sistem de fisiere montat; o incercare de demontare a acestuia va esua. Daca argumentul noclose este 0 atunci intrarea, iesirea si eroarea standard se vor redirecta in ``/dev/null''; se impiedica in acest fel contactul direct cu utilizatorul.
La orice moment de timp un proces se poate afla intr-una din urmatoarele stari:
Intr-un sistem cu N procesoare, pot exista maxim N procese aflate in executie. Un proces se afla in executie daca detine acces complet la un procesor.
La producerea unui eveniment se poate realiza trecerea dintr-o stare in alta. Cele mai importante tranzitii sunt:
In Linux, ca si in alte sisteme de operare, unui proces ii este asociata o cuanta de timp. Pe masura ce acesta ruleaza, el consuma din cuanta de timp. In momentul in care aceasta i-a expirat complet, va fi inlocuit cu un alt proces. Alta situatie de inlocuire apare atunci cand procesul intalneste o conditie care-l obliga sa astepte blocant.
Schimbarea de context este o operatie destul de costisitoare care necesita schimbarea registrelor, stivei, aducerea paginilor rezidente de pe disc, etc. Din aceasta cauza cuanta de timp nu poate fi aleasa foarte mica. Acest lucru ar insemna ca cea mai mare parte din timpul de rulare a unui proces s-ar pierde pe comutare. De asemenea, o cuanta mare de timp nu este de dorit, intrucat va produce asteptare mare a proceselor din coada ready si un timp de raspuns mare (interactivitate redusa). In Linux, datorita unor algoritmi si euristici eficiente, cuanta de timp asociata unui proces este destul de mare, lasand totusi proceselor un timp de raspuns foarte bun.
De asemenea, procesele dispun de o prioritate la scheduling. La o comutare de context, un proces prioritar va acapara procesorul in dauna unuia mai putin prioritar. In Linux/Unix prioritatea unui proces poate fi alterata cu ajutorul comenzii nice.
In general, procesele dispun de zone distincte de memorie, tabele de descriptori unice. Acest lucru inseamna ca un proces este o entitate destul de masiva. In anumite situatii se doreste ca anumite operatii sa nu necesite crearea unor procese pentru a nu consuma foarte multe resurse sau chiar se prefera ca unele zone sa ramana partajate. In aceasta situatie se vor folosi thread-uri. Thread-urile (fire de executie, denumite si lightweight processes) sunt subentitati ale unui proces care partajeaza zonele de memorie ale acestuia, tabela descriptorilor de fisier, etc. Informatiile care sunt diferite intre doua thread-uri sunt registrele si stiva. Restul informatiilor se pastreaza. Astfel, la comutarea intre doua thread-uri se vor salva doar registrele si stiva, rezultand un timp de comutare mult mai mic decat la procese.
In Unix crearea unor noi procese tine cont de pastrarea unei structuri arborescente, arbore multicai. Acest lucru inseamna ca un proces poate avea un singur proces parinte (parintele care l-a creat) dar poate avea mai multe procese fiu (procesele pe care le creeaza el).
Procesul init (procesul cu pid 1) se gaseste in varful ierarhiei de procese. El este responsabil cu crearea primelor procese care pot, la randul lor, sa creeze alte procese. init are si un alt rol, acela de a ``adopta'' procesele ramase orfane (cele al caror proces parinte s-a terminat). Pentru pastrarea structurii arborescente, un proces ramas orfan va avea ca nou parinte procesul init.
Functiile getpid si getppid pot fi folosite pentru a afla pid-ul procesului curent, respectiv pid-ul procesului parinte. Sintaxa de apel este:
#include <sys/types.h> #include <unistd.h> pid_t getpid (void); pid_t getppid (void);
Putem crea un nou proces folosind apelul fork. Acest apel de sistem duplica procesul curent si creeaza o noua intrare in tabela de procese. Noul proces este aproape identic cu cel original, executand acelasi cod, dar cu spatiul de date, mediul si descriptorii de fisier proprii.
Sintaxa de apel este:
#include <sys/types.h> #include <unistd.h> pid_t fork (void);
Functia fork este apelata o data, dar se intoarce de doua ori (pentru procesul original si pentru cel nou creat). Intrucat apelul fork genereaza un nou proces (adica vor fi in final doua procese: cel original si cel nou), iar procesele executa acelasi cod, trebuie facuta diferenta intre procesul parinte si procesul fiu (procesul parinte este cel care a apelat fork pentru crearea unui nou proces). Acest lucru este realizat de valoarea de retur a apelului fork.
Astfel, fork va intoarce 0 in cadrul procesului fiu, si pid-ul fiului in procesul parinte. Motivul pentru aceasta alegere este simplu: procesul fiu poate afla pid-ul procesului parinte apeland getppid, dar procesul parinte poate avea mai multi fii (si nu exista o functie care sa-i furnizeze procesului parinte pid-urile tuturor fiilor sai). Valoarea 0 poate fi folosita deoarece procesul cu pid-ul 0 (swapper) nu este un proces in ierarhia de procese (este un proces sistem care nu este parintele sau fiul nici unui alt proces).
Daca apelul esueaza, se intoarce -1.
Dupa ce apelul fork se intoarce, atat procesul fiu cat si procesul parinte continua executia de la instructiunea urmatoare.
Un fragment de cod tipic pentru folosirea apelului fork este:
pid_t pid; pid = fork (); switch (pid) { case -1: /* eroare */ ... break; case 0: /* proces fiu */ /* cod specific procesului fiu */ ... break; default: /* proces parinte */ /* cod specific procesului parinte */ ... break; } /* cod comun procesului fiu si procesului parinte */ ...
Prezentam in continuare doua exemple de folosire a apelului fork.
In cadrul primului exemplu un proces creeaza doi fii. Toate cele trei procese afiseaza un mesaj de identificare.
/* * simple_fork.c - program care exemplifica utilizarea apelului * fork(2); procesul parinte creeaza doua procese fiu; * atat procesul parinte cat si procesele fiu afiseaza * un mesaj de identificare */ #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> /* * in cadrul functiei main se vor crea cele doua noi procese si se * vor afisa mesajele de identificare */ int main (void) { pid_t pid; /* valoarea intoarsa de apelul fork */ /* se creeaza primul proces fiu */ pid = fork (); switch (pid) { case 0: /* procesul fiu */ /* procesul fiu scrie mesaj de identificare si iese */ write (STDOUT_FILENO, "Sunt procesul fiu 1.\n", 21); exit (EXIT_SUCCESS); break; case -1: /* eroare */ perror ("fork"); exit (EXIT_FAILURE); break; default: /* procesul parinte */ break; } /* * de aici incolo se executa numai codul procesului parinte, * deoarece primul proces fiu s-a terminat (cu exit) */ /* se creeaza cel de-al doilea proces fiu */ pid = fork (); switch (pid) { case 0: /* procesul fiu */ /* procesul fiu scrie mesaj de identificare si iese */ write (STDOUT_FILENO, "Sunt procesul fiu 2.\n", 21); exit (EXIT_SUCCESS); break; case -1: /* eroare */ perror ("fork"); exit (EXIT_FAILURE); break; default: /* procesul parinte */ break; } /* * din nou codul de aici va fi executat doar de parinte */ write (STDOUT_FILENO, "Sunt procesul parinte.\n", 23); return 0; }
Compilarea si rularea programului arata astfel:
razvan@ragnarok:~/cfiles/solab/lab2$ gcc -Wall -pedantic -ansi -o simple simple_fork.c razvan@ragnarok:~/cfiles/solab/lab2$ ./simple Sunt procesul fiu 1. Sunt procesul fiu 2. Sunt procesul parinte.
In cadrul celui de-al doilea exemplu evidentiem faptul ca un proces ramas orfan este ``adoptat'' de init.
/* * fork_init.c - program care arata ca daca un proces fiu ramane orfan * atunci va fi "adoptat" de init */ #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> /* * afisarea pid-ului procesului parinte */ static void print_ppid (void) { printf ("Proces fiu: parintele are pid-ul %d.\n", getppid ()); } /* * in cadrul functiei main se vor crea procesul fiu; procesul parinte * va astepta 3 secunde, isi va afisa pid-ul si apoi se va termina; * procesul fiu va afisa pid-ul procesului parinte imediat dupa creare * (in mod normal va trebui sa fie chiar pid-ul procesului parinte pe * care il afiseaza si acesta); dupa aceasta asteapta 5 secunde (timp in * care procesul parinte va muri) si apoi va afisa din nou * pid-ul procesului parinte; in mod normal acesta va fi 1 * * procesul parinte trebuie sa astepte ca fiul sa afiseze pentru prima * oara pid-ul procesului parinte; intrucat nu se poate sti cu siguranta * ce proces va fi planificat prima oara, e posibil ca sa se planifice * chiar procesul parinte; daca acesta s-ar termina imediat, atunci * procesul fiu va fi adoptat de init si ambele instante de afisare * a pid-ului procesului parinte vor prezenta acelasi pid: 1 */ int main (void) { pid_t pid; /* valoarea intoarsa de apelul fork */ /* se creeaza procesul parinte */ pid = fork (); switch (pid) { case 0: /* procesul fiu */ print_ppid (); /* afiseaza pid-ul parintelui */ sleep (5); /* asteapta 5 secunde */ print_ppid (); /* afiseaza pid-ul noului parinte */ exit (EXIT_SUCCESS); break; case -1: /* eroare */ perror ("fork"); exit (EXIT_FAILURE); break; default: /* procesul parinte */ sleep (3); printf ("Procesul parinte: am pid-ul %d.\n", getpid ()); exit (EXIT_SUCCESS); break; } return 0; }
Dupa rulare se obtine urmatorul rezultat:
razvan@ragnarok:~/cfiles/solab/lab2$ gcc -Wall -pedantic -ansi -o fork_init fork_init.c razvan@ragnarok:~/cfiles/solab/lab2$ ./fork_init Proces fiu: parintele are pid-ul 4653. Procesul parinte: am pid-ul 4653. razvan@ragnarok:~/cfiles/solab/lab2$ Proces fiu: parintele are pid-ul 1.
Se observa ca dupa ce procesul parinte se termina, se intoarce controlul interpretorului de comenzi si abia dupa aceea procesul fiu afiseaza mesajul cu pid-ul noului parinte (init).
In multe din sistemele de operare moderne, un apel fork nu inseamna duplicarea completa a unui proces. Anumite zone de memorie sunt partajate de catre cele doua procese si vor fi duplicat numai in momentul in care un proces scrie in acele zone de memorie. Aceasta tehnica se numeste copy on write (COW). Astfel zonele de memorie sunt partajate intre cele doua procese si sunt schimbate de kernel ca zone read-only. La o incercare de modificare a unei astfel de zone, se creeaza o copie a acesteia pentru noul proces. In Linux, aceasta tehnica este desavarsita, astfel incat singurul overhead impus de un apel fork este dat de timpul si memoria necesare pentru duplicarea tabelelor de pagini ale procesului parinte, si crearea unei strucuri proces (in cazul Linux aceasta este struct task_struct). Din acesta cauza crearea unui proces Linux este una foarte rapida comparativ cu alte sisteme de operare (chiar comparativ cu crearea thread-urilor pe alte sisteme).
Pe langa faptul ca are un identificator (pid), un proces apartine unui grup de procese. Un grup de procese este o colectie de unul sau mai multe procese. Fiecare grup de procese are un identificator unic - un intreg pozitiv stocat intr-o variabila de tip pid_t. Functia getpgrp intoarce identificatorul grupului in care se gaseste procesul. Sintaxa de apel este:
#include <sys/types.h> #include <unistd.h> pid_t getpgrp (void);
Fiecare grup de procese poate avea un lider de proces. Identificatorul grupului de procese este chiar identificatorul liderului (pid-ul liderului). Daca liderul unui grup de procese se termina, grupul continua sa existe, atata timp cat mai sunt procese in grup.
Pentru crearea unui grup de procese sau alaturarea la un grup deja existent, un proces poate folosi functia setpgid. Sintaxa de apel este:
#include <sys/types.h> #include <unistd.h> int setpgid (pid_t pid, pid_t pgid);
Astfel se procesul cu pid-ul pid va apartine grupului de procese cu identificatorul pgid. Daca cei doi parametri sunt egali atunci se creeaza un nou grup de procese, al carui lider este chiar procesul cu pid-ul pid. Un proces poate schimba grupul pentru el si procesele fiu, dar numai daca acestea nu au apelat exec. Folosirea pid-ului 0 se refera la procesul curent.
O sesiune este o colectie de unul sau mai multe grupuri de procese. Un proces stabileste o noua sesiune folosind functia setsid. Sintaxa de apel este:
#include <sys/types.h> #include <unistd.h> pid_t setsid (void);
Daca procesul care apeleaza aceasta functie nu este un lider de proces, functia creeaza o noua sesiune. Se intampla trei lucruri:
Acesta este modul in care un proces poate deveni daemon. O forma simpla de implementare a unei functii de tip daemon este prezentata mai jos:
/* * simple_daemon.c - fisier ce contine functia de creare a unui daemon */ #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> int my_daemonize (int nochdir, int noclose) { pid_t pid; int i; /* * in prima faza se creeaza un proces fiu; procesul parinte * se termina; avem garantia ca procesul (fiu) nu mai este * lider de grup */ pid = fork (); switch (pid) { case 0: /* procesul fiu */ break; case -1: /* eroare */ perror ("fork"); return -1; default: /* procesul parinte */ exit (EXIT_SUCCESS); } /* cod executat numai de procesul fiu */ /* procesul fiu creeaza o noua sesiune si devine lider */ if (setsid () < 0) { perror ("setsid"); return -1; } /* * daca nochdir este nul se schmba directorul curent * la directorul radacina, pentru a preveni folosirea * unui director aflat pe un sistem de fisier strain */ if (nochdir == 0) if (chdir ("/") < 0) { perror ("chdir"); return -1; } /* acordam drepturi depline de creare a fisierelor */ umask (0); /* * daca noclose este 0 redirectam intrarea, iesirea si * eroarea standard in /dev/null */ if (noclose == 0) { close (STDIN_FILENO); close (STDOUT_FILENO); close (STDERR_FILENO); for (i = 0; i < 3; i++) open ("/dev/null", O_RDWR); } return 0; }
In mod normal un proces se incheie fie cand se intalneste o instructiune return in cadrul functiei main, sau cand se atinge sfarsitul acestei functii. Un proces poate fi obligat sa-si incheie executia prin folosirea apelului exit. Sintaxa de apel este:
#include <stdlib.h> void exit (int status);
Parametrul status reprezinta codul de iesire al procesului. Valori utilizate frecvent sunt 0 pentru a semnala iesirea cu succces (sau macro-ul EXIT_SUCCESS) si 1 pentru a semnala iesirea cu eroare (sau macro-ul EXIT_FAILURE). Functia nu se intoarce (deoarece ea are ca efect chiar distrugerea contextului in care este apelata).
Codul de retur al procesului poate fi folosit de catre alte procese si este in special util procesului parinte pentru a cunoaste conditiile in care unul din procesele sale fiu si-a incheiat executia.
Un proces parinte asteapta terminarea fiilor sai folosind apelul wait sau waitpid. El poate recupera astfel codul de iesire al fiilor sai. Inainte de prezentarea celor doua apeluri, trebuie facuta o analiza a celor doua situatii care apar atunci cand procesul parinte sau procesul fiu se termina:
Sintaxa de apel pentru wait si waitpid este:
#include <sys/types.h> #include <sys/wait.h> pid_t wait (int *status); pid_t waitpid (pid_t pid, int *status, int options);
status va retine codul de iesire al procesului fiu care s-a terminat.
Comportamentul unui proces care apeleaza wait sau waitpid poate fi unul din urmatoarele:
Valoarea de retur a celor doua apeluri este pid-ul procesului care a fost asteptat si s-a terminat. O valoare de retur de -1 inseamna eroare. De asemenea, daca nu exista procese care s-au terminat, folosirea waitpid cu optiunea WNOHANG duce la o valore de retur 0.
Exista un set de macrodefinitii specializate pentru obtinerea informatiilor despre modul de terminare a procesului fiu prin analiza codului de iesire al acestuia obtinut de apelurile wait/waitpid. O astfel de macrodefinitie va intoarce o valoare nenula in situatia in care conditia pe care o verifica ea este adevarata.
/* * wait_zombie.c - programul exemplifica aparitia unui proces zombie */ #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> /* * se va crea un proces fiu si acesta se va termina imediat; procesul * parinte va astepta 10 secunde; in timpul acesta rularea unei comenzi * ps va prezenta procesul fiu pe post de zombie; dupa 10 secunde * procesul parinte va apela wait si va verifica daca procesul fiu * s-a terminat in mod normal */ int main (void) { pid_t pid; /* valoarea intoarsa de apelul fork(2) */ int status; /* folosit de wait(2) */ /* se creeaza procesul fiu */ pid = fork (); switch (pid) { case 0: /* procesul fiu */ printf ("Proces fiu: am pid-ul %d si ies.\n", getpid ()); exit (EXIT_SUCCESS); /* iese imediat */ break; case -1: /* eroare */ perror ("fork"); exit (EXIT_FAILURE); break; default: break; } /* * cod specific numai procesului parinte (procesul fiu * s-a terminat */ sleep (10); /* se asteapta 10 secunde */ printf ("Proces parinte: am pid-ul %d si astept fiul.\n", getpid ()); /* * se astepata procesul fiu - echivalent cu * waitpid (pid, &status, 0); */ if (wait (&status) < 0) { perror ("wait"); exit (EXIT_FAILURE); } if (WIFEXITED (status)) { printf ("Procesul fiu s-a terminat normal."); printf (" Codul de iesire este %d.\n", WEXITSTATUS (status)); } return 0; }
Iesirea programului si a rularii ps sunt prezentate mai jos:
razvan@ragnarok:~/cfiles/solab/lab2$ gcc -Wall -pedantic -ansi -o wait wait_zombie.c razvan@ragnarok:~/cfiles/solab/lab2$ ./wait Proces fiu: am pid-ul 4919 si ies. Proces parinte: am pid-ul 4918 si astept fiul. Procesul fiu s-a terminat normal. Codul de iesire este 0. razvan@ragnarok:~/cfiles/solab/lab2$ ps -a PID TTY TIME CMD 3880 pts/0 00:00:14 lyx 4264 pts/1 00:00:23 xemacs 4626 pts/1 00:00:00 gedit 4918 pts/1 00:00:00 wait 4919 pts/1 00:00:00 wait <defunct> 4920 pts/2 00:00:00 ps
Cu ajutorul apelului fork putem crea noi procese, iar cu ajutorul apelurilor din familia exec putem initia noi programe. Cand un proces foloseste una din functiile exec, procesul este complet inlocuit cu noul program, iar noul program incepe executia functiei sale main. PID-ul nu se schimba prin folosirea exec, deoarece nu se creeaza un nou proces. exec doar inlocuieste procesul curent (segmentele de text, date, heap, stiva) cu un program nou de pe disc.
Sunt 6 functii in familia de apeluri exec. Sintaxele de apel sunt:
#include <unistd.h> char **environ; int execl (const char *path, const char *arg0, ..., (char *) NULL); int execlp (const char *file, const char *arg0, ..., (char *) NULL); int execle (const char *path, const char *arg0, ..., (char *) NULL, const char *envp[]); int execv (const char *path, const char *argv[]); int execvp (const char *file, const char *argv[]); int execve (const char *path, const char *argv[], const char *envp[]);
Aceste 6 functii se incadreaza in doua categorii. execl, execlp, execle au un numar variabil de parametri care se incheie cu un pointer NULL. execv, execvp, execve au un vector de siruri ca al doilea parametru. In ambele cazuri, noul program porneste cu argumentele date in vectorul argv transmis ca parametru functiei main. De obicei toate functiile sunt implementate folosind execve, desi nu este obligatoriu.
Functiile care se termina in p (execlp, exevp) se deosebesc de celelalte prin faptul ca vor cauta variabila de mediu PATH pentru a gasi executabilul pentru fisierul prezentat. Daca executabilul nu este in PATH, va trebui transmis un nume complet (denumire absoluta).
Variabila globala environ este folosita pentru transmiterea unei valori mediului noului program. Alternativ, un argument al functiilor execve si execle permite transmiterea unui vector de siruri care vor fi folosite ca noul mediu al programului.
Un exemplu de folosire a celor 6 functii exec este prezentat mai jos (shamelessly copied from ``Beginning Linux Programming, 2nd Edition''):
/* * exemple de utilizare a functiilor din familia exec; se considera * pentru exemplificare comanda "ps -ax" * * din "Beginning Linux Programming, second edition" * capitolul 10 "Processes and Signals" */ #include <unistd.h> /* exemplu de lista de argumente */ /* argv[0] trebuie sa fie un nume de program */ const char *ps_argv[] = {"ps", "-ax", 0}; /* exemplu de mediu al procesului; nu este neaparat necesar */ const char *ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", 0}; /* apeluri posibile ale functiilor din familia exec */ execl ("/bin/ps", "ps", "-ax", 0); /* presupunem ca ps e in /bin */ execlp ("ps", "ps", "-ax", 0); /* presupunem ca /bin e in PATH */ execle ("/bin/ps", "ps", "-ax", 0, ps_env); /* se transmite mediul propriu */ execv ("/bin/ps", ps_argv); execvp ("ps", ps_argv); execve ("/bin/ps", ps_argv. ps_env);
Urmatorul exemplu este un exemplu complet. Un proces creeaza un proces fiu. Procesul fiu va folosi exec pentru ``ls -l'', iar procesul parinte va astepta terminarea fiului si, daca acesta a incheiat cu succes, va folosi exec pentru ``df -h''. Implementarea este o simplificare a operatorului ``&SPMamp;'' din interpretorul de comenzi. (daca avem ``cmd1 && cmd2'' se va executa cmd1 si apoi si cmd2, dar numai daca cmd1 a intors cod de iesire 0 - succes)
/* * simple_exec.c - utilizare simpla a apelului exec(3); implementare * foarte simplificata a operatorului && din * interpretorul de comenzi */ #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> /* * in cadrul functiei main se creeaza un proces fiu; acesta va executa * (folosind exec) comanda ls -l; procesul parinte asteapta terminarea * fiului si, daca fiul s-a terminat normal, apoi executa comanda df -h */ int main (void) { pid_t pid; /* valoare intoarsa de fork(2) */ int status; /* variabila utilizata de wait(2) */ /* se creeaza procesul fiu */ pid = fork (); switch (pid) { case 0: /* procesul fiu */ /* se executa comanda ls -l folosind execlp */ if (execlp ("ls", "ls", "-l", 0) < 0) { perror ("exec"); exit (EXIT_FAILURE); } exit (EXIT_SUCCESS); break; case -1: /* eroare */ perror ("fork"); exit (EXIT_FAILURE); break; default: /* procesul parinte */ break; } /* codul ce urmeaza va fi executat doar de procesul parinte */ /* se astepta terminarea procesului fiu */ if (wait (&status) < 0) { perror ("wait"); exit (EXIT_FAILURE); } /* se verifica daca procesul fiu s-a terminat normal */ if (WIFEXITED (status) && WEXITSTATUS (status) == 0) { /* * daca procesul fiu s-a terminat normal, se executa * comanda "df -h" folosind execlp */ if (execlp ("df", "df", "-h", 0) < 0) { perror ("exec"); exit (EXIT_FAILURE); } } return 0; }
Dupa rulare, programul va genera urmatoarea iesire:
razvan@ragnarok:~/cfiles/solab/lab2$ gcc -Wall -pedantic -ansi -o exec simple_exec.c razvan@ragnarok:~/cfiles/solab/lab2$ ./exec total 288 -rw-r--r-- 1 razvan razvan 1008 Oct 2 12:33 commands.txt -rwxr-xr-x 1 razvan razvan 12683 Oct 2 12:48 exec -rw-r--r-- 1 razvan razvan 854 Oct 2 12:35 exec_usage.c -rw-r--r-- 1 razvan razvan 2099 Oct 2 11:57 fork_init.c drwxr-xr-x 2 razvan razvan 4096 Oct 1 22:08 lab2 -rw-r--r-- 1 razvan razvan 13898 Oct 1 22:11 lab2-split.tgz -rw-r--r-- 1 razvan razvan 29620 Oct 1 22:10 lab2.html -rw-r--r-- 1 razvan razvan 34163 Oct 2 12:46 lab2.lyx -rw-r--r-- 1 razvan razvan 116665 Oct 1 22:04 lab2.ps -rw-r--r-- 1 razvan razvan 26803 Oct 1 22:08 lab2.tex -rw-r--r-- 1 razvan razvan 10509 Oct 1 22:11 lab2.tgz -rw-r--r-- 1 razvan razvan 1982 Oct 2 12:47 simple_exec.c -rw-r--r-- 1 razvan razvan 1967 Oct 2 11:55 simple_fork.c -rw-r--r-- 1 razvan razvan 1774 Oct 2 12:14 wait_zombie.c Filesystem Size Used Avail Use% Mounted on /dev/hda8 9.9G 5.2G 4.2G 56% / tmpfs 126M 0 126M 0% /dev/shm
Apelul system poate fi folosit pentru executia unui program ca si cum acesta ar fi fost executat din interpretorul de comenzi (este echivalent cu rularea comenzi sh -c comanda din interpretorul de comenzi). Este o metoda facila de creare a unui proces in cadrul altui proces (si de rulare a unui alt program). Sintaxa de apel este:
#include <stdlib.h> int system (const char *command);
Exemple de apel:
system ("ls -l"); system ("./myexec myarg"); system ("chmod a+x ~/bin/*");
Apelul sleep este folosit pentru a impune procesului curent sa doarma (asteptare blocanta) numarul de secunde specificat ca parametru. Sintaxa de apel este:
#include <unistd.h> unsigned int sleep (unsigned int seconds);
ps (process status) afiseaza informatii despre anumite procese active. Este utilitarul esential pentru urmarirea proceselor care ruleaza la un moment dat in cadrul unui sistem, aflarea pid-ului, parintelui, terminalului de unde au fost pornite, etc.
Implicit se vor afisa informatii despre procesele al caror detinator este utilizatorul curent si care au asociate aceluiasi terminal. Informatiile afisate implicit pentru fiecare proces sunt PID-ul, terminalul asociat (TTY), timpul CPU cumulat (TIME) si numele executabilului (CMD).
razvan@ragnarok:~/cfiles/solab/lab2$ ps -a PID TTY TIME CMD 3880 pts/0 00:00:14 lyx 4264 pts/1 00:00:23 xemacs 4626 pts/1 00:00:00 gedit 4918 pts/1 00:00:00 wait 4919 pts/1 00:00:00 wait <defunct> 4920 pts/2 00:00:00 ps
Pentru o afisare cu preferinte proprii se foloseste optiunea -o urmata de parametri care specifica ce coloane se doresc afisate. Spre exemplu:
ps -o pid,tid,class,rtprio,ni,pri,psr,pcpu,stat,comm ps -o stat,euid,tty,tpgid,sess,pgrp,ppid,pid,pcpu,comm ps -o pid,tt,user,fname,tmout,f,wchan
Toti acesti parametri sunt prezentati in pagina de manual (paragraful STANDARD FORMAT SPECIFIERS).
Optiuni utile sunt -e pentru selectarea tuturor proceselor si -sort, urmata tot de un parametru identificator standard care sa faca o sortare a informatiilor afisate.
Utilitarul top poate fi folosit pentru furnizarea unei vederi dinamice in timp real a proceselor din sistem si a gradului de utilizare a resurselor. Informatia este dinamica si se actualizeaza periodic.
Comanda nice afiseaza sau modifica prioritatea unui proces pentru scheduling. In Linux prioritatile proceselor folosind nice se situeaza intre -20 (cele mai prioritare) la 19 (cele mai putin prioritare) (cu cat e mai mare aceasta valoare, cu atat procesul e mai putin prioritar).
Daca se doreste ajustarea prioritatii se foloseste optiunea -n urmata de ajustare. Numai un utilizator privilegiat (root, superuser) poate scadea valoarea prioritatii unui proces.
Cateva exemple de utilizare (info coreutils nice):
$ nice nice ; implicit se mareste prioritatea cu 10 10 $ nice -n 10 nice ; la fel mai sus, numai ca se creste explicit 10 $ nice nice -n 3 nice ; sa mareste prioritatea cu 13 13 $ nice -n 30 nice ; limita superioara este 19 19
Comanda kill poate fi folosita pentru terminarea unui proces. Este utila in cazul in care un proces se blocheaza si nu se mai poate interactiona cu el, sau din diverse alte motive. Un mod obisnuit de utilizare foloseste ca parametru pid-ul procesului care se doreste a fi omorat. O folosire eficienta este sub forma:
$ kill -KILL pid
Aceasta folosire trimite unui proces semnalul SIGKILL fata de cel implicit SIGTERM; SIGKILL duce la o ``moarte sigura'' a procesului.
Cateva utilitare asemanatoare sunt pkill, skill si killall. killall permite specificarea numelui procesului in locul pid-ului.
This document was generated using the LaTeX2HTML translator Version 2002-2-1 (1.71)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.
The command line arguments were:
latex2html -no_subdir -split 0 -show_section_numbers /tmp/lyx_tmpdir40073iSbL7/lyx_tmpbuf0/lab2.tex
The translation was initiated by Razvan Adrian Deaconescu on 2005-10-11