טיפול באותות - %SIG#

%SIG הוא האש שבו Perl חושף מטפלי אותות. קביעת $SIG{NAME} מתקינה מטפל עבור האות NAME; מחיקתו משחזרת את פעולת ברירת המחדל. שני אותות־מדומים - __WARN__ ו־__DIE__ - אינם אותות מערכת הפעלה אמיתיים כלל אלא הוּוי מפרש אל warn ו־die.

טבלת האותות#

אותות מערכת הפעלה אמיתיים מגיעים מהרשימות POSIX ו־signal.h. הנפוצים - מפתחות שבאמת תשתמשו בהם:

מפתח

אות

פעולת ברירת מחדל

מתי מטפלים בו

INT

SIGINT

סיום

Ctrl-C: כיבוי מסודר

TERM

SIGTERM

סיום

kill <pid>: כיבוי מסודר

HUP

SIGHUP

סיום

ניתוק מסוף; דמונים טוענים מחדש תצורה

QUIT

SIGQUIT

סיום + core

Ctrl-: כמו INT אך עם core dump

PIPE

SIGPIPE

סיום

נכתב ל־pipe סגור; בדרך כלל מתעלמים

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, מסירת אותות דחויה. כאשר אות מגיע, מערכת ההפעלה קובעת דגל במפרש; המטפל ה־Perl בפועל רץ רק בנקודה הבטוחה הבאה - בדרך כלל בין ביצועי op-code. משמעות הדבר:

  • מטפל אינו יכול לקטוע read, open, connect, או כל syscall אחר באמצע הקריאה. syscalls איטיים חוזרים מוקדם עם 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 - ממזער את הזמן שהוא מבלה בריצה.

עבור יישומים החוסמים על syscall ארוך (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} (ראו משתני שגיאה) אומר לכם שה־syscall יצא משום שאות הגיע. הדגל נקבע על ידי המטפל; הלולאה הראשית רואה את ההתעוררות, בודקת מחדש את $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 - מקרה ה־pipe הסגור#

כשאתם מבצעים print ל־pipe שהקורא שלו נעלם, הקרנל שולח SIGPIPE. פעולת ברירת המחדל היא לסיים את התהליך - שזה רק לעיתים רחוקות מה שאתם רוצים. רוב תוכניות הסינון צריכות להתעלם מ־SIGPIPE ולבדוק במקום זאת את ערך ההחזרה של print:

local $SIG{PIPE} = 'IGNORE';

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

כשמתעלמים מהמטפל, ה־syscall מחזיר כישלון עם 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__ - הוּוי מפרש#

שני המפתחות הללו ב־%SIG אינם אותות מערכת הפעלה. הם נקראים כאשר המפרש עומד לפלוט אזהרה (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

הפרגמה הלקסיקלית use warnings FATAL => 'all' היא דרך ממוקדת יותר לעשות זאת - היא הופכת אזהרות לקטלניות רק בתחום הלקסיקלי שבו הפרגמה פעילה. השתמשו בה במקום __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__} עבור קוד חדש. הפרגמה 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__}.