美容行业,电力安全生产论文,聂凌峰
read the fucking source code!
--by 鲁迅
a picture is worth a thousand words.
--by 高尔基
说明:
mutex
互斥锁是linux内核中用于互斥操作的一种同步原语;
optimistic spinning
)也应用到了读写信号量上;
前戏都已经讲完了,来看看实际的实现过程吧。
mutex
在实现过程中,采用了optimistic spinning
自旋等待机制,这个机制的核心就是基于mcs锁机制
来实现的;
mcs锁机制
是由john mellor crummey
和michael scott
在论文中《algorithms for scalable synchronization on shared-memory multiprocessors》
提出的,并以他俩的名字来命名;
mcs锁机制
要解决的问题是:在多cpu系统中,自旋锁都在同一个变量上进行自旋,在获取锁时会将包含锁的cache line
移动到本地cpu,这种cache-line bouncing
会很大程度影响性能;
mcs锁机制
的核心思想:每个cpu都分配一个自旋锁结构体,自旋锁的申请者(per-cpu
)在local-cpu变量
上自旋,这些结构体组建成一个链表,申请者自旋等待前驱节点释放该锁;
osq(optimistci spinning queue)
是基于mcs算法的一个具体实现,并经过了迭代优化;
optimistic spinning
,乐观自旋,到底有多乐观呢?当发现锁被持有时,optimistic spinning
相信持有者很快就能把锁释放,因此它选择自旋等待,而不是睡眠等待,这样也就能减少进程切换带来的开销了。
看一下数据结构吧:
osq_lock
如下:
osq_unlock
如下:
osq_wait_next
,来等待获取下一个节点,并在获取成功后对下一个节点进行解锁;
在加锁和解锁的过程中,由于可能存在操作来更改osq队列,因此都调用了osq_wait_next
来获取下一个确定的节点:
终于来到了主题了,先看一下数据结构:
struct mutex { atomic_long_t owner; //原子计数,用于指向锁持有者的task struct结构 spinlock_t wait_lock; //自旋锁,用于wait_list链表的保护操作 #ifdef config_mutex_spin_on_owner struct optimistic_spin_queue osq; /* spinner mcs lock */ //osq锁 #endif struct list_head wait_list; //链表,用于管理所有在该互斥锁上睡眠的进程 #ifdef config_debug_mutexes void *magic; #endif #ifdef config_debug_lock_alloc struct lockdep_map dep_map; #endif };
在使用mutex
时,有以下几点需要注意的:
memset
或者拷贝来进行初始化;
tasklets, timer
)中使用;
从mutex_lock
加锁来看一下大概的流程:
mutex_lock
为了提高性能,分为三种路径处理,优先使用快速和中速路径来处理,如果条件不满足则会跳转到慢速路径来处理,慢速路径中会进行睡眠和调度,因此开销也是最大的。
__mutex_trylock_fast
中实现的,该函数的实现也很简单,直接调用atomic_long_cmpxchg_release(&lock->owner, 0ul, curr)
函数来进行判断,如果lock->owner == 0
表明锁未被持有,将curr
赋值给lock->owner
标识curr
进程持有该锁,并直接返回;
lock->owner
不等于0,表明锁被持有,需要进入下一个路径来处理了;
__mutex_lock_common
中实现的;
__mutex_lock_common
的传入参数为(lock, task_interruptible, 0, null, _ret_ip_, false
),该函数中很多路径覆盖不到,接下来的分析也会剔除掉无效代码;
中速路径的核心代码如下:
当发现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
,判断不满足自旋等待的条件,那么返回,让我们进入慢速路径吧,毕竟不能强求;
慢速路径的主要代码流程如下:
for(;;)
部分的流程可以看到,当没有获取到锁时,会调用schedule_preempt_disabled
将本身的任务进行切换出去,睡眠等待,这也是它慢的原因了;
mutex_flag
来进行判断处理,并最终唤醒等待在该锁上的任务;
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
网友评论