tech-kern archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

Re: How to wait on cv_timedwait() correctly



> Date: Tue, 11 Feb 2025 13:58:41 +0100
> From: Stephan <stephanwib%googlemail.com@localhost>
> 
> The only thing left to mention is that I am working on a 2 years old
> snapshot of the source as syncing to upstream does not seem to be
> easy.

Aha!  It looks like netbsd-10 shipped with this bug, which was in HEAD
between April 2020 and October 2023.


The bug -- which is more a performance bug than a correctness bug,
because it never loses wakeups but does cause more spurious wakeups --
was introduced in this commit in April 2020:

https://mail-index.netbsd.org/source-changes/2020/04/10/msg116017.html

That changed cv_signal so that it may wake more than one
_interruptible_ waiter (cv_timedwait, cv_wait_sig, cv_timedwait_sig).
It will still wake at most one _noninterruptible_ waiter (cv_wait).

If you change your code to use cv_wait instead of cv_timedwait, I bet
you'll observe that (just as an illustration -- obviously I expect you
really need the timeout).


Why would we have done this?  The motivation for that commit was to
allow users like zfs to do

	cv_broadcast(cv);
	cv_destroy(cv);

and not crash.  The reason this used to crash is that the cv_*wait*
calls that can fail, like cv_timedwait, i.e., the interruptible ones,
used to do something like this to avoid _losing_ explicit wakeups in
case of interruption:

	error = sleepq_block(...);
	if (error) {
		/* pass the explicit wakeup along to the next thread */
		cv_signal(cv);
	}

Having cv_signal wake all the interruptible waiters (at least until it
finds a noninterruptible one) obviates the need for interruptible
waiters to touch the cv by calling cv_signal after wakeup to pass the
explicit wakeup along.  So, with that change, you can safely
cv_destroy immediately after cv_broadcast.  But the change has the
side effect of waking more waiters than needed in some cases.


This is another unfortunate consequence of the behaviour of the
cv_*wait* routines where they can release the lock, sleep, re-acquire
the lock, and then fail with a nonzero error -- leaving the caller
with the confusing situation of having to re-check the condition _and_
consider the error code.

If the cv_*wait* routines were all guaranteed to return zero in the
event they had released and re-acquired the lock, this problem would
go away.  Unfortunately, while cv_wait_sig could work that way because
signal-pending state is persistent (so when you call it in a loop,
it'll notice the signal-pending state on the next iteration),
cv_timedwait can't because it doesn't keep state.  So we're stuck with
complex internal logic to work around the hazards of the bad API of
cv_timedwait.


(A long time previously, I had locally patched zfs not
to cv_broadcast/cv_destroy:

https://mail-index.netbsd.org/source-changes/2012/10/15/msg037924.html

But that local patch got lost at some point and it's a hefty
maintenance burden.)


The fix, committed in October 2023, was to do some careful bookkeeping
in sleepq_remove so that sleepq_block never returns an error code from
a legitimate wakeup:

https://mail-index.netbsd.org/source-changes/2023/10/08/msg147974.html

But that fix wasn't pulled up to netbsd-10 (and it might be hard, lots
of other nearby changes to disentangle), so netbsd-10 still issues a
lot of spurious wakeups for interruptible waits like cv_timedwait.

This would all be easier if we ditched the awful cv_timedwait API!
Even zfs mostly ignores the return value.


The attached module is a simpler test for the bug (build with
bsd.kmodule.mk, KMOD=cvsignal SRCS=cvsignal.c; change cv_wait to
cv_timedwait or cv_wait_sig to taste).
/*	$NetBSD$	*/

/*-
 * Copyright (c) 2025 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD$");

#include <sys/param.h>

#include <sys/condvar.h>
#include <sys/kthread.h>
#include <sys/module.h>
#include <sys/mutex.h>
#include <sys/proc.h>

MODULE(MODULE_CLASS_MISC, cvsignal, NULL);

static bool done;
static bool woken;
static kmutex_t lock;
static kcondvar_t cv;
static struct lwp *sleeper[3];

static void
doit(void *cookie)
{

	mutex_enter(&lock);
	while (!done)
		cv_wait(&cv, &lock);
	printf("sleeper %ld: %swakeup\n", (long)curlwp->l_lid,
	    woken ? "" : "first ");
	if (!woken) {
		woken = true;
		kpause("slppause", /*intr*/false, 2*hz, &lock);
		cv_broadcast(&cv);
	}
	mutex_exit(&lock);

	kthread_exit(0);
}

static int
cvsignal_modcmd(modcmd_t cmd, void *arg __unused)
{
	unsigned i;
	int error;

	switch (cmd) {
	case MODULE_CMD_INIT:
		mutex_init(&lock, MUTEX_DEFAULT, IPL_NONE);
		cv_init(&cv, "slpwait");
		for (i = 0; i < __arraycount(sleeper); i++) {
			error = kthread_create(PRI_NONE,
			    KTHREAD_MPSAFE|KTHREAD_MUSTJOIN, /*cpu*/NULL,
			    &doit, NULL, &sleeper[i], "sleeper/%u", i);
			if (error) {
				printf("%s: kthread_create: %d\n", __func__,
				    error);
				break;
			}
		}
		kpause("slpinit", /*intr*/false, hz, NULL);
		printf("waking one thread\n");
		mutex_enter(&lock);
		done = true;
		cv_signal(&cv);
		mutex_exit(&lock);
		break;
	case MODULE_CMD_FINI:
		for (i = __arraycount(sleeper); i --> 0;) {
			if (sleeper[i])
				(void)kthread_join(sleeper[i]);
		}
		cv_destroy(&cv);
		mutex_destroy(&lock);
		break;
	default:
		return ENOTTY;
	}

	return 0;
}


Home | Main Index | Thread Index | Old Index