在linux内核中,互斥量(mutex,即mutual exclusion)是一种保证串行化的睡眠锁机制。和spinlock的语义类似,都是允许一个执行线索进入临界区,不同的是当无法获得锁的时候,spinlock原地自旋,而mutex则是选择挂起当前线程,进入阻塞状态。正因为如此,mutex无法在中断上下文使用。和mutex更类似的机制(无法获得锁时都会阻塞)是binary semaphores,当然,mutex有更严格的使用规则:
1、只有mutex的owner可以才可以释放锁
2、不可以多次释放同一把锁
3、不允许重复获取同一把锁,否则会死锁
4、必须使用mutex初始化API来完成锁的初始化,不能使用类似memset或者memcp之类的函数进行mutex初始化
5、不可以多次重复对mutex锁进行初始化
6、线程退出后必须释放自己持有的所有mutex锁
当配置了DEBUG_MUTEXES的时候,内核会对上面的规则进行检查,防止用户误用mutex,产生各种问题。
下面是一个简单的mutex工作原理图:
传统的mutex只需要一个状态标记和一个等待队列就OK了,等待队列中是一个个阻塞的线程,thread owner当前持有mutex,当它离开临界区释放锁的时候,会唤醒等待队列中第一个线程(top waiter),这时候top waiter会去竞争持锁,如果成功,那么从等待队列中摘下,成为owner。如果失败,继续保持阻塞状态,等待owner释放锁的时候唤醒它。在owner task持锁过程中,如果有新的任务来竞争mutex,那么就会进入阻塞状态并插入等待队列的尾部。
相对于传统的mutex,linux内核进行了一些乐观自旋的优化,也就是说当线程持锁失败的时候,可以选择在mutex状态标记上自旋,等待owner释放锁,也可以选择进入阻塞状态并挂入等待队列。具体如何选择是在自旋等待的时间开销和进程上下文切换的开销之间进行平衡。此外为了防止多个线程自旋带来的性能问题,mutex的乐观自旋机制还引入了MCS锁,后面章节我们会详细描述。
1、互斥量对象
互斥量对象用struct mutex来抽象,其成员描述如下:
大部分的成员都非常好理解,除了osq这个成员,其工作原理示意图如下:
字如其名,Optimistic spin queue就是乐观自旋队列的意思,也就是形成一组处于自旋状态的任务队列。和等待队列不一样,这个队列中的任务都是当前正在执行的任务。Osq并没有直接将这些任务的task struct形成队列结构,而是把per-CPU的mcs lock对象串联形成队列。Mcs lock中有cpu number,通过这些cpu number可以定位到指定cpu上的current thread,也就定位到了自旋的任务。
虽然都是自旋,但是自旋方式并不一样。Osq队列中的头部节点是持有osq锁的,只有该任务处于对mutex的owner进行乐观自旋的状态(我们称之mutex乐观自旋)。Osq队列中的其他节点都是自旋在自己的mcs lock上(我们称之mcs乐观自旋)。当头部的mcs lock释放掉后(结束mutex乐观自旋,持有了mutex锁),它会将mcs lock传递给下一个节点,从而让spinner队列上的任务一个个的按顺序进入mutex的乐观自旋,从而避免了cache-line bouncing带来的性能开销。
2、等待任务对象
由于是sleep lock,我们需要把等待的任务挂入队列。在内核中,Struct mutex_waiter用来抽象等待mutex的任务,其成员描述如下:
3、MCS锁对象
在linux内核中,我们对睡眠锁(例如mutex、rwsem)进行了乐观自旋的优化,这涉及到MCS lock,struct optimistic_spin_node用来抽象乐观自旋的MCS lock,其成员描述如下:
Mutex模块的外部接口API如下:
和mutex_lock不一样,mutex_trylock只是尝试获取锁,如果成功,那么自然是好的,直接返回true,如果失败,也不会阻塞,只是返回false就可以了。代码主逻辑在__mutex_trylock_or_owner函数中,如下:
A、对于mutex的owner成员,它是一个原子变量,我们采用了大量的原子操作来访问或者更新它。然而判断持锁需要一连串的操作,我们并没有采用同步机制(例如自旋锁)来保护这一段的对owner成员操作,因此,我们这些操作放到一个for循环中,在操作的结尾处会判断是否有其他线程插入修改了owner成员,如果中间有其他线程插入,那么就需要重新来过。
B、如果task非空(task变量保存了owner中去掉flag部分的任务指针),并且也不等于current thread,那么说明mutex锁被其他线程持有,还没有释放锁(也有可能在是否锁的时候,把锁直接转交给了其他线程),因此直接break跳出循环,持锁失败。
C、如果task等于current thread,而且设置了MUTEX_FLAG_PICKUP的标记,那么说明持锁线程已经把该mutex锁转交给了本线程,等待本线程来拾取。如果没有MUTEX_FLAG_PICKUP标记,那么也是直接break跳出循环,递归持锁失败。
D、有两种情况会走到这里的时候,一种情况是task为空,说明该mutex锁处于unlocked状态。另外一种情况是task非空,等于current thread,并且mutex发生了handoff,该锁被转交给当前试图持锁的线程。无论哪种情况,都可以去执行持锁操作了。
E、调用atomic_long_cmpxchg_acquire尝试获取锁,如果成功获取了锁(没有其他线程插入修改owner这个原子变量),返回NULL。如果owner发生了变化,说明中间有其他线程插入,那么重新来过。
五、获取mutex锁
mutex_lock代码如下:
这里的might_sleep说明调用mutex_lock函数有可能会因为未能获取到mutex锁而进入阻塞状态。在原子上下文中(中断上下文、软中断上下文、持有自旋锁、禁止抢占等),我们不能调用可以引起阻塞的函数,因此在might_sleep函数中嵌入了这个检查,当原子上下文中调用mutex_lock函数的时候,内核会打印出内核栈的信息,从而定位这个异常。
当然,这个功能是在设置CONFIG_DEBUG_ATOMIC_SLEEP选项的情况下才生效的,如果没有设置这个选项,might_sleep函数退化为might_resched函数。在配置了抢占式内核(CONFIG_PREEMPT)或者非抢占式内核(CONFIG_PREEMPT_NONE)的情况下,might_resched是空函数。
在配置了主动抢占式内核(CONFIG_PREEMPT_VOLUNTARY)的情况下,might_resched会调用_cond_resched函数来主动触发一次抢占。主动抢占式内核通过在might_sleep函数中增加了潜在的调度点实现了比非抢占式内核更好的延迟特性,同时确保抢占带来的进程切换开销低于抢占式内核。
Mutex是一种睡眠锁,如果未能获取锁,那么当前线程会阻塞。不过也许我们试图获取的mutex还处于空闲状态,因此通过__mutex_trylock_fast来尝试获取mutex(mutex_lock的快速路径):
atomic_long_try_cmpxchg_acquire函数有三个参数,从左到右分别是value指针,old指针和new。该函数会对比*value和*old指针中的数值,如果相等执行赋值*value=new同时返回true。如果不相等,不执行赋值操作,直接返回false。
如果lock->owner的值等于0(即不仅task struct地址等于0,所有的flag也要等于0),那么将当前线程的task struct的指针赋值给lock->owner,表示该mutex锁已经被当前线程持有。如果lock->owner的值不等于0,表示该mutex锁已经被其他线程持有或者锁正在传递给top waiter线程,当前线程需要阻塞等待。需要特别说明的是上面描述的操作(比较和赋值)都是原子操作,不能有任何指令插入其中。
在未能获取mutex锁的情况下,我们需要调用__mutex_lock_slowpath函数进入慢速路径。由于会进入睡眠,因此这里需要明确当前线程需要处于的阻塞状态,主要有三种状态:D状态、S状态和KILLABLE。
当调用不同的持锁API的时候,当前线程可以处于各种不同的状态。对于mutex_lock(大部分场景)当前线程会进入D状态。主要的代码逻辑在__mutex_lock_common函数中,我们分段解读(省略wait/wound和调试部分的代码):
__mutex_trylock用来再次尝试获取锁,mutex_optimistic_spin则是mutex乐观自旋(Optimistic spinning)部分的代码。这两个操作只要有其一能成功获取mutex锁,那么就直接返回了。由于没有进入阻塞状态,因此这个路径也叫做中速路径。
__mutex_trylock在上一节已经讲解了,不再赘述。乐观自旋的思路是因为mutex锁可能是被其他CPU上正在执行中的线程持有,如果临界区比较短,那么有可能该mutex锁很快就被释放。这时候,与其进行一次上下文切换,还不如自旋等待,毕竟上下文切换的开销也是不小的。乐观自旋机制底层使用的是MCS锁,具体的细节我们会在其他文档中描述。
慢速路径的代码如下(省略部分代码):
A、所谓慢速路径其实就是阻塞当前线程,这里将current task挂入mutex的等待队列的尾部。这样的操作让所有等待mutex的任务按照时间的先后顺序排列起来,当mutex被释放的时候,会首先唤醒队首的任务,即最先等待的任务最先被唤醒。此外,在向空队列插入第一个任务的时候,会给mutex flag设置上MUTEX_FLAG_WAITERS标记,表示已经有任务在等待这个mutex锁了。
B、进入阻塞状态,触发一次调度。由于目前执行上下文处于关闭抢占状态,因此这里的调度使用了关闭抢占版本的schedule函数。
C、该任务被唤醒之后,如果是等待队列中的第一个任务,即top waiter,那么需要给该mutex设置MUTEX_FLAG_HANDOFF,这样即便本次唤醒后无法获取到mutex(有些在该mutex上乐观自旋的任务可能会抢先获得锁),那么下一次owner释放锁的时候,看到这个handoff标记也会进行锁的交接,不再是大家抢来抢去。通过这个机制,我们可以防止spinner队列中的任务抢占CPU资源,饿死waiter队列中的任务。
D、如果获取到mutex,那么就退出循环,否则继续进入阻塞状态等待。如果是队列中的第一个waiter,那么如果__mutex_trylock失败,那么就进入乐观自旋过程,这样会有更大的机会成功获取mutex锁。
Mutex乐观自旋的代码位于mutex_optimistic_spin函数中,进入乐观自旋函数的线程可能有下面几个结果:
1、成功获取osq锁,进入mutex乐观自旋状态,当owner释放mutex锁后,该线程结束乐观自旋,成功持有了mutex,返回true 2、未能获取osq锁,在自己的MCS锁上乐观自旋。一旦成功持锁,同步骤1 3、在MCS锁或者mcs锁乐观自旋的时候,由于各种原因(例如owner进入阻塞状态)而无法继续乐观自旋,那么mutex_optimistic_spin函数返回false,告知调用者乐观自旋失败,进入等待队列。
我们分两段来解析。首先来看第一段:
调用mutex_optimistic_spin函数的场景有两个,一个是waiter等于NULL,这是发生在mutex_lock的早期,这时候试图持锁的线程还没有挂入等待队列,因此waiter等于NULL。另外一个场景是持锁未果,挂入等待队列,然后被唤醒之后的乐观自旋。这时候试图持锁的线程已经挂入等待队列,因此waiter非空。在这种场景下,刚唤醒的top waiter线程会给与优待,因此不需要持有osq锁就可以长驱直入,进入乐观自旋。
A、当waiter为空时,因为是正常路径的持锁请求,所以在乐观自旋之前需要持有osq锁,只有获得了osq锁,当前线程才能进入mutex乐观自旋的过程。否则只能是在自己的MCS锁上自旋等待。
B、是否乐观自旋等待mutex可以从两个视角思考:一方面,如果本cpu已经设置了need resched标记,那说明有其他任务想要抢占当前试图持锁的任务。那么current task何必乐观自旋呢,赶紧的去sleep为其他任务让路吧。另外一方面需要从owner的行为来判断。如果owner正在其他cpu欢畅运行,那么可以考虑进入乐观自旋过程。
C、在基于共享内存的多核计算系统中,mutex的实现是通过一个共享变量(owner成员)和一个队列来完成复杂的控制的。如果有多个cpu上的线程同时乐观自旋在这个共享变量上,那么就会出现缓存踩踏现象。为了解决这个问题,我们控制不能让太多的线程进入mutex乐观自旋状态(轮询owner成员),只有那些获取了osq锁的线程才能进入。未能持osq锁的线程会进入mcs锁的乐观自旋过程,等待osq锁的owner(当前在mutex乐观自旋)释放osq锁。关于osq锁的细节我们在其他文章中描述。
完成了持osq锁之后(或者是被唤醒的top waiter线程,它会掠过osq持锁过程),我们就可以进入mutex乐观自旋了,代码如下:
A、首先还是调用__mutex_trylock_or_owner试图获取mutex锁,如果返回的owner非空(需要注意的是:这里的owner变量不包括mutex flag部分),那么说明mutex锁还在owner task手中。如果owner是空指针,说明原来持有锁的owner已经释放锁,同时这也就说明当前线程持锁成功,因此退出乐观自旋的循环。需要注意的是在退出mutex乐观自旋后会释放osq锁,从而会让spinner队列中的下一个mcs锁自旋的任务进入mutex乐观自旋状态。
B、如果__mutex_trylock_or_owner返回了非空owner,说明当前线程获取锁失败,那么可以进入mutex乐观自旋了。所谓自旋不是自旋在spinlock上,而是不断的循环检测锁的owner task是否发生变化以及owner task的运行状态。如果owner阻塞了或者当前cpu有resched的需求(可能唤醒更高级任务),那么就停止自旋,返回false,走入fail_unlock流程。
C、如果mutex锁的owner task发生变化(例如变成NULL)则mutex_spin_on_owner函数返回true,则说明可以跳转到for循环处再次尝试获取锁并进行乐观自旋。
mutex_unlock的代码如下:
如果一个线程获取了某个mutex锁之后,没有任何其他的线程试图进入临界区,那么这时候mutex的owner成员就是该线程的task struct地址,并且所有的mutex flag都是clear的。在这种情况下,将mutex的owner成员清零即可,不需要额外的操作,我们称之解锁快速路径(__mutex_unlock_fast)。
当然,如果有其他线程在竞争该mutex锁,那么情况会更复杂一些,这时候我们进入慢速路径(_mutex_unlock_slowpath),慢速路径的逻辑分成两段:一段是释放mutex锁,另外一段是唤醒top waiter线程。我们首先一起看第一段的代码,如下:
A、如果mutex flag中设定了handoff标记,那么说明owner在释放锁的时候要主动的把锁的owner传递给top waiter,不能让后来插入的乐观自旋的线程饿死top waiter。因此这时候我们还不能放锁,需要在__mutex_handoff函数中释放锁给top waiter。
B、将owner的task struct地址部分清掉,这也就是意味着owner task放弃了持锁。这时候,如果有乐观自旋的任务在轮询mutex owner,那么它会立刻感知到锁被释放,因此可以立刻获取mutex锁。在这样的情况下,即便后面唤醒了top waiter,但为时已晚。
C、如果等待队列中有任务阻塞在这个mutex中,那么退出循环,执行慢速路径中的第二段唤醒逻辑,否则直接返回,无需唤醒其他线程。
D、在操作owner的过程中,如果有其他线程对owner进行的修改(没有同步机制保证多线程对owner的并发操作),那么重新设定owner,再次进行检测。第二段唤醒top waiter的代码如下:
A、代码执行至此,需要唤醒top waiter,或者处理将锁转交top waiter的逻辑,无论哪种情况,都需要从等待队列中找到top waiter。找到后将其加入wake queue。
B、如果有任务(一般是top waiter,参考其唤醒后的代码逻辑)请求handoff mutex,那么调用__mutex_handoff函数可以直接将owner设置为top waiter任务,然后该任务在醒来之后直接pickup即可。这相当与给了top waiter一些特权,防止由于不断的插入乐观自旋的任务而导致无法获取CPU资源。
C、唤醒top waiter任务
本文简单的介绍了linux内核中的mutex同步机制,在移动环境中,mutex锁的性能表现不尽如人意,无论是吞吐量还是延迟。在重载的场景下,我们经常会遇到Ux线程阻塞在mutex而引起的手机卡顿问题,如何在手机平台上优化mutex锁的性能是我们OPPO内核团队一直在做的事情,也欢迎热爱技术的你积极参与。
参考文献:
1、内核源代码
2、linux-5.10.61\Documentation\scheduler\*
3、https://zhuanlan.zhihu.com/p/364130923
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ftb6fYP26ZBNS-_CNkFyTg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。