This is the mail archive of the libc-alpha@sourceware.org mailing list for the glibc project.


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

[RFC] Toward Shareable POSIX Signals


Hi libc-alpha@

I've written up a proposal for improving the application signal APIs,
written below. Might there be any interest in prototyping this work
in glibc?

Thanks in advance for taking a look!

Problem
=======

The Unix signal API comes down to us from a time when the process was
the natural unit of code division. In that world, making the handler
for a signal a process-wide property presented few real problems. Now
that it’s become common for multiple independent components to share a
process and for these components to each want to perform some action
in response to the receipt of a signal, the old Unix process-wide
signal handler approach has begun to break down. This document
proposes a scalable and backwards-compatible facility for allowing
these components to peacefully coexist.

Why do we use signals? Why might we want to share them?
-------------------------------------------------------

It’s useful to think about _why_ components might want to use signals
and why multiple independently-developed components sharing a process
might each want to use the same signal. I’ve describe a few use cases
for signal handling below and explain why different components might
want to share a signal.

HIGH-LEVEL LANGUAGE RUNTIME OPTIMIZATION: many runtimes for high-level
languages (e.g., ART) rely on signals to catch out-of-bound memory
accesses (e.g., to null pointers) and transform these accesses into
well-defined high-level language exceptions (e.g.,
NullPointerException). By relying on signals to detect these accesses
instead of inserting explicit access checks before every dereference,
runtime authors can greatly improve code performance in the common
case where code never dereferences invalid pointers. A runtime using
this technique typically checks, upon getting a SIGSEGV, whether the
code that triggered the SIGSEGV is “owned” by the runtime. If so, the
runtime arranges for a high-level exception to be raised. If not, the
runtime delegates responsibility for handling the signal to some other
component (e.g., a crash reporting engine). If multiple such runtimes
exist in a single process, it’s reasonable to expect each to respect
its own language’s exceptions and not interfere with other runtimes.

CHILD PROCESS EXIT NOTIFICATION: it’s surprisingly difficult to wait
for subprocess exit in the traditional Unix way: it’s legal to wait on
a process only once, and the wait*-family primitives are not
multiplexed with other kinds of waits (like poll(2)) and so require a
dedicated wait for each thread. Compounding the problem, it’s not
possible to wait for a list of specific child processes: the APIs
support waiting for all children, waiting for children of a specific
process group, or waiting for one specific child.

Some components work around the inability to wait on a list of
processes by using a blanket waitpid(-1, ..) to look for exits from
any of their children, ignoring the exits of any children they don’t
recognize. If two such components exist in a single process, then they
will each receive notifications for the exits of each other’s
children, ignore them, and (from the perspective of each runtime)
never receive child exit notifications.

A more scalable approach is to rely on SIGCHLD for child exit
notifications. Under this model, components wait for SIGCHLD (sharing
the signal using the mechanism that this document proposes), and upon
getting it, call waitpid(CHILD, &status, WNOHANG) for each CHILD that
component owns. (Or just the child whose PID matches the one in the
siginfo_t). Each component receives a notification for every child it
cares about without interfering with the operation of any other
component in the process.  Proposed non-portable mechanisms like
CLONE_FD would also allow for scalable subprocess waiting, but this
document proposes a portable, universal solution to the
multiple-child-wait problem that falls out of a general-purpose signal
sharing facility.

HANDLING MMAP IO ERRORS VIA SIGBUS: the kernel reports IO errors on
mmaped files by sending threads with failing IO SIGBUS. It’s perfectly
reasonable for multiple components in a single process to want to each
deal with IO errors in memory-mapped files they own.

SIGIO: multiple components might reasonably went to perform
asynchronous IO and receive completion notifications. While it’s
possible to use real time signals instead of SIGIO, there are only so
many of them and a coordination problem still exists with respect to
access to the signals.

SUSPENSION: SIGTSTP is useful in a variety of contexts.

MEMORY TRICKS: it’s reasonable to use SIGSEGV to perform tricks like
user-space page faulting of compressed files, access checks, and so
on. There ought to be no reason in principle that multiple such
components couldn’t exist in the same process.

INSTRUCTION EMULATION: trapping SIGILL is a legal way to provide a
fallback for instructions that might not exist on a particular
architecture. Again, multiple such components, each supporting
different instructions, might exist, and these components
should cooperate.

EXTERNAL SIGNALING: it’s traditional for processes to reload
configuration files, metadata, and so on upon the receipt of
SIGHUP. Why shouldn’t multiple components in a process each listen for
this notification? The same argues applies for triggering cleanups
upon receiving a SIGTERM or SIGINT.

CRASH REPORTING: it’s useful to allow for multiple crash reporting
components in a single process so as to report different information
to different users. For example, in Android, an application may want
to install something like breakpad to report crashes in an
application-specific way, but also trigger the system’s debugd handler
to print informative messages in the system log.

Previous Work
=============

Manual Signal Chaining
----------------------

APPROACH: The most straightforward way for two components to share a
signal handler is for one component’s signal handler to delegate to
another. That is, component A loads first and installs its own signal
handler. Component B loads consequently, retrieves the signal handler
set by component A, and then installs its own. When B’s signal handler
runs, it performs whatever logic is necessary, then calls component
A’s handler.

PROBLEMS:

    1. Error-prone implementation: the POSIX signal API has evolved
    over many years and includes features that chaining implementers
    may not honor, like SA_RESETHAND, SA_NODEFER and signal-specific
    masks set via sigaction. Unless the chaining library goes to great
    pains to be exactly correct, signaled “chained” from some other
    handler will likely run in an unexpected environment and
    may malfunction.

    2 Default unsafe: unless components go out of their way to chain
    to existing handlers, they will clobber handlers already
    installed, and naive testing may not reveal this problem until a
    program tries to combine components in ways that their authors did
    not expect.

    3. Component unloading: any “chained” approach interacts poorly
    with component unloading. If component A above were unloaded,
    component B would have no way of knowing about it, and B, in its
    signal handler, would attempt to call into unloaded code and
    explore many kinds of undefined behavior. Uninstallation of signal
    handlers also breaks.

    4. Signal ordering: component B’s handler will always run before
    component A’s handler. If component B is some kind of
    general-purpose catch-all handler (e.g., for a crash reporting
    component), and component A wants to handle just one kind of
    signal and recover, we’ll do the (probably-heavyweight,
    maybe-fatal) work for B before even getting started with A, with
    consequences ranging from behavioral weirdness to instacrashing.

sigchainlib
-----------

APPROACH: sigchainlib is part of ART; it’s essentially an elaboration
of the manual signal chaining approach. Instead of relying on each
component author to manually chain signals, it uses ELF symbol
interposition to provide to other components in an ART-using process
alternative implementations of the POSIX signal functions.

PROBLEMS: sigchainlib is an improvement over asking the rest of the
ecosystem to manually defer to ART’s own signal handler, but it still
suffers from problems #1 and #3, and to some extent #4. (Whether #4
applies depends on whether you believe it’s legitimate to want a
signal handler to run before ART’s.) Additionally, sigchainlib
requires dynamic linking to operate properly, and the scheme fails to
operate correctly in a statically-linked process. It’s also not
possible to correctly intercept all signal handler requests: for
example, if a component using sigchainlib is loaded into a process
that has privately stashed a pointer to the sigaction function.

Windows Vectored Exception Handlers
-----------------------------------

Windows isn’t a POSIX system and doesn’t have signals per se, but it
does have a similar concept of a global unhandled “exception” (e.g.,
SIGSEGV-equivalent) handler. Vectored Exception Handlers allow
multiple components to cooperate in handling these exceptions and
operate very similarly to the mechanism that this document proposes.

Proposed New Standard API
=========================

Summary
-------

We can fix the signal-sharing problem by providing a new API
explicitly designed for cooperative handling of signals and layering
it “on top” of the traditional signal handling functions, first giving
shared signal handlers a chance to run, and then automatically falling
back to the traditional model.

Why not a library?
------------------

Coordination. While it’s possible to provide the interface below via a
user library, having it in the base system solves the problem of
having multiple independent components coordinate their signal sharing
mechanisms, and it allows the system to ensure that these shared
signal handlers interact properly with the legacy signal API. If
everyone used a signal multiplexing library, we’d just have to
coordinate the signal multiplexing libraries.

Why not signalfd?
-----------------

Signalfd is definitely useful, but it’s non-standard and doesn’t
support all the use cases of conventional signal handlers: e.g.,
support synchronous signals like SIGTSTP and SIGSEGV, non-local
control flow in a signal handler, and so on. In addition, even for the
cases for which signalfd is a viable solution, the interface with
signal-handling code is completely different, complicating
porting. It’s relatively easy to port legacy sigaction-based signal
handlers to the shared handler model.

Definition
----------

enum signal_disposition {
  SIGNAL_CONTINUE_SEARCH = 0,
  SIGNAL_CONTINUE_EXECUTION = 1,
};

typedef <opaque> signal_registration;
/* Declare opaquely */ INVALID_SIGNAL_REGISTRATION;

signal_registration signal_register(
  int signum,
  sigset_t mask,
  int flags,
  enum signal_disposition (*shared_handler)(
    int signo,
    siginfo_t *info,
    struct ucontext *context));

void signal_unregister(
  signal_registration registration);

Semantics
---------

signal_register registers a shared handler for a specific signal and
returns an opaque cookie that can later be used to unregister that
specific handler. signum, mask, and flags are as for sigaction(2),
except that SA_SIGINFO, SA_ONSTACK, and SA_RESTART are
implied. signal_unregister unregisters a handler registered with
signal_register. (signal_register fails by returning
INVALID_SIGNAL_REGISTRATION and setting errno.)

(It’s okay for two functions can be async-signal-unsafe, I think.)

An additional flag, SA_LOW_PRIORITY, has the following effect: all
handlers registered without SA_LOW_PRIORITY run before handlers
registered with SA_LOW_PRIORITY. SA_LOW_PRIORITY allows a component
like breakpad to express that it wants to run after other handlers
even if installed later. The handler function works like a
sigaction(2) SA_SIGINFO handler except for its return value,
described below.

When a signal arrives, instead of running the handler registered with
sigaction(2), we run the shared signal handler functions registered
with signal_register for that signal. Each of these shared signal
handlers returns either SIGNAL_CONTINUE_SEARCH or
SIGNAL_CONTINUE_EXECUTION. If a shared handler returns
SIGNAL_CONTINUE_EXECUTION, the system terminates signal processing and
resumes whatever it was doing before receiving the signal. (It’s the
moral equivalent of returning normally from a legacy signal handler.)
If a shared handler returns SIGNAL_CONTINUE_SEARCH, the system another
registered shared handler for that signal. If all shared handlers for
a signal return SIGNAL_CONTINUE_SEARCH, the legacy POSIX signal
handling rules apply.

Execution order: the system runs all non-SA_LOW_PRIORITY handlers in
order of installation, then all SA_LOW_PRIORITY handlers in order
of installation.

Non-Local Control Flow
----------------------

It’s occasionally useful to longjmp out of a signal handler. It’s
reasonable to want to return non-locally from a shared signal handler
too --- that is, to resume program execution after
SIGNAL_CONTINUE_EXECUTION in a different state from the state the
program had when we entered the shared signal handler. Since the
signal system probably wants to maintain some kind of state to track
its progress through its shared signal handler list, a plain longjmp
out of a shared signal handler will likely leave the system in an
unspecified state.

One way to allow non-local returns is to provide a longjmp wrapper;
shared signal handlers can call this function to reset the system’s
internal state before jumping. A second option is to allow handlers to
mutate the context argument describing program state. By modifying
this structure and affecting the state of the program we “return”
into, a shared signal handler can achieve the effect of a longjmp
without actually returning non-locally.

That is, the intent is to ban longjmp out of shared signal handlers
and achieve the effect of longjmp by having shared signal handlers
mutate the ucontext structure they receive, then returning with
SIGNAL_CONTINUE_EXECUTION.

Interaction with signal handler inheritance
-------------------------------------------

Upon exec(), the system “squashes” all process signal handlers into
either SIG_IGN (if the pre-exec process had configured SIG_IGN as the
signal/sigaction handler for a signal) or SIG_DFL (in any other
case). The intent of this proposal is to preserve this behavior and
make the installation of shared signal handlers irrelevant for
purposes of process inheritance. That is, if a process sets a signal
handler to SIG_IGN *and* uses signal_register to install a handler, a
post-exec process starts with no shared signal handlers and the legacy
sigaction handler set to SIG_IGN.


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]