summaryrefslogtreecommitdiff
path: root/util
diff options
context:
space:
mode:
authorDenis V. Lunev <den@openvz.org>2026-06-10 13:58:50 +0200
committerKevin Wolf <kwolf@redhat.com>2026-08-03 10:42:29 +0200
commite554413bd2b4d6e1f31eebfb499bce5238b91646 (patch)
treec9d6a0b973242321b5a4b7470d5e3d62fe3684a0 /util
parent5add514ba36ed2cc2a5aa3be64c4adc72d4d9003 (diff)
downloadqemu-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.c53
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);