作者:胖蚂蚁 | 来源:互联网 | 2023-05-25 19:27
mutex同spinlock一样,必须由持有lock的线程来释放这个lock。如果当前没有其他线程在等待,那么"owner"的低3个bits为0,"owner"的值也就直接等于当前线程的"task_struct"指针的值,此时释放mutex要做的只是将"owner"的值设为0。bool __mutex_unlock_fast(struct mutex *lock){
if (atomic_long_cmpxchg_release(&lock->owner, curr, 0UL) == curr)
return true;
...如果有线程在等待,那么释放mutex时需要保留"owner"中作为标志位的低3个bits。
void __sched __mutex_unlock_slowpath(struct mutex *lock, ...){
// 释放mutex,同时获取记录状态的低3个标志位
unsigned long old = atomic_long_cmpxchg_release(&lock->owner,
owner, __owner_flags(owner));
...
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);
// 将该线程加入wake_q
struct task_struct *next = waiter->task;
wake_q_add(&wake_q, next);
}
spin_unlock(&lock->wait_lock);
// 唤醒该线程
wake_up_q(&wake_q);获取"wait_list"队列头部的等待线程,并调用wake_up_q()唤醒该线程。Linux内核中的mutex同spinlock一样,也存在AB-BA的死锁问题。前面讨论spinlock的AB-BA死锁时曾讲到,在这个时候,如果其中一方能主动让出自己持有的锁,「僵局」就可以迎刃而解。2013年,Marten Lankhorst基于普通的mutex开发了一种新的机制。如下图所示,当检测到发生死锁时,Task-2主动unlock(B),那么Task-1就可以继续往下执行,Task-1执行完后释放lock B和lock A,那么Task-2也可以继续往下执行。这种机制被命名为"ww-mutex",其中"ww"代表Wound-Wait,可理解为是主动让出mutex的一方在“受伤地等待”,不过一方受伤总比双方都死掉好。ww-mutex需要能够识别mutex获取路径的依赖,并制定一套选择哪个线程作为让出一方的规则。
struct ww_mutex {
struct mutex base;
struct ww_acquire_ctx *ctx;此外,ww-mutex还支持按stamp,而不是FIFO的顺序选择等待队列中的waiter。这样可以避免上文所说的“较晚进入等待的spinner先于较早进入等待的waiter获得mutex”的情况,当一个线程发现有比它自己的stamp更早的waiter存在,它就不会成为spinner。
bool ww_mutex_spin_on_owner(...){
if (!waiter && (atomic_long_read(&lock->owner) & MUTEX_FLAG_WAITERS))
return false;
...Linux中的spinlock和mutex机制都介绍完了,下面通过对比来说明两者分别适合于什么样的应用场景。spinlock的开销在于暂时获取不到锁时,对所在CPU的持续占有,而传统的mutex的开销则在于释放CPU和重新获取CPU所带来的上下文切换。不过,现在的mutex设计已经通过optimistic spinning糅合了spinlock的行为,在资源不足时是否主动让出CPU已经不再构成两者真正的区别。两者语义上的差异(或者说spinlock和mutex能同时存在的原因)是在线程试图获取和持有spinlock期间,调度都是关闭的,因而要求临界区的执行时间必须较短。相比而言,使用mutex的限制条件更加宽松。如果支持的操作可能会导致睡眠,比如copy_from_user()或者kmalloc(GFP_KERNEL),则只能使用mutex。