Return from a sleep as soon as a wake arrives - #377
Conversation
7b75c19 to
e73d1f0
Compare
e73d1f0 to
9c92fe9
Compare
|
The TSAN red on the previous push came from moving both halves of the sleep's predicate onto the wake. They are not the same condition. A tagged counter over one run of Almost every early return was This push tests The measurement only separates the two versions inside the full sanitizer suite. Run on its own the test sits at 1800 to 2400 ms either way; under
|
9c92fe9 to
3fb4be8
Compare
3fb4be8 to
2d6482a
Compare
| * pressure taking 3150 ms across 29390 returns, against 432 ms across 3 | ||
| * on this cadence. | ||
| */ | ||
| bool stop_due = elapsed_ns >= stop_due_ns; |
There was a problem hiding this comment.
thread_stop_requested() is three conditions, and only one of them is the handoff the EINTR measurement above is about. thread_stop_is_leader_work_only() already separates them, so exit_group and an execve teardown could be answered on the wake that is now in hand while the handoff keeps its SLEEP_CHUNK_NS cadence: stop_due || !thread_stop_is_leader_work_only(). As written, a guest thread in a multi-second sleep still holds up teardown for up to 100 ms after the wake has already reached it.
There was a problem hiding this comment.
The sleep tests thread_stop_requested() && (stop_due || !thread_stop_is_leader_work_only()), so exit_group and an execve teardown end it on the wake while the handoff keeps the SLEEP_CHUNK_NS cadence. The stop test also runs before the signal test, because a thread an execve reaps would otherwise claim a process-directed signal into its private set and take it along. Under make check-tsan the handoff sleep in test-exec-handoff is 403 ms across 3 interruptions, the same as without the change.
| * signal that arrives first is answered by the check at the top of the | ||
| * sleep loop, which is fast whether or not the wait itself is joined. | ||
| */ | ||
| struct timespec settle = {0, (150 + i * 11) * 1000000L}; |
There was a problem hiding this comment.
The settle delay is a guess, not a handshake. If the sleeper has not reached the wait when pthread_kill fires, the signal is answered by the signal_pending() test at the top of the sleep loop and the iteration records a fast gap on the unfixed tree too. A slow pthread_create under the sanitizer lanes can put several iterations in that state, and once half of them land there the median stops measuring the thing this test exists for. Have the sleeper publish an atomic flag immediately before it calls nanosleep and spin on that flag before the settle delay.
There was a problem hiding this comment.
The sleeper sets sleeper_armed immediately before nanosleep or clock_nanosleep, and median_gap spins on it, bounded at SLEEP_SEC, before the settle delay.
| wakeup_wait_ns(5000000000LL, before_ring); | ||
| long long waited_ms = now_ms() - t0; | ||
| pthread_join(ringer, NULL); | ||
| host_check(waited_ms < 1000, "a signal releases a parked wait", |
There was a problem hiding this comment.
Both new waits assert only an upper bound, so a wakeup_wait_ns that returns at once and never parks passes them: it would report about 0 ms, well under the 1000 ms ceiling, and the stale-counter check above passes for the same reason. Add the lower bound that separates the two, something like waited_ms >= 40, since the ringer sleeps 50 ms before it signals.
There was a problem hiding this comment.
Added waited_ms >= 40. t0 also moved ahead of pthread_create: taken after it, a ringer already partway through its 50 ms could put a correct wait under the bound on a slow start.
rt_sigsuspend picks the first signal that would reach the guest, from the private set and then the shared set, and moves a shared one into the caller's private set under sig_lock so no sibling vCPU delivers it first. A sleep woken on the same broadcast as its siblings needs the same pick, so it moves to signal_claim_waking_locked, with no change to what rt_sigsuspend does.
2d6482a to
fbe1446
Compare
A guest thread that sleeps is a host thread parked in the sleep handler, outside hv_vcpu_run, where hv_vcpus_exit cannot reach it. sys_nanosleep answered that by splitting the interval into 100 ms chunks and testing signal_pending between them, so a queued signal waited out the rest of a chunk: uniform over 0 to 100 ms, 50 ms on average, against 0.12 ms on Linux. The wake those chunks stand in for already exists. Queueing a guest signal, exit_group and futex_interrupt all reach wakeup_pipe_signal, and io.c and poll.c already join it. A pure timeout has no descriptor to watch alongside it, so it takes the condition variable this adds to that same call rather than the pipe, which leaves every existing caller of the signal covering it unchanged. One broadcast wakes every sleeper at once, and signal_pending shows the shared set to each of them, so a single kill() would return EINTR from all of them while only one runs the handler. The sleep claims the signal under sig_lock through signal_claim_interruption instead, and only the claiming thread is interrupted, as complete_signal picks one thread on Linux. The claim passes over a signal that delivery would discard, so SIG_IGN and default-ignored signals do not interrupt the sleep. A signal, exit_group and an execve teardown end the sleep on the wake. An execve handed to the leader keeps the chunk cadence: a handoff rings thousands of times a second, and answering every ring turns each into a guest-visible EINTR that the guest answers by reissuing the rest of its interval. Teardown is tested before the claim, so a thread an execve reaps does not take a process-directed signal with it. The wake counter is what makes the park safe. A caller tests its predicate outside wake_lock, so a wake landing between the test and the park would otherwise be broadcast to a thread that has not arrived yet. Reading the counter first turns that case into a park that returns immediately. The counter is atomic so that read takes no lock, since a caller reads it once per pass and the ringer holds the lock to bump it. The remaining interval comes from CLOCK_MONOTONIC rather than from the chunk, because the park returns early on another thread's wake and a condition variable may return spuriously. Measured: signal to handler 50 ms median before, 0.069 ms after, over 12 trials with the delay before the signal walked across the quantum. One kill() aimed at four sleeping threads interrupted 2 to 4 of them without the claim and exactly 1 with it, as on Linux. Short sleeps do not regress: 1 us requested takes 5 us against 8 us before, and 1 ms takes 1274 us against 1273 us, median of 200. The 400 ms guest sleep test-exec-handoff runs under handoff pressure takes 403 ms across 3 returns under a TSAN build of the whole sanitizer suite, against 432 ms across 3 before the change.
fbe1446 to
76ba1ad
Compare
|
This unblocks the LTP test! Thanks for this improvement! :) |
A guest thread parked in
nanosleepdoes not notice a queued signal until its own 100 ms recheck quantum expires.sys_nanosleepsplits the interval into 100 ms chunks and testssignal_pendingbetween them, so the delay is uniform over 0 to 100 ms, about 50 ms on average, against 0.12 ms under Linux 6.12.The wake those chunks stand in for already exists: queueing a guest signal,
exit_groupandfutex_interruptall reachwakeup_pipe_signal, andio.candpoll.calready join it. A pure timeout has no descriptor to watch alongside the pipe, so it parks on a condition variable added to that same call instead, which leaves all five existing callers of the signal covering it unchanged.A signal, exit_group and an execve teardown end the sleep on the wake. An execve handed to the leader keeps the
SLEEP_CHUNK_NScadence: a handoff issues a stop request thousands of times a second, and answering every ring turns each into a guest-visible EINTR that the guest answers by reissuing the rest of its interval; the numbers that cost are below. One broadcast also wakes every sleeper, so the sleep claims a process-directed signal undersig_lockbefore reporting EINTR, reusing the pickrt_sigsuspendmakes, moved into a helper in the first commit.Reproduction
tests/test-nanosleep-signal-latency.cholds one thread in a sleep and times the gap betweenpthread_killand the handler running, for bothnanosleepandclock_nanosleep(TIMER_ABSTIME). The delay before the signal walks forward across iterations: a fixed delay phase-locks every iteration to one offset inside the quantum and reports that point as though it were the whole distribution.On the parent commit the test fails with 60.6 ms and 28.9 ms medians against its 20 ms ceiling. With this change it passes, and it also passes under the QEMU reference lane.
tests/test-nanosleep-process-signal.cparks four threads innanosleepand sends the group onekill(). Without the claim 2 to 4 sleepers return EINTR while the handler runs once; with it exactly one does, as under QEMU.Measurements
nanosleep,pselectandpthread_cond_timedwait_relative_npare indistinguishable in resolution at 200 samples (3 / 2 / 3 us for a 1 us request), so the primitive swap costs nothing. Short sleeps come out slightly faster because the old path rebuilt atimespecand ran the saturating remainder arithmetic once per chunk.The stop cadence is what the 400 ms guest sleep in
test-exec-handoffmeasures, under handoff pressure. It only separates the two revisions inside the full sanitizer suite; run on its own the test sits at 1800 to 2400 ms either way.Same tree built with and without the change, alternating.
make check-tsanCommands
make check,make check-tsan,make check-asan,make check-ubsan: all exit 0, withcheck-tsangreen over four consecutive runsmake check-format,make lint: exit 0make check-ascii: clean across 427 source filestest-wakeup-pipe-hostgoes from 5 to 9 checks, covering the counter advancing, a stale counter not parking, a wait holding until the wake, and a signal releasing a parked waitRebased on 4023647.
Summary by cubic
Guest threads parked in
nanosleeporclock_nanosleep(TIMER_ABSTIME)used to notice queued signals only when their 100 ms recheck quantum expired, adding about 50 ms average signal-to-handler latency. They now wake immediately viawakeup_pipe_signal(), cutting that to about 0.07 ms without regressing short sleeps.Bug Fixes
wakeup_pipe_signal(), since they have no descriptor to watch; existing callers are unchanged.CLOCK_MONOTONIC, because the wait can return early on any process-wide wake.sig_lockby one sleeper, so a singlekill()interrupts one thread instead of every sleeper.Written for commit 76ba1ad. Summary will update on new commits.