blob: b219e17be1de6c9527dbf85cd3022e6f82549e8b [file]
/*
* Copyright (c) 2016 Wind River Systems, Inc.
* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief mutex kernel services
*
* This module contains routines for handling mutex locking and unlocking.
*
* Mutexes implement a priority inheritance algorithm that boosts the priority
* level of the owning thread to match the priority level of the highest
* priority thread waiting on any mutex it holds.
*
* When a thread pends on a contended mutex, the priority boost propagates
* through the ownership chain: if the mutex owner is itself blocked on
* another mutex, that owner is also boosted, and so on until the end of
* the chain, a deadlock cycle is detected, or the walk's hop limit is
* reached (see Known limitations below).
*
* Each thread maintains a list of all mutexes it currently holds
* (held_mutexes). On unlock, the thread's priority is recalculated by
* scanning the wait queues of all remaining held mutexes, ensuring the
* correct priority is maintained when multiple mutexes are involved.
*
* Known limitations:
* - Bounded chain walk: the ownership-chain walk stops after
* MUTEX_CHAIN_WALK_MAX_HOPS hops. Owners beyond that hop count keep their
* pre-boost priority. This is a deliberate bound on the work done under
* mutex_lock, not a claim that chains longer than the limit cannot
* legally form.
* - Deferred priority drop on timeout: when a waiter times out, the owner's
* priority is adjusted in the waiter's context after it resumes, not at ISR
* level. The owner may run at a slightly higher priority than it should for
* a short time, but all threads will eventually run correctly.
* Fixing this requires acquiring mutex_lock from the timer handler, which
* is not safe: on single-core, mutex_lock disables interrupts and the ISR
* would spin forever if a thread holds it; on SMP, it reverses the lock
* ordering in k_mutex_unlock (mutex_lock -> _sched_spinlock) and deadlocks.
* - Chain priority-down not propagated on timeout: when a timeout occurs in a
* multi-hop ownership chain, only the immediate owner's priority is adjusted;
* deeper owners remain over-boosted until they release their mutexes. The
* impact and self-correcting nature are the same as above.
* Propagating the drop through the full chain would require a chain walk of
* unbounded length in the timer handler under the scheduler lock, adding
* unbounded ISR latency.
*/
#include <zephyr/kernel.h>
#include <zephyr/toolchain.h>
#include <ksched.h>
#include <scheduler.h>
#include <kthread.h>
#include <wait_q.h>
#include <errno.h>
#include <zephyr/init.h>
#include <zephyr/internal/syscall_handler.h>
#include <zephyr/tracing/tracing.h>
#include <zephyr/sys/check.h>
#include <zephyr/logging/log.h>
#include <zephyr/llext/symbol.h>
LOG_MODULE_DECLARE(os, CONFIG_KERNEL_LOG_LEVEL);
/*
* We use a global spinlock rather than a per-object lock because the
* priority-inheritance chain walk, the held-mutexes recalculation, the
* K_FOREVER deadlock check, and the unlock handoff all read or write
* fields (owner, mutex_pended_on, held_mutexes) belonging to mutexes
* other than the one passed to the API entry point.
*/
static struct k_spinlock mutex_lock;
#ifdef CONFIG_OBJ_CORE_MUTEX
static struct k_obj_type obj_type_mutex;
#endif /* CONFIG_OBJ_CORE_MUTEX */
int z_impl_k_mutex_init(struct k_mutex *mutex)
{
mutex->owner = NULL;
mutex->lock_count = 0U;
z_waitq_init(&mutex->wait_q);
#if Z_MUTEX_PI_ENABLED
mutex->held_node.next = NULL;
#endif
k_object_init(mutex);
#ifdef CONFIG_OBJ_CORE_MUTEX
k_obj_core_init_and_link(K_OBJ_CORE(mutex), &obj_type_mutex);
#endif /* CONFIG_OBJ_CORE_MUTEX */
SYS_PORT_TRACING_OBJ_INIT(k_mutex, mutex, 0);
return 0;
}
#ifdef CONFIG_USERSPACE
static inline int z_vrfy_k_mutex_init(struct k_mutex *mutex)
{
K_OOPS(K_SYSCALL_OBJ_INIT(mutex, K_OBJ_MUTEX));
return z_impl_k_mutex_init(mutex);
}
#include <zephyr/syscalls/k_mutex_init_mrsh.c>
#endif /* CONFIG_USERSPACE */
#if Z_MUTEX_PI_ENABLED
/*
* Upper bound on ownership-chain walk hops. This is a bound on walk cost
* under mutex_lock, not a real limit on chain length -- equal-priority
* threads can legally form longer chains.
*/
#define MUTEX_CHAIN_WALK_MAX_HOPS 16
static int32_t new_prio_for_inheritance(int32_t target, int32_t limit)
{
int new_prio = z_is_prio_higher(target, limit) ? target : limit;
new_prio = z_get_new_prio_with_ceiling(new_prio);
return new_prio;
}
static bool adjust_owner_prio(struct k_mutex *mutex, int32_t new_prio)
{
if (mutex->owner->base.prio != new_prio) {
LOG_DBG("%p (ready (y/n): %c) prio changed to %d (was %d)",
mutex->owner, z_is_thread_ready(mutex->owner) ?
'y' : 'n',
new_prio, mutex->owner->base.prio);
return z_thread_prio_set(mutex->owner, new_prio);
}
return false;
}
/*
* Scan all mutexes held by a thread and return the highest priority
* among all their waiters, floored at floor_prio.
*/
static int32_t held_mutexes_highest_waiter_prio(struct k_thread *thread,
int32_t floor_prio)
{
sys_snode_t *node;
int32_t prio = floor_prio;
LOCK_SCHED_SPINLOCK {
SYS_SLIST_FOR_EACH_NODE(&thread->held_mutexes, node) {
struct k_mutex *m =
CONTAINER_OF(node, struct k_mutex, held_node);
struct k_thread *waiter = z_waitq_head(&m->wait_q);
if (waiter != NULL) {
int32_t wprio = z_get_new_prio_with_ceiling(waiter->base.prio);
if (z_is_prio_higher(wprio, prio)) {
prio = wprio;
}
}
}
}
return prio;
}
#endif
int z_impl_k_mutex_lock(struct k_mutex *mutex, k_timeout_t timeout)
{
k_spinlock_key_t key;
#if Z_MUTEX_PI_ENABLED
bool resched = false;
#endif
__ASSERT(!arch_is_in_isr(), "mutexes cannot be used inside ISRs");
SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_mutex, lock, mutex, timeout);
key = k_spin_lock(&mutex_lock);
if (likely((mutex->lock_count == 0U) || (mutex->owner == _current))) {
mutex->lock_count++;
mutex->owner = _current;
#if Z_MUTEX_PI_ENABLED
LOG_DBG("%p took mutex %p, count: %d, orig prio: %d",
_current, mutex, mutex->lock_count,
_current->orig_prio);
/* Add to held list on first acquisition; recursive locks skip this. */
if (mutex->lock_count == 1U) {
/* Capture the thread's true pre-inheritance priority
* the first time it acquires any mutex.
*/
if (sys_slist_is_empty(&_current->held_mutexes)) {
_current->orig_prio = _current->base.prio;
}
sys_slist_append(&_current->held_mutexes, &mutex->held_node);
}
#else
LOG_DBG("%p took mutex %p, count: %d",
_current, mutex, mutex->lock_count);
#endif
k_spin_unlock(&mutex_lock, key);
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, lock, mutex, timeout, 0);
return 0;
}
if (unlikely(K_TIMEOUT_EQ(timeout, K_NO_WAIT))) {
k_spin_unlock(&mutex_lock, key);
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, lock, mutex, timeout, -EBUSY);
return -EBUSY;
}
SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_mutex, lock, mutex, timeout);
#if Z_MUTEX_PI_ENABLED
/* Record the mutex this thread is blocking on for priority inheritance. */
_current->mutex_pended_on = mutex;
/*
* Walk the ownership chain, boosting each owner's priority to match
* the highest-priority waiter. Detects deadlock cycles along the way.
*/
{
int32_t boost_prio = new_prio_for_inheritance(
_current->base.prio,
mutex->owner->base.prio);
struct k_mutex *chain_mutex = mutex;
struct k_thread *chain_owner = mutex->owner;
int hops = 0;
#if defined(CONFIG_MUTEX_DEADLOCK_DETECT)
bool all_forever = K_TIMEOUT_EQ(timeout, K_FOREVER);
#endif
LOG_DBG("chain boosting prio on mutex %p", mutex);
while (chain_owner != NULL) {
#if defined(CONFIG_MUTEX_DEADLOCK_DETECT)
/*
* Deadlock detection: only when _current and every
* chain member visited so far wait forever. all_forever
* is updated on every hop, so a finite timeout anywhere
* in the chain -- not just at the hop that closes the
* cycle -- correctly rules out a false positive.
* z_is_thread_pending() guards against a stale
* mutex_pended_on in a thread that timed out but has
* not yet cleared the field.
*/
all_forever = all_forever &&
z_is_inactive_timeout(&chain_owner->base.timeout);
if (all_forever &&
chain_owner->mutex_pended_on != NULL &&
z_is_thread_pending(chain_owner) &&
chain_owner->mutex_pended_on->owner == _current) {
__ASSERT(false,
"mutex deadlock: thread %p waiting on "
"mutex %p (owner %p)",
_current, mutex, mutex->owner);
break;
}
#endif /* CONFIG_MUTEX_DEADLOCK_DETECT */
if (z_is_prio_higher(boost_prio, chain_owner->base.prio)) {
resched = adjust_owner_prio(chain_mutex, boost_prio)
|| resched;
}
/*
* Stop the chain walk if the owner is not actually
* pending — its mutex_pended_on may be stale (e.g. it
* just timed out but has not yet cleared the field).
* Always boost the current owner first, then decide
* whether to continue.
*/
if (chain_owner->mutex_pended_on == NULL ||
!z_is_thread_pending(chain_owner)) {
break;
}
chain_mutex = chain_owner->mutex_pended_on;
/*
* Cap the walk to prevent spinning on a cycle that
* does not pass through the starting mutex.
* MUTEX_CHAIN_WALK_MAX_HOPS bounds walk cost under
* mutex_lock; it is not a real limit on chain length.
* The chain_mutex == mutex check is a fast path for
* the common 2-hop cycle through the starting mutex.
*/
if (chain_mutex == mutex ||
++hops >= MUTEX_CHAIN_WALK_MAX_HOPS) {
break;
}
chain_owner = chain_mutex->owner;
}
}
#endif
int got_mutex = z_pend_curr(&mutex_lock, key, &mutex->wait_q, timeout);
LOG_DBG("on mutex %p got_mutex value: %d", mutex, got_mutex);
LOG_DBG("%p got mutex %p (y/n): %c", _current, mutex,
got_mutex ? 'y' : 'n');
#if Z_MUTEX_PI_ENABLED
if (got_mutex == 0) {
/* Lock granted; clear the pending mutex pointer. */
_current->mutex_pended_on = NULL;
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, lock, mutex, timeout, 0);
return 0;
}
/* timed out */
LOG_DBG("%p timeout on mutex %p", _current, mutex);
key = k_spin_lock(&mutex_lock);
/*
* Clear mutex_pended_on under mutex_lock so no concurrent chain walk
* can observe the stale pointer and detect a false deadlock cycle.
*/
_current->mutex_pended_on = NULL;
/*
* Check if mutex was unlocked after this thread was unpended.
* If so, skip adjusting owner's priority down.
*/
if (likely(mutex->owner != NULL)) {
/*
* Recalculate the owner's priority across all mutexes it still
* holds; another held mutex may still justify a partial boost.
*/
int32_t new_prio = held_mutexes_highest_waiter_prio(
mutex->owner,
mutex->owner->orig_prio);
LOG_DBG("adjusting prio down on mutex %p", mutex);
resched = adjust_owner_prio(mutex, new_prio) || resched;
}
if (resched) {
z_reschedule(&mutex_lock, key);
} else {
k_spin_unlock(&mutex_lock, key);
}
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, lock, mutex, timeout, -EAGAIN);
return -EAGAIN;
#else
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, lock, mutex, timeout, got_mutex);
return got_mutex;
#endif
}
#ifdef CONFIG_USERSPACE
static inline int z_vrfy_k_mutex_lock(struct k_mutex *mutex,
k_timeout_t timeout)
{
K_OOPS(K_SYSCALL_OBJ(mutex, K_OBJ_MUTEX));
return z_impl_k_mutex_lock(mutex, timeout);
}
#include <zephyr/syscalls/k_mutex_lock_mrsh.c>
#endif /* CONFIG_USERSPACE */
int z_impl_k_mutex_unlock(struct k_mutex *mutex)
{
struct k_thread *new_owner = NULL;
__ASSERT(!arch_is_in_isr(), "mutexes cannot be used inside ISRs");
SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_mutex, unlock, mutex);
CHECKIF(mutex->owner == NULL) {
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, unlock, mutex, -EINVAL);
return -EINVAL;
}
/*
* The current thread does not own the mutex.
*/
CHECKIF(mutex->owner != _current) {
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, unlock, mutex, -EPERM);
return -EPERM;
}
/*
* Attempt to unlock a mutex which is unlocked. mutex->lock_count
* cannot be zero if the current thread is equal to mutex->owner,
* therefore no underflow check is required. Use assert to catch
* undefined behavior.
*/
__ASSERT_NO_MSG(mutex->lock_count > 0U);
LOG_DBG("mutex %p lock_count: %d", mutex, mutex->lock_count);
/*
* If we are the owner and count is greater than 1, then decrement
* the count and return and keep current thread as the owner.
*/
if (mutex->lock_count > 1U) {
mutex->lock_count--;
goto k_mutex_unlock_return;
}
k_spinlock_key_t key = k_spin_lock(&mutex_lock);
#if Z_MUTEX_PI_ENABLED
/* Remove this mutex from the owner's list of held mutexes */
sys_slist_find_and_remove(&_current->held_mutexes, &mutex->held_node);
/*
* If the thread's priority was boosted, recalculate it by scanning
* the remaining held mutexes. The released mutex has already been
* removed from held_mutexes, so it is not included in the scan.
*/
if (_current->base.prio != _current->orig_prio) {
int32_t new_prio = held_mutexes_highest_waiter_prio(
_current, _current->orig_prio);
adjust_owner_prio(mutex, new_prio);
}
#endif
/* Pick the new owner (if any) and complete the wake atomically
* under _sched_spinlock, so a racing in-flight timeout handler
* cannot observe a half-initialized wake-up.
*/
LOCK_SCHED_SPINLOCK {
new_owner = z_unpend_first_thread_locked(&mutex->wait_q);
mutex->owner = new_owner;
LOG_DBG("new owner of mutex %p: %p (prio: %d)",
mutex, new_owner, new_owner ? new_owner->base.prio : -1000);
if (unlikely(new_owner != NULL)) {
/*
* new owner is already of higher or equal prio than first
* waiter since the wait queue is priority-based: no need to
* adjust its priority
*/
#if Z_MUTEX_PI_ENABLED
if (sys_slist_is_empty(&new_owner->held_mutexes)) {
new_owner->orig_prio = new_owner->base.prio;
}
sys_slist_append(&new_owner->held_mutexes, &mutex->held_node);
new_owner->mutex_pended_on = NULL;
#endif
arch_thread_return_value_set(new_owner, 0);
z_sched_ready_locked(new_owner);
} else {
mutex->lock_count = 0U;
}
}
if (unlikely(new_owner != NULL)) {
z_reschedule(&mutex_lock, key);
} else {
k_spin_unlock(&mutex_lock, key);
}
k_mutex_unlock_return:
SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_mutex, unlock, mutex, 0);
return 0;
}
#ifdef CONFIG_USERSPACE
static inline int z_vrfy_k_mutex_unlock(struct k_mutex *mutex)
{
K_OOPS(K_SYSCALL_OBJ(mutex, K_OBJ_MUTEX));
return z_impl_k_mutex_unlock(mutex);
}
#include <zephyr/syscalls/k_mutex_unlock_mrsh.c>
#endif /* CONFIG_USERSPACE */
#ifdef CONFIG_OBJ_CORE_MUTEX
K_OBJ_TYPE_DEFINE(obj_type_mutex, k_mutex, K_OBJ_TYPE_MUTEX_ID, NULL);
#endif /* CONFIG_OBJ_CORE_MUTEX */