mutex 锁机制简介
互斥体的语义
- 一次只有一个任务可以持有互斥锁
- 只有所有者才能解锁互斥体
- 不允许多次解锁
- 不允许递归锁定
- 互斥对象必须通过 API 初始化
- 互斥对象不得通过 memset 或复制来初始化
- 任务可能无法在持有互斥体的情况下退出
- 不得释放持有锁的内存区域
- 持有的互斥体不得重新初始化
- 互斥体不能在硬件或软件中断上下文中使用,例如微线程和定时器
核心数据结构
struct mutex {
/*
* 当前拥有互斥锁的线程标识。以原子方式存储和访问线程标识。
* owner 为持有所的任务的地址 | 标志位
* 原子操作要去地址是64位对齐的,所以低三位的地址固定为 0,这里借用低三位来做为锁状态的标志位
*/
atomic_long_t owner;
// 原子自旋锁,用于保护互斥锁的等待队列(wait_list)和其他相关字段的访问。
raw_spinlock_t wait_lock;
/*
* midill path
* 乐观自旋队列(optimistic spin queue),用于在互斥锁的拥有者上自旋等待。
*/
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq;
#endif
//该锁的等待队列,在慢速路径上等待该锁的所有任务都挂载此处
struct list_head wait_list;
//用于调试目的的特殊字段
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
//一个锁依赖映射(lockdep_map),用于跟踪锁的依赖关系。
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
锁的状态
-
MUTEX_FLAG_WAITERS
该互斥锁至少有一个等待被唤醒的任务在等待队列上
-
MUTEX_FLAG_HANDOFF
该互斥锁正在从一个任务转移到另一个任务
-
MUTEX_FLAG_PICKUP
该互斥锁已经完成了转移,即该互斥锁正在被一个新任务接管
锁的原理
在锁的结构体中有一个重要的成员变量 owner
,该成员变量保存了锁的持有者的地址和锁的状态信息,而我们在判断一个锁是否被持有的标准也是查看此变量是否为 0 —— 因为一个变量或函数的地址为 0 表示该地址无效。
如果一个互斥锁的 owner
不为 0 表示被该锁已经被持有,这时我们将申请该锁的任务加入到锁的等待队列 wait_list
上。等到锁被释放时,就从等待队列上摘取最早等待的任务,并唤醒该任务去获取锁。
因为互斥锁只能同时被一个任务所持有,所以每次只唤醒一个任务,而没有必要唤醒所有的任务从而产生惊群现象。
锁的三种路径
-
快速路径
尝试通过使用当前任务的
atomic_long_try_cmpxchg_acquire()
函数来原子地获取锁 。
这只适用于无竞争的情况(cmpxchg() 检查 0UL,因此上面的所有 3 个状态位都必须为 0)。
如果锁被争用,它将转到下一个可能的路径。 -
中速路径
中速路经又名乐观旋转,在锁所有者正在运行并且没有其他具有更高优先级(need_resched)的任务准备运行时
尝试旋转以获取。 因为如果锁拥有者正在运行,很可能很快就会释放锁。互斥量旋转器使用 MCS 锁进行排队,以便只有一个旋转器可以竞争互斥量。 -
慢速路径
这是最后的手段,如果等待者仍然无法获取锁,则将任务添加到等待队列中并休眠,直到被解锁路径唤醒。
正常情况下,它会以 TASK_UNINTERRUPTIBLE 状态阻塞。
MCS 锁
-
介绍
MCS(Mellor-Crummey and Scott)锁是一种用于实现互斥访问的自旋锁算法。它提供了一种公平的互斥机制,允许等待线程按照先到先服务的顺序获取锁,避免了饥饿问题。
-
MCS 锁的基本思想
将等待线程组织成一个链表,每个线程都持有一个自己的节点(MCS 节点)。这个链表的头节点是当前拥有锁的线程,而每个节点都包含一个指向下一个节点的指针。当一个线程想要获取 MCS 锁时,它需要在链表中申请一个节点,并将节点的指针设置为 NULL。然后,线程在前一个节点上自旋等待,直到它的前一个节点将其指针设置为该节点。这样,线程就可以安全地进入临界区。
当线程完成临界区的操作后,它会检查自己的节点的指针是否为 NULL,如果不为 NULL,说明后续有等待的线程,线程需要释放自己的节点,并通知下一个等待的线程可以获取锁。
-
优缺点
MCS 锁的主要优点是提供了公平性,保证了等待线程按照先到先服务的顺序获取锁,避免了饥饿问题。
然而,与其他自旋锁算法相比,MCS 锁的实现相对较复杂,需要维护链表和节点之间的指针关系。
在实际使用中,MCS 锁通常用于多处理器系统或多核心系统中,以提供对共享资源的互斥访问。它在一些操作系统和并发编程框架中得到广泛应用。
锁的核心函数
- 加锁
快速路径函数
static __always_inline bool __mutex_trylock_fast(struct mutex *lock)
{
unsigned long curr = (unsigned long)current;
unsigned long zero = 0UL;
// 只执行一次原子交换指令,成功返回 true 失败返回 false
if (atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr))
return true;
return false;
}
中速路径函数
static __always_inline bool 年mutex_optimistic_spin(struct mutex *lock, struct ww_acquire_ctx *ww_ctx,
struct mutex_waiter *waiter)
{
if (!waiter) {
/*
* mutex_can_spin_on_owner() 函数的目的是消除 osq_lock() 和 osq_unlock() 的开销,
* 以防无法进行旋转。 由于 waiter-spinner 无论如何都不会获取 OSQ 锁,
* 因此无需调用 mutex_can_spin_on_owner()。
*/
if (!mutex_can_spin_on_owner(lock))
goto fail;
/*
* 为了避免互斥旋转器试图一次性获取互斥锁的蜂拥而至,
* 旋转器需要先获取 MCS(排队)锁,然后再在所有者字段上旋转。
*/
if (!osq_lock(&lock->osq))
goto fail;
}
// 自旋,直到没有 owner 或者 系统不允许自旋
for (;;) {
struct task_struct *owner;
/* Try to acquire the mutex... */
owner = __mutex_trylock_or_owner(lock);
if (!owner)
break;
// 有一个 owner,等待它释放锁或进入睡眠状态。
if (!mutex_spin_on_owner(lock, owner, ww_ctx, waiter))
goto fail_unlock;
cpu_relax();
}
if (!waiter)
osq_unlock(&lock->osq);
return true;
fail_unlock:
if (!waiter)
osq_unlock(&lock->osq);
fail:
/*
* 如果我们因为 need_resched() 而脱离了旋转路径,请在尝试锁定互斥锁之前立即重新安排。
* 这可以避免在我们获得互斥体后立即被调度出去。
*/
if (need_resched()) {
__set_current_state(TASK_RUNNING);
schedule_preempt_disabled();
}
return false;
}
慢速路径函数
static __always_inline int __sched
__mutex_lock_common(struct mutex *lock, unsigned int state, unsigned int subclass,
struct lockdep_map *nest_lock, unsigned long ip,
struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx)
{
struct mutex_waiter waiter;
struct ww_mutex *ww;
int ret;
if (!use_ww_ctx)
ww_ctx = NULL;
// 调度抢占点
might_sleep();
MUTEX_WARN_ON(lock->magic != lock);
ww = container_of(lock, struct ww_mutex, base);
// 在 ww_ctx 存在的情况下,如果两个地址相同,则说明是已经加锁的情况
if (ww_ctx) {
if (unlikely(ww_ctx == READ_ONCE(ww->ctx)))
return -EALREADY;
/*
* 被 kill (接收到信号) 后重置受伤标志。
* 没有其他进程可以在这里竞争并伤害我们,
* 因为如果我们没有持有任何锁,它们就无法拥有有效的所有者指针。
*/
if (ww_ctx->acquired == 0)
ww_ctx->wounded = 0;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
nest_lock = &ww_ctx->dep_map;
#endif
}
preempt_disable();
// 锁递归检查
mutex_acquire_nest(&lock->dep_map, subclass, 0, nest_lock, ip);
// 尝试上锁成功(通过快速路径 || 乐观路径)
if (__mutex_trylock(lock) ||
mutex_optimistic_spin(lock, ww_ctx, NULL)) {
lock_acquired(&lock->dep_map, ip);
if (ww_ctx)
ww_mutex_set_context_fastpath(ww, ww_ctx);
preempt_enable();
return 0;
}
// 要操作等待队列了,所以要先加锁 wait_lock 保护,再操作等待队列
raw_spin_lock(&lock->wait_lock);
// 在将任务加入等待对列前再次检查是否可以获取锁,如果获取到锁直接跳出
if (__mutex_trylock(lock)) {
if (ww_ctx)
__ww_mutex_check_waiters(lock, ww_ctx);
goto skip_wait;
}
debug_mutex_lock_common(lock, &waiter);
// 等待节点的任务为当前任务
waiter.task = current;
if (use_ww_ctx)
waiter.ww_ctx = ww_ctx;
lock_contended(&lock->dep_map, ip);
if (!use_ww_ctx) {
// 将任务按照 FIFO 的方式加入到锁的等待队列中
__mutex_add_waiter(lock, &waiter, &lock->wait_list);
} else {
// 按照中速路径将任务加入到等待队列中
ret = __ww_mutex_add_waiter(&waiter, lock, ww_ctx);
if (ret)
goto err_early_kill;
}
// 设置当前任务的状态
set_current_state(state);
for (;;) {
bool first;
// 再次获取锁
if (__mutex_trylock(lock))
goto acquired;
// 检查等前任务的信号,如果有挂起的信号,则退出
if (signal_pending_state(state, current)) {
ret = -EINTR;
goto err;
}
if (ww_ctx) {
ret = __ww_mutex_check_kill(lock, &waiter, ww_ctx);
if (ret)
goto err;
}
// 解锁等待队列的锁
raw_spin_unlock(&lock->wait_lock);
// 该函数为开启抢占,调度,返回后恢复禁止抢占
schedule_preempt_disabled();
// 检查是否是第一个等待者。
first = __mutex_waiter_is_first(lock, &waiter);
set_current_state(state);
// 锁移交成功
if (__mutex_trylock_or_handoff(lock, first) ||
(first && mutex_optimistic_spin(lock, ww_ctx, &waiter)))
break;
// 获取锁失败,则重新操作——为了和前面保持一直,这里需要加锁循环
raw_spin_lock(&lock->wait_lock);
}
// 获取锁成功后,需要操作等待队列,所以加锁
raw_spin_lock(&lock->wait_lock);
acquired:
// 设置当前任务状态为运行态
__set_current_state(TASK_RUNNING);
if (ww_ctx) {
if (!ww_ctx->is_wait_die &&
!__mutex_waiter_is_first(lock, &waiter))
__ww_mutex_check_waiters(lock, ww_ctx);
}
// 从等待队列上删除本节点
__mutex_remove_waiter(lock, &waiter);
debug_mutex_free_waiter(&waiter);
skip_wait:
lock_acquired(&lock->dep_map, ip);
if (ww_ctx)
ww_mutex_lock_acquired(ww, ww_ctx);
// 释放锁,并允许抢占(其中回运行一次抢占调度)
raw_spin_unlock(&lock->wait_lock);
preempt_enable();
return 0;
err:
// 如果出错,恢复任务正常的运行态,并从等待队列中删除自己
__set_current_state(TASK_RUNNING);
__mutex_remove_waiter(lock, &waiter);
err_early_kill:
raw_spin_unlock(&lock->wait_lock);
debug_mutex_free_waiter(&waiter);
mutex_release(&lock->dep_map, ip);
preempt_enable();
return ret;
}
- 解锁
static noinline void __sched __mutex_unlock_slowpath(struct mutex *lock, unsigned long ip)
{
struct task_struct *next = NULL;
DEFINE_WAKE_Q(wake_q);
unsigned long owner;
// 调用 lockdep 中的 release 函数,用户释放锁检查
mutex_release(&lock->dep_map, ip);
/*
* 在(可能)获取自旋锁之前释放锁,以便其他竞争者可以尽快继续处理事情。
*
* 除非 HANDOFF 时,在这种情况下我们不能清除所有者字段,而是将其设置为顶级等待者。
*/
owner = atomic_long_read(&lock->owner);
for (;;) {
MUTEX_WARN_ON(__owner_task(owner) != current);
MUTEX_WARN_ON(owner & MUTEX_FLAG_PICKUP);
// 锁正在交接
if (owner & MUTEX_FLAG_HANDOFF)
break;
// 清除锁的状态;如果老状态为存在等待者,则跳出循环,否则退出
if (atomic_long_try_cmpxchg_release(&lock->owner, &owner, __owner_flags(owner))) {
if (owner & MUTEX_FLAG_WAITERS)
break;
return;
}
}
// 要操作等待队列,所以使用 wait_lock 进行保护
raw_spin_lock(&lock->wait_lock);
debug_mutex_unlock(lock);
// 在等待列表不为空时,从等待列表中选取第一个任务,并将其放入到唤醒等待队列中
if (!list_empty(&lock->wait_list)) {
struct mutex_waiter *waiter =
list_first_entry(&lock->wait_list,
struct mutex_waiter, list);
next = waiter->task;
debug_mutex_wake_waiter(lock, waiter);
wake_q_add(&wake_q, next);
}
// 如果锁允许交接,则直接设置该任务为锁的持有者
if (owner & MUTEX_FLAG_HANDOFF)
__mutex_handoff(lock, next);
raw_spin_unlock(&lock->wait_lock);
// 唤醒等待队列上的任务
wake_up_q(&wake_q);
}