|
7. Comunicarea intre procese folosind semnale
In lucrarea trecuta a fost prezentata una din modalitatile de comunicare
intre procese cele mai folosite, si anume comunicarea prin pipes.
In aceasta lucrare ne propunem sa analizam cum se poate face uz de
facilitatile UNIX referitoare la semnale pentru a realiza
comunicarea intre procese.
1. Semnale
Semnalele sunt o modalitate de exprimare a evenimentelor care apar asincron
in sistem.
Un proces oarecare poate atat sa genereze, cat si sa primeasca semnale.
In cazul in care un proces primeste un semnal, el poate alege sa reactioneze
la semnalul respectiv intr-unul din urmatoarele trei moduri:
- Sa le capteze, executand o actiune oarecare, prin intermediul unei functii de tratare a semnalului (signal handler)
- Sa le ignore
- Sa execute actiunea implicita la primirea unui semnal, care poate fi, dupa caz, terminarea procesului sau ignorarea semnalului respectiv.
Semnalele pot fi de mai multe tipuri, care corespund in general unor actiuni
specifice. Fiecare semnal are asociat un numar, iar acestor numere le corespund
unele constante simbolice definite in bibliotecile sistemului de operare.
Standardul POSIX.1 defineste cateva semnale care trebuie sa existe in orice
sistem UNIX. Aceste semnale sunt cuprinse in urmatoarea lista:
SIGHUP |
1 |
Hangup - terminalul folosit de proces a fost inchis |
SIGINT |
2 |
Interrupt - semnifica in mod uzual intreruperea procesului prin Ctrl-C |
SIGQUIT |
3 |
Quit - are semnificatia unei cereri de iesire din program a utilizatorului |
SIGILL |
4 |
Illegal Instruction - se genereaza atunci cand procesul a executat o instructiune al carei opcode nu are corespondent in setul de instructiuni sau pentru care nu exista privilegii suficiente |
SIGABRT |
6 |
Abort - semnal de terminare anormala a procesului generat de abort(3) |
SIGFPE |
8 |
Floating Point Exception - semnal generat de o eroare in virgula mobila (de exemplu, in cazul unei impartiri cu zero) |
SIGKILL |
9 |
Kill - semnal care are ca efect distrugerea imediata a procesului |
SIGUSR1, SIGUSR2 |
10, 12 |
User Defined 1, 2 - semnale a caror generare si tratare sunt lasate in seama utilizatorului, folosite de obicei pentru generarea de diverse actiuni in logica unui proces |
SIGSEGV |
11 |
Segmentation Fault - se genereaza atunci cand un proces a facut referinta la o adresa de memorie invalida |
SIGPIPE |
13 |
Broken Pipe - se genereaza atunci cand s-a incercat scrierea intr-un pipe care are descriptorul de la capatul de citire inchis |
SIGALRM |
14 |
Timer Alarm - semnal generat in urma expirarii timer-ului pozitionat de alarm(2) |
SIGTERM |
15 |
Terminate - Semnifica o cerere de terminare normala a procesului |
SIGCONT |
18 |
Continue - Are ca rezultat continuarea unui proces oprit prin SIGSTOP |
SIGSTOP |
19 |
Stop - Are ca rezultat suspendarea executiei procesului pana cand aceasta va fi reluata prin primirea unui semnal SIGCONT |
Diferitele variante de UNIX existente implementeaza aceste semnale,
adaugand si altele noi. Pentru a obtine o lista cu toate semnalele existente
in Linux, de exemplu, consultati pagina de manual
signal(7).
2. Tratarea semnalelor
2.1 Apelul sistem signal()
Modalitatea ANSI prin care se poate realiza captarea semnalelor in
scopul tratarii lor este data de apelul sistem
signal(). Acest apel sistem are urmatoarea forma:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Executia acestui apel sistem va face ca, pentru semnalul cu numarul
signum specificat ca si parametru, sa se instaleze o noua rutina de
tratare specificata prin parametrul handler. Acest parametru trebuie
sa aiba tipul sighandler_t, care inseamna de fapt ca trebuie sa fie
adresa unei functii care nu returneaza nimic si are un singur parametru formal
de tip intreg. Acest parametru va primi ca valoare, in momentul executiei
rutinei de tratare a semnalului, numarul semnalului care se trateaza. Acest
lucru este util, de exemplu, atunci cand se doreste sa se trateze mai multe
tipuri de semnale folosind aceeasi rutina.
Exista doua valori speciale pentru parametrul handler, si anume:
SIG_IGN, care instruieste procesul sa ignore semnalul specificat,
si SIG_DFL, care reseteaza tratarea semnalului la comportamentul
implicit.
Apelul sistem signal() returneaza
rutina anterioara de tratare a semnalului, sau SIG_ERR in caz de
eroare. Acest lucru se poate intampla, de exemplu, cand semnalul cu
numarul specificat nu exista, sau cand rutina de tratare specificata nu
are tipul corespunzator. Tot SIG_ERR se returneaza si atunci cand
signum este SIGKILL sau SIGSTOP, care sunt semnale
ce nu pot fi nici captate, nici ignorate.
Desi marele avantaj al apelului sistem signal()
asupra altor metode de captare a semnalelor este simplitatea,
folosirea lui are si cateva dezavantaje. Unul dintre acestea este
portabilitatea redusa, care apare din cauza ca diversele implementari ale
acestului apel sistem in diverse sisteme UNIX se comporta in mod diferit in
cazul in care se capteaza semnalul. De exemplu, implementarea originala
UNIX (folosita si de sistemele UNIX System V si Linux libc4 si libc5) este
aceea ca, dupa o executie a rutinei de tratare, aceasta este resetata
la valoarea SIG_DFL, ceea ce inseamna ca rutina trebuie sa apeleze
signal() inainte de terminarea ei
pentru a se "reinstala" in vederea tratarii urmatoarei aparitii a
semnalului. Pe de alta parte, sistemele din familia 4.3BSD (si Linux
glibc2) nu reseteaza rutina de tratare, dar blocheaza aparitiile
semnalului respectiv in timpul executiei rutinei de tratare. Folosirea
lui signal() nu este deci de dorit
atunci cand se doreste scrierea de software care trebuie sa fie usor
portabil pe alte variante de UNIX.
Urmatorul subparagraf va prezenta o metoda de captare a semnalelor care
ofera mai multa flexibilitate decat cea descrisa anterior, prin folosirea
apelului sistem sigaction() si a
apelurilor asociate cu acesta.
2.2 Apelul sistem sigaction()
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
int sigsuspend(const sigset_t *mask);
Apelul sistem sigaction() este similar cu
apelul sistem signal(). El are menirea
de a defini comportarea procesului la primirea unui semnal. Acest apel primeste
ca parametri numarul semnalului (signum), si doua structuri de tip
struct sigaction * (act si oldact). Executia sa va avea ca
efect instalarea noii actiuni pentru semnalul specificat din act (daca
acest parametru este nenul), si salvarea actiunii curente in oldact (la
fel, daca parametrul este nenul).
Structura de tip struct sigaction este definita in felul urmator:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
Parametrul sa_handler reprezinta noua rutina de tratare a semnalului
specificat, similar cu parametrul handler prezentat la
signal(). Alternativ, daca in sa_flags
este setat indicatorul SA_SIGINFO (prezentat mai jos), se poate defini
o rutina care primeste trei parametri in loc de unul singur, si in acest caz
ea se specifica prin parametrul sa_sigaction. Pentru mai multe detalii
legate de folosirea acestei din urma modalitati, consultati pagina de manual
sigaction(2). In unele cazuri, acesti
doi parametri sunt prinsi intr-un union, si deci se recomanda sa se
specifice doar unul din ei.
Parametrul sa_mask va specifica setul de semnale care vor fi blocate
in timpul executiei rutinei de tratare a semnalului dat. Acest parametru este
de tipul sigset_t, care este de fapt o masca de biti, cu cate un bit
pentru fiecare semnal definit in sistem. Operatiile asupra acestui tip de
masca se fac folosind functiile din familia
sigsetops(3). Sintaxa si functionarea acestor apeluri
este foarte simpla, consultati pagina de manual pentru a vedea exact care
sunt parametrii lor si modul de functionare.
Parametrul sa_flags va specifica un set de indicatori care afecteaza
comportarea procesului de tratare a semnalelor. Acest parametru se formeaza
prin efectuarea unei operatii de SAU logic folosind una sau mai multe din
urmatoarele valori:
-
SA_NOCLDSTOP - daca signum este SIGCHLD, procesul nu
va primi un semnal SIGCHLD atunci cand procesul fiu este suspendat
(de exemplu cu SIGSTOP), ci numai cand acesta isi termina executia;
-
SA_ONESHOT sau SA_RESETHAND - va avea ca efect resetarea rutinei
de tratare a semnalului la SIG_DFL dupa prima rulare a rutinei,
asemanator cu comportamentul implementarii originale a apelului
signal();
-
SA_ONSTACK - executia rutinei de tratare va avea loc folosind alta
stiva;
-
SA_RESTART - ofera compatibilitate cu comportamentul semnalelor in
sistemele din familia 4.3BSD;
-
SA_NOMASK sau SA_NODEFER - semnalul in discutie nu va fi inclus
in mod automat in sa_mask (comportamentul implicit este acela de a
impiedica aparitia unui semnal in timpul executiei rutinei de tratare a
semnalului respectiv);
-
SA_SIGINFO - se specifica atunci cand se doreste utilizarea lui
sa_siginfo in loc de sa_handler. Pentru mai multe detalii,
consultati pagina de manual sigaction(2).
Apelul sigprocmask() este folosit pentru
a modifica lista semnalelor care sunt blocate la un moment dat. Acest lucru
se face in functie de valoarea parametrului how, in felul urmator:
- SIG_BLOCK - adauga la lista semnalelor blocate semnalele din
lista set data ca parametru;
- SIG_UNBLOCK - sterge din lista semnalelor blocate semnalele aflate
in lista set;
- SIG_SETMASK - face ca doar semnalele din lista set sa se
regaseasca in lista semnalelor blocate.
Daca parametrul oldset este nenul, in el se va memora valoarea
listei semnalelor blocate anterioara executiei lui
sigprocmask().
Apelul sistem sigpending() permite
examinarea semnalelor care au aparut in timpul in care ele au fost blocate,
prin returnarea acestor semnale in masca set data ca parametru.
Apelul sigsuspend() inlocuieste temporar
masca semnalelor blocate a procesului cu cea data in parametrul mask
si suspenda procesul pana la primirea unui semnal.
Apelurile sigaction(),
sigprocmask() si
sigpending()() intorc valoarea 0 in caz de succes si -1
in caz de eroare. Apelul sigsuspend()
intoarce intotdeauna -1, iar in mod normal variabila errno este setata
la valoarea EINTR.
In urma executiei cu eroare a unuia din apelurile de mai sus, variabila
errno poate sa ia una din urmatoarele valori:
- EINVAL - atunci cand a fost specificat un semnal invalid, adica
nedefinit in implementarea curenta, sau unul din semnalele SIGKILL
sau SIGSTOP;
- EFAULT - atunci cand una din variabilele date ca parametru indica
spre o zona de memorie care nu face parte din spatiul de adrese al procesului;
- EINTR - atunci cand apelul a fost intrerupt.
3. Alte functii pentru lucrul cu semnale
3.1 Apelul sistem kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
Acest apel sistem este folosit pentru a trimite un semnal unui anumit proces
sau grup de procese. In functie de valoarea parametrului pid, executia
apelului va avea unul din urmatoarele efecte:
- Daca pid > 0, semnalul va fi trimis procesului care are PID-ul
egal cu pid;
- Daca pid == 0, semnalul va fi trimis tuturor proceselor din
acelasi grup de procese cu procesul curent;
- Daca pid == -1, semnalul va fi trimis tuturor proceselor care
ruleaza in sistem (de notat faptul ca, in majoritatea implementarilor, nu
se poate trimite in acest fel catre procesul init un semnal pentru care
acesta nu are prevazuta o rutina de tratare, si de asemenea, faptul ca de
obicei in acest fel procesului curent nu i se trimite semnalul respectiv);
- Daca pid < -1, se va trimite semnalul catre toate procesele
din grupul de procese cu numarul -pid.
Daca valoarea lui sig este zero, nu se va trimite nici un semnal,
dar apelul va executa verificarile de eroare. Acest lucru este util in cazul
in care se doreste sa se stie, de exemplu, daca avem suficiente permisiuni
pentru a trimite un semnal catre un proces dat.
Acest apel returneaza 0 in caz de succes si -1 in caz de eroare, setand
variabila errno la una din urmatoarele valori in cazul executiei cu
eroare:
- EINVAL - in cazul specificarii unui semnal invalid;
- ESRCH - in cazul in care procesul sau grupul de procese
specificat nu exista;
- EPERM - in cazul in care nu se dispune de permisiuni suficiente
pentru a trimite semnalul respectiv procesului specificat.
3.2 Functia raise()
#include <signal.h>
int raise(int sig);
Aceasta functie este folosita pentru a trimite un semnal catre procesul
curent. Executia ei este similara cu executia urmatorului apel:
kill(getpid(), sig)
Functia returneaza 0 in caz de succes, si o valoare diferita de zero in
caz de eroare.
3.3 Functia abort()
#include <stdlib.h>
void abort(void);
Aceasta functie are ca efect trimiterea catre procesul curent a unui semnal
SIGABRT, care are ca efect terminarea anormala a procesului, mai putin
daca semnalul este tratat de o rutina care nu se termina. Daca executia lui
abort() are ca efect terminarea
procesului, toate fisierele deschise in interiorul acestuia vor fi inchise.
Este important de notat ca daca semnalul SIGABRT este ignorat sau
blocat, executia acestei functii nu va tine cont de acest lucru si procesul
va fi terminat in mod anormal.
3.4 Functia alarm()
#include <unistd.h>
unsigned int alarm(int seconds);
Executia acestei functii are ca rezultat trimiterea, dupa scurgerea unui
numar de secunde dat de seconds, a unui semnal de tipul SIGALRM
catre procesul curent. Daca o alarma a fost deja programata, ea este anulata
in momentul executiei ultimului apel, iar daca valoarea lui seconds
este zero, nu va fi programata o alarma noua.
In urma executiei, se returneaza numarul de secunde ramase din alarma
precedenta, sau 0 daca nu era programata nici o alarma.
4. Exemplu
Acest exemplu este preluat din cartea UNIX. Aplicatii (Autor:
Dan Cosma), aparuta la Editura de Vest in anul 2001.
Programul evidentiaza modul de utilizare a functiei
signal() si comunicarea intre procese folosind semnale.
Programul este format din doua procese, parinte si fiu. Parintele
(procesul a) numara continuu incepand de la zero, pana in momentul
in care este intrerupt de catre utilizator. Intreruperea se face generand
catre proces semnalul SIGINT, in mod explicit (folosind comanda
kill) sau implicit (apasand Ctrl-C in consola in care programul
ruleaza lansat in foreground). Pentru vizualizarea optima a rezultatelor,
la fiecare pas procesul apeleaza functia usleep()
generand astfel o intarziere de cate 1000 microsecunde.
De fiecare data cand procesul a ajunge cu numertoarea la un numar
divizibil cu 10, el trimite un semnal SIGUSR1 catre procesul b
(fiul). Acesta trateaza semnalul SIGUSR1 afisand pe ecran un mesaj.
Daca parintele a fost intrerupt (de la tastatura, prin Ctrl-C, de exemplu),
il informeaza pe fiu de aparitia acestui eveniment trimitandu-i semnalul
SIGUSR2. Fiul primeste aceasta notificare si se termina, afisand un
mesaj. Parintele ii preia in acest moment starea, dupa care se incheie si el.
Este necesar sa facem cateva observatii importante asupra implementarii de
mai jos. In primul rand, programul defineste cate o functie de tratare pentru
fiecare eveniment relevant:
-
Procesul a asociaza semnalului SIGINT functia
process_a_ends( ). Conform codului acestei functii, in momentul primirii
semnalului SIGINT, procesul va trimite un semnal SIGUSR2 catre
procesul b, dupa care apeleaza functia wait()
, asteptand terminarea acestuia.
-
Procesul b defineste doua functii de tratare: pentru SIGUSR1
(functia process_b_writes( )) si pentru SIGUSR2 (functia
process_b_ends( )). Comportamentul acestor functii este banal,
reducandu-se la afisarea unui mesaj si, in al doilea caz, la terminarea
procesului prin exit().
In al doilea rand, se observa ca procesul b ignora semnalul
SIGINT. Motivul pentru care s-a decis acest lucru este pentru a-l face
imun la actiunea implicita pe care acest semnal ar putea sa o aiba asupra
procesului. Un efect nedorit ar fi cazul in care utilizatorul apasa Ctrl-C
in consola din care a lansat programul, generand astfel SIGINT. Daca
semnalul nu ar fi ignorat, procesul b s-ar termina inainte de a avea
ocazia sa primeasca SIGUSR2 de la parinte, deci terminarea lui nu ar
respecta scenariul pe care si l-a propus sa-l implementeze aceasta aplicaite
(efectul vizibil ar fi ca mesajul "Process b ends" nu ar mai aparea pe ecran).
In continuare, trebuie remarcat faptul ca, la inceputul programului principal,
chiar inainte de crearea procesului fiu, sunt ignorate semnalele SIGUSR1
si SIGUSR2. Aceasta secventa de cod poate parea putin importanta, dar
necesitatea ei este justificata de efectele mai greu de prevazut pe care le
are executia paralela a proceselor. Pentru a inelege explicatia, trebuie
facuta mai intai precizarea ca, in Linux, dupa apelul funciei
fork(), procesul fiu este inirializat cu
o copie a asocierilor semnal - functie de tratare ale praintelui. Daca
semnalele SIGUSR1 si SIGUSR2 nu ar fi ignorate inca inainte de
crearea procesului fiu, s-ar putea intampla urmatorul fenomen: procesele
a si b sunt create intr-un timp foarte scurt, iar procesul
a ajunge sa intre in executie inainte de b. El numara rapid si
ajunge la un multiplu de 10 inainte ca procesul b sa isi armeze
functiile de tratare pentru semnale. Procesul a trimite catre b
semnalul SIGUSR1, dar cum acesta nu a declarat inca nici o functie
pentru tratarea acestui semnal, sistemul de operare va proceda prin aplicarea
comportamentului implicit al semnalului, adica terminarea procesului b.
Se observa, deci, ca procesul b se incheie in mod eronat, inainte de a
isi indeplini scopul pentru care a fost proiectat. Daca, insa, semnalul
SIGUSR1 este deja ignorat, in cel mai rau caz procesul b pierde
cateva notificari de la a, lucru care, avand in vedere executia in
paralel a proceselor, poate fi acceptat in scenariul nostru.
O ultima observatie este ca rezultatul functiei fork()
este atribuit in program unei variabile globale child_pid
pentru ca aceasta valoare sa poata fi accesibila ambelor functii de
tratare a semnalelor din procesul a. (Procesul a fiind parinte,
valoarea acestei variabile va fi chiar identificatorul de proces al lui b
).
/* (C) 2001 Dan Cosma */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
pid_t child_pid = 0;
int n = 0;
void process_a_ends(int sig)
{
int status;
if (kill(child_pid, SIGUSR2) < 0)
{
printf("Error sending SIGUSR2 to child\n");
exit(2);
}
/* waiting for the child to end */
wait(&status);
printf("Child ended with code %d\n", WEXITSTATUS(status));
printf("Process a ends.\n");
exit(0);
}
void process_a()
{
int i;
if (signal(SIGINT, process_a_ends) == SIG_ERR)
{
printf("Error setting handler for SIGTERM\n");
exit(1);
}
for (i = 0;;i++)
{
usleep(1000);
if (i%10 == 0)
if (kill(child_pid, SIGUSR1) < 0)
{
printf("Error sending SIGUSR1 to child\n");
exit(2);
}
}
}
void process_b_writes(int sig)
{
printf("Process b received SIGUSR1: %d\n", ++n);
}
void process_b_ends(int sig)
{
printf("Process b ends.\n");
exit(0);
}
void process_b()
{
/* Ignoring SIGINT. Process b will end only when receives SIGUSR2 */
if (signal(SIGINT, SIG_IGN) == SIG_ERR)
{
printf("Error ignoring SIGINT in process b\n");
exit(3);
}
/* Setting the signal handlers */
if (signal(SIGUSR1, process_b_writes) == SIG_ERR)
{
printf("Error setting handler for SIGUSR1\n");
exit(4);
}
if (signal(SIGUSR2, process_b_ends) == SIG_ERR)
{
printf("Error setting handler for SIGUSR2\n");
exit(5);
}
/* Infinite loop;
process b only responds to signals */
while(1)
;
}
int main()
{
int status;
/* First, ignore the user signals,
to prevent interrupting the child
process before setting the
appropriate handlers */
signal(SIGUSR1, SIG_IGN);
signal(SIGUSR2, SIG_IGN);
/* Creating the child process.
A global variable
is used to store the child process ID
in order to be able to use it from the
signal handlers */
if ((child_pid = fork()) < 0)
{
printf("Error creating child process\n");
exit(1);
}
if (child_pid == 0) /* the child process */
{
process_b();
exit(0);
}
else /* the parent process */
{
process_a();
}
/* this is still the parent code */
return 0;
}
Scrieti un program UNIX in C care realizeaza urmatoarele:
- Sistemul este format din doua procese: parinte si fiu
- Fiul ignora semnalul SIG_TERM
- Parintele instaleaza o alarma care il va intrerupe dupa 7 secunde; intre
timp, va afisa pe ecran, in mod continuu, caracterul 'A'. La primirea
semnalului de alarma va anunta fiul prin semnalul SIGUSR1, dar va continua sa
afiseze (la infinit) caracterul 'A'
- Fiul afiseaza continuu caracterul 'B', pana in momentul in care primeste
semnalul SIGUSR1, moment in care afiseaza un mesaj si se termina
- Cand fiul se termina, parintele preia starea lui si se termina si el
Vor fi folosite functii de tratare a semnalelor (una pentru generarea SIGUSR1
in parinte si doua pentru terminarea proceselor parinte si fiu) si functiile
alarm(), kill(). Indicatie: in UNIX, cand un proces fiu se termina, parintele
primeste automat semnalul SIGCHLD.
Nota: Pentru captarea semnalelor se va folosi
sigaction(). Daca se doreste, se poate realiza in plus
si o varianta cu signal().
Autor: Stejarel Veres
|
|