This is an old revision of the document!
În lumea reală, un proces poate cunoaște o multitudine de situații neprevăzute, care-i afectează cursul normal de execuție. Dacă procesul nu le poate trata, ele sunt pasate, mai departe, sistemului de operare. Cum sistemul de operare nu poate ști dacă procesul își poate continua execuția în mod normal, fără efecte secundare nedorite, este obligat să termine procesul în mod forțat. O rezolvare a acestei probleme o reprezintă semnalele.
terminarea
procesului.ignore
un semnal, sistemul de operare nu va mai trimite acel semnal procesului.blocheze
un semnal, sistemul de operare nu va mai trimite semnalele de acel tip spre procesul în cauză, dar va salva numai primul semnal de acel tip, restul pierzându-se. Când procesul hotărăște că vrea să primească, din nou, semnale de acel tip, dacă există vreun semnal în așteptare, acesta va fi trimis.
Mulțimea tipurilor de semnale este finită; sistemul de operare ține, pentru fiecare proces, o tabelă cu acțiunile
alese de acesta, pentru fiecare tip de semnal. La fiecare moment de timp aceste acțiuni sunt bine determinate. La pornirea procesului tabela de acțiuni este inițializată cu valorile implicite. Modul de tratare a semnalului nu este decis la primirea semnalului de către proces, ci se alege, în mod automat, din tabelă.
Semnalele sunt sincrone/asincrone cu fluxul de execuție al procesului care primește semnalul dacă evenimentul care cauzează trimiterea semnalului este sincron/asincron cu fluxul de execuție al procesului.
asincron
dacă nu este sincron. Exemple de evenimente asincrone: un semnal trimis de un alt proces (semnalul de terminare unui proces copil), sau o cerere de terminare externă (utilizatorul dorește să reseteze calculatorul).Un semnal primit de un proces poate fi generat:
sistemul de operare
- în cazul în care acesta raportează diferite erori;proces
- care-și poate trimite și singur semnale (semnalul va trece tot prin sistemul de operare).
În anumite cazuri, există nevoia de a ști, în mod sigur, că un semnal trimis a ajuns la destinație și, implicit, că procesul va răspunde la el (efectuând una din acțiunile posibile). Sistemul de operare oferă un alt mod de a trimite un semnal, prin care se garantează
fie că semnalul a ajuns la destinație, fie că această acțiune a eșuat. Acest lucru este realizat prin crearea unei stive de semnale, de o anumită capacitate (ea trebuie să fie finită, pentru a nu produce situații de overflow). La trimiterea unui semnal, sistemul de operare verifică dacă stiva este plină. În acest caz, cererea eșuează, altfel semnalul este pus în stivă și operația se termină cu succes. Modul clasic de a trimite semnale este analog cu acesta (stiva are dimensiunea 1) cu excepția faptului că nu se oferă informații despre ajungerea la destinație a unui semnal.
Noțiunea de semnal este folosită pentru a indica alternativ fie un anumit tip de semnal, fie efectiv obiectele de acest tip.
În general, evenimentele care generează semnale se încadrează în trei categorii majore:
eroare
indică faptul că un program a făcut o operație nepermisă și nu-și poate continua execuția. Însă, nu toate tipurile de erori generează semnale (de fapt, cele mai multe nu o fac). De exemplu, deschiderea unui fișier inexistent este o eroare, dar nu generează un semnal; în schimb, apelul de sistem open returnează -1, indicând că apelul s-a terminat cu eroare. În general, erorile asociate cu anumite biblioteci sunt raportate prin întoarcerea unei valori speciale. Erorile care generează semnale sunt cele care pot apărea oriunde în program, nu doar în apelurile din biblioteci. Ele includ împărțirea cu zero și accesarea nevalidă a memoriei.eveniment extern
este, în general, legat de I/O și de alte procese. Exemple: apariția de noi date de intrare, expirarea unui timer, terminarea execuției unui proces copil.cerere explicită
indică utilizarea unui apel de sistem, cum ar fi kill, pentru a genera un semnal.Semnalele pot fi generate sincron sau asincron:
sincron
se raportează la o acțiune specifică din program, și este trimis (dacă nu este blocat) în timpul acelei acțiuni. Cele mai multe erori generează semnale în mod sincron. De asemenea, semnalele pot fi generate în mod sincron și prin anumite cereri explicite trimise de un proces lui însuși. Pe anumite mașini, anumite tipuri de erori hardware (de obicei, excepțiile în virgulă mobilă) nu sunt raportate complet sincron, și pot ajunge câteva instrucțiuni mai târziu.asincrone
sunt generate de evenimente necontrolabile de către procesul care le primește. Aceste semnale ajung la momente de timp impredictibile. Evenimentele externe generează semnale în mod asincron, la fel ca și cererile explicite trimise de alte procese.Un tip de semnal dat este fie sincron, fie asincron. De exemplu, semnalele pentru erori sunt, în general, sincrone deoarece erorile generează semnale în mod sincron. Însă, orice tip de semnal poate fi generat sincron sau asincron cu o cerere explicită.
Când un semnal este generat, el intră într-o stare de așteptare (pending). În mod normal, el rămâne în această stare pentru o perioadă de timp foarte mică și apoi este trimis procesului destinație. Însă, dacă acel tip de semnal este, în momentul de față, blocat, el ar putea rămâne în starea de așteptare nedefinit, până când semnalele de acel tip sunt deblocate. O dată deblocat acel tip de semnal, el va fi trimis imediat.
Când semnalul a fost primit, fie imediat, fie cu întârziere, acțiunea specificată pentru acel semnal este executată. Pentru anumite semnale, cum ar fi SIGKILL
și SIGSTOP
, acțiunea este fixată (procesul este terminat), dar, pentru majoritatea semnalelor, programul poate alege să:
Programul își specifică alegerea utilizând funcții precum signal sau sigaction. În timp ce handler-ul rulează, acel tip de semnal este în mod normal blocat (deblocarea se va face printr-o cerere explicită în handler-ul care tratează semnalul).
Dacă acțiunea specificată pentru un tip de semnal este să îl ignore, atunci orice semnal de acest tip, care este generat pentru procesul în cauză, este ignorat. Același lucru se întâmplă dacă semnalul este blocat în acel moment. Un semnal neglijat în acest mod nu va fi primit niciodată, nici dacă programul specifică ulterior o acțiune diferită pentru acel tip de semnal și apoi îl deblochează. Dacă este primit un semnal pentru care nu s-a specificat niciun tip de acțiune, se execută acțiunea implicită. Fiecare tip de semnal are propria lui acțiune implicită. Pentru majoritatea semnalelor acțiunea implicită este terminarea procesului. Pentru anumite tipuri de semnale, care reprezintă evenimente fără consecințe majore, acțiunea implicită este să nu se facă nimic.
Când un semnal forțează terminarea unui proces, părintele procesului poate determina cauza terminării examinând codul de terminare raportat de funcțiile wait
și waitpid
. Informațiile pe care le poate obține includ faptul că terminarea procesului a fost cauzată de un semnal, precum și tipul semnalului. Dacă un program pe care îl rulați din linia de comandă este terminat de un semnal, shell-ul afișează, de obicei, niște mesaje de eroare.
Semnalele care în mod normal reprezintă erori de program au o proprietate specială: când unul din aceste semnale termină procesul, el scrie și un fișier core dump care înregistrează starea procesului în momentul terminării. Puteți examina fișierul cu un debugger, pentru a afla ce anume a cauzat eroarea.
Dacă generați un semnal, care reprezintă o eroare de program, printr-o cerere explicită, și acesta termină procesul, fișierul este generat ca și cum semnalul ar fi fost generat de o eroare.
signal
și apoi fie operația va eșua (cu errno
setat pe EINTR
), fie se va reporni operația. Sistemele System V se comportă ca în primul caz, cele BSD ca în cel de-al doilea. De la glibc v2 încoace, comportamentul este același ca și pe BSD, totul depinzând de definiția macrou-ului _BSD_SOURCE. Comportamentul poate fi controlat de către programator folosind sigaction
cu flag-ul SA_RESTART
.
Această secțiune prezintă numele pentru diferite tipuri standard de semnale și descrie ce fel de evenimente indică.
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 33) SIGRTMIN 34) SIGRTMIN+1 35) SIGRTMIN+2 36) SIGRTMIN+3 37) SIGRTMIN+4 38) SIGRTMIN+5 39) SIGRTMIN+6 40) SIGRTMIN+7 41) SIGRTMIN+8 42) SIGRTMIN+9 43) SIGRTMIN+10 44) SIGRTMIN+11 45) SIGRTMIN+12 46) SIGRTMIN+13 47) SIGRTMIN+14 48) SIGRTMIN+15 49) SIGRTMAX-15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
Numele de semnale sunt definite în header-ul signal.h
. În general, semnalele au roluri predefinite, dar acestea pot fi suprascrise de programator.
Cele mai cunoscute sunt următoarele semnale:
SIGINT
- transmis la apăsarea combinației CTRL+C
;SIGQUIT
- transmis la apăsarea combinației de taste CTRL+\
;SIGSEGV
- transmis în momentul accesării unei locații nevalide de memorie, etc;SIGKILL
- nu poate fi ignorat sau suprascris. Transmiterea acestui semnal are ca efect terminarea procesului, indiferent de context.Cel mai bun mod de a afișa un mesaj de descriere a unui semnal este utilizarea funcțiilor strsignal și psignal. Aceste funcții folosesc un număr de semnal pentru a specifica tipul de semnal care trebuie descris. Mai jos este prezentat un exemplu de folosire a acestor funcții:
#include <stdio.h> #include <stdlib.h> #define __USE_GNU #include <string.h> #include <signal.h> int main(void) { char *sig_p = strsignal(SIGKILL); printf("signal %d is %s\n", SIGKILL, sig_p); psignal(SIGKILL, "death and decay"); return 0; }
Pentru compilare și rulare secvența este:
so@spook$ gcc -Wall -g -o msg_signal msg_signal.c so@spook$ ./msg_signal signal 9 is Killed death and decay: Killed
Pentru a putea efectua operații de blocare/deblocare semnale avem nevoie să știm, la fiecare pas din fluxul de execuție, starea fiecărui semnal. Sistemul de operare are, de asemenea, nevoie de același lucru pentru a putea lua o decizie asupra unui semnal care trebuie trimis unui proces (el are nevoie de acest gen de informație pentru fiecare proces în parte). În acest scop se folosește o mască de semnale proprie fiecărui proces.
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
Tipul de date folosit de sistemele UNIX pentru a reprezenta măștile de semnale este sigset_t
. Variabilele de acest tip sunt neinițializate
. Operațiile pe acest tip de date sunt:
Funcțiile următoare sunt folosite pentru a manipula masca de biți. Ele nu decid acțiunea de blocare sau deblocare a unui semnal, ci doar setează semnalul respectiv în masca de biți (pentru adăugare se pune bitul corespunzător semnalului pe 1, iar pentru ștergere pe 0), pentru ca apoi să se folosească sigprocmask
pentru a seta acțiunea de blocare/deblocare efectivă. Mai multe detalii despre aceste funcții găsiți aici.
int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(sigset_t *set, int signo);
sigaddset
, sigdelset
și sigismember
asupra unui sigset_t
, acest tip trebuie inițializat folosind sigemptyset
sau sigfillset
. Comportamentul este nedefinit în caz contrar.
SIGINT
:
sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); while (1) { sleep(5); sigprocmask(SIG_BLOCK, &set, NULL); sleep(5); sigprocmask(SIG_UNBLOCK, &set, NULL); }
O altă valoare pe care o poate lua primul parametru al funcției sigprocmask este SIG_SETMASK
, care specifică pur și simplu că vechea mască (al treilea parametru) e înlocuită cu cel de-al doilea parametru (noua mască). Un exemplu de folosire a acesteia puteți găsi la această adresă.
Tratarea semnalelor se realizează prin asocierea unei funcții (handler) unui semnal. Funcția va fi apelată în momentul în care procesul recepționează semnalul respectiv.
În mod tradițional, funcția folosită pentru asocierea de handler-e pentru tratarea unui semnal erasignal. Pentru a preîntâmpina deficiențele acestei funcții, standardul POSIX a definit funcția sigaction pentru asocierea unui handler cu un semnal. sigaction
oferă mai mult control, cu prețul unui grad de complexitate mai mare.
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
Componenta importantă a funcției sigaction
este structura cu același nume, descrisă în pagina de manual a funcției:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; };
Dacă în câmpul sa_flags
se precizează flag-ul SA_SIGINFO
, handler-ul folosit este cel specificat de sa_sigaction
. Altfel, handler-ul folosit este sa_handler
. Masca de semnale care ar trebui blocate în timpul execuției handler-ului este reprezentată de sa_mask
.
Un exemplu de asociere a unui handler de tratare a unui semnal este prezentat mai jos:
#include <signal.h> ... /* SIGUSR2 handler */ static void usr2_handler(int signum) { /* actions that should be taken when the signal signum is received */ ... } int main(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_flags = SA_RESETHAND; /* restore handler to previous state */ sa.sa_handler = usr2_handler; sigaction(SIGUSR2, &sa, NULL); return 0; }
SIG_IGN
pentru ignorarea semnalului sau SIG_DFL
pentru rularea acțiunii implicite (terminarea procesului, ignorarea semnalului etc).
Pentru transmiterea unui semnal, se poate folosi funcția kill sau funcția sigqueue. Funcția kill are dezavantajul că nu garantează recepționarea semnalului de procesul destinație. Dacă este nevoie să se trimită un semnal unui proces și să se știe sigur că a ajuns se recomandă folosirea funcției sigqueue:
int sigqueue(pid_t pid, int signo, const union sigval value);
Funcția trimite semnalul signo
, cu parametrii specificați de value
, procesului cu identificatorul pid
. Dacă semnalul este zero, se fac verificări pentru cazurile de eroare posibile, dar nu se trimite niciun semnal. Semnalul nul poate fi folosit pentru a verifica faptul că pid-ul este valid.
Valoarea ce poate fi trimisă odată cu semnalul este un union:
union sigval { int sival_int; void *sival_ptr; };
Un parametru trimis astfel apare în câmpul si_value
al structurii siginfo_t
, primite de handler-ul de semnal. În mod evident, nu are sens transmiterea de pointeri dintr-un proces în altul.
Condițiile cerute pentru ca un proces să aibă permisiunea de a trimite un semnal altui proces sunt aceleași ca și în cazul lui kill
. Dacă semnalul specificat este blocat în acel moment, funcția va ieși imediat și dacă flagul SA_SIGINFO
este setat și există resurse necesare, semnalul va fi pus în coadă în starea pending (un proces poate avea în coadă maxim SIGQUEUE_MAX
semnale). De asemenea, când semnalul este primit, câmpul si_code
, pasat structurii siginfo
, va fi setat la SI_QUEUE
, și si_value
va fi setat la value
.
Dacă flagul SA_SIGINFO
nu este setat, atunci signo
, dar nu în mod necesar și value
, vor fi trimise, cel puțin o dată, procesului care trebuie să primească semnalul.
Soluția cea mai bună pentru a aștepta un semnal se poate realiza prin utilizarea funcției sigsuspend:
int sigsuspend(const sigset_t *set);
Funcția înlocuiește masca de semnale blocate a procesului, cu set, și suspendă procesul până când este primit un semnal care nu este blocat de noua mască. La ieșire, funcția restaurează vechea mască de semnale.
În secvența de mai jos, funcția sigsuspend este folosită pentru a întrerupe procesul curent până la recepționarea semnalului SIGINT
. Semnalele SIGKILL și SIGSTOP
, deși prezente în masca de semnale, nu vor fi blocate:
sigset_t set; /* block all signals except SIGINT */ sigfillset(&set); sigdelset(&set, SIGINT); /* wait for SIGINT */ sigsuspend(&set);
În Linux, folosirea timer-elor este legată de folosirea semnalelor. Acest lucru se întâmplă întrucât cea mai mare parte a funcțiilor de tip timer folosesc semnale.
Un timer este, de obicei, un întreg a cărui valoare este decrementată în timp. În momentul în care întregul ajunge la 0, timer-ul expiră. În Linux, expirarea timer-ului are drept rezultat, în general, transmiterea unui semnal. Definirea unui “timer handler” (rutină apelată în momentul expirării timer-ului) este, astfel, echivalentă cu definirea unui handler pentru semnalul asociat.
Înregistrarea unui timer, în Linux, înseamnă specificarea unui interval după care un timer expiră și configurarea handler-ului care va rula. Configurarea handler-ului se poate realiza atât prin intermediul funcției sigaction
(în momentul în care timer-ul expiră se generază un semnal, care la rândul lui generează rularea handler-ului asociat), sau direct prin intermediul parametrilor funcției timer_create.
Utilizarea unui timer presupune mai mulți pași:
int timer_create(clockid_t clockid, struct sigevent *evp, timer_t *timerid)
Timer-ul creat se identifică prin timerid
. Prin intermediul structurii sigevent
se setează modul în care va interacționa timer-ul cu procesul/thread-ul care l-a lansat. Exemplu de folosire:
timer_t timerid; struct sigevent sev; sev.sigev_notify = SIGEV_SIGNAL; /* notification method */ sev.sigev_signo = SIGRTMIN; /* Timer expiration signal */ sev.sigev_value.sival_ptr = &timerid; timer_create(CLOCK_REALTIME, &sev, &timerid);
Prin intermediul primului argument se poate măsura timpul real al sistemului, timpul de rulare al procesului sau timpul de rulare al procesului în user-space și kernel-space. La timeout timer-ul va livra semnalul salvat în sev.sigev_signo
.
int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec * old_value);
Armarea timer-ului presupune completarea structurii itimerspec
în care specifică timpul de pornire al timer-ului, cât și intervalul de expirare al timeout-ului (intervalele sunt măsurate în secunde și nanosecunde). Exemplu de folosire:
its.it_value.tv_sec = freq_nanosecs / 1000000000; /* Initial expiration in secs*/ its.it_value.tv_nsec = freq_nanosecs % 1000000000;/* Initial expiration in nsecs*/ its.it_interval.tv_sec = its.it_value.tv_sec; /* Timer interval in secs */ its.it_interval.tv_nsec = its.it_value.tv_nsec; /* Timer interval in nanosecs */ timer_settime(timerid, 0, &its, NULL);
int timer_delete(timer_t timerid);
-lrt
SIGALRM
). nanosleep are un apel mai complex, dar oferă rezoluție până la ordinul nanosecundelor și este signal-safe
(nu interacționează cu semnale).