1.概述
Mutex互斥锁是Linux内核中用于互斥操作的一种同步原语;
互斥锁是一种休眠锁,锁争用时可能存在进程的睡眠与唤醒,context的切换带来的代价较高,适用于加锁时间较长的场景;
互斥锁每次只允许一个进程进入临界区,有点类似于二值信号量;
互斥锁在锁争用时,在锁被持有时,选择自选等待,而不立即进行休眠,可以极大的提高性能,这种机制(optimistic spinning)也应用到了读写信号量上;
互斥锁的缺点是互斥锁对象的结构较大,会占用更多的CPU缓存和内存空间;
与信号量相比,互斥锁的性能与扩展性都更好,因此,在内核中总是会优先考虑互斥锁;
互斥锁按为了提高性能,提供了三条路径处理:快速路径,中速路径,慢速路径;
1. mutex是具有严格语义的简单、直接的互斥锁:
(1) 一次只能有一个任务持锁
(2) 只有锁的持有者才能释放锁
(3) 不允许多次释放锁
(4) 不允许递归持锁
(5) 必须通过 API 初始化锁
(6) 不能通过 memset 或拷贝来初始化锁
(7) 任务不应该在持锁的情况下退出
(8) 不能释放锁所在的内存区域
(9) 已经持有的锁不能重复初始化
(10) 此锁不能在硬中断或软中断上下文使用,例如 tasklets 和计时器
当启用 DEBUG_MUTEXES 时,这些语义将完全强制执行。 此外,除了强制执行上述规则外,互斥锁调试代码还实现了许多使锁调试更容易和更快的附加功能:
(1) 只要它们在调试输出中打印就使用互斥体的符号名称
(2) 获取点跟踪,函数名称的符号查找
(3) 打印输出系统中所有锁的列表,
(4) 所有者跟踪
(5) 检测自递归锁并打印出所有相关信息
(6) 检测多任务循环死锁并打印出所有受影响的锁和任务(并且只有那些任务)
注:上述来自 include/linux/mutex.h 中 struct mutex 结构的注释信息。
2. 一个简单的mutex工作原理图
传统的mutex只需要一个状态标记和一个等待队列就OK了,等待队列中是一个个阻塞的线程,thread owner当前持有mutex,当它离开临界区释放锁的时候,会唤醒等待队列中第一个线程(top waiter),这时候top waiter会去竞争持锁,如果成功,那么从等待队列中摘下,成为owner。如果失败,继续保持阻塞状态,等待owner释放锁的时候唤醒它。在owner task持锁过程中,如果有新的任务来竞争mutex,那么就会进入阻塞状态并插入等待队列的尾部。
相对于传统的mutex,linux内核进行了一些乐观自旋的优化,也就是说当线程持锁失败的时候,可以选择在mutex状态标记上自旋,等待owner释放锁,也可以选择进入阻塞状态并挂入等待队列。具体如何选择是在自旋等待的时间开销和进程上下文切换的开销之间进行平衡。此外为了防止多个线程自旋带来的性能问题,mutex的乐观自旋机制还引入了MCS锁,后面章节我们会详细描述。
3.相关结构和原理
3.1 struct mutex
struct mutex {
/*
* 1. 标记mutex对象被哪一个task(struct task_struct*)持有,如果为NULL表示还没有
* 被任何一个任务持有。
* 2. 由于task_struct至少是8字节对齐的,其最低3bit可以用来做标志位,分别为:
* (1) MUTEX_FLAG_WAITERS:表示 wait_list 成员链表非空,即有任务阻塞在此mutex锁上,
* owner在unlock的时候必须要执行唤醒动作。
* (2) MUTEX_FLAG_HANDOFF:为了防止mutex等待队列中的任务饿死,在唤醒top waiter时会
* 设置这个标志(由于乐观自旋任务的不断插入,唤醒的top waiter任务也不一定能获取到锁),
* 设置这个标志后,owner在解锁时会将锁直接转交给top waiter,而不是让唤醒的top waiter
* 去竞争锁。
* (3) MUTEX_FLAG_PICKUP:此标志表示mutex已经完事具备(即完成了转交),只等待top waiter
* 来持锁。
*/
atomic_long_t owner;
//用于保护 wait_list 成员链表的自旋锁
spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER //默认使能
/*
* 在配置了 MUTEX_SPIN_ON_OWNER 的时候,mutex支持乐观自旋机制,osq成员就是乐观自旋时需
* 要持有的MCS锁队列,其只有一个tail成员,如果等于0说明是个空队列,没有乐观自旋任务。否
* 则tail指向队列的尾部。注意tail不是指针是个cpu number,optimistic_spin_node对象是per-cpu
* 的,有cpu number就能定位到 optimistic_spin_node 对象。
*/
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
/*
* mutex是个睡眠锁,当任务无法获取到锁又不具备乐观自旋条件时会挂入到这个等待队列,等待owner
* 释放锁。
*/
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES //默认关闭
//debug相关成员
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC //默认关闭
//debug相关成员
struct lockdep_map dep_map;
#endif
ANDROID_OEM_DATA_ARRAY(1, 2);
};
注:只要是64bit机器且结构体中有大于等于8字节的成员,结构体间的对齐就是8字节对齐的,最低3bit可以用来做标志位。
大部分的成员都非常好理解,除了osq这个成员,其工作原理示意图如下:
注:图上串联的是 struct optimistic_spin_node 结构。
3.1.1 乐观自旋和handoff机制
1.乐观自旋
在早版本的mutex中,进程拿不到锁就会将自己加入等待队列没然后等待持锁者释放锁时将自己唤醒。如果持锁者非常快就释放锁,等锁仍然需要经历睡眠-唤醒的开销,并且会带来额外的延迟,但是mutex的属性又决定了等锁不能一直占用cpu,所以使用了既要减少延迟又不过分占用cpu的折中机制,乐观自旋。
如果了解spinlock就知道自旋本质就是循环等待,spinlock就是通过不停的循环检测条件是否满足,直到条件满足才会退出循环,伪代码如下:
while(1) {
if 条件满足可以拿锁
break;
}
这种自旋永远不会让出CPU,好处是实时性特别好,一旦别人释放锁等待者马上可以持锁,坏处也显而易见,会占满一个cpu用于等锁。
而乐观自旋则适用于对实时性要求不高的场合,乐观指的是当持锁者还在cpu上运行时,可以期待持锁者很快就会释放锁,这时候我们可以占用cpu等待,但是占用时间又不能过长。等调度器告诉我们该放开cpu的时候,要及时的将自己调度走。这个机制伪代码如下:
while(1) {
if 条件满足
goto 持锁
if 需要调度 | 持锁者不在运行状态
return 乐观自旋失败
}
加入等待队列
让出cpu,开始睡眠
2.osq_lock
osq_lock 是 MCS lock以及qspinlock的一种定制化,主要服务于mutex以及rw-sem两种锁,很少独立使用,所以这里介绍简单一点。
他也是一种乐观自旋机制,意味着满足乐观自旋条件(锁者还在cpu上运行 && 调度器没有要求我们让出cpu)的时候,可以一直占用cpu进行等待。
如果不满足自旋条件,退出,返回失败。
3.handoff 机制
handoff这里应该翻译为“接力”,它的出现是为了弥补乐观自旋带来的漏洞,既mutex并非先到先得。锁大多被设计为先到先得,是为了实现公平,防止某些进程一直得不到资源被饿死。而乐观自旋是为了减少进程切换的开销,降低锁带来的延迟,但是乐观自旋会打破先到先得的规则。
假设有A, B, C, D四个进程,进程A持有mutex,进程B也需要这把锁,所以它开始乐观自旋等待。但是它所在的CPU比较繁忙,很快调度器告诉它该放弃cpu了,因此B将自己加入等待队列队首,开始睡眠。此时C D依次开始等锁,它们运气比较好,都在乐观自旋等待。
A释放锁时,按照规则应该将其交给B,但是B已经睡眠,将其唤醒拿锁会有额外的开销,于是A释放锁,让乐观自旋C的进程拿锁,C释放锁时,D在乐观自旋等待,此时如果仍然考虑开销而将锁交给D,那么某些情况下B的等待时间会非常长。
因此A释放锁时,C拿到锁,但是A仍然会唤醒B,B醒来发现自己还是不能持锁,于是在锁owner里面加上MUTEX_FLAG_HANDOFF flag,告诉当前持锁者 C,“我已经错过一次了,下次不要直接释放锁让别人去拿,而是把锁给我”,这样C释放锁时检测到handoff flag会直接把锁的owner设置成B,并且加入新的flag MUTEX_FLAG_PICKUP,相当于通知B “我已经把锁给你了”,然后唤醒B。
#define MUTEX_FLAG_HANDOFF 0x02
#define MUTEX_FLAG_PICKUP 0x04
4、外部接口
4.1 初始化接口
1 静态初始化
owner初始化为0,wait_lock初始化为unlock状态,wait_list初始化为空链表头。
#define DEFINE_MUTEX(mutexname) \
struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
#define __MUTEX_INITIALIZER(lockname) \
{ .owner = ATOMIC_LONG_INIT(0) \
, .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock) \
, .wait_list = LIST_HEAD_INIT(lockname.wait_list) \
__DEBUG_MUTEX_INITIALIZER(lockname) \
__DEP_MAP_MUTEX_INITIALIZER(lockname) }
2 动态初始化
与静态的结果类似,但是需要预先申请mutex的内存。
void
__mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key)
{
atomic_long_set(&lock->owner, 0);
spin_lock_init(&lock->wait_lock);
INIT_LIST_HEAD(&lock->wait_list);
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
osq_lock_init(&lock->osq);
#endif
...
}
4.2 持锁
大致流程如下图所示:
详细介绍mutex_lock:
-
mutex_lock为了提高性能,分为三种路径处理,优先使用快速和中速路径来处理,如果条件不满足则会跳转到慢速路径来处理,慢速路径中会进行睡眠和调度,因此开销也是最大的。
void __sched mutex_lock(struct mutex *lock)
{
might_sleep();
//__mutex_trylock_fast直接调用atomic_long_cmpxchg_release(&lock->owner, 0UL, curr)函数来进行判断,
//如果lock->owner == 0表明锁未被持有,将curr赋值给lock->owner标识curr进程持有该锁,并直接返回;
if (!__mutex_trylock_fast(lock))
__mutex_lock_slowpath(lock);
}
EXPORT_SYMBOL(mutex_lock);
static __always_inline bool __mutex_trylock_fast(struct mutex *lock)
{
unsigned long curr = (unsigned long)current;
unsigned long zero = 0UL;
if (atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr))
return true;
return false;
}
快速路径是在__mutex_trylock_fast中实现的,该函数的实现也很简单,直接调用atomic_long_cmpxchg_release(&lock->owner, 0UL, curr)函数来进行判断,如果lock->owner == 0表明锁未被持有,将curr赋值给lock->owner标识curr进程持有该锁,并直接返回;
lock->owner不等于0,表明锁被持有,需要进入下一个路径来处理了;
慢速路径:
static noinline void __sched
__mutex_lock_slowpath(struct mutex *lock)
{
__mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_);
}
static int __sched
__mutex_lock(struct mutex *lock, long state, unsigned int subclass,
struct lockdep_map *nest_lock, unsigned long ip)
{
return __mutex_lock_common(lock, state, subclass, nest_lock, ip, NULL, false);
}
中速路径和慢速路径的处理都是在__mutex_lock_common中实现的;
__mutex_lock_common的传入参数为(lock, TASK_INTERRUPTIBLE, 0, NULL, RET_IP, false),该函数中很多路径覆盖不到,接下来的分析也会剔除掉无效代码。
4.21__mutex_lock_common
// mutex_lock 传参:(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_, NULL, false)
static __always_inline int __sched
__mutex_lock_common(struct mutex *lock, long 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;//赋值为NULL,之后都不用看它了
might_sleep();//该函数可能sleep
#ifdef CONFIG_DEBUG_MUTEXES
DEBUG_LOCKS_WARN_ON(lock->magic != lock);
#endif
ww = container_of(lock, struct ww_mutex, base);
if (ww_ctx) {
if (unlikely(ww_ctx == READ_ONCE(ww->ctx)))
return -EALREADY;
/*
* Reset the wounded flag after a kill. No other process can
* race and wound us here since they can't have a valid owner
* pointer if we don't have any locks held.
*/
if (ww_ctx->acquired == 0)
ww_ctx->wounded = 0;
}
preempt_disable();//关闭抢占
mutex_acquire_nest(&lock->dep_map, subclass, 0, nest_lock, ip);
/*
* __mutex_trylock 用来再次尝试获取锁(上节已讲),mutex_optimistic_spin 则是mutex乐观自旋
*(Optimistic spinning)部分的代码。
* 这两个操作只要有其一能成功获取mutex锁,那么就直接返回了。由于没有进入阻塞状态,因此这
* 个路径也叫做中速路径。
*/
if (__mutex_trylock(lock) ||
mutex_optimistic_spin(lock, ww_ctx, NULL)) {
/* got the lock, yay! */
lock_acquired(&lock->dep_map, ip);
if (ww_ctx)
ww_mutex_set_context_fastpath(ww, ww_ctx);
preempt_enable();
return 0;
}
//------------------ 尝试持锁和乐观自旋都失败了 --------------------------
spin_lock(&lock->wait_lock); /* 持有 lock->wait_lock 锁 */
/*
* After waiting to acquire the wait_lock, try again.
*/
//因为spinlock可能会消耗一点时间,这段时间内所得状态可能发生变化,再尝试一次
if (__mutex_trylock(lock)) {
if (ww_ctx)
__ww_mutex_check_waiters(lock, ww_ctx);
goto skip_wait;
}
debug_mutex_lock_common(lock, &waiter);
lock_contended(&lock->dep_map, ip);
if (!use_ww_ctx) {
/* add waiting tasks to the end of the waitqueue (FIFO): */
//将自身使用尾插加入等待队列,如果我们是等待队列的队首,那么将owner加上MUTEX_FLAG_WAITERS标记,说明等待队列现在不为空了
__mutex_add_waiter(lock, &waiter, &lock->wait_list);
#ifdef CONFIG_DEBUG_MUTEXES
waiter.ww_ctx = MUTEX_POISON_WW_CTX;
#endif
} else {
/*
* Add in stamp order, waking up waiters that must kill
* themselves.
*/
ret = __ww_mutex_add_waiter(&waiter, lock, ww_ctx);
if (ret)
goto err_early_kill;
waiter.ww_ctx = ww_ctx;
}
waiter.task = current;
//将自身设置为TASK_UNINTERRUPTIBLE状态
set_current_state(state);
for (;;) {
bool first;
/*
* Once we hold wait_lock, we're serialized against
* mutex_unlock() handing the lock off to us, do a trylock
* before testing the error conditions to make sure we pick up
* the handoff.
*/
if (__mutex_trylock(lock))// 再次尝试获取lock
goto acquired;
/*
* Check for signals and kill conditions while holding
* wait_lock. This ensures the lock cancellation is ordered
* against mutex_unlock() and wake-ups do not go missing.
*/
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;
}
spin_unlock(&lock->wait_lock);/* 释放 lock->wait_lock 锁 */
//开抢占,然后将自己调度出去让出cpu
//开始睡眠,之前使用set_current_state将自身设置为UN状态,不会被调度器自动调度,需要等待其他进程唤醒
//被唤醒之后,关闭抢占,继续向下运行
schedule_preempt_disabled();
//这里就是被其他进程唤醒了
//first用来标记我们是否是等待队列的队首
/*
* 该任务被唤醒之后,如果是等待队列中的第一个任务,即top waiter,那么需要给该 mutex 的 owner
* 设置 MUTEX_FLAG_HANDOF 标志,这样即便本次唤醒后无法获取到mutex(有些在该mutex上乐观自旋的
* 任务可能会抢先获得锁),那么下一次owner释放锁的时候,看到这个handoff标记也会进行锁的交接,
* 不再是大家抢来抢去。
* 通过这个机制,我们可以防止spinner队列中的任务抢占CPU资源,饿死waiter队列中的任务。
*
* 该任务被释放锁的动作给唤醒了,但是还是在链表中,说明抢锁没有抢过别人!若它变成top-waiter了
* 那么设置标志下次将锁给 lock->wait_list 中睡眠的线程,而不是乐观自旋的线程了。
*/
first = __mutex_waiter_is_first(lock, &waiter);
if (first)
__mutex_set_flag(lock, MUTEX_FLAG_HANDOFF);
set_current_state(state);
/*
* Here we order against unlock; we must either see it change
* state back to RUNNING and fall through the next schedule(),
* or we must see its unlock and acquire.
*//* 这里在被释放锁的行为唤醒后,再次尝试获取锁 */
/*
* 如果尝试获取到mutex,那么就退出循环。如果是等待队列中的top-waiter,那么就进入乐观自旋过程,
* 这样会有更大的机会成功获取mutex锁。
* 否则继续进入阻塞状态继续等待。
*/
if (__mutex_trylock(lock) ||
(first && mutex_optimistic_spin(lock, ww_ctx, &waiter)))
break;
spin_lock(&lock->wait_lock);
}
spin_lock(&lock->wait_lock);
acquired:
__set_current_state(TASK_RUNNING);
if (ww_ctx) {
/*
* Wound-Wait; we stole the lock (!first_waiter), check the
* waiters as anyone might want to wound us.
*/
if (!ww_ctx->is_wait_die &&
!__mutex_waiter_is_first(lock, &waiter))
__ww_mutex_check_waiters(lock, ww_ctx);
}
__mutex_remove_waiter(lock, &waiter); /* 将当前线程从 lock->wait_list 链表中删除 */
debug_mutex_free_waiter(&waiter);
skip_wait:
/* got the lock - cleanup and rejoice! */
lock_acquired(&lock->dep_map, ip);
if (ww_ctx)
ww_mutex_lock_acquired(ww, ww_ctx);
spin_unlock(&lock->wait_lock);
preempt_enable();/* 这里才开的抢占 */
return 0;
err:
__set_current_state(TASK_RUNNING);
__mutex_remove_waiter(lock, &waiter);
err_early_kill:
spin_unlock(&lock->wait_lock);
debug_mutex_free_waiter(&waiter);
mutex_release(&lock->dep_map, ip);
preempt_enable();
return ret;
}
4.22.中速路径
中速路径的核心代码如下:
当发现mutex锁的持有者正在运行(另一个CPU)时,可以不进行睡眠调度,而可以选择自选等待,当锁持有者正在运行时,它很有可能很快会释放锁,这个就是乐观自旋的原因;
自旋等待的条件是持有锁者正在临界区运行,自旋等待才有价值;
__mutex_trylock_or_owner函数用于尝试获取锁,如果获取失败则返回锁的持有者。互斥锁的结构体中owner字段,分为两个部分:1)锁持有者进程的task_struct(由于L1_CACHE_BYTES对齐,低位比特没有使用);2)MUTEX_FLAGS部分,也就是对应低三位,如下:
MUTEX_FLAG_WAITERS:比特0,标识存在非空等待者链表,在解锁的时候需要执行唤醒操作;
MUTEX_FLAG_HANDOFF:比特1,表明解锁的时候需要将锁传递给顶部的等待者;
MUTEX_FLAG_PICKUP:比特2,表明锁的交接准备已经做完了,可以等待被取走了;
mutex_optimistic_spin用于执行乐观自旋,理想的情况下锁持有者执行完释放,当前进程就能很快的获取到锁。实际需要考虑,如果锁的持有者如果在临界区被调度出去了,task_struct->on_cpu == 0,那么需要结束自旋等待了,否则岂不是傻傻等待了。
mutex_can_spin_on_owner:进入自旋前检查一下,如果当前进程需要调度,或者锁的持有者已经被调度出去了,那么直接就返回了,不需要做接下来的osq_lock/oqs_unlock工作了,节省一些额外的overhead;
osq_lock用于确保只有一个等待者参与进来自旋,防止大量的等待者蜂拥而至来获取互斥锁;
for(;;)自旋过程中调用__mutex_trylock_or_owner来尝试获取锁,获取到后皆大欢喜,直接返回即可;
mutex_spin_on_owner,判断不满足自旋等待的条件,那么返回,让我们进入慢速路径吧,毕竟不能强求;
1中速路径有两个重要函数:__mutex_trylock和mutex_optimistic_spin
__mutex_trylock:用于非阻塞的尝试持锁。成功返回true,否则返回false。
__mutex_trylock_or_owner:非阻塞的尝试持锁,成功则返回NULL,失败返回当前锁的owner。
static inline bool __mutex_trylock(struct mutex *lock)
{
return !__mutex_trylock_or_owner(lock);
}
static inline struct task_struct *__mutex_trylock_or_owner(struct mutex *lock)
{
unsigned long owner, curr = (unsigned long)current;
owner = atomic_long_read(&lock->owner);// 读取出当前的锁的持有者
for (;;) { /* must loop, can race against a flag */
// 获取owner的flag
unsigned long old, flags = __owner_flags(owner);//__owner_flags :return owner & MUTEX_FLAGS;
unsigned long task = owner & ~MUTEX_FLAGS;// MUTEX_FLAGS 0x7,// 消除指针的flag位 获取原始的task地址
if (task) {// 如果存在owner
/*
* 如果task非空,并且也不等于current thread,那么说明mutex锁被其他线程持有,还没有释放
* 锁(也有可能在释放锁的时候,把锁直接转交给了其他线程),因此直接break跳出循环,持锁
* 失败。
*/
if (likely(task != curr))// 如果owner不是本task 则获取失败
break;
// 如果当前flags不包含MUTEX_FLAG_PICKUP,也就是锁当前状态不是在等待别的task拿起获取,则获取失败
/*
* 如果持锁的task等于当前任务,而且设置了 MUTEX_FLAG_PICKUP 的标记,那么说明持锁线程已
* 经把该mutex锁转交给了本线程,等待本线程来拾取。如果没有 MUTEX_FLAG_PICKUP 标记,那么
* 也是直接break跳出循环,递归持锁失败。
*/
if (likely(!(flags & MUTEX_FLAG_PICKUP)))
break;
flags &= ~MUTEX_FLAG_PICKUP;// 说明获取锁成功 则消除flags的MUTEX_FLAG_PICKUP位
} else {
#ifdef CONFIG_DEBUG_MUTEXES
DEBUG_LOCKS_WARN_ON(flags & MUTEX_FLAG_PICKUP);
#endif
}
/*
* We set the HANDOFF bit, we must make sure it doesn't live
* past the point where we acquire it. This would be possible
* if we (accidentally) set the bit on an unlocked mutex.
*/
/* * 有两种情况会走到这里:
* (1) 一种情况是task为空,说明该mutex锁处于unlocked状态。
* (2) 一种情况是task非空,但是等于当前线程,并且mutex发生了handoff,该锁被转交给当前试图
* 持锁的线程。无论哪种情况,都可以去执行持锁操作了。
*/
flags &= ~MUTEX_FLAG_HANDOFF;// 消除flags的MUTEX_FLAG_HANDOFF位
/*
* 原子比较赋值操作,若 lock->owner 的值和 owner 的值相等,那么就对 lock->owner 赋值为
* curr|flags,否则不赋值。返回值为 lock->owner 之前的值。
*/
old = atomic_long_cmpxchg_acquire(&lock->owner, owner, curr | flags);
if (old == owner)
return NULL;
owner = old;
}
/* 持锁失败,返回持锁线程 */
return __owner_task(owner);
}
2.mutex_optimistic_spin
用于实现乐观自旋的主函数。进入这个函数时,有两种身份。第一种我们没有加入等待队列,这是第一次尝试,那我们是spiner。第二种我们加入了等待队列,那我们是waiter。
对于没有加入等待队列的spiner,需要先竞争osq_lock,同样的,osq_lock也拥有乐观自旋机制,竞争到了这把锁,才能乐观自旋等待锁的owner发生变化。
对于waiter,只有队首进程才会被唤醒,其余进程都是D状态沉睡,因此不存在大量竞争的问题,可以直接在owner上spin等待。
static __always_inline bool
mutex_optimistic_spin(struct mutex *lock, struct ww_acquire_ctx *ww_ctx,
struct mutex_waiter *waiter)
{
//对于还没有加入等待队列的等待者,需要先竞争osq_lock,防止过多的等待者竞争owner
//进入这个函数时,有两种身份。第一种我们没有加入等待队列,这是第一次尝试了关系选,
//那我们是spiner。第二种我们加入了等待队列,那我们是waiter。
if (!waiter) {
/*
* The purpose of the mutex_can_spin_on_owner() function is
* to eliminate the overhead of osq_lock() and osq_unlock()
* in case spinning isn't possible. As a waiter-spinner
* is not going to take OSQ lock anyway, there is no need
* to call mutex_can_spin_on_owner().
*/
if (!mutex_can_spin_on_owner(lock))//检测乐观自旋条件是否满足,不满足返回false
goto fail;
/*
* In order to avoid a stampede of mutex spinners trying to
* acquire the mutex all at once, the spinners need to take a
* MCS (queued) lock first before spinning on the owner field.
*/
if (!osq_lock(&lock->osq))
goto fail;
}
//在循环中等待,owner每发生一次变化,就尝试持锁一次
//直到持锁成功或者乐观自旋条件不满足,才退出循环
for (;;) {
struct task_struct *owner;
/* Try to acquire the mutex... */
owner = __mutex_trylock_or_owner(lock);//尝试非阻塞的持锁,成功返回NULL,否则返回持锁者的owner
if (!owner)
break;
/*
* There's an owner, wait for it to either
* release the lock or go to sleep.
*/
//等待owner发生变化,返回true
//如果是自旋条件不满足,自身时间用完需要被调度或者持锁者进放弃了CPU,返回false
if (!mutex_spin_on_owner(lock, owner, ww_ctx, waiter))
goto fail_unlock;
/*
* The cpu_relax() call is a compiler barrier which forces
* everything in this loop to be re-loaded. We don't need
* memory barriers as we'll eventually observe the right
* values at the cost of a few extra spins.
*/
cpu_relax();
}
if (!waiter)
osq_unlock(&lock->osq);
return true;
fail_unlock:
if (!waiter)
osq_unlock(&lock->osq);
fail:
/*
* If we fell out of the spin path because of need_resched(),
* reschedule now, before we try-lock the mutex. This avoids getting
* scheduled out right after we obtained the mutex.
*/
//如果进程是因为需要被调度才导致乐观自旋失败,那么需要做一些额外的处理
if (need_resched()) {
/*
* We _should_ have TASK_RUNNING here, but just in case
* we do not, make it so, otherwise we might get stuck.
*/
//调度器调度前需要将自身设置为R状态
//开抢占,立刻让出CPU,下次被调度回来时会返回当前函数,然后关闭抢占,返回false
__set_current_state(TASK_RUNNING);
schedule_preempt_disabled();
}
return false;
}
mutex_optimistic_spin函数会在每次唤醒时调用,如果当前task是wait-list的首位task,但是__mutex_trylock_or_owner获取锁失败,则尝试自旋等待一会,mutex_can_spin_on_owner:进入自旋前检查一下,如果当前进程需要调度,或者锁的持有者已经被调度出去了,那么直接就返回了,不需要做接下来的osq_lock/oqs_unlock工作了,节省一些额外的overhead;
mutex_spin_on_owner,判断不满足自旋等待的条件,那么返回,让我们进入慢速路径吧,毕竟不能强求;
mutex_optimistic_spin的实现细节如下,lock->osq是一个类似于MCS的自旋锁,在每次尝试加锁前会进行一些检查,确认当前有必要继续去spin在当前owner上,osq_lock用于确保只有一个等待者参与进来自旋,防止大量的等待者蜂拥而至来获取互斥锁;。
如果出现以下场景则退出spin:
- 出现need_resched标志:说明当前task即将被调度
- !task->on_cpu:当前task已经不在cpu上执行
- cpu处于preempted:cpu存在抢占
否则就尝试去获取mutex,直到成功或者没有必要继续spin则退出。
接下来,for(;;)自旋过程中调用__mutex_trylock_or_owner来尝试获取锁,获取到后皆大欢喜,直接返回即可;
static inline int mutex_can_spin_on_owner(struct mutex *lock)
{
struct task_struct *owner;
int retval = 1;
if (need_resched())
return 0;
rcu_read_lock();
owner = __mutex_owner(lock);
/*
* As lock holder preemption issue, we both skip spinning if task is not
* on cpu or its cpu is preempted
*/
if (owner)
retval = owner->on_cpu && !vcpu_is_preempted(task_cpu(owner));on_cpu为1表示锁持有者正在临界区执行
rcu_read_unlock();
/*
* If lock->owner is not set, the mutex has been released. Return true
* such that we'll trylock in the spin path, which is a faster option
* than the blocking slow path.
*/
return retval;
}
4.23.慢速路径
慢速路径的主要代码流程如下:
-
从for(;;)部分的流程可以看到,当没有获取到锁时,会调用schedule_preempt_disabled将本身的任务进行切换出去,睡眠等待,这也是它慢的原因了;
//------------------ 尝试持锁和乐观自旋都失败了 --------------------------
spin_lock(&lock->wait_lock); /* 持有 lock->wait_lock 锁 */
/*
* After waiting to acquire the wait_lock, try again.
*/
//因为spinlock可能会消耗一点时间,这段时间内所得状态可能发生变化,再尝试一次
if (__mutex_trylock(lock)) {
if (ww_ctx)
__ww_mutex_check_waiters(lock, ww_ctx);
goto skip_wait;
}
debug_mutex_lock_common(lock, &waiter);
lock_contended(&lock->dep_map, ip);
if (!use_ww_ctx) {
/* add waiting tasks to the end of the waitqueue (FIFO): */
//将自身使用尾插加入等待队列,如果我们是等待队列的队首,那么将owner加上MUTEX_FLAG_WAITERS标记,说明等待队列现在不为空了
__mutex_add_waiter(lock, &waiter, &lock->wait_list);
#ifdef CONFIG_DEBUG_MUTEXES
waiter.ww_ctx = MUTEX_POISON_WW_CTX;
#endif
} else {
/*
* Add in stamp order, waking up waiters that must kill
* themselves.
*/
ret = __ww_mutex_add_waiter(&waiter, lock, ww_ctx);
if (ret)
goto err_early_kill;
waiter.ww_ctx = ww_ctx;
}
waiter.task = current;
//将自身设置为TASK_UNINTERRUPTIBLE状态
set_current_state(state);
for (;;) {
bool first;
/*
* Once we hold wait_lock, we're serialized against
* mutex_unlock() handing the lock off to us, do a trylock
* before testing the error conditions to make sure we pick up
* the handoff.
*/
if (__mutex_trylock(lock))// 再次尝试获取lock
goto acquired;
/*
* Check for signals and kill conditions while holding
* wait_lock. This ensures the lock cancellation is ordered
* against mutex_unlock() and wake-ups do not go missing.
*/
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;
}
spin_unlock(&lock->wait_lock);/* 释放 lock->wait_lock 锁 */
//开抢占,然后将自己调度出去让出cpu
//开始睡眠,之前使用set_current_state将自身设置为UN状态,不会被调度器自动调度,需要等待其他进程唤醒
//被唤醒之后,关闭抢占,继续向下运行
schedule_preempt_disabled();
//这里就是被其他进程唤醒了
//first用来标记我们是否是等待队列的队首
/*
* 该任务被唤醒之后,如果是等待队列中的第一个任务,即top waiter,那么需要给该 mutex 的 owner
* 设置 MUTEX_FLAG_HANDOF 标志,这样即便本次唤醒后无法获取到mutex(有些在该mutex上乐观自旋的
* 任务可能会抢先获得锁),那么下一次owner释放锁的时候,看到这个handoff标记也会进行锁的交接,
* 不再是大家抢来抢去。
* 通过这个机制,我们可以防止spinner队列中的任务抢占CPU资源,饿死waiter队列中的任务。
*
* 该任务被释放锁的动作给唤醒了,但是还是在链表中,说明抢锁没有抢过别人!若它变成top-waiter了
* 那么设置标志下次将锁给 lock->wait_list 中睡眠的线程,而不是乐观自旋的线程了。
*/
first = __mutex_waiter_is_first(lock, &waiter);
if (first)
__mutex_set_flag(lock, MUTEX_FLAG_HANDOFF);
set_current_state(state);
/*
* Here we order against unlock; we must either see it change
* state back to RUNNING and fall through the next schedule(),
* or we must see its unlock and acquire.
*//* 这里在被释放锁的行为唤醒后,再次尝试获取锁 */
/*
* 如果尝试获取到mutex,那么就退出循环。如果是等待队列中的top-waiter,那么就进入乐观自旋过程,
* 这样会有更大的机会成功获取mutex锁。
* 否则继续进入阻塞状态继续等待。
*/
if (__mutex_trylock(lock) ||
(first && mutex_optimistic_spin(lock, ww_ctx, &waiter)))
break;
spin_lock(&lock->wait_lock);
}
spin_lock(&lock->wait_lock);
acquired:
__set_current_state(TASK_RUNNING);
if (ww_ctx) {
/*
* Wound-Wait; we stole the lock (!first_waiter), check the
* waiters as anyone might want to wound us.
*/
if (!ww_ctx->is_wait_die &&
!__mutex_waiter_is_first(lock, &waiter))
__ww_mutex_check_waiters(lock, ww_ctx);
}
__mutex_remove_waiter(lock, &waiter); /* 将当前线程从 lock->wait_list 链表中删除 */
debug_mutex_free_waiter(&waiter);
skip_wait:
/* got the lock - cleanup and rejoice! */
lock_acquired(&lock->dep_map, ip);
if (ww_ctx)
ww_mutex_lock_acquired(ww, ww_ctx);
spin_unlock(&lock->wait_lock);
preempt_enable();/* 这里才开的抢占 */
return 0;
err:
__set_current_state(TASK_RUNNING);
__mutex_remove_waiter(lock, &waiter);
err_early_kill:
spin_unlock(&lock->wait_lock);
debug_mutex_free_waiter(&waiter);
mutex_release(&lock->dep_map, ip);
preempt_enable();
return ret;
}
4.3 释放锁
解锁分为两种情况,1.之前存在等待队列的进程被偷取了至少一次持锁机会,该进程向owner写入了handoff。2.正常的解锁,释放锁自由竞争决定下一个持锁者。
不论如何,如果存在等待队列,都会唤醒等待队列的队首进程,这样该进程回去尝试持锁,其运行到condition_point(3)处 try_lock 失败则设置handoff机制防止自己饿死。。
存在handoff时:
1. 将队首进程加入唤醒队列
2. 将等待队列的队首的task_struct直接写入到lock->owner,并且设置pickup flag
3. 唤醒等待队列的队首进程,
不存在handoff时:
1. 清除owner的task部分,这里在owner上自旋的等待者就可以获取锁了
1. 如果等待队列存在等待者,需要继续处理,否则可以直接return
1. 将队首进程加入唤醒队列,唤醒等待队列的队首进程
void __sched mutex_unlock(struct mutex *lock)
{
#ifndef CONFIG_DEBUG_LOCK_ALLOC
if (__mutex_unlock_fast(lock))
return;
#endif
__mutex_unlock_slowpath(lock, _RET_IP_);
}
EXPORT_SYMBOL(mutex_unlock);
-
释放锁的流程相对来说比较简单,也分为快速路径与慢速路径,快速路径只有在调试的时候打开;
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;
mutex_release(&lock->dep_map, ip);
/*
* Release the lock before (potentially) taking the spinlock such that
* other contenders can get on with things ASAP.
*
* Except when HANDOFF, in that case we must not clear the owner field,
* but instead set it to the top waiter.
*/
//读取当前的owner,owner是task | flag,当前task部分为当前进程的task_struct
owner = atomic_long_read(&lock->owner);
for (;;) {
unsigned long old;
#ifdef CONFIG_DEBUG_MUTEXES
DEBUG_LOCKS_WARN_ON(__owner_task(owner) != current);
DEBUG_LOCKS_WARN_ON(owner & MUTEX_FLAG_PICKUP);
#endif
//如果owner被设置了HANDOFF,有等待者已经错过至少一次持锁机会,为了防止其饿死,需要将锁直接交给他
if (owner & MUTEX_FLAG_HANDOFF)
break;
------------------------- 无需处理HANDOFF的情况 ------------------------------
//清除owner的task部分,这里在owner上自旋的等待者就可以获取锁了
//此处只需要判断当前owner的task部分为NULL就可以了
//为何要留下flag部分?因为新的持锁者需要继承flag部分防止flag丢失
old = atomic_long_cmpxchg_release(&lock->owner, owner,
__owner_flags(owner));
if (old == owner) { //设置成功,如果等待队列存在等待者,需要继续处理,否则可以直接结束了
if (owner & MUTEX_FLAG_WAITERS)
break;
return;
}
owner = old;
}
spin_lock(&lock->wait_lock);
debug_mutex_unlock(lock);
if (!list_empty(&lock->wait_list)) {//等待队列不为空,将队首进程加入唤醒队列
/* get the first entry from the 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);
}
//需要处理handoff,将等待队列的队首的task_struct直接写入到lock->owner,并且设置pickup flag
//意味着锁不再通过竞争获取,直接交接给等待队列队首
if (owner & MUTEX_FLAG_HANDOFF)
__mutex_handoff(lock, next);
spin_unlock(&lock->wait_lock);
wake_up_q(&wake_q);//唤醒等待队列的队首进程
}