Repository navigation
fix(signal): install signal handlers before returning in thread-context builds - #6407
Open
binaryfire wants to merge 2 commits into
Open
binaryfire wants to merge 2 commits into
binaryfire wants to merge 2 commits into
Conversation
With the sigaction backend, the signal handler only sets a flag, and the reactor's signal end callback calls the PHP handler. A signal caught while PHP code runs before the loop waits stayed pending until the next event or timer. A Process::signal() handler could run a full timer interval late, and System::waitPid() could wait until its timeout. get_timeout_msec() now returns 0 while a caught signal is waiting, but only on the reactor that owns the signal end callback, so other reactors keep blocking normally. kqueue now gets its timeout from get_timeout_msec() too. It handles SIGCHLD with sigaction, and it also ignored pending deferred tasks whenever a timer was set. This covers signals already pending when the timeout is checked. A signal caught between that check and the wait call is still delayed, as before. This affects builds without signalfd, such as macOS and builds with enable_signalfd disabled, and SIGCHLD with kqueue.
In thread-context builds each coroutine runs on its own thread, and those threads block all signals. signalfd needs the signal blocked on the reactor thread, and a thread can only change its own signal mask, so Process::signal(), System::waitSignal() and System::waitPid() deferred swoole_signal_set() to the reactor thread. Until the deferred task ran, the signal kept its default action. A signal sent right after Process::signal() or System::waitSignal() returned could kill the process. A waitSignal() cancelled before the deferred task ran left the signal registered, and the task wrote to the finished coroutine's stack. Thread-context builds no longer use signalfd. The sigaction backend is process-wide, so registration is complete when the call returns. The deferred registrations, the waitpid catch-up and the tests' waits for deferred registration are removed.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #6407 +/- ##
==========================================
- Coverage 84.10% 84.09% -0.02%
==========================================
Files 115 115
Lines 20237 20239 +2
Branches 3638 3639 +1
==========================================
- Hits 17021 17020 -1
- Misses 3216 3219 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In builds with
--enable-thread-context,Process::signal()returns andSystem::waitSignal()yields before their signal handlers are installed. A SIGUSR1, SIGTERM or SIGINT received in that gap kills the process instead of reaching the registered handler:A standard build prints
got 10anddone. A thread-context build is killed by SIGUSR1. The existingsignal_twice.phptfails the same way.Cause
Each coroutine runs on its own thread, and those threads block all signals (#5081). signalfd needs the signal blocked on the thread running the event loop, and a thread can only change its own signal mask. So these calls defer
swoole_signal_set()to the event loop withswoole_event_defer(), and the signal's previous disposition stays in effect until the loop runs the task.Cancelling
System::waitSignal()before that task runs is also broken: the task still registers the signal, and it writes to the finished coroutine's stack.Fix
Thread-context builds no longer use signalfd. sigaction applies to the whole process, so a registration from a coroutine thread is complete when the call returns, as on macOS or with
enable_signalfd => false. The deferred registrations are removed. That includesSystem::waitPid(), which deferred SIGCHLD the same way and used a catch-up from #6337 to reap children that exited first.php --ri swooleno longer lists signalfd in these builds, andenable_signalfdhas no effect there.With sigaction, the handler only sets a flag, and the event loop calls the PHP handler after it wakes. A signal caught while PHP code runs before the loop waits stays pending until the next event or timer. Standard builds already show this on macOS and with
enable_signalfd => false, and thread-context builds would show it whenever a signal arrives while a coroutine runs.Reactor::get_timeout_msec()now returns 0 while a caught signal is waiting, only on the reactor that dispatches signals. kqueue now uses it too, since it handles SIGCHLD with sigaction. This fixes signals already pending when the timeout is checked; the existing race between that check and the wait call remains.Why not keep signalfd in these builds
Process::signal(), and lets other coroutines run inside the call.Tests
signal_dispatch_before_wait.phpt: aProcess::signal()handler and aSystem::waitPid()whose signal is caught before the loop waits run straight away. It runs withenable_signalfd => falseon Linux, and with poll and kqueue on macOS.waitSignal_immediate.phpt: a signal sent as soon aswaitSignal()yields is delivered, and a cancelled wait leaves no registration behind.signal_restore_action.phptandwaitSignal_ignored.phptno longer wait for the deferred registration.On master, the new tests and
signal_twice.phptfail in a thread-context build, andsignal_dispatch_before_wait.phptalso fails in a standard build. All pass with this change. The kqueue path is only covered by the macOS CI job.This overlaps #6230 in the same signal code. The two merge cleanly, and I'll rebase whichever lands second if needed.