diff options
| author | Denis V. Lunev <den@openvz.org> | 2026-06-10 13:58:50 +0200 |
|---|---|---|
| committer | Kevin Wolf <kwolf@redhat.com> | 2026-08-03 10:42:29 +0200 |
| commit | e554413bd2b4d6e1f31eebfb499bce5238b91646 (patch) | |
| tree | c9d6a0b973242321b5a4b7470d5e3d62fe3684a0 /util | |
| parent | 5add514ba36ed2cc2a5aa3be64c4adc72d4d9003 (diff) | |
| download | qemu-e554413bd2b4d6e1f31eebfb499bce5238b91646.tar.gz qemu-e554413bd2b4d6e1f31eebfb499bce5238b91646.zip | |
coroutine: fix lost wakeup in qemu_co_sleep_wake()
cache_clean_timer_del_and_wait() cancels the cache-cleaner coroutine
by setting s->cache_clean_interval = 0 and calling qemu_co_sleep_wake()
to cut short its qemu_co_sleep_ns_wakeable(). qemu_co_sleep_wake() is
fire-and-forget: it reads w->to_wake and silently returns when it is
NULL. A sleeper that is between two iterations -- has just released
s->lock but has not yet set w->to_wake inside qemu_co_sleep() -- loses
the wake:
iothread0 timer coroutine main thread (qcow2 close)
------------------------- -------------------------
while-body (holding s->lock):
read interval = 600
wait_ns = 600 * NS
release s->lock
take s->lock
interval = 0
qemu_co_sleep_wake(w):
w->to_wake == NULL -> skip
return
qemu_co_queue_wait(exit, s->lock):
release s->lock
yield
qemu_co_sleep_ns_wakeable:
aio_timer_init(+600 s)
qemu_co_sleep:
cas scheduled NULL -> "qsns"
w->to_wake = co
yield [sleeps 600 s]
cache_clean_timer_del_and_wait() then blocks on cache_clean_timer_exit
until the original 600 s expiry fires, and qcow2_close() holds BQL the
whole time so the VM stalls behind it.
block_copy_kick() has the same shape. Fix the primitive once instead
of working around it in each caller.
Use a tri-state for QemuCoSleep::to_wake:
NULL - idle
co - sleeper parked
PENDING - wake delivered, no sleeper yet (sticky)
qemu_co_sleep_wake() xchgs PENDING into to_wake: a real sleeper is
woken, NULL/PENDING is left untouched so the wake stays sticky.
qemu_co_sleep() cmpxchg-publishes itself as the sleeper; if a wake
was delivered before it got there or races the publish, the cmpxchg
observes PENDING and returns without yielding. On normal resume
qemu_co_sleep() clears the PENDING the waker left behind so the next
sleep starts clean.
A double-fire (real wake plus timer callback) is harmless: the first
xchg returns the coroutine and wakes it; the second returns PENDING
and is a no-op. Cancellation latency through qemu_co_sleep_wake() is
now bounded by aio_co_wake() rather than by the sleep duration.
Fixes: f86dde9a15 ("qcow2: Fix cache_clean_timer")
Signed-off-by: Denis V. Lunev <den@openvz.org>
Cc: Hanna Czenczek <hreitz@redhat.com>
Cc: Kevin Wolf <kwolf@redhat.com>
Message-ID: <20260610115850.2410566-2-den@openvz.org>
Reviewed-by: Kevin Wolf <kwolf@redhat.com>
Signed-off-by: Kevin Wolf <kwolf@redhat.com>
Diffstat (limited to 'util')
| -rw-r--r-- | util/qemu-coroutine-sleep.c | 53 |
1 files changed, 38 insertions, 15 deletions
diff --git a/util/qemu-coroutine-sleep.c b/util/qemu-coroutine-sleep.c index edef117284..19ded0b6fd 100644 --- a/util/qemu-coroutine-sleep.c +++ b/util/qemu-coroutine-sleep.c @@ -18,20 +18,29 @@ static const char *qemu_co_sleep_ns__scheduled = "qemu_co_sleep_ns"; +/* + * Sentinel stored in QemuCoSleep::to_wake by qemu_co_sleep_wake() when no + * sleeper has parked yet. The next qemu_co_sleep() consumes it and returns + * without yielding, so a wake that races the arming of a sleep is never + * lost. + */ +#define QEMU_CO_SLEEP_PENDING ((Coroutine *)(uintptr_t)1) + void qemu_co_sleep_wake(QemuCoSleep *w) { Coroutine *co; - co = w->to_wake; - w->to_wake = NULL; - if (co) { - /* Write of schedule protected by barrier write in aio_co_schedule */ - const char *scheduled = qatomic_cmpxchg(&co->scheduled, - qemu_co_sleep_ns__scheduled, NULL); - - assert(scheduled == qemu_co_sleep_ns__scheduled); - aio_co_wake(co); + co = qatomic_xchg(&w->to_wake, QEMU_CO_SLEEP_PENDING); + if (co == NULL || co == QEMU_CO_SLEEP_PENDING) { + /* No sleeper, or a wake is already pending. */ + return; } + + /* Write of scheduled protected by barrier write in aio_co_schedule */ + const char *scheduled = qatomic_cmpxchg(&co->scheduled, + qemu_co_sleep_ns__scheduled, NULL); + assert(scheduled == qemu_co_sleep_ns__scheduled); + aio_co_wake(co); } static void co_sleep_cb(void *opaque) @@ -43,6 +52,7 @@ static void co_sleep_cb(void *opaque) void coroutine_fn qemu_co_sleep(QemuCoSleep *w) { Coroutine *co = qemu_coroutine_self(); + Coroutine *prev; const char *scheduled = qatomic_cmpxchg(&co->scheduled, NULL, qemu_co_sleep_ns__scheduled); @@ -53,11 +63,23 @@ void coroutine_fn qemu_co_sleep(QemuCoSleep *w) abort(); } - w->to_wake = co; + /* + * Publish ourselves as the sleeper. A wake delivered before we got here, + * or one racing this publish, leaves QEMU_CO_SLEEP_PENDING in to_wake; + * the cmpxchg then fails and we consume the wake without yielding. + */ + prev = qatomic_cmpxchg(&w->to_wake, NULL, co); + if (prev == QEMU_CO_SLEEP_PENDING) { + qatomic_set(&w->to_wake, NULL); + qatomic_set(&co->scheduled, NULL); + return; + } + assert(prev == NULL); + qemu_coroutine_yield(); - /* w->to_wake is cleared before resuming this coroutine. */ - assert(w->to_wake == NULL); + /* The waker left QEMU_CO_SLEEP_PENDING; clear it for the next sleep. */ + qatomic_set(&w->to_wake, NULL); } void coroutine_fn qemu_co_sleep_ns_wakeable(QemuCoSleep *w, @@ -70,9 +92,10 @@ void coroutine_fn qemu_co_sleep_ns_wakeable(QemuCoSleep *w, timer_mod(&ts, qemu_clock_get_ns(type) + ns); /* - * The timer will fire in the current AiOContext, so the callback - * must happen after qemu_co_sleep yields and there is no race - * between timer_mod and qemu_co_sleep. + * A wake racing with the arming of the sleep -- including the timer + * we just armed firing in another AioContext before qemu_co_sleep() + * publishes itself -- is captured by the sticky PENDING state in + * qemu_co_sleep_wake() and consumed here without yielding. */ qemu_co_sleep(w); timer_del(&ts); |
