Signali UNIX


Koncept signalov UNIX

Signali so v bistvu programske prekinitve in omogočajo obravnavo asinhronih dogodkov. V prvih verzijah UNIX so bili dokaj nezanesljivi in so se včasih zgubljali".

UNIX verzije 7 ima 15, BSD-UNIX pa 31 (oštevilčenih) signalov. Zaradi jasnosti je vsakemu signalu prirejeno simbolično ime, ki začenja s SIG (na primer SIGKILL, SIGQUIT,..). Ta imena so definirana v datoteki <signal.h>. Posebno poznan je signal številka 9, ki mu pravimo SIGKILL. Z njim lahko ukinemo "zablodeli" proces, če le poznamo številko tega procesa. Omenimo še, da sta dva signala (SIGUSR1 in SIGUSR2) direktno namenjena uporabniku in nimata kakšnega "sistemskega" pomena.

Signale generirajo dogodki na terminalu (na primer pritisk na tipko DELETE generira signal SIGINT), lahko jih povzroče aparaturne ali programske izjeme (na primer delitev z 0). Lahko jih tudi sprožimo iz komandne vrstice ali pa programsko z nekaterimi funkcijami (na primer kill( ) ).

Nekaj primerov tvorbe signala s komandne vrstice:

kill -9 125 # ukinjamo proces 125 
kill -KILL 112 # ukinjamo proces 112, ukaz je ekvivalenten prejsnjemu 
kill -USR2 116 # procesu 116 posiljamo signal SIGUSR2
Jedru lahko damo dispozicijo, kako naj odgovori na signal. Možni so 3 tipi reakcij:
 
Ignoriranje signala. To lahko velja za vse signale razen za SIGKILL in SIGSTOP. Ni pa priporočljivo ignoriranje signalov, ki jih generira aparaturna oprema (delitev z 0, nelegalna inštrukcija).
Signal ujamemo (catch). Jedru moramo povedati, katero funkcijo naj pokliče.
Izvede naj se privzeta (default) akcija. Večinoma pomeni to ukinitev danega procesa. Včasih povzroči to tudi izpis pomnilnika (core dump).

Funkcija signal( ) Je v bistvu vmesnik med mehanizmom signalov in našim programom. Njeno (precej splošno) definicijo zasledimo tudi v ANSI C. Na sistemih UNIX so lahko njeni ekvivalenti različni. Funkcija signal ( ) ima dva argumenta.. Prvi (signo) je številka signala. Drugi argument je kazalec na funkcijo, ki ji pravimo rokovalnik signalov (signal handler). To funkcijo sami napišemo.

Spodnji primer kaže definicijo rokovalnika signalov userFunc( ), ki se bo sprožil ob sprejemu (ujetju) enega od obeh "uporabniških signalov". Navezavo rokovalnika na oba signala smo vzpostavili s funkcijo signal( ) v funkciji main( ).
 
/* Program sigZgledA : primer definicije rokovalnika signalov */ 
/* in vzpostavitve zveze z njim s funkcijo signal */
#include <signal.h>
static void userFunc (int signo) { 
    /* isti rokovalnik za oba signala: argument je stevilka ujetega signala */ 
    if(signo == SIGUSR1) { 
        printf("Prejel sem signal SIGUSR1\n"); 
        signal(SIGUSR1,userFunc); 
    } 
    else if (signo == SIGUSR2) { 
        printf("Prejel sem signal SIGUSR2\n"); 
        signal(SIGUSR2,userFunc); 
    } 
}
int main(void) {         
    if (signal(SIGUSR1, userFunc)  == SIG_ERR) printf("Ne morem ujeti signala SIGUSR1\n");         
    if (signal(SIGUSR2, userFunc) == SIG_ERR) printf("Ne morem ujeti signala SIGUSR2\n");              for        (; ;) pause( ); /* smo blokirani v zanki */  
}

Tak program poženimo "v ozadju" in mu posredujmo signale z ukazom kill. Pogovor z računalnikom bi imel naslednjo obliko (pri tem podajamo na desni strani komentar posameznih vrstic):

$sigZledA  &                              # Prozili smo program sigZgledA v "ozadju"
[ 156]                                          # Racunalnik javi PID prozenega procesa
$kill -USR1 156                           # Procesu s PID= 156 posljemo signal USR1
Prejel sem signal SIGUSR1           # Odgovor procesa 156, ki ostaja v ozadju ...

V nekaterih verzijah UNIX klic "našega" rokovalnika žal preklopi dispozicijo za dani signal na privzeto (default) alternativo. To pa pomeni, da bi naslednji signal naą proces verjetno ukinil. Problem rešimo tako, da v rokovalniku s ponovno uporabo funkcije signal( ) dispozicijo spet vzpostavimo tako, kot želimo.

Poudarimo še nekaj pojmov v zvezi s signali: Signal je generiran (oziroma poslan procesu), ko nastopi utrezen dogodek. Signal je sprejet (delivered), ko se sproži primerna akcija. Vmes signal visi (pending). Proces lahko blokira posredovanje signala (in ta ostane viseč). Pri nekaterih sistemih (POSIX.1) lahko taki procesi čakajo v vrsti.


Nezanesljivi signali

Problem, ki lahko nastopi v našem prejšnjem primeru, je v časovnem zamiku med sprejetjem signala in ponovno vzpostavitvijo rokovalnika. Ponovni nastop signala v tem času lahko povzroči ukinitev našega procesa. Te nerodnosti morda sploh ne zasledimo dokler se nam slučajno to ne zgodi.

Včasih si zaželimo, da bi sistemu rekli: "V danem hipu nas signali ne zanimajo (in naj nas morda ne motijo), vendar si zapomni, da so nastopili". To lahko naredimo z uvedbo primerne zastavice, ki jo sprejem signala nastavi, proces bo pa na to reagiral malo kasneje. To ponazoruje naslednji primer:
 
#include <signal.h>
int flag=0; int sigNum;
static void userFunc( ) { 
    signal(sigNum,userFunc); /* ponovna vzpostavitev */ 
    flag = 1; /* zastavico bo testiral glavni program */ 
}
main( ) {         
     sigNum        = SIGINT;  
     signal(sigNum,        userFunc);  
     for  (;;) {  
           flag = 0; /* brisanje        zastavice */  
            printf (" cakam na signal \n");  
            while (flag == 0) pause( ); /* cakamo na signal */  
            printf("Prejel signal\n");  
             
}

Tudi v tem primeru nastopa problem "časovnega okna"in s tem morebitne izgube signala med testiranjem zastavice in "spanjem" procesa. Proces je morda že testiral zastavico in ugotovil njeno vrednost 0. Če je nato prišlo do tvorbe signala pred vstopom v funkcijo pause() (tako nam lahko zagode razvrščanje procesov), bo proces zaspal za vedno, signal pa bo obvisel.

Problem prekinjenih sistemskih klicev.

Ta problem zasledimo predvsem pri "počasnih" sistemskih klicih. Imejmo proces, ki je trenutno blokiran zaradi sistemskega klica. Signal mora tak klic prekiniti (ker morda želimo prekiniti "obešen" program. To pa pomeni, da bi v našem programu morali po vsakem sistemskem klicu testirati vzrok povratka iz sistemskega klica (spremenljivko errno). Program postane zaradi dodatnih testov nepregleden. Pri sistemu 4.2BSD so v ta namen uvedli mehanizem avtomatskega restarta prekinjenih sistemskih klicev. UNIX sistem V tega mehanizma ne podpira.


"Reentrant" funkcije:

Zaradi asinhronosti dogodkov lahko pride do tega, da funkcija za rokovanje s signalom uporablja iste rutine kot prekinjani proces. Te rutine oziroma funkcije morajo zato biti "reentrant" oziroma v rokovalniku ne smemo uporabljati funkcij, ki uporabljajo statične spremenljivke, ki kličejo malloc( ) ipd. Upoštevati moramo tudi, da imamo za vsak proces le eno spremenljivko errno. Skrb rokovalne funkcije je zato, da errno shrani in nato restavrira. Pregled funkcij s signali

Funkcija kill( ) pošilja signal procesu ali skupini procesov. Podobna je funkcija raise( ), ki omogoča procesu, da si pošlje signal sam sebi.

int kill(pid_t pid, int signo);
int raise(int signo);
V prvem primeru odloča argument pid, kateri procesi dobe signal. Signal lahko pošljemo določenemu procesu (pid>0) ali skupini procesov (pid=0), ki pripadajo isti skupini kot proces, ki signal tvori. Upoštevati pa moramo, da PID pri sistemu UNIX čez nekaj časa reciklirajo.

Funkcija alarm( ) nastavi timer, ki po izteku generira signal SIGALARM. Njena splošna oblika je naslednja:

unsigned int alarm(insigned int seconds);
Posredovanje signala SIGALARM bo (malo) po izteku časa, saj moramo upoštevati še delovanje razvrščevalnika. (Torej nikakor ne v realnem času).

Funkcija pause( ) suspendira proces, dokler ne pride do ulovljenja primernega signala.

S kombinacijo alarm(0) in pause( ) lahko realiziramo ekvivalent klica sleep. Pri tem lahko pride do več problemov: Kaj, če je pred prvim alarmom bil klican še kakąen? Kaj če je sistem zelo zaseden in pride do prevelikega zamika med klicem alarm( ) in klicem pause( ) (med obema velja tekmovanje (race condition)).


Množice signalov (signal sets)

Sistem UNIX omogoča, da nastavljamo maske, s katerimi določamo, kateri signali lahko vplivajo na naą proces, katere lahko blokiramo (in tako ignoriramo). S temi maskami lahko tudi ugotavljamo, kateri signali zaradi nas visijo. Praviloma je taka maska dolga eno besedo, kar zadošča za pomnenje statusa 15 oziroma 31 signalov. Funkcije, s katerimi operiramo nad "množico" signalov, so naslednje:

sigemptyset( )        Brisanje maske v celoti
sigfillset( )             Setiranje maske v celoti
sigaddset( )            Setiranje posameznih bitov v maski
sigdelset( )             Brisanje posameznih bitov v maski
sigprocmask( )       Blokiranje, deblokiranje signalov, nastavljanje maske
sigpending( )          Ugotavljanje, kateri signali visijo
sigaction( )             Preverjanje in spreminjanje akcij, ki so prirejene danemu signalu

V nadaljevanju vidimo še primer uporabe teh funkcij:
 
#include <signal.h>
static void sigQuit(int signo) { 
    printf("Ujet signal SIGQUIT\n"); 
    if (signal(SIGQUIT,SIG_DFL)==SIG_ERR) printf("Ne morem resetirati SIGQUIT\n"); 
    return; 
}
int main(void) {         
   sigset_t        newmask, oldmask, pendmask; /* deklaracija treh mask */         
   if (signal(SIGQUIT,sigQuit)==SIG_ERR) printf("Napaka:Ne morem ujeti signala        SIGQUIT \n");  
   sigemptyset(&newmask);        /* resetiranje maske v celoti */  
   sigaddset(        &newmask, SIGQUIT); /* nastavimo bit, ustrezen signalu SIGQUIT*/         
    if(sigprocmask(SIG_BLOCK,        &newmask, &oldmask)<0)  
          printf("Napaka: SIG_BLOCK \n"); sleep(5); /* signal SIGQUIT bo obvisel,        ce pride v casu teh 5 sekund */  
    if (sigpending(&pendmask)        <0) printf(" Napaka: sigpending \n");  
   if (sigismember(        &pendmask, SIGQUIT))  
           printf("Visi signal SIGQUIT \n"); /* setirajmo signalno masko za deblokiranje        SIGQUIT */  
   if        (sigprocmask(SIG_SETMASK,        &oldmask, NULL) <0) printf(" Napaka: SIG_SETMASK\n");         
   printf("SIGQUIT        deblokiran \n");  
   sleep(5);        /* SIGQUIT bo koncal z datoteko core */  
   exit(0);         
}

LINUX: signali
Primer s signali1Primer s signali 2