Τα σήματα (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.
Δύο είναι οι πιο γνωστές συναρτήσεις αποστολής σημάτων:
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.
Ένα πρόγραμμα εφαρμογής μπορεί να ορίσει μια συνάρτηση που ονομάζεται χειριστής σήματος, η οποία ενεργοποιείται μόλις παραληφθεί το αντίστοιχο σήμα. Λέμε οτι ο χειριστής σήματος παγιδεύει ή συλλαμβάνει το σήμα. Έτσι μια διεργασία έχει τις παρακάτω τρείς επιλογές σχετικά με ένα σήμα:
Η παραλαβή σήματος είναι απλή:
int (*signal(int sig, void (*func)()))()
η συνάρτηση signal()θα καλέσει μια από τις συναρτήσεις func() με βάση τη τιμή του σήματις sig. Αν η κλήση της συνάρτηση signal() είναι επιτυχής τότε επιστρέφει δείκτη σε συνάρτηση func(). Αν δεν είναι επιτυχής τότε επιστρέφει -1 και ενημερώνει το errno.
Ο δείκτης σε συνάρτηση func() μπορεί να πάρει τρείς τιμές:
Οι δείκτες 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 */Στο δεύτερο παράδειγμα, η γονική διερασία πάλι δημιουργεί με fork() μια θυγατρική διεργασία και μετά την περιμένει να τελειώσει την αρχικοποίησή της. Η θυγατρική διεργασία ειδοποιεί τη γονική όταν είναι έτοιμη στέλνοντας ένα σήμα
/* 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);
}
SIGINT
με τη συνάρτηση kill()
.
signal arrives, set this variable. */Υπάρχουν αρκετές άλλες συναρτήσεις που ορίζονται στη βιβλιοθήκη signal.h:
int sighold(int sig) -- προσθέτει το σήμα sig στη μάσκα των σημάτων που αναστέλλονται από την καλούσα διεργασία.
int sigrelse(int sig) -- αφαιρεί το σήμα sig από τη μάσκα των σημάτων που αναστέλλονται από την καλούσα διεργασία.
int sigignore(int sig) -- αναθέτει το χειρισμό του σήματος sig στο SIG_IGN
int sigpause(int sig) -- αφαιρεί το σήμα sig από τη μάσκα της καλούσας διεργασίας και αναστέλει τη καλούσα διεργασία μέχρι να ληφθεί σήμα.