diff --git a/core/include/mutex.h b/core/include/mutex.h index e0072e7974..db2dd7fd72 100644 --- a/core/include/mutex.h +++ b/core/include/mutex.h @@ -126,6 +126,18 @@ typedef struct { list_node_t queue; } mutex_t; +/** + * @brief A cancellation structure for use with @ref mutex_lock_cancelable + * and @ref mutex_cancel + * + * @note The contents of this structure are internal. + */ +typedef struct { + mutex_t *mutex; /**< The mutex to lock */ + thread_t *thread; /**< The thread trying to lock the mutex */ + uint8_t cancelled; /**< Flag whether the mutex has been cancelled */ +} mutex_cancel_t; + /** * @brief Static initializer for mutex_t. * @details This initializer is preferable to mutex_init(). @@ -158,6 +170,23 @@ static inline void mutex_init(mutex_t *mutex) mutex->queue.next = NULL; } +/** + * @brief Initialize a mutex cancellation structure + * @param mutex The mutex that the calling thread wants to lock + * @return The cancellation structure for use with @ref mutex_lock_cancelable + * and @ref mutex_cancel + * + * @note This function is considered internal. Out of tree users should be + * aware that breaking API changes or removal of this API without + * an deprecation period might happen. + */ +static inline mutex_cancel_t mutex_cancel_init(mutex_t *mutex) +{ + mutex_cancel_t result = { mutex, thread_get_active(), 0 }; + + return result; +} + /** * @brief Tries to get a mutex, non-blocking. * @@ -174,10 +203,11 @@ static inline int mutex_trylock(mutex_t *mutex) { unsigned irq_state = irq_disable(); int retval = 0; + if (mutex->queue.next == NULL) { mutex->queue.next = MUTEX_LOCKED; retval = 1; - }; + } irq_restore(irq_state); return retval; } @@ -195,6 +225,30 @@ static inline int mutex_trylock(mutex_t *mutex) */ void mutex_lock(mutex_t *mutex); +/** + * @brief Locks a mutex, blocking. This function can be canceled. + * + * @param[in,out] mc Mutex cancellation structure to work on + * + * @retval 0 The mutex was locked by the caller + * @retval -ECANCELED The mutex was ***NOT*** locked, operation was + * canceled. See @ref mutex_cancel + * + * @note This function is considered internal. Out of tree users should be + * aware that breaking API changes or removal of this API without + * an deprecation period might happen. + * + * @pre Must be called in thread context + * @pre @p mc has been initialized with @ref mutex_cancel_init by the + * calling thread. + * @pre @p mc has ***NOT*** been used for previous calls to + * this function. (Reinitialize before reusing!) + * + * @post The mutex referred to by @p mc is locked and held by the calling + * thread, unless `-ECANCELED` was returned. + */ +int mutex_lock_cancelable(mutex_cancel_t *mc); + /** * @brief Unlocks the mutex. * @@ -215,6 +269,71 @@ void mutex_unlock(mutex_t *mutex); */ void mutex_unlock_and_sleep(mutex_t *mutex); +/** + * @brief Cancels a call to @ref mutex_lock_cancelable + * + * @param[in,out] mc Mutex cancellation structure referring to the + * thread calling @ref mutex_lock_cancelable and to + * the mutex to cancel the operation on + * + * @note This function is considered internal. Out of tree users should be + * aware that breaking API changes or removal of this API without + * an deprecation period might happen. + * + * @pre @p mc is used to cancel at most one call to + * @ref mutex_lock_cancelable. (You can reinitialize the same memory + * to safely reuse it.) + * @warning You ***MUST NOT*** call this function once the thread referred to by + * @p mc re-uses the mutex object referred to by @p mc (not counting + * the call to @ref mutex_lock_cancelable @p mc was used in). + * @note It is safe to call this function from IRQ context, e.g. from a timer + * interrupt. + * @note It is safe to call this function more than once on the same @p mc + * while it is still valid (see the warning above). The first call will + * cancel the operation and subsequent calls will have no effect. + * + * @details If @p thread is currently running (or pending), a subsequent call + * from @p thread to @ref mutex_lock_cancelable will also fail + * + * Canonical use: + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c} + * static void timeout_cb(void *_arg) { + * mutex_cancel(arg); + * } + * + * int ztimer_mutex_lock_timeout(ztimer_clock_t *clock, mutex_t *mutex, + * uint32_t timeout) + * { + * mutex_cancel_t mc = mutex_cancel_init(mutex); + * ztimer_t t; + * t.callback = timeout_cb; + * t.arg = &mc; + * ztimer_set(clock, &t, timeout); + * if (0 == mutex_lock_cancelable(mutex)) { + * ztimer_remove(clock, &t); + * return 0; + * } + * return -ECANCELED; + * } + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * In the above example a simple implementation of how to implement mutex + * locking with a timeout is given. There are two corner cases: + * + * 1. The call to @ref mutex_cancel could occur *before* the call to + * @ref mutex_lock_cancelable. (E.g. for `timeout == 0`.) + * 2. The call to @ref mutex_cancel could occur right after the mutex was + * *successfully* obtained, but before `ztimer_remove()` was executed. + * + * In the first corner case the cancellation is stored in @p mc. Hence, the + * subsequent call to @ref mutex_lock_cancelable gets indeed canceled. In the + * second corner case the cancellation is also stored in @p mc but never used - + * the mutex cancellation structure @p mc is not allowed to be reused without + * reinitialization. + */ +void mutex_cancel(mutex_cancel_t *mc); + #ifdef __cplusplus } #endif diff --git a/core/mutex.c b/core/mutex.c index 6ec660bd3f..f3b232a7f8 100644 --- a/core/mutex.c +++ b/core/mutex.c @@ -82,6 +82,39 @@ void mutex_lock(mutex_t *mutex) } } +int mutex_lock_cancelable(mutex_cancel_t *mc) +{ + unsigned irq_state = irq_disable(); + + DEBUG("PID[%" PRIkernel_pid "] mutex_lock_cancelable()\n", + thread_getpid()); + + if (mc->cancelled) { + DEBUG("PID[%" PRIkernel_pid "] mutex_lock_cancelable cancelled " + "early.\n", thread_getpid()); + irq_restore(irq_state); + return -ECANCELED; + } + + mutex_t *mutex = mc->mutex; + if (mutex->queue.next == NULL) { + /* mutex is unlocked. */ + mutex->queue.next = MUTEX_LOCKED; + DEBUG("PID[%" PRIkernel_pid "] mutex_lock_cancelable() early out.\n", + thread_getpid()); + irq_restore(irq_state); + return 0; + } + else { + _block(mutex, irq_state); + if (mc->cancelled) { + DEBUG("PID[%" PRIkernel_pid "] mutex_lock_cancelable() " + "cancelled.\n", thread_getpid()); + } + return (mc->cancelled) ? -ECANCELED: 0; + } +} + void mutex_unlock(mutex_t *mutex) { unsigned irqstate = irq_disable(); @@ -148,3 +181,33 @@ void mutex_unlock_and_sleep(mutex_t *mutex) irq_restore(irqstate); thread_yield_higher(); } + +void mutex_cancel(mutex_cancel_t *mc) +{ + unsigned irq_state = irq_disable(); + mc->cancelled = 1; + + mutex_t *mutex = mc->mutex; + thread_t *thread = mc->thread; + if (thread_is_active(thread)) { + /* thread is still running or about to run, so it will check + * `mc-cancelled` in time */ + irq_restore(irq_state); + return; + } + + if ((mutex->queue.next != MUTEX_LOCKED) + && (mutex->queue.next != NULL) + && list_remove(&mutex->queue, (list_node_t *)&thread->rq_entry)) { + /* Thread was queued and removed from list, wake it up */ + if (mutex->queue.next == NULL) { + mutex->queue.next = MUTEX_LOCKED; + } + sched_set_status(thread, STATUS_PENDING); + irq_restore(irq_state); + sched_switch(thread->priority); + return; + } + + irq_restore(irq_state); +} diff --git a/tests/mutex_cancel/Makefile b/tests/mutex_cancel/Makefile new file mode 100644 index 0000000000..8bff23ceea --- /dev/null +++ b/tests/mutex_cancel/Makefile @@ -0,0 +1,5 @@ +include ../Makefile.tests_common + +USEMODULE += xtimer + +include $(RIOTBASE)/Makefile.include diff --git a/tests/mutex_cancel/main.c b/tests/mutex_cancel/main.c new file mode 100644 index 0000000000..8261ea9b7f --- /dev/null +++ b/tests/mutex_cancel/main.c @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Test application for testing the mutex_cancel function in + * core_mutex + * + * @author Marian Buschsieweke + * @} + */ + +#include +#include + +#include "mutex.h" +#include "test_utils/expect.h" +#include "thread.h" +#include "xtimer.h" + +static mutex_t testlock = MUTEX_INIT; + +static void cb_unlock(void *mutex) +{ + mutex_unlock(mutex); +} + +static void cb_cancel(void *mc) +{ + mutex_cancel(mc); +} + +int main(void) +{ + xtimer_t xt; + puts( + "Test Application for mutex_cancel / mutex_lock_cancelable\n" + "=========================================================\n" + ); + + printf("%s: ", "Test without cancellation"); + mutex_cancel_t mc = mutex_cancel_init(&testlock); + expect(mutex_lock_cancelable(&mc) == 0); + puts("OK"); + + printf("%s: ", "Test early cancellation"); + mc = mutex_cancel_init(&testlock); + mutex_cancel(&mc); + expect(mutex_lock_cancelable(&mc) == -ECANCELED); + puts("OK"); + + printf("%s: ", "Verify no side effects on subsequent calls"); + mc = mutex_cancel_init(&testlock); + xt.callback = cb_unlock; + xt.arg = &testlock; + xtimer_set(&xt, US_PER_MS * 10); + expect(mutex_lock_cancelable(&mc) == 0); + puts("OK"); + + printf("%s: ", "Test late cancellation"); + mc = mutex_cancel_init(&testlock); + xt.callback = cb_cancel; + xt.arg = &mc; + xtimer_set(&xt, US_PER_MS * 10); + expect(mutex_lock_cancelable(&mc) == -ECANCELED); + puts("OK"); + + puts("TEST PASSED"); + + return 0; +} diff --git a/tests/mutex_cancel/tests/01-run.py b/tests/mutex_cancel/tests/01-run.py new file mode 100755 index 0000000000..60dd8f94f3 --- /dev/null +++ b/tests/mutex_cancel/tests/01-run.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +# @author Marian Buschsieweke + +import sys +from testrunner import run + + +def testfunc(child): + child.expect("TEST PASSED") + + +if __name__ == "__main__": + sys.exit(run(testfunc))