1. Scopul lucrării Lucrarea de față prezintă o modalitate de comunicare între procese și anume prin semnale (signals). În lucrare sunt prezentate toate semnalele existente sub sistemul de operare UNIX, precum și modul de lucru cu acestea. Semnalele se folosesc în general pentru situații de excepție, alarme, terminări neprevăzute și mai rar pentru comunicația între procese. 2. Considerații teoretice Semnalele reprezintă calea cea mai simplă, cea mai veche și cea mai rigidă de comunicație între procese. Se poate defini semnalul ca o avertizare pe care o primește un anumit proces. Semnalul în sine nu conține informație utilă, tot ce contează fiind tipul său. Orice semnal are o sursă și este generat dintr-o anumită cauză. Un semnal poate veni de la un alt proces care dorește să comunice cu procesul în cauză, de exemplu să se sincronizeze cu acesta la transmiterea unor date sau pur și simplu să-i provoace terminarea. Altă sursă de semnale este nucleul care, în anumite situații (incidențe hard, apelarea procedurii shutdown ...), poate trimite mesaje către procese. Un proces poate să-și trimită și singur un semnnal, așa cum este cazul la apelurile abort și alarm. În sfârșit, un semnal poate fi trimis la cererea explicită a unui utilizator, prin apelul comenzii sistem kill. Procesul care primește semnalul nu poate determina sursa sa. Cauza unui semnal este, de regulă, reflectată de tipul său. În UNIX SVR2 sunt definite 19 tipuri de semnale, care sunt prezentate în tab.1.
Tab.1. Semnale existente în primele versiuni Unix Semnalele notate cu * produc vidaj de memorie. În funcția signal, semnalul poate fi specificat prin numărul sau sau prin simbolul sau, definit în fișierul /usr/include/signal.h. Având în vedere ca versiunile SVR4 și 4.3+BSD, au fiecare cate 31 de semnale, din lipsa de spațiu nu se vor prezenta restul semnalelor. Pentru detalii a suplimentare a se consulta bibliografia. Un proces care a recepționat un semnal poate să-l trateze în trei moduri: a) Ignora semnalul și continua activitatea sa. Dintre semnale, doar SIGKILL nu poate fi ignorat (nucleul are posibilitatea terminării oricărui proces). b) Sarcina tratării semnalul este lăsată nucleului. În acest caz, cu excepția semnalelor SIGCLD și SIGPWR ( și mai nou SIGINFO, SIGURG și SIGWINCH) care sunt ignorate, toate celelalte semnale duc la terminarea procesului, eventual cu vidaj de memorie. Aceasta este tratarea implicită. c) Tratarea semnalului printr-o procedură proprie, care este lansată în mod automat la apariția semnalului. La terminarea procedurii, procesul este reluat din punctul din care a fost întrerupt. Reluarea nu se va face imediat, ci în regim de multiprogramare, pe baza priorității procesului din acel moment. La apelul funcției de sistem fork procesul fiu creat moștenește de la procesul părinte acțiunea atașată semnalelor Funcția exec lasă tratarea semnalelor în sarcina nucleului, chiar dacă ele erau tratate de către proces. Se păstrează doar acțiunea prevăzută inițial de ignorare a unor semnale de către proces. Explicația constă în faptul că prin apelul exec textul programului curent este distrus, deci și procedurile de tratare a semnalelor. Indiferent de modul cum reacționează programul la un anumit semnal, după terminarea acțiunii, dacă semnalul nu era ignorat, el este actualizat pentru viitor la acțiune implicită. Excepție fac semnalele SIGILL și SIGTRAP care sunt generate foarte des și ar fi ineficientă actualizarea lor de fiecare dată. Dacă în timpul execuției unei funcții sistem se recepționează un semnal, apelul este terminat cu eroare. În particular, dacă s-a lansat o operație de intrare/ieșire cu un periferic lent și se recepționează un semnal, funcția returnează -1, iar variabila errno ia valoarea EINTR. În acest caz, programul poate să relanseze operația de intrare/iesire testând variabila errno. Semnalele sosite către un proces nu sunt memorate într-o coadă de așteptare. Dacă ele au fost ignorate, se pierd pentru totdeauna. Singura excepție este semnalul SIGCLD care așteaptă să fie luat în considerare când procesul părinte execută apelul wait pentru a lua la cunoștință de terminarea fiului. Aceasta pentru ca procesul fiu să poată termina înainte de execuția de către părinte a apelului wait. Dacă semnalul n-ar fi memorat, procesul părinte ar rămâne blocat până la terminarea unui alt fiu, care foarte probabil n-are nici o legătură cu procesul curent. Semnalul SIGCLD nu este memorat în cazul în care părintele a actualizat explicit la valoarea SIG_IGN rutină de tratare a semnalului. Datorită faptului că semnalele ignorate se pierd, acest mecanism de comunicație între procese nu este prea des folosit, pentru că poate provoca probleme. Un semnal poate fi trimis în orice moment, ocazional de un alt proces, dar de regula de la kernel ca rezultat al unui eveniment excepțional. Semnalul nu conține informație, el se rezumă doar la tipul sau. Procesul care primește semnalul nu poate identifica sursa semnalului. Rolul funcției de sistem signal este stabilirea modului de tratare a unui semnal de către un proces la primirea lui. Interfața sa este următoarea: #include <signal.h > int (*signal (semnal, funcție))() int semnal; int (*funcție)(); sau #include <signal.h> void (*signal (int semnal, void (*funcție)(int)))( int); S-a prezentat atât stilul vechi de prototip (când nu era definit tipul void) cât și cel nou pentru a fi mai clar tipul argumentelor: un întreg și un pointer la o funcție. Argumentul semnal este semnalul pentru care se stabilește modul de tratare, iar funcție descrie modul de tratare a semnalului. Acesta poate avea următoarele trei valori: a) SIG_DFL - pentru a specifica tratarea semnalului de către nucleu. Aceasta este acțiunea implicită de tratare a unui semnal. b) SIG_IGN - pentru a ignora semnalul. Semnalul SIGKILL și SIGSTOP nu pot fi ignorate de nici un proces. c) un pointer la o funcție care tratează semnalul. La primirea semnalului, funcția specificată este apelată. Această funcție poartă numele de rutina de tratare sau captare a semnalului. Ea are ca unic argument numărul semnalului pentru care este activată. Dupa ce un semnal a fost tratat, recepția aceluiași semnal devine cea implicită. Dacă se dorește tratarea din nou a aceluiași semnal, în cadrul funcției trebuie prevăzut acest lucru. Valoarea întoarsă de funcția signal este adresa rutinei anterioare de tratare a semnalului. Cazul de eroare este semnalat prin constanta SIG_ERR definită în fișierul err.c: #define SIG_ERR ( void (*)())-1 Se recomandă ca în cazul în care procesul are de executat înaintea terminării normale câteva operații (ștergerea unor fișiere temporare, restaurarea unor fișiere etc.) acesta să capteze semnalele SIGHUP, SIGINT și SIGTERM și în cadrul lor să execute operațiile respective. Pe parcursul unei aplicații, semnalul SIGQUIT nu trebuie interceptat, pentru a putea termina procesul cu Ctrl-\, terminare însoțită și de vidaj de memorie (core dump). În cazul în care un proces este lansat în background, deoarece el se execută cu semnalele SIGQUIT și SIGINT ignorate funcția de tratare a acestor semnale trebuie să fie SIG_IGN. În caz contrar programul ar putea fi întrerupt la apăsarea tastelor de întrerupere. Dacă un proces părinte captează semnalele SIGINT și SIGQUIT și așteaptă terminarea unui fiu există pericolul ca unul dintre aceste semnale să-l scoată din starea de așteptare înainte ca fiul să termine. Se recomandă în acest caz ca procesul părinte să ignore aceste semnale cât timp se află în starea de așteptare. În conjuncție cu semnalele, de multe ori trebuie executate salturi nelocale într-un program. Există două funcții care permit realizarea acestora. Funcția setjmp fixează un punct de salt, prin salvarea stării procesorului și a stivei din acel punct, iar funcția longjmp realizează saltul la un punct de salt, specificat prin parametrul său. Din funcția setjmp se produce revenirea în 2 cazuri: a) la stabilirea cu succes a punctului de salt, funcția întoarce valoarea 0; b) la efectuarea unui salt nelocal funcția întoarce valoarea val cu care a fost apelată funcția longjmp. Sintaxa funcțiilor este: #include <setjmp.h int setjmp( jmp_buf jmpenv); void longjmp( jmp_buf jmpenv, int val); Apelul sistem kill trimite un semnal la un proces sau grup de procese. Interfața apelului este: #include <sys/types.h #include <signal.h int kill( pid_t pid, int sem); Returnează 0 în caz de succes, -1 la eroare. Argumentul pid specifică procesul sau grupul de procese spre care se trimite semnalul sem. Există patru condiții diferite pentru valoarea argumentului pid în apelul kill: pid 0 Semnalul este transmis procesului al cărui identificator de proces este pid. pid == 0 Semnalul e transmis tuturor proceselor din același grup cu emițătorul și la care emițătorul are dreptul să trimită semnalul (se exclud procesele swapper (pid=0), init (pid=1) și pagedaemon (pid=2)). Acest lucru este des utilizat în comanda kill 0 pentru a șterge procesele ce se execută în background fără a fi referite cu numărul de identificare al lor. pid < 0 (pid!=-1) Semnalul este transmis tuturor proceselor al căror ID de grup este egal cu valoarea absolută a pid-ului și la care emițătorul are dreptul să trimită semnalul. pid==-1 Posix.1 lăsă condiția nespecificată. SVR4 și 4.3+BSD folosesc această valoare în semnale broadcast. Ele nu sunt transmise proceselor sistem descrise mai sus. Dacă apelantul este superuser, semnalul este transmis tuturor proceselor. Acest lucru este utilizat pentru a trimite SIGTERM la toate procesele la întreruperea sistemului. Dacă apelantul nu e superuser, semnalul este transmis tuturor proceselor care au uid real ( sau suid, dacă implementrea permite - SVR4) egal cu uid real sau efectiv al apelantului. Acest lucru este utilizat pentru a șterge toate procesele indiferent de grup. Semnalele broadcast sunt folosite doar în scopul administrării sistemului. Regula de bază la transmiterea unui semnal spre un proces este ca procesul transmițător să aibă dreptul să trimită semnalul ( ID utilizatorului real sau efectiv al procesului transmițător să fie egal cu uid real sau efectiv al procesului receptor). Superuser-ul poate trimite un mesaj la orice proces. Apelul alarm permite poziționarea unui timer. La expirarea timpului este generat semnalul SIGALRM. Dacă semnalul este ignorat sau nu e captat acțiunea implicită este de a termina procesul. Interfața să este: #include <unistd.h unsigned int alarm(unsigned int sec); Returnează 0 sau numărul de secunde rămase de la cererea anterioară de activare a semnalului SIGALRM. Argumentul sec reprezintă numărul de secunde după care semnalul SIGALRM este activat. La expirarea timpului, semnalul e generat de nucleu, dar tratarea semnalului poate să fie întârziată din cauza planificatorului de procese. Exista un singur orologiu de alarmă într-un proces. Un apel nou suprascrie valoarea anterioară. Dacă sec este 0 cererile anterioare de activare a semnalului SIGALRM sunt anulate. Cu toate că acțiunea implicită pentru SIGALRM este de a termina procesul, multe procese ce folosesc apelul sistem alarm captează acest semnal. Înainte de terminare pot fi realizate diverse operații de ștergere. Permite suspendarea (stare de așteptare) procesului apelant până la primirea unui semnal. Interfața ei este: #include <uninstd.h int pause(void); Returnează (-1) cu errno=EINTR. Dacă semnalul care sosește nu este tratat sau ignorat, se produce terminarea procesului cu eventual vidaj de memorie. Din apelul pause se revine dacă din rutina de tratare se revine. În acest caz pause returnează valoarea (-1) cu variabila errno având valoarea EINTR. Apelul sistem pause este cel mai mult utilizat în relație cu apelul sistem alarm. Funcția permite inspectarea și/sau modificarea acțiunii asociate unui semnal. Această funcție înlocuiește funcția signal din versiunile de Unix mai vechi. Interfața este următoarea: #include <signal.h int sigaction( int sem, const struct sigaction *act, struct sigaction *vact); Returnează 0 în caz de succes, (-1) în caz contrar. Argumentul sem este numărul semnalului a cărui acțiune este inspectată și/sau modificată. Dacă act!=NULL acțiunea se modifica. Dacă vact!=NULL sistemul returnează acțiunea precedentă atașată semnalului. Structura sigaction flosită de funcție este: struct sigaction { void ( *sa_handler)(); /* adresa rutinei de tratare */ sigset_t sa_mask; /* semnale suplimentare de blocat */ int sa_flags; /* opțiuni pentru semnale */ }; La modificarea acțiunii atașate unui semnal, în cazul în care sa_handler referă o rutina de tratare semnal, câmpul sa_mask specifică un set de semnale care sunt adăugate la masca de semnale a procesului înainte de apelul rutinei de tratare. La revenirea din rutina de tratare masca de semnale este restaurată la valoarea ei initială. În acest mod, pe timpul execuției rutinei de tratare, se pot bloca anumite semnale. Semnalul tratat este automat mascat, astfel ca o altă apariție (sau mai multe) a acestui semnal va fi blocată atâta timp cât tratarea primei apariții nu a fost încheiată ( deosebirea esențiala între funcția signal din versiunile anterioare de Unix și funcția sigaction!). După deblocarea semnalului, indiferent dacă a fost o singura apariție sau mai multe, rutina de tratare va fi invocată o singura dată. Asocierea unei acțiuni unui semnal este permanentă până la o modificare explicită printr-un apel sigaction. Câmpul sa_flag poate avea diferite valori, funcție de versiunea de sistem flosită ( a se vedea bibliografia). Folosind funcția sigaction, se poate implementa funcția signal (mai completa decât funcția similară existentă în SVR2): /* A se compila cu: gcc -c -o sig.o sig.c Funcția signal implementata prin funcția sigaction (SVR4) */ #include <signal.h> #include "hdr.h" Sigfunc * signal( int sig, Sigfunc *func) { struct sigaction act, old_act; act.sa_handler=func; sigemptyset( &act.sa_mask); act.sa_flags=0; if ( sig==SIGALRM) { #ifdef SA_INTERRUPT /* SunOS */ act.sa_flags |= SA_INTERRUPT; #endif } else { #ifdef SA_RESTART /* SVR4, 4.3BSD */ act.sa_flags |=SA_RESTART; #endif } printf(" S I G A C T I O N \n"); if ( sigaction( sig, &act, &old_act) <0) return( SIG_ERR); return( old_act.sa_handler); } Indicatorul SA_RESTART trebuie poziționat pentru toate semnalele mai puțin SIGALRM pentru ca orice apel întrerupt de unul dintre aceste semnale să fie reactivat automat. SIGALRM a fost omis pentru a putea fixa o limită de timp la operațiile de I/E cu periferice lente. Sistemul SunOS reactivează implicit apelurile sistem întrerupte, deci specificând indicatorul SA_INTERRUPT în cazul semnalului SIGALRM apelurile sistem nu vor fi întrerupte. Exemplul de mai jos ilustrează diferenta între funcția signal (existentă în SVR2) și funcția signal implementată prin funcția sigaction. În primul caz semnalul e captat (tratat) o singura dată, iar la a doua generare a sa programul se termină. În al doilea caz, semnalul fiind blocat pe timpul tratării sale, fiecare generare a sa este tratată. Pentru a termina programul se generează SIGQUIT prin tasta Ctrl-\. /* A se compila și compara rezultatele folosind cele doua implementari ale funcției signal */ #include <stdio.h #include <sys/types.h #include <sys/wait.h #include <signal.h #include "hdr.h" void sig_intr( int); int main( void) { char buf[MAXLINE]; pid_t pid; int status; if ( signal( SIGINT, sig_intr) == SIG_ERR) err_sys("Eroare signal"); printf(" "); while ( fgets( buf, MAXLINE, stdin) != NULL) { buf[ strlen( buf) 1] = 0; if ( (pid=fork()) < 0)
exit(0); } void sig_intr( int sig) { printf("intrerupt\t\t\tCaptat semnalul SIGINT\n "); fflush( stdout); } Având în vedere mare diversitate de funcții ce lucrează cu semnale se recomandă consultarea bibliografiei. 3. Aplicații 3.1. Să se scrie un program care ilustreaza modurile de tratare ale unui semnal. #include <signal.h #include "hdr.h" static void sig_usr1( int); /* generat cu kill USR1 <pid */ static void sig_intr( int); /* generat la Ctrl-C și rearmat */ static void sig_quit( int); /* generat cu Ctrl-\ și resetat */ static void sig_alarm(int); /* generat dupa scurgerea timpului t din alarm(t) */ int main( void) { if ( signal( SIGALRM, sig_alarm) == SIG_ERR) err_sys("Eroare signal( SIGALRM, ...)"); if ( signal( SIGUSR1, sig_usr1) == SIG_ERR) err_sys("Eroare signal( SIGUSR1, ...)"); if ( signal( SIGINT, sig_intr) == SIG_ERR) err_sys("Eroare signal( SIGINT, ...)"); if ( signal( SIGQUIT, sig_quit) == SIG_ERR) err_sys("Eroare signal( SIGQUIT, ...)"); for ever pause(); } static void sig_alarm( int sig) { printf("Receptionat semnalul SIGALRM\n"); return; } static void sig_quit( int sig) { printf("Receptionat semnalul SIGQUIT\n"); if ( signal( SIGQUIT, SIG_DFL) == SIG_ERR) err_sys("Nu se poate reseta acest semnal ..."); return; } static void sig_intr( int sig) { printf("Receptionat semnalul SIGINT\n"); if ( signal( SIGINT, sig_intr) == SIG_ERR) err_sys("Nu se poate rearma acest semnal ..."); return; } static void sig_usr1( int sig) { printf("Receptionat semnalul SIGUSR1\n"); alarm(1); printf("Alarma se va declansa dupa 1 sec!.\n"); return; } 3.2. Să se scrie un programul care citește o linie de la intrarea standard și o afișează la ieșirea standard. Programul stabilește un interval de timp pentru efectuarea operației. Dacă operația nu s-a realizat în acest răstimp ea este abandonată. #include <setjmp.h #include <signal.h #include "hdr.h" static void sig_alarm(); int main( void) { int n; char line[MAXLINE]; if ( signal( SIGALRM, sig_alarm) == SIG_ERR) err_sys("Eroare signal( SIGALRM, ...)"); alarm( 10); if ( ( n=read( 0, line, MAXLINE)) < 0) err_sys("Eroare read"); alarm(0); write( 1, line, n); exit(0); } static void sig_alarm( int sig) { return; } Codul are două inconveniente: a) Dacă nucleul întârzie procesul între apelurile alarm și read pentru un interval mai mare decât perioada alarmei ( 10 secunde), apelul read se blochează pentru totdeauna. b) Dacă funcțiile de sistem sunt automat relansate, apelul read nu este întrerupt când se revine din rutina de tratare a semnalului SIGALRM. În acest caz alarma nu-și are rostul. 3.3. Următorul program ilustrează cum pot fi create într-un program regiuni ce nu pot fi întrerupte, semnalul de întrerupere fiind amânat până la terminarea regiunii. Programul preia linii de text de la intrarea standard pe care le afișează la ieșirea standard. Pe timpul preluării textului de la tastatură programul nu va putea fi întrerupt. #include <signal.h #include <setjmp.h #define MAXLIN 81 jmp_buf cs_stack; int în_reg, s_inreg; void beg_reg( ); /* funcție ce marcheaza inceputul regiunii */ void end_reg( ); /* funcție ce marcheaza sfarsitul regiunii */ int tratare( ); /* rutina de tratare a semnalului */ int read_line( ); void main( ) { int nr_car; char buff[MAXLIN]; signal( SIGINT, tratare ); if( setjmp( cs_stack ) ){ /* prelucrare intrerupere */ printf("Programul este intrerupt prin semnal !\n"); exit(1); } else while( 1 ){ printf( " " ); beg_reg( ); nr_car = read_line( buff ); end_reg( ); if( nr_car0 ) {
printf( " Program terminat normal !\n" ); } int tratare( ) { if( în_reg ){ signal( SIGINT, SIG_IGN ); s_inreg=1; return 0; } else { signal( SIGINT, tratare ); longjmp( cs_stack, 1 ); } return 0; } void beg_reg( void ) { în_reg = 1; s_inreg = 0; } void end_reg( void ) { în_reg = 0; if( s_inreg ){ s_inreg = 0; signal( SIGINT, tratare ); longjmp( cs_stack, 1 ); } } int read_line( buff ) char *buff; { char c; int i=0; for( c = getchar( ); i<MAXLIN && c!='\n'; c = getchar( ) ) buff[ i++ ] = c; buff[i] = '\0'; return i; } 4. Probleme propuse 4.1. Să se scrie un program utilizat pentru copierea intrării standard într-un fișier, specificat prin linia de comandă. Dacă în 3 intervale succesive de 10 sec nu este introdus nici un caracter de la tastatură, programul este terminat. La fiecare interval de 10 sec. neutilizate, utilizatorul va fi atenționat. 4.2. Folosind alarm și pause să se implementeze o funcție sleep2. 4.3. Scrieți un program ce preia de la tastatură câte 2 numere reale pe care efectuează operațiile +, - , * și /. Programul trebuie să trateze erorile aritmetice care pot să apară. 4.4. Un program copiază intrarea standard într-un fișier specificat prin linia de comanda, utilizând un fișier intermediar în acest scop. Scrieti programul în asa fel încât, dacă procesul este întrerupt prin oricare din semnalele SIGINT, SIGQUIT, SIGHUP, SIGTERM, să fie anulate toate efectele execuției programului până la întrerupere. 4.5. După execuția lui fork, nu se știe ce proces va continua primul. În situația în care acestea accesează o resursa partajată, pot să apară probleme de nesincronizare. Pentru ca două procese aflate în relație părinte-fiu să se sincronizeze, se pot crea următoarele funcții: TELL_WAIT() realizează anumite inițializări; TELL_PARENT(pid) anunță părintele ca poate continua execuția; WAIT_PARENT() așteaptă părintele până ce acesta termină accesul la resursa partajată; TELL_CHILD(pid) anunță părintele ca a terminat accesul; WAIT_CHILD( ) așteaptă procesul fiu până ce acesta termină accesul la resursa partajată; Utilizând semnalele, să se implementeze aceste funcții de sincronizare. 4.6. Să se scrie următorul program pentru a testa sicronizarea părinte-fiu, utilizând funcțiile din programul 4.5. 4.7. Să se scrie două programe MASTER.C și SLAVE.C. Programul master se lansează la un terminal și generează câte un proces fiu (SLAVE) pentru fiecare terminal specificat în linia de comandă. Procesul MASTER poate comunica cu procesele SLAVE prin fișier PIPE. să se scrie programele astfel încât procesele SLAVE culeg câte o linie de text pe care o trimit procesului MASTER care le preia și le afișează pe ecran. Sincronizarea se realizează prin semnale. 4.8. Se dau trei fișiere cu numele NUME.TXT, PREN.TXT și NOTA.TXT în care sunt înregistrate, câte unul pe linie, numele, prenumele și nota obținută de mai mulți studenți. Să se scrie un program, prin lansarea căruia se generează 3 procese, fiecare proces citind datele dintr-un fișier dintre cele trei. Să se sincronizeze funcționarea celor trei procese astfel încât pe ecran să se afișeze linii având structura: NUME PRENUME NOTA 4.9. Să se elimine neajunsurile aplicației 3.2. folosind setjmp și longjmp. 4.10. Să se scrie un program C care să implementeze problema filozofilor. |