Διακοπές και Σήματα <signal.h>


  Σε αυτή την ενότητα θα δούμε την επικοινωνία ελέγχου μεταξύ διεργασιών. Όταν μια διεργασία τερματίζεται ανώμαλα, συνήθως προσπαθεί να στείλει κάποιο σήμα που να δείχνει τι συνέβη. Το UNIX/Linux και τα προγράμματα C μπορούν να παγιδεύσουν αυτά τα σήματα. Επίσης τα σήματα μπορούν να χρησιμοποιηθούν και για προγραμματισμένη από το χρήστη ανταλλαγή πληροφοριών ελέγχου μεταξύ διεργασιών.

Τα σήματα (signals) είναι διακοπές λογισμικού (software interrupts), σε αντίθεση με τις τυπικές διακοπές υλικού, που παράγονται όταν συμβεί κάποιο γεγονός. Τα σήματα μπορεί να είνα σύγχρονα, δηλαδή να προκαλούν συγχρονισμό διεργασιών, όπως τα SIGFPE και SIGSEGV, αλλά συνήθως είναι ασύγχρονα, δηλαδή οι διεργασίες που τα παραλαμβάνουν τα χειρίζονται στο δικό τους χρόνο. Τα σήματα έχουν διάφορα επίπεδα προτεραιότητας. Για παράδειγμα, σήματα που παράγονται όταν το σύστημα διαγνώσει κάποιο σφάλμα (bus error, memory fault, illegal operation, ..) ή όταν ο χρήστης ζητήσει αναστολή ή τερματισμό διαργασίας (ctrl-c ή kill) θεωρύνται επείγοντα και η παραλαμβάνουσα διεργασία πρέπει τα εξυπηρετήσει άμεσα. Το σύστημα ορίζει τον αριθμό, τον τύπο και τη προτεραιότητα των σημάτων. Ορισμένα σήματα με σχετικά χαμηλό επίπεδο προτεραιότητας μπορούν να αποκλειστούν, να αγνοηθούν ή να καθυστερήσει η εξυπηρέτησή τους, με ευθύνη του προγραμματιστή (maskable -nonmaskable signals/interrupts).  Τα σήματα που αποκλείονται ορίζονται σε μιά 'μάσκα' που τη διαχειριζόμαστε με ειδικές συναρτήσεις. Σε περίπτωση που η διεργασία παραλαβής δεν προβλέπει κάποιο χειρισμό ενός σήματος, το πιθανότερο είναι οτι η παραλαβή του σήματος καταλήγει στο τερματισμό της διεργασίας. Γενιά το σύστημα έχει μια προεπιλεγμένη ενέργεια που ακολουθεί τη παραλαβή κάθε σήματος:

Τα σήματα χωρίζονται σε πέντε κατηγορίες:

Οι ορισμοί των συνηθισμένων σημάτων υπάρχουν στη βιβλιοθήκη signal.h.

Μερικά χρήσιμα:

SIGHUP  1 /* hangup */ SIGINT  2 /* interrupt */
SIGQUIT 3 /* quit */ SIGILL  4 /* illegal instruction */
SIGABRT 6 /* used by abort */ SIGKILL 9 /* hard kill */
SIGALRM 14 /* alarm clock */  
SIGCONT 19 /* continue a stopped process */  
SIGCHLD 20 /* to parent on child stop or exit */  

Τα σήματα μπορούν να λάβουν αριθμούς από 0 έως 31.

Αποστολή Σημάτων -- kill(), raise()

Δύο είναι οι πιο γνωστές συναρτήσεις αποστολής σημάτων:

int kill(int pid, int sig) 

Κλήση συστήματος που στέλνει το σήμα sig στη διεργασία pid. Αν pid  > 0 το σήμα αποστέλεται στην αντίστοιχη διεργασία. Αν το pid = 0 το σήμα αποστέλεται σε όλες τις διεργασίες, εκτός από αυτές του συστήματος.

Η συνάρτηση kill() επιστρέφει 0 σε επιτυχή εκτέλεση, -1 σε περίπτωση σφάλματος και θέτει το errno ανάλογα. 

Ως γνωστό υπάρχει και οι εντολές kill, killall του φλοιού UNIX/Linux που έχουν αντίστοιχη  λειτουργία. Δείτε τις σχετικές σελίδες εγχειριδίου.

int raise(int sig) 

Στέλνει το σήμα sig στην εκτελούμενη διεργασία. Η συνάρτηση raise() χρησιμοποιεί τη kill() ως εξής:

kill(getpid(), sig);

Σημείωση: δεδομένου οτι η αποστολή σημάτων ουσιαστικά τερματίζει διεργασίες είναι λογικό το σύστημα να παρέχει προστασία στις εκτελούμενες διεργασίες αλλιώς ο καθένας θα μπορούσε να τις τερματίσει. 

Βασικός κανόνας: η αποστολή και παραλαβή σημάτων επιτρέπεται μόνο μεταξύ διεργασιών που έχουν τον ίδιο ιδιοκτήτη (χρήστη). Εξαιρούνται τα σήματα που αποστέλει ο διαχειριστής (root).

Το σήμα SIGKILL δεν μπορεί να αγνοηθεί ούτε να παγιδευτεί και οδηγεί πάντα στο τερματισμό της διεργασίας παραλαβής.

Παράδειγμα 'αυτοκτονίας': η κλήση kill(getpid(),SIGINT); στέλενει σήμα τερματισμού στη καλούσα διεργασία, δηλαδή στον ευατ'ο της. Αυτό αντιστοιχεί στην κλήση της exit().  Το ctrl-c στη γραμμή εντολών ουσιαστικά στέλνει SIGINT στη διεργασία (εντολή ή εφαρμογή) που εκτελείται.

Επίσης υπάρχει και η συνάρτηση

unsigned int alarm(unsigned int seconds) 

που αποστέλει το σήμα χρονοδιακοπής (αφύπνισης) SIGALRM στη διεργασία που την καλεί, μετά από seconds δευτερόλεπτα. Περίπου αντίστοιχα λειτουργεί στην γραμμή εντολών η εντολή UNIX/Linux sleep.

Χειρισμός Σημάτων -- signal()

Ένα πρόγραμμα εφαρμογής μπορεί να ορίσει μια συνάρτηση που ονομάζεται χειριστής σήματος, η οποία ενεργοποιείται μόλις παραληφθεί το αντίστοιχο σήμα. Λέμε οτι ο χειριστής σήματος παγιδεύει ή συλλαμβάνει το σήμα. Έτσι μια διεργασία έχει τις παρακάτω τρείς επιλογές σχετικά με ένα σήμα:

Συνήθως οι χειριστές σήματος εκτελούνται χωρίς αλλαγή του χώρου εργασίας της διεργασίας. Έτσι, μετά τον τερματισμό του χειριστή διακοπής η ροή εκτέλεσης επιστρέφει στο σημείο που διεκόπη από το σήμα. Αυτό μπορεί να αλλάξει ανάλογα με το σήμα και τη διεργασία, ανάλογα με την δουλειά που κάνει ο χειριστής σήματος. Όμως οι αλλαγές στο χώρο εργασίας είναι εθύνη του προγραμματιστή.

Η παραλαβή σήματος είναι απλή:

int (*signal(int sig, void (*func)()))() 

η συνάρτηση signal()θα καλέσει μια από τις συναρτήσεις func() με βάση τη τιμή του σήματις sig. Αν η κλήση της συνάρτηση signal() είναι επιτυχής τότε επιστρέφει δείκτη σε συνάρτηση func(). Αν δεν είναι επιτυχής τότε επιστρέφει -1 και ενημερώνει το errno.

Ο δείκτης σε συνάρτηση func() μπορεί να πάρει τρείς τιμές:

SIG_DFL
-- δείκτη σε προεπιλεγμένη συνάρτηση του συστήματος SID_DFL(), που τερματίζει τη διεργασία μόλις παραλάβει το σήμα sig.
SIG_IGN
-- δείκτη σε προεπιλεγμένη συνάρτηση του συστήματος SIG_IGN() που θα αγνοήσει το σήμα sig (ΕΚΤΟΣ από το SIGKILL).
Διεύθυνση συνάρτησης
-- δείκτη σε συνάρτηση που ορίζει ο χρήστης.

Οι δείκτες SIG_DFL and SIG_IGN ορίζονται στη πρότυπη βιβλιοθήκη signal.h.

Έτσι αν θέλουμε να αγνοήσουμε το ctrl-c από το πληκτρολόγιο, γράφουμε:

   signal(SIGINT, SIG_IGN);

Αν πάλι θέλουμε το SIGINT να προκαλεί τερματισμό, γράφουμε:

   signal(SIGINT, SIG_DFL);

Το πρόγραμμα που ακολουθεί παγιδεύει το ctrl-c και δεν τερματίζεται. Υπάρχει μια συνάρτηση sigproc() η οποία χειρλιζεται το σήμα ctrl-c όταν αυτό παραληφθεί. Επίσης υπάρχει μια συνάρτηση quitproc() που τερματίζει το πρόγραμμα ανα συλλάβει το σήμα SIGQUIT :


#include <stdio.h>
 
void sigproc(void);
 
void quitproc(void);
 
main()
{ signal(SIGINT, sigproc);
signal(SIGQUIT, quitproc);
printf("ctrl-c disabled use ctrl-\\to quit\n");
for(;;); /* infinite loop */}
 
void sigproc()
{ signal(SIGINT, sigproc);
/* NOTE some versions of UNIX will reset signal to default
after each call. So for portability reset signal each time */
 
printf("you have pressed ctrl-c \n");
}
 
void quitproc()
{ printf("ctrl-\\ pressed to quit\n|);
exit(0); /* normal exit status */
}

Εδώ βλέπετε ένα πααδειγμα συνδυασμού χειριστή σήματος με χρονοδιακόπτη. Το σώμα του βρόχου εκτελείται μέχρι να ληφθεί σήμα SIGALRM

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

/* This flag controls termination of the main loop. */
int keep_going = 1;

/* The signal handler just clears the flag and re-enables itself. */
void catch_alarm (int sig)
{
keep_going = 0;
signal (sig, catch_alarm);
}

void do_stuff (void)
{
puts ("Doing stuff while waiting for alarm....");
}

int main (void)
{
/* Establish a handler for SIGALRM signals. */
signal (SIGALRM, catch_alarm);

/* Set an alarm to go off in a little while. */
alarm (2);

/* Check the flag once in a while to see when to quit. */
while (keep_going)
do_stuff ();

return EXIT_SUCCESS;
}

Παραδείγματα

Ακολουθούν δύο προγράμματα επικοινωνίας γονικής και θυγατρικής διεργασίας, με χρήση των συναρτήσεων kill() και signal().

Στο πρώτο πρόγραμμα η συνάρτηση fork() δημιουργεί μια θυγατρική διεργασία από τη γονική. Ο έλεγχος του pid καθορίζει αν πρόκειται για το παιδί (pid  ==  0) ή το γονέα (pid != 0, το pid του παιδιού).

Ο γονέας στέλνει μηνύματα στο παιδί με χρήση του pid του και της διεργασίας kill(). Το παιδί λαμβάνει αυτά τα σήματα με τη συνάρτηση signal() και τα χειρίζεται με τις κατάλληλες συναρτήσεις.

/* sig_talk.c --- Example of how 2 processes can talk */
/* to each other using kill() and signal() */
/* We will fork() 2 process and let the parent send a few */
/* signals to it`s child */

/* cc sig_talk.c -o sig_talk */

#include <stdio.h>
#include <signal.h>

void sighup(); /* routines child will call upon sigtrap */
void sigint();
void sigquit();

main()
{ int pid;

/* get child process */

if ((pid = fork()) < 0) { /* unsuccesful fork */
perror("fork");
exit(1);
}

if (pid == 0)
{ /* child */
signal(SIGHUP,sighup); /* set function calls */
signal(SIGINT,sigint);
signal(SIGQUIT, sigquit);
for(;;); /* loop for ever */
}
else /* parent */
{ /* pid hold id of child */
printf("\nPARENT: sending SIGHUP\n\n");
kill(pid,SIGHUP);
sleep(3); /* pause for 3 secs */
printf("\nPARENT: sending SIGINT\n\n");
kill(pid,SIGINT);
sleep(3); /* pause for 3 secs */
printf("\nPARENT: sending SIGQUIT\n\n");
kill(pid,SIGQUIT);
sleep(3);
}
}

void sighup()

{ signal(SIGHUP,sighup); /* reset signal */
printf("CHILD: I have received a SIGHUP\n");
}

void sigint()

{ signal(SIGINT,sigint); /* reset signal */
printf("CHILD: I have received a SIGINT\n");
}

void sigquit()

{ printf("My DADDY has Killed me!!!\n");
exit(0);
}

Στο δεύτερο παράδειγμα, η γονική διερασία πάλι δημιουργεί με fork() μια θυγατρική διεργασία και μετά την περιμένει να τελειώσει την αρχικοποίησή της. Η θυγατρική διεργασία ειδοποιεί τη γονική όταν είναι έτοιμη στέλνοντας ένα σήμα SIGINT με τη συνάρτηση kill().

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>


/* When a SIGINT signal arrives, set this variable. */
int usr_interrupt = 0;

void synch_signal (int sig)
{
  usr_interrupt = 1;
}

/* The child process executes this function. */
void child_function (void)
{
  /* Perform initialization. */
  printf ("I'm here!!! My pid is %d.\n", (int) getpid ());

  /* Let parent know you're done. */
  kill (getppid (), SIGINT);

  /* Continue with execution. */
  printf ("Bye, now....\n");
  exit (0);
}

int main (void)
{
  pid_t child_id;

  /* Establish the signal handler. */
  signal (SIGINT, synch_signal);

  /* Create the child process. */
  child_id = fork();
  if (child_id == 0)
    child_function (); /* Does not return. */

  /* Busy wait for the child to send a signal. */
  while (!usr_interrupt)
    ;

  /* Now continue execution. */
  printf ("That's all, folks!\n");

  return 0;
}

Άλλες Συναρτήσεις

Υπάρχουν αρκετές άλλες συναρτήσεις που ορίζονται στη βιβλιοθήκη signal.h:

int sighold(int sig) -- προσθέτει το σήμα sig στη μάσκα των σημάτων που αναστέλλονται από την καλούσα διεργασία.

int sigrelse(int sig) -- αφαιρεί το σήμα sig από τη μάσκα των σημάτων που αναστέλλονται από την καλούσα διεργασία.

int sigignore(int sig) -- αναθέτει το χειρισμό του σήματος sig στο SIG_IGN

int sigpause(int sig) -- αφαιρεί το σήμα sig από τη μάσκα της καλούσας διεργασίας και αναστέλει τη καλούσα διεργασία μέχρι να ληφθεί σήμα.

Ασκήσεις

ΜΜοιραΜΜ
Dave Marshall
1/5/1999
μετάφραση και προσαρμογή στα Ελληνικά Κ.Γ. Μαργαρίτης
20/3/2008