Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,20 @@ $(BUILD_DIR)/test-wait-signal-latency: \
@echo " CROSS $< (with -lpthread)"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread

# test-futex-requeue-race requeues a parked sibling thread and then races a
# signal against a wake at the new word.
$(BUILD_DIR)/test-futex-requeue-race: \
tests/test-futex-requeue-race.c | $(BUILD_DIR)
@echo " CROSS $< (with -lpthread)"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread

# test-futex-signal-latency aims a signal at a sibling thread parked in a futex
# wait.
$(BUILD_DIR)/test-futex-signal-latency: \
tests/test-futex-signal-latency.c | $(BUILD_DIR)
@echo " CROSS $< (with -lpthread)"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread

# test-nanosleep-process-signal parks several threads in a sleep and sends the
# group one signal.
$(BUILD_DIR)/test-nanosleep-process-signal: \
Expand Down Expand Up @@ -573,10 +587,10 @@ $(BUILD_DIR)/test-sigtimedwait: tests/test-sigtimedwait.c | $(BUILD_DIR)
@echo " CROSS $< (with -lpthread)"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread

# test-osync-requeue drives a raw FUTEX_REQUEUE against a plain-FUTEX_WAIT
# waiter (musl unlock_requeue pattern) to guard the os_sync wake-at-source
# degradation; needs -lpthread.
$(BUILD_DIR)/test-osync-requeue: tests/test-osync-requeue.c | $(BUILD_DIR)
# test-futex-requeue-latency drives a raw FUTEX_REQUEUE against a
# plain-FUTEX_WAIT waiter (musl unlock_requeue pattern), which a requeue that
# only walked part of the waiters would strand; needs -lpthread.
$(BUILD_DIR)/test-futex-requeue-latency: tests/test-futex-requeue-latency.c | $(BUILD_DIR)
@echo " CROSS $< (with -lpthread)"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread

Expand Down
4 changes: 0 additions & 4 deletions scripts/check-eintr-contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@
"A finite epoll wait has spent part of the guest's timeout, and ready "
"events outrank leader work because kqueue consumes the edge.",
),
"runtime/futex.c::futex_os_sync_wait": (
"forbids",
"The address-wait path plain FUTEX_WAIT takes; its deadline is " "relative.",
),
"runtime/futex.c::futex_wait_inner": (
"forbids",
"Relative FUTEX_WAIT has spent part of its timeout; FUTEX_WAIT_BITSET "
Expand Down
3 changes: 1 addition & 2 deletions src/core/shim.S
Original file line number Diff line number Diff line change
Expand Up @@ -1009,8 +1009,7 @@ attn_bail:
* because every wait there genuinely blocks.
*
* No address check beyond alignment. The host resolves nothing for a wake
* either: futex_wake matches enqueued waiters by raw uaddr, and the os_sync
* top-up returns 0 rather than EFAULT when guest_ptr fails. So an unmapped or
* either: futex_wake matches enqueued waiters by raw uaddr. So an unmapped or
* tagged address answers 0 on both sides, and hashing the raw value here is
* what keeps the two agreeing.
*/
Expand Down
688 changes: 178 additions & 510 deletions src/runtime/futex.c

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/runtime/futex.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ void futex_interrupt_request(void);
void futex_interrupt_clear(void);
int futex_interrupt_consume(void);

/* Tell @t that a signal it can take was queued, and wake it if it is parked in
* FUTEX_WAIT or FUTEX_WAIT_BITSET. The kick is remembered, so one that lands
* before @t parks is answered when it does. FUTEX_LOCK_PI and futex_waitv do
* not take part and keep their polling quantum.
*/
void futex_kick(thread_entry_t *t);

/* Main futex syscall entry point. op: futex operation (FUTEX_WAIT, FUTEX_WAKE,
* etc.) uaddr: guest virtual address of the futex word val: expected value
* (WAIT) or max wakeups (WAKE) timeout_gva: guest pointer to timespec (or 0 for
Expand Down
37 changes: 29 additions & 8 deletions src/runtime/thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,21 @@ _Static_assert(THREAD_SLOT_KEPT(tpending.pending),
"tpending.pending is cleared by the memset");
_Static_assert(THREAD_SLOT_KEPT(in_syscall),
"in_syscall is cleared by the memset");
_Static_assert(THREAD_SLOT_KEPT(futex_park_lock),
"futex_park_lock is cleared by the memset");
_Static_assert(THREAD_SLOT_KEPT(futex_park_cond),
"futex_park_cond is cleared by the memset");
_Static_assert(THREAD_SLOT_KEPT(futex_kick),
"futex_kick is cleared by the memset");

/* Clear a slot that thread_alloc is about to hand to a new thread.
*
* A whole-struct memset cannot be used. Three scans walk the table lock-free
* and each loads active and then a payload field with no lock in between, so
* one of them can still be reading this slot after it went inactive and while
* the recycle runs. Plain-writing any field they read is a data race, so the
* five they touch are stored atomically and the memset skips them.
* A whole-struct memset cannot be used. The scans that walk the table lock-free
* each load active and then a payload field with no lock in between, so one of
* them can still be reading this slot after it went inactive and while the
* recycle runs. Plain-writing any field they read is a data race, so the memset
* skips them all: five are stored atomically here, and the futex park fields
* are left as the last owner cleared them (runtime/thread.h).
*/
static void thread_slot_clear(thread_entry_t *t)
{
Expand Down Expand Up @@ -648,9 +655,9 @@ static void thread_join_workers_mode(bool exec_mode)
* re-check latency of every such state or a worker that WOULD wind down
* cleanly gets abandoned on timing alone: the interruptible io wait
* re-checks every 200ms (io.c wait helper) and the futex paths every 100ms
* (FUTEX_OS_SYNC_POLL_CAP_NS quanta), so 500ms covers the slowest bound
* with margin. One shared deadline keeps worst-case shutdown at the cap
* even with many stuck workers, rather than multiplying a per-worker cap by
* (FUTEX_POLL_QUANTUM_NS quanta), so 500ms covers the slowest bound with
* margin. One shared deadline keeps worst-case shutdown at the cap even
* with many stuck workers, rather than multiplying a per-worker cap by
* nworkers. A worker still alive past the cap is detached and, if this pass
* claimed its handle, marked join_abandoned so a later pass does not touch
* it again. The vCPU is NOT destroyed here because HVF vCPUs are
Expand Down Expand Up @@ -853,6 +860,20 @@ bool thread_signal_deliverable(uint64_t sigbit)
return false;
}

void thread_kick_futex_waiters(uint64_t shared_pending)
{
/* Lock-free scan, on the same terms as thread_signal_deliverable above. A
* stale blocked or pending read costs a kick the thread answers by parking
* again, or misses one its own polling quantum still covers. The caller is
* skipped: it tests its own pending signals on the way out.
*/
THREAD_FOR_EACH_ACTIVE_RELAXED (t) {
uint64_t pending = pending_load(&t->tpending.pending) | shared_pending;
if (t != current_thread && (pending & ~thread_blocked_load(t)))
futex_kick(t);
}
}

/* Fork quiesce. */

bool thread_quiesce_siblings(void)
Expand Down
36 changes: 26 additions & 10 deletions src/runtime/thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@
* signal API without an include cycle.
*/
typedef struct thread_entry {
/* Read without thread_lock. Three scans walk the table lock-free
* (thread_pending_union, thread_tid_alive, thread_signal_deliverable): each
* loads active, then one of blocked, guest_tid, or tpending.pending, with
* nothing in between. A scanner that saw active as 1 just before this slot
* went inactive can still be loading one of those when thread_alloc
* recycles the slot, so none of them may be plain-written there. They are
* grouped here, cleared with atomic stores, and deliberately excluded from
* the memset that clears the rest. Keep them ahead of the rest of the
* struct; thread.c asserts that each one sits below where its memset
* starts.
/* Read without thread_lock. Four scans walk the table lock-free
* (thread_pending_union, thread_tid_alive, thread_signal_deliverable,
* thread_kick_futex_waiters): each loads active, then one or two of
* blocked, guest_tid, and tpending.pending, with nothing in between. A
* scanner that saw active as 1 just before this slot went inactive can
* still be loading one of those when thread_alloc recycles the slot, so
* none of them may be plain-written there. They are grouped here, cleared
* with atomic stores, and deliberately excluded from the memset that clears
* the rest. Keep them ahead of the rest of the struct; thread.c asserts
* that each one sits below where its memset starts.
*/
_Atomic int active; /* Non-zero while thread is running. Stays int (not
* bool) because lock-free paths in thread.c use
Expand All @@ -74,6 +74,15 @@ typedef struct thread_entry {
*/
_Atomic int32_t in_syscall;

/* Where this thread is parked in a futex wait, for futex_kick, which loads
* these with no lock (runtime/futex.c states the protocol). Not cleared on
* recycle: a thread leaves its wait before it exits, and a kick left set
* costs the next owner one early pass of its wait loop.
*/
pthread_mutex_t *_Atomic futex_park_lock; /* Mutex of a condvar park */
pthread_cond_t *_Atomic futex_park_cond; /* Valid under that mutex */
_Atomic uint32_t futex_kick; /* A kick not answered yet */

/* Thread-directed pending signals (Linux task->pending). tgkill/tkill and
* pthread_kill queue here so only this thread consumes them. Written and
* read under the signal module's sig_lock; do not touch without it. Only
Expand Down Expand Up @@ -468,6 +477,13 @@ void thread_wake_exit_waiters(void);
*/
bool thread_signal_deliverable(uint64_t sigbit);

/* Kick every other thread that has a pending signal it does not block, so one
* parked in a futex wait re-checks now rather than when its polling quantum
* ends. @shared_pending is the process-directed pending set. Lock-free scan;
* safe from any thread.
*/
void thread_kick_futex_waiters(uint64_t shared_pending);
Comment thread
xalestar marked this conversation as resolved.
Resolved

/* Fork quiesce helpers. */

/* Quiesce all sibling vCPUs for fork snapshot consistency. Calls hv_vcpus_exit
Expand Down
9 changes: 8 additions & 1 deletion src/syscall/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,14 @@
* cwd_lock dropped
* pid_lock (syscall/proc.c): process table / wait state
* pidfd_lock (syscall/proc-pidfd.c): pidfd registry
* futex bucket (runtime/futex.c): per-bucket, index-ordered if >1
* futex bucket (runtime/futex.c): per-bucket, index-ordered if >1. Also
* taken alone by futex_kick, beneath
* whatever lock a signal is queued under,
* the FUSE session lock among them. That
* closes no cycle: a bucket lock holds only
* another bucket's and a futex_waitv group
* lock beneath it, and a signal is queued
* under neither
* cwd_lock (syscall/proc-state.c): cached guest cwd. A leaf on every
* path but one: proc_acquire_cwd_view
* returns still holding it, and both
Expand Down
5 changes: 5 additions & 0 deletions src/syscall/signal.c
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,10 @@ void signal_set_shim_globals_guest(guest_t *g)
* wait -- which hv_vcpus_exit cannot reach because it is not inside hv_vcpu_run
* -- wakes and rechecks signal_pending(). Matches how exit_group and
* futex_interrupt already signal the pipe.
*
* A futex wait is on neither, so it gets a kick of its own. The shared set is
* read without sig_lock, like the pending hint: a signal queued before this
* call is in it, and a bit that has since been taken costs one spare kick.
*/
static inline void attention_raise(void)
{
Expand All @@ -552,6 +556,7 @@ static inline void attention_raise(void)
else
thread_interrupt_all();
wakeup_pipe_signal();
thread_kick_futex_waiters(pending_load(&sig_state.shared.pending));
}

/* Predicate matches the deliverability gate used by signal_queue and
Expand Down
Loading
Loading