Signal handling — %SIG#

%SIG is the hash where Perl exposes signal handlers. Setting $SIG{NAME} installs a handler for signal NAME; deleting it restores the default action. Two pseudo-signals — __WARN__ and __DIE__ — are not real OS signals at all but interpreter hooks into warn and die.

The signal table#

Real OS signals come from the POSIX and signal.h lists. The common ones — keys you will actually use:

Key

Signal

Default action

When you handle it

INT

SIGINT

terminate

Ctrl-C: graceful shutdown

TERM

SIGTERM

terminate

kill <pid>: graceful shutdown

HUP

SIGHUP

terminate

terminal hangup; daemons reload config

QUIT

SIGQUIT

terminate + core

Ctrl-: like INT but with a core dump

PIPE

SIGPIPE

terminate

wrote to a closed pipe; usually ignored

CHLD

SIGCHLD

ignore

a child process died (waitpid here)

USR1

SIGUSR1

terminate

application-defined event

USR2

SIGUSR2

terminate

application-defined event

ALRM

SIGALRM

terminate

alarm() timer expired

WINCH

SIGWINCH

ignore

terminal window resized

IO

SIGIO

terminate

async I/O ready

The full list — including signals you cannot catch (KILL, STOP) and platform-specific signals — comes from the POSIX module, which exports the numeric constants (SIGINT, SIGTERM, …) and the WIFEXITED-family wait macros.

Installing a handler#

Three handler-value forms are accepted:

$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

Don’t use a bareword ($SIG{INT} = 'handler') — that references a name in the main:: package, which Perl looks up each time the signal arrives. If you rename handler, the bareword form silently keeps targeting the old name.

The handler receives one argument — the signal name as a string:

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

Safe signals — the deferred-delivery rule#

Since Perl 5.8, signal delivery is deferred. When a signal arrives, the OS sets a flag in the interpreter; the actual Perl handler runs only at the next safe point — typically between op-code executions. This means:

  • A handler cannot interrupt a read, open, connect, or any other syscall mid-call. Slow syscalls return early with EINTR in $! and the handler runs after.

  • A handler cannot interrupt a regex match in progress. A long-running regex on a malicious input will not be cut short by SIGINT.

  • The handler runs in regular Perl context. It can call any Perl code, allocate memory, throw exceptions — none of the re-entrancy hazards of C signal handlers.

This is the right trade-off for almost every program. If you genuinely need pre-5.8 unsafe signal handling — handlers that run synchronously in the signal context, with all the re-entrancy hazards — set the PERL_SIGNALS=unsafe environment variable before starting Perl. Do not do this casually.

PetaPerl implements deferred-delivery signal handling.

The graceful-shutdown pattern#

The standard shape: a flag set by the handler, checked by the main loop:

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;
}

The main loop polls the flag at a place where it is safe to unwind cleanly. The handler does only «set the flag» — no logging, no I/O — minimising the time it spends running.

For applications that block on a long syscall (accept, read, select), the loop has to be structured to retry on 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} (see error variables) tells you the syscall exited because a signal arrived. The flag was set by the handler; the main loop sees the wake-up, re-checks $shutting_down, and exits cleanly.

Reaping child processes — SIGCHLD#

When a child process exits, the parent gets SIGCHLD. The handler must call waitpid to reap the zombie:

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

The while loop with WNOHANG (from POSIX :sys_wait_h) handles the case where two children exit close together — only one SIGCHLD is delivered (it is not queued), so the handler must drain all pending exits each time it runs.

$SIG{CHLD} = 'IGNORE' is a special case: it does not just ignore the signal, it tells the kernel to auto-reap children. Use this when your script forks children whose exit status you do not care about.

SIGPIPE — the closed-pipe case#

When you print to a pipe whose reader has gone away, the kernel sends SIGPIPE. The default action is to terminate the process — which is rarely what you want. Most filter programs should ignore SIGPIPE and check the print return value instead:

local $SIG{PIPE} = 'IGNORE';

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

With the handler ignored, the syscall returns failure with EPIPE rather than killing the program.

Timeouts via SIGALRM#

Combined with alarm and an eval block, SIGALRM provides a portable timeout for any blocking operation:

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> });

The pattern:

  1. local $SIG{ALRM} — handler is in scope only for this call.

  2. alarm $seconds — schedule the signal.

  3. Perform the blocking work.

  4. alarm 0 — cancel the timer if work succeeded.

  5. Inside the handler, die "TIMEOUT\n" — the trailing \n suppresses the «at line N» suffix and makes the error value easy to test against.

__WARN__ and __DIE__ — interpreter hooks#

These two keys in %SIG are not OS signals. They are called when the interpreter is about to emit a warning (warn) or throw a fatal exception (die).

$SIG{__WARN__} — warning interception#

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.

A __WARN__ handler that does nothing silences the warning entirely. A handler that calls die upgrades warnings into exceptions:

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

The lexical pragma use warnings FATAL => 'all' is a more targeted way to do this — it makes warnings fatal only in the lexical scope where the pragma is active. Use it instead of __WARN__ for «treat warnings as errors» requirements.

$SIG{__DIE__} — die interception#

A __DIE__ handler runs when die is about to throw — both for uncaught exceptions and inside eval. Inside an eval, the handler runs before the exception is caught, which is the classic source of confusion: a global __DIE__ handler that logs every «death» will fire on every eval { ... } that calls die for ordinary control flow.

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

If you want a global death logger, gate it on $^S:

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

Realistically, avoid $SIG{__DIE__} for new code. The pragma use warnings and the error variables cover what you need without the surprise. The modern try/catch feature handles exceptions explicitly without the action-at-a-distance of $SIG{__DIE__}.

Listing available signals#

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

Or, for the numeric→name mapping:

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;

This is what most signal-listing utilities do internally.

See also#

  • POSIX — the canonical source of SIG* numeric constants and the WIFEXITED/WEXITSTATUS macros for decoding child status in a SIGCHLD handler.

  • kill — the partner of %SIG: send a signal to a process (typically to test your own handler).

  • alarm — schedule a SIGALRM; the basis of timeout patterns.

  • waitpid — reap a child after SIGCHLD.

  • fork — produces the children whose deaths SIGCHLD reports.

  • Error variables$! after EINTR, $? after waitpid, $@ after die from inside a handler.

  • Interpreter introspection$^S for the __DIE__ «are we inside eval» check.

  • try/catch — the modern alternative to $SIG{__DIE__}-based exception handling.