-
Notifications
You must be signed in to change notification settings - Fork 738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Signal Handling #16
Comments
Couple of questions:
|
It masks out all the signals on the current thread. The intent is to run it early on in
Each reactor could opt into receiving signals. In practice, this means you should do one of the following:
|
I've been thinking about this issue and can't decide how to implement signal handling for OS X.
Emulated To summarise possible implementations:
|
@vbuslov I've been pretty slow on this, but here is my point of view:
I think what I would do is to
Does that sound plausible? Did I miss anything? Anything else I can add? |
Sadly the Async-Signal-Safe rules only help you with libc, not libstd, jemalloc, or your own code. You're also compelled not to deadlock or otherwise reach unsafety on your own even if you're not calling libc unsafely. So, for instance, you can't do allocations except on the stack, and you can't access any data structures where code needs to complete to dynamically uphold safety invariants. I believe you can get away with the self-pipe trick + sending This does seem like it makes it hard to support multiple reactors. :( Perhaps there's no use case for multiple reactors independently interested in signals (since signals are process-wide anyway), and they can all select on a single fd, or something. BTW, do you have partial work on this? I'm interested in seeing signal and |
Ugh, another problem here: signal dispositions get inherited to child processes, and at the very least, (Even if mio were able to conspire with So I think a I'm not seeing a solution to this other than using the self-pipe trick on all platforms (where the signal could get delivered on any thread, but that's OK), with the side effect of requiring all code to be |
Hey @geofft. Are you on IRC? Would be glad to chat about this 😄 |
@geofft Yeah, ping us on the #mio channel (Mozilla IRC server: irc.mozilla.org). |
@wycats and I talked a bit on Sunday about the inheritance issue. To summarize how we understand things:
So, we need to go around to all the C libraries that spawn helper processes, and make sure that they properly unmask signals (and potentially reset all signal handlers first) between fork and exec. We also need to get the same change into Rust's (Note that I'm still unhappy about the |
Somewhat unrelatedly, I think I'm concluding that it's strictly worse to use See also GHC ticket #2451, comment 19 onwards:
If we want to get one siginfo per Alternatively, we could decide that the semantics for mio signal delivery involve siginfos as a best-effort thing, but acknowledge that they may get coalesced, and you may only get one siginfo if multiple signals of the same type were delivered between calls to the reactor. |
I think there are three major subtasks in this issue:
I am of the opinion that items 1 and 2 are worthwhile tasks. 3 is not. I might be wrong, I am willing to defer to the results of a survey of developers for the market viability of signal delivery as a feature. My instinct is that, frankly, if someone is interested in organizing their management of signals they've made some wrong architectural decisions and I am not interested in helping someone do the wrong thing. That's as much as I'll say about item 3. Re item 1, retrying on eintr is a pretty good thing to do in general (except for close()1) While I don't think much effort spent on signals is warranted as a lib designer, I do think that we should operate correctly if someone does have a legitimate use case for SIGUSR or SIGCHLD. Re item 2, in my career as a POSIX dev, pretty much every mature software dev organization on *nix had an application framework context that they initialized. It set a static global variable named something intelligent like "volatile int __running_g = 1;" And the application context just set a handler for SIGINT and set that __running_g to 0, then the event loop would shut down, worker threads would spin down, etc. It doesn't matter which thread gets picked to handle the signal, the handler gets run, the global atomic variable gets tripped and things begin shutting down gracefully. I'd probably recommend a configuration setting in Mio EventLoop which is handle_sigint or something to let people opt out. [1] close() isn't really as broken as the article referenced above might suggest, the most correct behavior is to not retry on close. The worst case is very remote possibility is you do leak a file descriptor. I don't know what the exact odds are, but let's say a 1 in 1,000,000 chance, let's also put the odds of a signal occuring during a close() in your program at 1,000,000 for round numbers. Also a paranoid sysadmin put the per-user fd limit at 4096 for some silly reason. Well then you'd need to handle 4e15 signals in your process to run out of fds. If you came anywhere close to that, I'd say that you're doing something really dumb with signals in the first place :) |
The things that have me interested in signal handling are clean handling of I'm actually less concerned about I think retry-on- |
Maybe we're saying the same thing here, but I don't think it's possible to generically designate a signal handling thread. Unknown future libraries in the application will create a thread and ruin everything :) I am all for providing some cleanly wrapped library functions that let a user do it themselves, provided they know what they're doing. Unless I'm missing something, all they'd need at that point to interact with EventLoop is a Notify Sender channel. I don't even think it would require a special callback in Handler, notify should be sufficient, obviously Message would need to understand how to carry said information, but that is entirely up to the API user, IMO. |
By the way, it was pointed out to me that you don't get reliable signal delivery over re-entrant So this simplifies the API that mio needs to expose, and gets us closer to offering a clean abstraction like we do for sockets or timers, instead of something that's closely related to OS quirks. It also lets us get away with using I do wonder if there's value in mio offering subprocess management in the mainloop, and offering an API that doesn't specifically reference |
After talking more with @geofft, I believe that signal handling would be better off as a standalone library (built on top of nix). The reasons:
I'm hoping that @geofft will write this library 😄 |
UNIX specifies that signal dispositions and masks get inherited to child processes, but in general, programs are not very robust to being started with non-default signal dispositions or to signals being blocked. For example, libstd sets `SIGPIPE` to be ignored, on the grounds that Rust code using libstd will get the `EPIPE` errno and handle it correctly. But shell pipelines are built around the assumption that `SIGPIPE` will have its default behavior of killing the process, so that things like `head` work: ``` geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 geofft@titan:/tmp$ cat bash.rs fn main() { std::process::Command::new("bash").status(); } geofft@titan:/tmp$ ./bash geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe [...] ``` Here, `head` is supposed to terminate the input process quietly, but the bash subshell has inherited the ignored disposition of `SIGPIPE` from its Rust grandparent process. So it gets a bunch of `EPIPE`s that it doesn't know what to do with, and treats it as a generic, transient error. You can see similar behavior with `find / | head`, `yes | head`, etc. This PR resets Rust's `SIGPIPE` handler, as well as any signal mask that may have been set, before spawning a child. Setting a signal mask, and then using a dedicated thread or something like `signalfd` to dequeue signals, is one of two reasonable ways for a library to process signals. See tokio-rs/mio#16 for more discussion about this approach to signal handling and why it needs a change to `std::process`. The other approach is for the library to set a signal-handling function (`signal()` / `sigaction()`): in that case, dispositions are reset to the default behavior on exec (since the function pointer isn't valid across exec), so we don't have to care about that here. As part of this PR, I noticed that we had two somewhat-overlapping sets of bindings to signal functionality in `libstd`. One dated to old-IO and probably the old runtime, and was mostly unused. The other is currently used by `stack_overflow.rs`. I consolidated the two bindings into one set, and double-checked them by hand against all supported platforms' headers. This probably means it's safe to enable `stack_overflow.rs` on more targets, but I'm not including such a change in this PR. r? @alexcrichton cc @Zoxc for changes to `stack_overflow.rs`
UNIX specifies that signal dispositions and masks get inherited to child processes, but in general, programs are not very robust to being started with non-default signal dispositions or to signals being blocked. For example, libstd sets `SIGPIPE` to be ignored, on the grounds that Rust code using libstd will get the `EPIPE` errno and handle it correctly. But shell pipelines are built around the assumption that `SIGPIPE` will have its default behavior of killing the process, so that things like `head` work: ``` geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 geofft@titan:/tmp$ cat bash.rs fn main() { std::process::Command::new("bash").status(); } geofft@titan:/tmp$ ./bash geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe [...] ``` Here, `head` is supposed to terminate the input process quietly, but the bash subshell has inherited the ignored disposition of `SIGPIPE` from its Rust grandparent process. So it gets a bunch of `EPIPE`s that it doesn't know what to do with, and treats it as a generic, transient error. You can see similar behavior with `find / | head`, `yes | head`, etc. This PR resets Rust's `SIGPIPE` handler, as well as any signal mask that may have been set, before spawning a child. Setting a signal mask, and then using a dedicated thread or something like `signalfd` to dequeue signals, is one of two reasonable ways for a library to process signals. See tokio-rs/mio#16 for more discussion about this approach to signal handling and why it needs a change to `std::process`. The other approach is for the library to set a signal-handling function (`signal()` / `sigaction()`): in that case, dispositions are reset to the default behavior on exec (since the function pointer isn't valid across exec), so we don't have to care about that here. As part of this PR, I noticed that we had two somewhat-overlapping sets of bindings to signal functionality in `libstd`. One dated to old-IO and probably the old runtime, and was mostly unused. The other is currently used by `stack_overflow.rs`. I consolidated the two bindings into one set, and double-checked them by hand against all supported platforms' headers. This probably means it's safe to enable `stack_overflow.rs` on more targets, but I'm not including such a change in this PR. r? @alexcrichton cc @Zoxc for changes to `stack_overflow.rs`
UNIX specifies that signal dispositions and masks get inherited to child processes, but in general, programs are not very robust to being started with non-default signal dispositions or to signals being blocked. For example, libstd sets `SIGPIPE` to be ignored, on the grounds that Rust code using libstd will get the `EPIPE` errno and handle it correctly. But shell pipelines are built around the assumption that `SIGPIPE` will have its default behavior of killing the process, so that things like `head` work: ``` geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 geofft@titan:/tmp$ cat bash.rs fn main() { std::process::Command::new("bash").status(); } geofft@titan:/tmp$ ./bash geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe [...] ``` Here, `head` is supposed to terminate the input process quietly, but the bash subshell has inherited the ignored disposition of `SIGPIPE` from its Rust grandparent process. So it gets a bunch of `EPIPE`s that it doesn't know what to do with, and treats it as a generic, transient error. You can see similar behavior with `find / | head`, `yes | head`, etc. This PR resets Rust's `SIGPIPE` handler, as well as any signal mask that may have been set, before spawning a child. Setting a signal mask, and then using a dedicated thread or something like `signalfd` to dequeue signals, is one of two reasonable ways for a library to process signals. See tokio-rs/mio#16 for more discussion about this approach to signal handling and why it needs a change to `std::process`. The other approach is for the library to set a signal-handling function (`signal()` / `sigaction()`): in that case, dispositions are reset to the default behavior on exec (since the function pointer isn't valid across exec), so we don't have to care about that here. As part of this PR, I noticed that we had two somewhat-overlapping sets of bindings to signal functionality in `libstd`. One dated to old-IO and probably the old runtime, and was mostly unused. The other is currently used by `stack_overflow.rs`. I consolidated the two bindings into one set, and double-checked them by hand against all supported platforms' headers. This probably means it's safe to enable `stack_overflow.rs` on more targets, but I'm not including such a change in this PR. r? @alexcrichton cc @Zoxc for changes to `stack_overflow.rs`
UNIX specifies that signal dispositions and masks get inherited to child processes, but in general, programs are not very robust to being started with non-default signal dispositions or to signals being blocked. For example, libstd sets `SIGPIPE` to be ignored, on the grounds that Rust code using libstd will get the `EPIPE` errno and handle it correctly. But shell pipelines are built around the assumption that `SIGPIPE` will have its default behavior of killing the process, so that things like `head` work: ``` geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 geofft@titan:/tmp$ cat bash.rs fn main() { std::process::Command::new("bash").status(); } geofft@titan:/tmp$ ./bash geofft@titan:/tmp$ for i in `seq 1 20`; do echo "$i"; done | head -1 1 bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe bash: echo: write error: Broken pipe [...] ``` Here, `head` is supposed to terminate the input process quietly, but the bash subshell has inherited the ignored disposition of `SIGPIPE` from its Rust grandparent process. So it gets a bunch of `EPIPE`s that it doesn't know what to do with, and treats it as a generic, transient error. You can see similar behavior with `find / | head`, `yes | head`, etc. This PR resets Rust's `SIGPIPE` handler, as well as any signal mask that may have been set, before spawning a child. Setting a signal mask, and then using a dedicated thread or something like `signalfd` to dequeue signals, is one of two reasonable ways for a library to process signals. See tokio-rs/mio#16 for more discussion about this approach to signal handling and why it needs a change to `std::process`. The other approach is for the library to set a signal-handling function (`signal()` / `sigaction()`): in that case, dispositions are reset to the default behavior on exec (since the function pointer isn't valid across exec), so we don't have to care about that here. As part of this PR, I noticed that we had two somewhat-overlapping sets of bindings to signal functionality in `libstd`. One dated to old-IO and probably the old runtime, and was mostly unused. The other is currently used by `stack_overflow.rs`. I consolidated the two bindings into one set, and double-checked them by hand against all supported platforms' headers. This probably means it's safe to enable `stack_overflow.rs` on more targets, but I'm not including such a change in this PR. r? @alexcrichton cc @Zoxc for changes to `stack_overflow.rs`
Signals, how do they work?
The goal of this proposal is to make it possible to write programs using mio that can successfully handle Unix signals.
Unfortunately, signal handling is somewhat tricky, made even more complex by the way Unix delivers signals to multi-threaded programs.
The basics:
(
SIGKILL
andSIGSTOP
cannot be caught).program that are handling the signal.
signal unmasked, the operating system will deliver it to an arbitrary
thread.
SIGSEGV
,SIGFPE
), thesignal is delivered to the thread that generated the failure.
sigwaitinfo
function allows a thread to synchronously wait for a setof signals. It can atomically unmask a set of signals and wait, which
avoids race conditions.
signalfd
creates a file descriptor that can be used tosynchronously (and atomically) wait on signals, in a way that can work with
the multiplexing APIs (
epoll_wait
).sigwaitinfo
to emulatesignalfd
,at the cost of an additional thread that is responsible for waiting on
signals.
At a high level, the goal for
mio
is to allow a consumer of a reactor to register interest in signals, to be delivered to a mio handler.This means that programs that want to use this facility will ignore signals on all threads, and we will use
sigwaitinfo
orsignalfd
to allow the reactor to register interest in signals. This also means that only one reactor can be interested in signals. Otherwise, an arbitrary interested reactor would receive the signals.If a program uses this facility without successfully ignoring signals, signals may be delivered to random user threads instead of the reactor.
Initialization
To make it easy for users to successfully ignore signals across their entire program, a new function,
mio::initialize
, is added. Programs should run this function at the beginning of theirmain
function, to ensure that any subsequent threads ignore signals, ensuring that the mio reactor gets notified of signals.Handler API
mio::Handler
gets a new callback:The information provided to the handler will be the subset of
siginfo_t
that is reliably supported on Linux and Darwin. It will be organized by signal type; in particular fields related toSIGCHLD
will be grouped together.Control
In order to ensure that signal notifications are sent to the reactor loop, we need to:
only delivered when waiting.
delivered to other threads.
It is in theory possible to use this facility without control over signal masking, but that will mean that signals can be missed if they get dispatched to another thread. For programs that want to handle signals, this is very unlikely to be desirable.
EINTR
When a thread recieves a signal during a blocking system call, the system call may return with an
EINTR
error.Typically, this means that system calls must guard themselves against this possibility and attempt to retry the call in the case of
EINTR
. There is even a (not fully reliable)sigaction
flagSA_RESTART
that can be used to help with this problem.For mio internals, this problem should not exist:
using the
signal
handler method, notsigaction
.signalfd
orsigwaitinfo
) to unmask thesignals only at the same time as we know we are waiting for a signal.
signal.
For programs that want to sanely handle signals with mio, this problem also should not exist:
signals process-wide.
time that we are waiting for a signal.
queued and delivered to the expected system call, and never interrupt other
system calls.
Implementation
Linux (epoll +
signalfd
)On Linux, we simply use
signalfd
to accept all signals, after having masked all signals on the thread.We then register
signalfd
withepoll
. Whenepoll_wait
returns, if the FD was readable, we consume signals viaread
, get the associatedsignalfd_siginfo
stuctures and call the handler.Other (Self-Pipe + Emulated
signalfd
)In the interest of simplicity, on platforms without
signalfd
, we will emulate it using a separate thread and self-pipe.sigwaitinfo
in a loop to listen for signalssiginfo_t
structures into the pipe(e.g. kqueue)
The text was updated successfully, but these errors were encountered: