内核mutex实现原理

mutex锁的特点

1.不同于信号量,mutex需要谁拿的锁谁来释放锁。
2.不同于自旋锁,mutex临界区允许睡眠。
3.不同于自旋锁,mutex在拿锁时若锁被别人持有,会根据锁的持有者是否正在运行来决定是乐观自旋或是睡眠等待。

mutex锁的定义

struct mutex {
        atomic_long_t    owner;  //owner中记录了锁的持有者的task_struct地址,且低3bit记录了锁的状态
        spinlock_t    wait_lock; //用来保护wait_list链表
        struct list_head   wait_list; //等待链表,等着拿锁的进程会被记录在此list上,操作wait_list需要wait_lock的保护
        .....
};

owner成员可被认为是mutex锁的本体,此成员为空则说明锁未被持有,非空则说明锁被持有。实际上owner被划分为两个域——task field和flags field,分别是63bit至3bit存放锁的持有者的task_struct地址,2bit至0bit存放锁的状态。其中bit0表示wait_list是否为空,bit1和bit2用来实现handoff机制。

相关概念

乐观自旋?

举例,不带乐观自旋的mutex版本中,若A线程持有锁且正在运行,此时B线程想拿锁,发现锁正被A拿着,B将立即进入睡眠状态,在A线程释放锁后再去唤醒B线程来拿锁。但是我们通常认为持有锁的A若是在运行的情况下,通常会很快的退出临界区并释放锁,那么B去睡眠则是不必要的,毕竟睡眠和唤醒也是有代价的,完全可以多等一会即可。那么当mutex开启乐观自旋的feature后,若B拿不到锁,且发现锁的持有者A仍占用着CPU运行时,则不再去睡眠,而是像自旋锁一样进行自旋等待,直到A释放锁,但期间若A失去CPU(即p->on_cpu为0),那么B将不会继续傻傻的自旋等待,而是进入睡眠。

handoff机制

当mutex支持乐观自旋时,那么会存在这样的情况:wait_list中有一些进程在睡眠并等待被唤醒拿锁,同时还有一些进程不在wait_list中且不断的自旋等锁或乐观自旋等锁。wait_list上的进程大概率是抢不过自旋拿锁的进程的,这是因为调度延时的存在。当被唤醒的进程真正获得cpu时,锁早就被自旋等锁的进程给偷走了(偷锁),这会在一定程度的造成wait_list的等待者长时间“饥饿”。于是社区的peterz大神为mutex增加了handoff机制用来防止这种情况的发生。具体实现利用了owner中的flags field(2bit至0bit)。

拿锁的几种场景

快速路径: mutex_lock->__mutex_trylock_fast

owner的task field和flags field都为空,说明锁是未被任何人持有,直接拿锁即可(即将当前进程的task_strcut设置进owner中)

慢速路径:mutex_lock->__mutex_lock_slowpath

场景1:owner的task field为空,但是flags field不为空,快路径失败,走慢速路径。(这样的lock属于一个中间态,说明unlock路径正在进行中)

场景2:慢速路径中的乐观自旋,当前进程进入慢路径后,尝试拿锁但是拿不到,会不断检测持有锁的进程是否正在运行,若是那么当前进程则会关闭抢占并进行自旋,不断调用__mutex_trylock尝试拿锁并最终拿到锁。

场景3:进入慢速路径后发现锁的持有者正在睡眠,或是场景2在自旋的过程中发现锁的持有者进入了睡眠,那么当前进程将停止自旋并被会加入wait_list进入睡眠。等待被锁的持有者unlock时被唤醒,且唤醒后立马拿到了锁。

场景4:在场景3的情况下,被锁的持有者在unlock时唤醒(注意每次只唤醒一个进程,且此进程是wait_list上的最早的等待进程),唤醒后发现锁被偷了,那么将通过触发handoff机制,再下次释放锁时一定能拿到锁。

注意:以上慢速路径的拿锁操作最终都是通过__mutex_trylock进行的。__mutex_trylock函数中又分三种情况:1. task field为空,说明锁已经被释放,可直接拿到锁。 2. task field非空,判断task field是否指向自己,若不指向自己则明锁正被用着呢,拿锁失败。3.若task field指向自己,说明这个锁正在通过handoff机制把锁交给自己,拿锁一定成功。(自行对照代码v5.12)

handoff机制

对handoff的实现做个简单介绍,具体对应慢速路径下的场景4,以及__mutex_trylock中的情况3。在__mutex_trylock中会判断owner的task field,若指向当前进程,说明当前进程是wait list中的第一个等待进程,且之前被唤醒过一次,但是没拿到锁,因为锁被自旋等锁的进程给偷走了。为了下次一定能拿到锁,当前进程就在flags field中设置了MUTEX_FLAG_HANDOFF标志位,在下次释放锁的unlock路径中若检测到此flag,在释放锁时就不会再像往常一样把task field设置为0,而是设置为wait list中最早的等待者的task_struct地址。这样在__mutex_trylock函数中通过判断task field是否指向自己就可以保证最早的等待者这次一定不会被偷锁。

笔者在阅读代码时发现当前的handoff机制是存在一些bug的:即偷锁发生时有一定概率导致handoff机制失效,和peterz沟通后一起做了修复patch1 patch2

关于偷锁的思考

既然偷锁有可能导致wait_list上的进程饥饿,那么为什么还要允许偷锁的发生呢?其实允许偷锁的行为可以算是对mutex的一种性能优化,若是不允许偷锁那么下次拿锁的就一定是被唤醒的等锁进程,但是从进程被唤醒到该进程真正获得cpu运行,这中间会存在一个时间差,即调度延时。且在这段时间内若还有其他进程一直在trylock尝试偷锁那么将不会偷成功,这些进程都会因此block住。由于调度延时某些场景下会比较久(例:runqueue上的进程很多),不如先将锁给正在trylock的进程。于是mutex的当前实现中就允许了发生一次偷锁。

链接: 浅析mutex实现原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值