文章目录
kernel mutex 原理
mutex是可睡眠的锁,通常用于保护对实时性要求不高的数据,结构和用法也并不复杂,但是实现原理还是有一些不好理解的地方。
本文基于kernel 4.19的代码分析mutex的初始化/加锁/解锁流程,对于使用而言,并不需要去深入了解其原理,但是如果你的工作与系统稳定性相关,需要频繁的接触一些死锁,panic问题,那么就得非常熟悉常用的锁结构了。
如果你的内核版本与此不同,那么实现可能也会有所差异。分析时不考虑开启死锁检测(lockdep)功能,不使用ww_acquire_ctx结构,不开启CONFIG_DEBUG_MUTEXES。此版本为第一版的随手笔记,后续会进行修订。
数据结构
完整的mutex定义如下:
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
在大部分系统上,会开启CONFIG_MUTEX_SPIN_ON_OWNER
,但是CONFIG_DEBUG_MUTEXES
,以及CONFIG_DEBUG_LOCK_ALLOC
并不会开启,同时也不会使用struct ww_acquire_ctx
结构,这可以极大地简化mutex的流程。
对于我们而言,mutex的定义如下:
struct mutex {
atomic_long_t owner; //锁的持有者会将自己的task_struct地址写入,来表明自己持有锁
//同时由于task_struct是cacheline对齐的,因此其最后3bit可以作为flag来使用
spinlock_t wait_lock; //用来保护结构的spinlock
struct optimistic_spin_queue osq; //qspinlock的修改版本,用于实现乐观自旋,只有持有osq才能在owner上乐观自旋
struct list_head wait_list; //等待队列的头部,某些等锁的进程会将自己加入该队列等待通知可以持锁
}
owner的flag:
#define MUTEX_FLAG_WAITERS 0x01 //表明等待队列中有进程在等待
#define MUTEX_FLAG_HANDOFF 0x02 //用于告知持锁者需要将锁交给等待队列队首,而不是直接释放
#define MUTEX_FLAG_PICKUP 0x04 //用于通知等待队列队首进程锁交接完成
#define MUTEX_FLAGS 0x07 //掩码
owner & MUTEX_FLAGS 也就得到了flag部分
owner & ~MUTEX_FLAGS 就可以获取到持锁者的task_struct
乐观自旋和handoff机制
乐观自旋
在早版本的mutex中,进程拿不到锁就会将自己加入等待队列没然后等待持锁者释放锁时将自己唤醒。如果持锁者非常快就释放锁,等锁仍然需要经历睡眠-唤醒的开销,并且会带来额外的延迟,但是mutex的属性又决定了等锁不能一直占用cpu,所以使用了既要减少延迟又不过分占用cpu的折中机制,乐观自旋。
如果了解spinlock就知道自旋本质就是循环等待,spinlock就是通过不停的循环检测条件是否满足,直到条件满足才会退出循环,伪代码如下:
while(1) {
if 条件满足可以拿锁
break;
}
这种自旋永远不会让出CPU,好处是实时性特别好,一旦别人释放锁等待者马上可以持锁,坏处也显而易见,会占满一个cpu用于等锁。
而乐观自旋则适用于对实时性要求不高的场合,乐观指的是当持锁者还在cpu上运行时,可以期待持锁者很快就会释放锁,这时候我们可以占用cpu等待,但是占用时间又不能过长。等调度器告诉我们该放开cpu的时候,要及时的将自己调度走。这个机制伪代码如下:
while(1) {
if 条件满足
goto 持锁
if 需要调度 | 持锁者不在运行状态
return 乐观自旋失败
}
加入等待队列
让出cpu,开始睡眠
osq_lock
osq_lock 是 MCS lock以及qspinlock的一种定制化,主要服务于mutex以及rw-sem两种锁,很少独立使用,所以这里介绍简单一点。
他也是一种乐观自旋机制,意味着满足乐观自旋条件(锁者还在cpu上运行 && 调度器没有要求我们让出cpu)的时候,可以一直占用cpu进行等待。
如果不满足自旋条件,退出,返回失败。
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
理解了handoff和乐观自旋机制,阅读后面的代码就很容易了。
初始化
锁的初始化主要有两种,静态和动态。静态主要用于初始化例如全局变量和栈变量的mutex,这些变量无需考虑内存的申请和释放。
动态初始化则一般用于结构体中内嵌的mutex,它们的内存一般来自于slub,需要动态的申请和销毁。
静态初始化:
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) }
动态初始化:
与静态的结果类似,但是需要预先申请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
...
}
持锁
总体流程为:
1.如果owner是0,那就成功拿锁,否则失败,进lock_slowpath路径
2.再次尝试mutex_trylock持锁,成功直接返回
3.mutex_optimistic_spin试图乐观自旋等待,成功直接返回
4.将自己加入等待队列,进入睡眠
5.被唤醒,设置handoff flag,尝试持锁,失败则进入乐观自旋,再次失败则继续睡眠,重复这一步直到持锁成功
6.成功之后将自身移出等待队列
mutex_lock 是最常用的持锁函数,只是用来做个简单的判断。
void __sched mutex_lock(struct mutex *lock)
{
//需要调度则让出cpu
might_sleep();
//核心是 atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr)
//即如果owner是0,那就成功拿锁,否则失败,进入__mutex_lock_slowpath路径
if (!__mutex_trylock_fast(lock))
__mutex_lock_slowpath(lock);
}
这是两个过渡的函数,它的主要工作交给下一级,相关参数后再后面解析
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
此函数是持锁过程的核心函数,逻辑实现基本都在这个函数。
TASK_UNINTERRUPTIBLE
定义等锁时进程的状态,这里是UN状态,也就是常说的D状态
_RET_IP_
是给编译器使用的,这里用来记录调用此函数的调用者
nest_lock ww_ctx use_ww_ctx
这些功能以及debug都是不开启的,我已经在代码中将涉及的部分删除掉便于阅读。如果你看到的代码和我的不一样,那就是删除了这部分导致的。
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;
bool first = false;
struct ww_mutex *ww;
int ret;
//需要调度那么进入睡眠
might_sleep();
preempt_disable();
//trylock用于非阻塞的尝试获取锁,如果成功那么就可以不用等待了
//mutex_optimistic_spin是乐观自旋的实现函数,返回成功代表持锁成功,失败则是乐观自旋条件不满足,需要继续处理
//这两个函数后面都会有详细的分析
//这里先尝试非阻塞持锁,失败则尝试乐观自旋等待,只要一个成功则持锁完成,可以返回了
if (__mutex_trylock(lock) ||
mutex_optimistic_spin(lock, ww_ctx, use_ww_ctx, NULL)) {
preempt_enable();
return 0;
}
------------------ 尝试持锁和乐观自旋都失败了 --------------------------
spin_lock(&lock->wait_lock);
//因为spinlock可能会消耗一点时间,这段时间内所得状态可能发生变化,再尝试一次
if (__mutex_trylock(lock)) {
goto skip_wait;
}
//将自身使用尾插加入等待队列,如果我们是等待队列的队首,那么将owner加上MUTEX_FLAG_WAITERS标记,说明等待队列现在不为空了
__mutex_add_waiter(lock, &waiter, &lock->wait_list);
waiter.task = current;
//将自身设置为UN状态
set_current_state(state);
for (;;) {
//尝试解锁
if (__mutex_trylock(lock))
goto acquired;
//检查信号情况,决定是否先处理信号
if (unlikely(signal_pending_state(state, current))) {
ret = -EINTR;
goto err;
}
spin_unlock(&lock->wait_lock);
//开抢占,然后将自己调度出去让出cpu
//开始睡眠,之前使用set_current_state将自身设置为UN状态,不会被调度器自动调度,需要等待其他进程唤醒
//被唤醒之后,关闭抢占,继续向下运行
schedule_preempt_disabled();
//这里就是被其他进程唤醒了
//first用来标记我们是否是等待队列的队首
//队首进程被唤醒,那么设置owner的MUTEX_FLAG_HANDOFF flag
if (!first) {
first = __mutex_waiter_is_first(lock, &waiter);
if (first)
__mutex_set_flag(lock, MUTEX_FLAG_HANDOFF);
}
set_current_state(state);
//再次尝试获取锁,获取失败不直接进入睡眠,而是尝试再次乐观自旋
//一般而言被唤醒就应该是该持锁的时候,但是乐观自旋机制会让其他进程有机会偷取锁,这点会在解锁时体现
if (__mutex_trylock(lock) || //condition_point(3)
(first && mutex_optimistic_spin(lock, ww_ctx, use_ww_ctx, &waiter)))
break;
//还是失败,那么继续循环,留下MUTEX_FLAG_HANDOFF flag
spin_lock(&lock->wait_lock);
}
spin_lock(&lock->wait_lock);
//这里意味着退出了循环成功获取到锁
acquired:
//设置为正常的R状态,将自身移出等待队列
__set_current_state(TASK_RUNNING);
mutex_remove_waiter(lock, &waiter, current);
if (likely(list_empty(&lock->wait_list)))
__mutex_clear_flag(lock, MUTEX_FLAGS);
skip_wait:
/* got the lock - cleanup and rejoice! */
lock_acquired(&lock->dep_map, ip);
spin_unlock(&lock->wait_lock);
preempt_enable();
return 0;
err:
__set_current_state(TASK_RUNNING);
mutex_remove_waiter(lock, &waiter, current);
err_early_kill:
spin_unlock(&lock->wait_lock);
preempt_enable();
return ret;
}
__mutex_trylock
用于非阻塞的尝试持锁。成功返回true,否则返回false。
static inline bool __mutex_trylock(struct mutex *lock)
{
return !__mutex_trylock_or_owner(lock);
}
__mutex_trylock_or_owner
非阻塞的尝试持锁,成功则返回NULL,失败返回当前锁的owner。
static inline struct task_struct *__mutex_trylock_or_owner(struct mutex *lock)
{
unsigned long owner, curr = (unsigned long)current;
//读取owner
owner = atomic_long_read(&lock->owner);
for (;;) {
unsigned long old, flags = __owner_flags(owner);
unsigned long task = owner & ~MUTEX_FLAGS;
//task 为从owner中提取的持锁者
//flags 即为owner中的flag部分
//如果task不为空,那么说明有人持锁了
if (task) { //condition_point(1)
//这两条判断是handoff功能的实现,如果触发handoff机制,那么上一个持锁者会将锁直接交接给我们,也就是将owner设置为我们的task_struct,并且在flag部分加上MUTEX_FLAG_PICKUP
//所以如果这两条都成立,那么我们设置的handoff起作用了,上个持锁者直接将锁交接给了我们
if (likely(task != curr))
break;
if (likely(!(flags & MUTEX_FLAG_PICKUP)))
break;
flags &= ~MUTEX_FLAG_PICKUP;
}
//到达此处,要么handoff成功,要么无人持锁,总之我们可以尝试持锁了
flags &= ~MUTEX_FLAG_HANDOFF;
//如果lock->owner在这段时间内未发生改变,那么我们将自己的task与设定的flag写入,返回持锁成功
old = atomic_long_cmpxchg_acquire(&lock->owner, owner, curr | flags);
if (old == owner)
return NULL;
//如果发生了改变,重新尝试一次
owner = old;
}
//break出循环,说明持锁条件不满足,那么返回锁的当前owner
return __owner_task(owner);
}
mutex_optimistic_spin
用于实现乐观自旋的主函数。进入这个函数时,有两种身份。第一种我们没有加入等待队列,这是第一次尝试了关系选,那我们是spiner。第二种我们加入了等待队列,那我们是waiter。
对于没有加入等待队列的spiner,需要先竞争osq_lock,同样的,osq_lock也拥有乐观自旋机制,竞争到了这把锁,才能乐观自旋等待锁的owner发生变化。
对于waiter,只有队首进程才会被唤醒,其余进程都是D状态沉睡,因此不存在大量竞争的问题,可以直接在owner上spin等待。
函数流程的伪代码大致为:
while(1) {
尝试持锁
if 持锁成功
return true
自旋等待owner发生变化
if 发生变化
continue
if 自旋条件不满足
return false
}
static __always_inline bool
mutex_optimistic_spin(struct mutex *lock, struct ww_acquire_ctx *ww_ctx,
struct mutex_waiter *waiter)
{
//对于还没有加入等待队列的等待者,需要先竞争osq_lock,防止过多的等待者竞争owner
if (!waiter) {
//检测乐观自旋条件是否满足,不满足返回false
if (!mutex_can_spin_on_owner(lock))
goto fail;
if (!osq_lock(&lock->osq))
goto fail;
}
//在循环中等待,owner每发生一次变化,就尝试持锁一次
//直到持锁成功或者乐观自旋条件不满足,才退出循环
for (;;) {
struct task_struct *owner;
//尝试非阻塞的持锁,成功返回NULL,否则返回持锁者的owner
owner = __mutex_trylock_or_owner(lock); //condition_point(2)
if (!owner)
break;
//等待owner发生变化,返回true
//如果是自旋条件不满足,自身时间用完需要被调度或者持锁者进放弃了CPU,返回false
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:
//如果进程是因为需要被调度才导致乐观自旋失败,那么需要做一些额外的处理
if (need_resched()) {
//调度器调度前需要将自身设置为R状态
//开抢占,立刻让出CPU,下次被调度回来时会返回当前函数,然后关闭抢占,返回false
__set_current_state(TASK_RUNNING);
schedule_preempt_disabled();
}
return false;
}
解锁
解锁分为两种情况,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)
{
//如果owner中没有任何flag,意味着等待队列中没有成员,那么直接将owner置0即可
//不用考虑自旋的等待者,他们没有睡眠,可以自己处理自己的持锁流程
if (__mutex_unlock_fast(lock))
return;
//否则就需要处理等待队列
__mutex_unlock_slowpath(lock, _RET_IP_);
}
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;
//读取当前的owner,owner是task | flag,当前task部分为当前进程的task_struct
owner = atomic_long_read(&lock->owner);
for (;;) {
unsigned long old;
//如果owner被设置了HANDOFF,有等待者已经错过至少一次持锁机会,为了防止其饿死,需要将锁直接交给他
if (owner & MUTEX_FLAG_HANDOFF)
break;
------------------------- 无需处理HANDOFF的情况 ------------------------------
//清除owner的task部分,这里在owner上自旋的等待者就可以获取锁了
//因为condition_point(1)处只需要判断当前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);
//等待队列不为空,将队首进程加入唤醒队列
if (!list_empty(&lock->wait_list)) {
struct mutex_waiter *waiter =
list_first_entry(&lock->wait_list,
struct mutex_waiter, list);
next = waiter->task;
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);
}
加粗样式