Χειρισμός σημάτων - %SIG#

Η %SIG είναι το hash όπου η Perl εκθέτει τους χειριστές σημάτων. Η ρύθμιση $SIG{NAME} εγκαθιστά χειριστή για το σήμα NAME· η διαγραφή του επαναφέρει την προεπιλεγμένη ενέργεια. Δύο ψευδο-σήματα - __WARN__ και __DIE__ - δεν είναι καθόλου πραγματικά σήματα OS αλλά hooks του διερμηνέα στις warn και die.

Ο πίνακας σημάτων#

Τα πραγματικά σήματα OS προέρχονται από τις λίστες POSIX και signal.h. Τα κοινά - κλειδιά που θα χρησιμοποιήσετε πραγματικά:

Κλειδί

Σήμα

Προεπιλεγμένη ενέργεια

Πότε το χειρίζεστε

INT

SIGINT

τερματισμός

Ctrl-C: ομαλός τερματισμός

TERM

SIGTERM

τερματισμός

kill <pid>: ομαλός τερματισμός

HUP

SIGHUP

τερματισμός

αποσύνδεση τερματικού· οι δαίμονες επαναφορτώνουν ρύθμιση

QUIT

SIGQUIT

τερματισμός + core

Ctrl-: όπως INT αλλά με core dump

PIPE

SIGPIPE

τερματισμός

εγγραφή σε κλειστό σωλήνα· συνήθως αγνοείται

CHLD

SIGCHLD

αγνόηση

πέθανε μια διεργασία-παιδί (waitpid εδώ)

USR1

SIGUSR1

τερματισμός

γεγονός ορισμένο από εφαρμογή

USR2

SIGUSR2

τερματισμός

γεγονός ορισμένο από εφαρμογή

ALRM

SIGALRM

τερματισμός

ο χρονομετρητής alarm() έληξε

WINCH

SIGWINCH

αγνόηση

αλλαγή μεγέθους παραθύρου τερματικού

IO

SIGIO

τερματισμός

ασύγχρονο I/O έτοιμο

Η πλήρης λίστα - συμπεριλαμβανομένων σημάτων που δεν μπορείτε να πιάσετε (KILL, STOP) και σημάτων ειδικών πλατφόρμας - προέρχεται από το άρθρωμα POSIX, που εξάγει τις αριθμητικές σταθερές (SIGINT, SIGTERM, …) και τις μακροεντολές wait της οικογένειας WIFEXITED.

Εγκατάσταση χειριστή#

Γίνονται αποδεκτές τρεις μορφές τιμής χειριστή:

$SIG{INT}  = \&handler;          # code reference - preferred
$SIG{INT}  = sub { ... };         # anonymous sub  - also fine
$SIG{INT}  = 'IGNORE';            # ignore the signal
$SIG{INT}  = 'DEFAULT';           # restore default action
$SIG{INT}  = '';                  # same as DEFAULT
$SIG{INT}  = undef;               # same as DEFAULT

Μη χρησιμοποιείτε bareword ($SIG{INT} = 'handler') - αυτό αναφέρεται σε όνομα στο πακέτο main::, που η Perl αναζητά κάθε φορά που φτάνει το σήμα. Αν μετονομάσετε τη handler, η μορφή bareword σιωπηρά συνεχίζει να στοχεύει το παλιό όνομα.

Ο χειριστής λαμβάνει ένα όρισμα - το όνομα σήματος ως συμβολοσειρά:

sub handler {
    my ($signame) = @_;
    print "received SIG$signame\n";
}
$SIG{INT}  = \&handler;
$SIG{TERM} = \&handler;

Ασφαλή σήματα - ο κανόνας αναβαλλόμενης παράδοσης#

Από την Perl 5.8, η παράδοση σημάτων είναι αναβαλλόμενη. Όταν φτάσει ένα σήμα, το OS θέτει μια σημαία στον διερμηνέα· ο πραγματικός χειριστής Perl εκτελείται μόνο στο επόμενο ασφαλές σημείο - τυπικά μεταξύ εκτελέσεων εντολών. Αυτό σημαίνει:

  • Ένας χειριστής δεν μπορεί να διακόψει μια read, open, connect, ή οποιαδήποτε άλλη κλήση συστήματος στη μέση. Οι αργές κλήσεις συστήματος επιστρέφουν πρόωρα με EINTR στη $! και ο χειριστής εκτελείται μετά.

  • Ένας χειριστής δεν μπορεί να διακόψει μια αντιστοίχιση regex σε εξέλιξη. Ένα μακροχρόνιο regex σε κακόβουλη είσοδο δεν θα κοπεί σύντομα από SIGINT.

  • Ο χειριστής εκτελείται σε κανονικό περιβάλλον Perl. Μπορεί να καλέσει οποιονδήποτε κώδικα Perl, να δεσμεύσει μνήμη, να εκτοξεύσει εξαιρέσεις - κανένας κίνδυνος επανεισόδου χειριστών σημάτων C.

Αυτή είναι η σωστή ισορροπία για σχεδόν κάθε πρόγραμμα. Αν πραγματικά χρειάζεστε χειρισμό σημάτων μη ασφαλή προ-5.8 - χειριστές που εκτελούνται σύγχρονα στο περιβάλλον σήματος, με όλους τους κινδύνους επανεισόδου - θέστε τη μεταβλητή περιβάλλοντος PERL_SIGNALS=unsafe πριν ξεκινήσετε την Perl. Μην το κάνετε αυτό αβασάνιστα.

Η PetaPerl υλοποιεί χειρισμό σημάτων αναβαλλόμενης παράδοσης.

Το μοτίβο ομαλού τερματισμού#

Η τυπική μορφή: μια σημαία που τίθεται από τον χειριστή, ελέγχεται από τον κύριο βρόχο:

my $shutting_down = 0;
$SIG{INT}  = sub { $shutting_down = 1 };
$SIG{TERM} = sub { $shutting_down = 1 };

while (my $job = $queue->next) {
    last if $shutting_down;       # check at a safe point
    process($job);
}

if ($shutting_down) {
    log_and_close();
    exit 0;
}

Ο κύριος βρόχος ελέγχει τη σημαία σε σημείο όπου είναι ασφαλές να ξετυλιχθεί καθαρά. Ο χειριστής κάνει μόνο «θέτω τη σημαία» - χωρίς καταγραφή, χωρίς I/O - ελαχιστοποιώντας τον χρόνο εκτέλεσής του.

Για εφαρμογές που μπλοκάρουν σε μεγάλη κλήση συστήματος (accept, read, select), ο βρόχος πρέπει να δομηθεί ώστε να επαναλαμβάνει σε EINTR:

my $shutting_down = 0;
$SIG{INT}  = sub { $shutting_down = 1 };

while (!$shutting_down) {
    my $client = accept($srv, $sock);
    unless (defined $client) {
        next if $!{EINTR};        # signal woke us up - re-check flag
        die "accept: $!";
    }
    handle($client);
}

Η $!{EINTR} (δείτε μεταβλητές σφάλματος) σας λέει ότι η κλήση συστήματος εξήλθε επειδή έφτασε ένα σήμα. Η σημαία τέθηκε από τον χειριστή· ο κύριος βρόχος βλέπει την αφύπνιση, ξαναελέγχει τη $shutting_down, και εξέρχεται καθαρά.

Συγκομιδή διεργασιών-παιδιών - SIGCHLD#

Όταν μια διεργασία-παιδί εξέρχεται, ο γονέας λαμβάνει SIGCHLD. Ο χειριστής πρέπει να καλέσει τη waitpid για να συγκομίσει το zombie:

$SIG{CHLD} = sub {
    while ((my $kid = waitpid(-1, WNOHANG)) > 0) {
        my $status = $?;
        log_child_death($kid, $status);
    }
};

Ο βρόχος while με WNOHANG (από τη POSIX :sys_wait_h) χειρίζεται την περίπτωση όπου δύο παιδιά εξέρχονται κοντά μεταξύ τους - παραδίδεται μόνο ένα SIGCHLD (δεν μπαίνει σε ουρά), οπότε ο χειριστής πρέπει να εκκενώσει όλες τις εκκρεμείς εξόδους κάθε φορά που εκτελείται.

Η $SIG{CHLD} = 'IGNORE' είναι ειδική περίπτωση: δεν αγνοεί απλώς το σήμα, λέει στον πυρήνα να συγκομίζει αυτόματα τα παιδιά. Χρησιμοποιήστε το όταν το σενάριό σας κάνει fork παιδιά των οποίων η κατάσταση εξόδου δεν σας ενδιαφέρει.

SIGPIPE - η περίπτωση κλειστού σωλήνα#

Όταν κάνετε print σε σωλήνα του οποίου ο αναγνώστης έχει φύγει, ο πυρήνας στέλνει SIGPIPE. Η προεπιλεγμένη ενέργεια είναι τερματισμός της διεργασίας - κάτι που σπάνια θέλετε. Τα περισσότερα προγράμματα φίλτρου πρέπει να αγνοούν SIGPIPE και να ελέγχουν αντ” αυτού την τιμή επιστροφής print:

local $SIG{PIPE} = 'IGNORE';

print {$child_pipe} $data
    or die "write to pipe: $!";  # $! will be EPIPE if the reader closed

Με αγνοημένο τον χειριστή, η κλήση συστήματος επιστρέφει αποτυχία με EPIPE αντί να σκοτώσει το πρόγραμμα.

Χρονικά όρια μέσω SIGALRM#

Σε συνδυασμό με τη alarm και ένα μπλοκ eval, ο SIGALRM παρέχει φορητό χρονικό όριο για κάθε λειτουργία που μπλοκάρει:

sub with_timeout {
    my ($seconds, $code) = @_;
    my $result;
    eval {
        local $SIG{ALRM} = sub { die "TIMEOUT\n" };
        alarm $seconds;
        $result = $code->();
        alarm 0;                  # cancel the timer
        1;
    } or do {
        die $@ unless $@ eq "TIMEOUT\n";
        return undef;
    };
    return $result;
}

my $line = with_timeout(5, sub { <$slow_socket> });

Το μοτίβο:

  1. local $SIG{ALRM} - ο χειριστής έχει εμβέλεια μόνο για αυτή την κλήση.

  2. alarm $seconds - προγραμματισμός του σήματος.

  3. Εκτέλεση της εργασίας που μπλοκάρει.

  4. alarm 0 - ακύρωση του χρονομετρητή αν η εργασία πέτυχε.

  5. Μέσα στον χειριστή, die "TIMEOUT\n" - η τελική \n καταστέλλει το επίθεμα «at line N» και κάνει την τιμή σφάλματος εύκολη στον έλεγχο.

__WARN__ και __DIE__ - hooks διερμηνέα#

Αυτά τα δύο κλειδιά στη %SIG δεν είναι σήματα OS. Καλούνται όταν ο διερμηνέας πρόκειται να εκπέμψει μια προειδοποίηση (warn) ή να εκτοξεύσει μια μοιραία εξαίρεση (die).

$SIG{__WARN__} - υποκλοπή προειδοποιήσεων#

my @warnings;
{
    local $SIG{__WARN__} = sub {
        my ($msg) = @_;
        push @warnings, $msg;
    };
    do_thing_that_might_warn();
}
# Now @warnings holds every warning that fired inside the block,
# without anything having printed to STDERR.

Ένας χειριστής __WARN__ που δεν κάνει τίποτα αποσιωπά εντελώς την προειδοποίηση. Ένας χειριστής που καλεί die αναβαθμίζει τις προειδοποιήσεις σε εξαιρέσεις:

local $SIG{__WARN__} = sub { die $_[0] };
eval { something_warny() };       # warns become catchable

Η λεξιλογική pragma use warnings FATAL => 'all' είναι πιο στοχευμένος τρόπος να γίνει αυτό - κάνει τις προειδοποιήσεις μοιραίες μόνο στη λεξιλογική εμβέλεια όπου η pragma είναι ενεργή. Χρησιμοποιήστε τη αντί για __WARN__ για απαιτήσεις «αντιμετώπισε τις προειδοποιήσεις ως σφάλματα».

$SIG{__DIE__} - υποκλοπή die#

Ένας χειριστής __DIE__ εκτελείται όταν η die πρόκειται να εκτοξεύσει - τόσο για μη πιασμένες εξαιρέσεις όσο και μέσα σε eval. Μέσα σε eval, ο χειριστής εκτελείται πριν πιαστεί η εξαίρεση, κάτι που είναι η κλασική πηγή σύγχυσης: ένας καθολικός χειριστής __DIE__ που καταγράφει κάθε «θάνατο» θα ενεργοποιηθεί σε κάθε eval { ... } που καλεί die για κανονική ροή ελέγχου.

$SIG{__DIE__} = sub { print STDERR "dying: $_[0]" };
eval { die "expected error" };    # the handler still runs!

Αν θέλετε καθολική καταγραφή θανάτων, φιλτράρετε μέσω $^S:

$SIG{__DIE__} = sub {
    return if $^S;                # inside eval - let it be caught
    print STDERR "uncaught: $_[0]";
};

Ρεαλιστικά, αποφύγετε τη $SIG{__DIE__} σε νέο κώδικα. Η pragma use warnings και οι μεταβλητές σφάλματος καλύπτουν ό,τι χρειάζεστε χωρίς εκπλήξεις. Το σύγχρονο χαρακτηριστικό try/catch χειρίζεται εξαιρέσεις ρητά χωρίς τη δράση εξ αποστάσεως της $SIG{__DIE__}.

Εμφάνιση διαθέσιμων σημάτων#

use Config;
my @signals = split / /, $Config{sig_name};
print "available signals: @signals\n";
# HUP INT QUIT ILL TRAP ABRT BUS FPE KILL ...

Ή, για την αντιστοίχιση αριθμός→όνομα:

use Config;
my @names = split / /, $Config{sig_name};
my @nums  = split / /, $Config{sig_num};
my %sig_num  = map { $names[$_] => $nums[$_] }  0..$#names;
my %sig_name = map { $nums[$_]  => $names[$_] } 0..$#names;

Αυτό κάνουν εσωτερικά τα περισσότερα εργαλεία εμφάνισης σημάτων.

Δείτε επίσης#

  • POSIX - η κανονική πηγή αριθμητικών σταθερών SIG* και μακροεντολών WIFEXITED/WEXITSTATUS για αποκωδικοποίηση κατάστασης παιδιού σε χειριστή SIGCHLD.

  • kill - ο σύντροφος της %SIG: αποστολή σήματος σε διεργασία (τυπικά για δοκιμή του δικού σας χειριστή).

  • alarm - προγραμματισμός SIGALRM· η βάση μοτίβων χρονικού ορίου.

  • waitpid - συγκομιδή παιδιού μετά από SIGCHLD.

  • fork - παράγει τα παιδιά των οποίων τους θανάτους αναφέρει η SIGCHLD.

  • Μεταβλητές σφάλματος - η $! μετά από EINTR, η $? μετά από waitpid, η $@ μετά από die μέσα από χειριστή.

  • Ενδοσκόπηση διερμηνέα - η $^S για τον έλεγχο __DIE__ «βρισκόμαστε μέσα σε eval».

  • try/catch - η σύγχρονη εναλλακτική στον χειρισμό εξαιρέσεων βασισμένο σε $SIG{__DIE__}.