0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看威廉希尔官方网站 视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux读写锁逻辑解析—尝试获取写锁

冬至子 来源:内核工匠 作者:郭健Cojack 2023-12-04 11:12 次阅读

Rwsem的count成员还有一些bit用来标记当前读写锁状态(waiter bit和handoff bit),也需要根据情况进行调整:

1.jpg

A、如果等待队列为空了,肯定是要清除waiter flag,同时要清除handoff flag,毕竟没有什么等待任务可以递交锁了。

B、虽然队列非空,但已经唤醒了reader,那么需要清除handoff标记,毕竟top waiter已经被唤醒去持锁了,完成了锁的递交。

C、完成sem->count的调整

第二轮将唤醒的reader加入唤醒队列,具体的逻辑如下:

1.jpg

主要是把等待任务对象的task成员设置为NULL,唤醒之后根据这个成员来判断是正常唤醒还是异常唤醒路径。

这里对唤醒等待队列上的reader和writer处理是不一样的。对于writer,唤醒之然后被调度到之后再去试图持锁。对于reader,在唤醒路径上就已经持锁(增加rwsem的reader count,并且修改了相关的状态标记)。之所以这么做主要是降低调度的开销,毕竟若干个reader线程被唤醒之后,获得CPU资源再去持锁,持锁失败然后继续阻塞,这些都会增加调度的负载。

七、尝试获取写锁

和down_write不一样,down_write_trylock只是尝试获取写锁,如果成功,那么自然是好的,直接返回1,如果失败,也不会阻塞,只是返回0就可以了。代码主逻辑在rwsem_write_trylock函数中,如下:

1.jpg

tmp的初始值设定为RWSEM_UNLOCKED_VALUE(0值),对于writer而言,只有rwsem是空锁的时候才能进入临界区。如果当前的sem->count等于0,那么给sem->count赋值RWSEM_WRITER_LOCKED,标记持锁成功,并且把owner设定为当前task。

atomic_long_try_cmpxchg_acquire函数有三个参数,从左到右分别是value,old和new。该函数会对比value和old,如果相等那么执行赋值value=new同时返回true。如果不相等,不执行赋值操作,直接返回false。

八、获取写锁

Writer获取写锁的代码主要在__down_write_common函数中,如下:

1.jpg

rwsem_write_trylock(快速路径)上一节已经描述,我们主要看慢速路径的逻辑(乐观自旋我们下面会讲,这里暂且略过):

1.jpg

首先准备好一个等待任务对象(栈上)并初始化,将其挂入等待队列。在真正睡眠之前,我们需要做一些唤醒动作(和reader持锁过程类似,有可能在挂入等待队列的时候,临界区线程恰好离开,变成空锁),具体逻辑如下:

1.jpg

A、如果我们是等待队列的top waiter(等待队列从空变为非空),那么需要设定RWSEM_FLAG_WAITERS标记,直接进入后续阻塞逻辑。如果不是,那么逻辑要复杂点,需要扫描一下之前挂入队列的任务,看看是否需要唤醒。

B、如果是writer持锁,那么不需要任何唤醒动作,毕竟writer是排他的

C、如果是空锁状态,我们需要唤醒top waiter(RWSEM_WAKE_ANY,top writer或者reader们)。你可能会疑问:为何空锁还要唤醒等待队列的线程?当前线程快马加鞭去持锁不就OK了吗?这主要是和handoff逻辑相关,这时候更应该持锁的是等待队列中设置了handoff的那个waiter,而不是当前writer。如果是reader在临界区内,那么,我们将唤醒本等待队列头部的所有reader(RWSEM_WAKE_READERS)。

D、上面仅仅是标记唤醒者,这里的代码段完成具体的唤醒动作

下面进入具体writer的阻塞过程:

1.jpg

A、调用rwsem_try_write_lock试图持锁,如果成功持锁则退出循环,不再阻塞。有两个逻辑路径会路过这里。一个是线程持锁失败进入这里,另外一个是阻塞后被唤醒试图持锁。

B、有pending的信号,异常路径退出

C、持锁失败但是设置了handoff,那么该线程对owner进行自旋等待,以便加快锁的传递。

D、进入阻塞状态

E、唤醒之后,重新试图持锁。Writer和reader不一样,writer是唤醒之后自己再通过rwsem_try_write_lock试图持锁,而reader是在唤醒路径上持锁。

rwsem_try_write_lock代码如下:

1.jpg

A、如果已经设置了handoff,并且自己不是top waiter(top waiter才是锁要递交的对象),返回false,持锁失败。如果是top waiter,那么就设置handoff_set,标记自己就是锁递交的目标任务。

B、如果当前rwsem已经有了owner,那么说明该锁被偷走了。在适当的条件下(等待超时)设置handoff标记,防止后续继续被抢。如果已经设置了handoff就不必重复设置了。

C、如果当前rwsem没有owner,则持锁成功,清除handoff标记并根据情况设置waiter标记。

D、通过原子操作来持锁,成功操作后退出循环,否则是有其他线程插入,需要重复上面的逻辑。

1.jpg

至此我们要不获取了锁并清除了handoff bit(B逻辑块),或者没有获取锁,仅仅是设置了handoff bit(A逻辑块)。

九、释放写锁

除了清除了owner task成员,其他逻辑和释放读锁类似,不再赘述。

十、乐观自旋的条件

只有writer在进入慢速路径的时候才会进行乐观自旋,而rwsem_can_spin_on_owner函数用来判断writer是否可以乐观自旋:

1.jpg

A、本cpu上需要reschedule,还自旋个毛线,赶紧去睡眠也顺便触发一次调度

B、读取sem->owner,标记部分保存在flags临时变量中,任务指针保存在owner中

C、如果该rwsem已经禁止了对应的nonspinnable标志,那么肯定是不能乐观自旋了。如果当前rwsem没有禁止,那么需要看看owner的状态。这里需要特别说明的是:为了方便debug,我们在释放读锁的时候并不会清除owner task。也就是说,对于reader而言,owner中的task信息是最后进入临界区的那个reader,仅此而已,实际这个task可能已经离开临界区,甚至已经销毁都有可能。所以,如果rwsem是reader拥有,那么其实判断owner是否在cpu上运行是没有意义的,因此owner是reader的话是允许进行乐观自旋的(ret的缺省值是true),通过超时来控制自旋的退出。如果rwsem是writer拥有,那么owner的的确确是正在持锁的线程,如果该线程没有在CPU上运行(不能很快离开临界区),那么也不能乐观自旋。

十一、rwsem_spin_on_owner

函数rwsem_spin_on_owner的功能是对rwsem的owner task进行乐观自旋(即不断轮询其状态,仅writer有效),详细的代码逻辑如下:

1.jpg

A、在自旋之前,首先要获得初始的状态(owner task指针以及2-bit LSB flag),当这些状态发生变化才好退出自旋。

B、rwsem_owner_state函数会根据当前的owner task和flag判断当前的owner state。owner state的状态总结如下:

1.jpg

只有明确的知道当前rwsem的owner是某个writer线程且没有禁止自旋的时候才开启下面的自旋过程。对于其他情况,例如reader owned的场景,我们不需要spin on owner,直接返回。

C、只要owner task或者flag其一发生变化,这里就会停止轮询,同时也会返回当前的状态,说明停止自旋的原因。例如当owner task(一定是writer)离开临界区的时候会清空rwsem的owner域(owner task和flag会清零),这时候自旋的writer会停止自旋,到外层函数会去试图持锁。当然也有可能是其他自旋writer抢到了锁,owner task从A切到B。无论那种情况,统一终止对owner的自旋。

D、如果当前cpu需要reschedule或者owner task没有正在运行,那么也需要停止自旋

十二、Writer的乐观自旋

和mutex的乐观自旋的概念是类似的,想要进行rwsem的乐观自旋,首先要获取osq锁,只有获得了osq lock才能进入rwsem的乐观自旋,否则自旋在per cpu的mcs lock上。Writer通过rwsem_optimistic_spin完成整个乐观自旋的过程。对于writer owned场景,自旋发生在rwsem_spin_on_owner中,上一节已经描述了,这里我们主要看reader owned的情况,这时候通过for loop不断自旋去持锁:

1.jpg

2.jpg

A、对于rwsem,只有writer-owned场景能清楚的知道owner task是哪一个。因此,如果是writer-owned场景,会在rwsem_spin_on_owner函数进行自旋。对于非writer-owned场景(reader-owned场景或者禁止了乐观自旋),在rwsem_spin_on_owner函数中会直接返回。从rwsem_spin_on_owner函数返回会给出owner state,如果需要退出乐观自旋,那么这里break掉,自旋失败,下面就准备挂入等待队列了。

B、每次退出rwsem_spin_on_owner并且没有要退出自旋的时候,都试着去获取rwsem,如果持锁成功那么退出乐观自旋。

C、C和D是对reader-owned场景的处理。每次rwsem的owner state发生变化(从non-reader变成reader-owned状态)时都会重新初始化 rspin_threshold。

D、Owner state没有发生变化,那么当前试图持锁的writer可以进行乐观自旋,但是需要有一个度,毕竟rwsem的临界区内可能有多个reader线程,这有可能使得writer乐观自旋很长时间。设置自旋门限阈值的公式是Spinning threshold = (10 + nr_readers/2)us,最大25us(30 reader)。一旦自旋超期,那么将调用rwsem_set_nonspinnable禁止乐观自旋。

E、对于writer-owned场景,need_resched在函数rwsem_spin_on_owner中完成,对于reader-owned场景,也是需要检查owner task所在cpu的resched情况。毕竟当前任务如果有调度需求,无论reader持锁还是writer持锁场景都要停止自旋。

F、在reader-owned场景中,由于无法判定临界区reader们的执行状态,因此rt线程的乐观自旋需要更加的谨慎,毕竟有可能自旋的rt线程和临界区的reader在一个CPU上从而导致活锁现象。当然也不能禁止rt线程的自旋,毕竟在临界区为空的情况下,rt自旋会有一定的收益的。允许rt线程自旋的场景有两个:

a) lock owner正在释放锁,sem->owner被清除但是锁还没有释放。

b) 锁是空闲的并且sem->owner已清除,但是在我们尝试获取锁之前另一个任务刚刚进入并获取了锁(例如一个自旋的writer先于我们进入临界区)。

十三、关于handoff

1、设置handoff标记

设置handoff往往是发生在唤醒持锁阶段。对于等待队列的writer,唤醒之后要调度执行后才去持锁,这是一个长路径,很可能被其他的write或者reader把锁抢走。唤醒等待队列中的reader们有点不一样,在唤醒路径上就会从这一组待唤醒的reader们选出一个代表(一般是top waiter)去持锁,然后再一个个的唤醒。在这个reader代表线程持锁的时候也有可能由于writer偷锁而失败(reader虽然也会偷锁,但是偷锁的reader也会唤醒等待队列的reader们,完成top waiter未完成的工作)。

无论是reader还是writer,如果唤醒后持锁失败,并且等待时间已经超过了RWSEM_WAIT_TIMEOUT,这时候就会设置handoff bit,防止等待队列的waiter饿死。具体设置handoff bit的场景如下:

1.jpg

2、清除handoff标记

标记了hand off之后,快速路径、乐观偷锁(reader)、乐观自旋(writer)都无法完成持锁,锁最终会递交给top waiter的线程,完成持锁。一旦完成持锁,handoff标记就会被清除。具体清除handoff bit的场景包括:

1.jpg

3、确保锁的所有权递交给top waiter

1.jpg

十四、结论

标准linux内核的读写锁是在公平性、吞吐量和延迟选择了比较均衡的策略,这样的策略在手机平台上(特别是重载场景下)不能算是“优秀”,只能是合格吧。实际上,在手机用户交互场景中,我们更期望是确保用户体验相关线程的持锁时延,同时兼顾吞吐量。在这样的背景下,OPPO内核团队对linux中的读写锁进行了优化,下一次有机会可以分享我们在读写锁的持锁时延方面做的改进。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10855

    浏览量

    211604
  • Linux
    +关注

    关注

    87

    文章

    11296

    浏览量

    209352
  • 状态机
    +关注

    关注

    2

    文章

    492

    浏览量

    27530
  • Spin
    +关注

    关注

    0

    文章

    4

    浏览量

    8027
收藏 人收藏

    评论

    相关推荐

    Linux下线程间通讯---读写和条件变量

    读写,它把对共享资源的访问者划分成读者和者,读者只对共享资源进行读访问,者则需要对共享资源进行操作。件变量是线程可用的一种同步机制,
    的头像 发表于 08-26 20:44 1470次阅读
    <b class='flag-5'>Linux</b>下线程间通讯---<b class='flag-5'>读写</b><b class='flag-5'>锁</b>和条件变量

    Linux读写逻辑解析Linux为何会引入读写

    除了mutex,在linux内核中,还有一个经常用到的睡眠就是rw semaphore(后文简称为rwsem),它到底和mutex有什么不同呢?
    的头像 发表于 12-04 11:04 923次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>读写</b><b class='flag-5'>锁</b><b class='flag-5'>逻辑</b><b class='flag-5'>解析</b>—<b class='flag-5'>Linux</b>为何会引入<b class='flag-5'>读写</b><b class='flag-5'>锁</b>?

    FPGA代码时,产生了存器有什么影响吗

    经常看到各种HDL代码时说要避免生成存器,但是在某些情况,我不关心那种情况即使它生成了存器,对我的工程实现也没有什么影响啊,想请教下各位大神,既然这样,为什么还要避免生成存器(
    发表于 01-08 23:54

    Lock体系结构和读写机制解析

    问题,JDK中还有另一套读写机制。读写中维护一个共享读和一个排它
    发表于 01-05 17:53

    《有》/《无》/《签约》/《解锁》/《越狱》/《激活》专

    《有》/《无》/《签约》/《解锁》/《越狱》/《激活》专业威廉希尔官方网站 词解析 在讨论区里,大家看到:《有版》,《无版》,《解
    发表于 02-03 11:05 954次阅读

    Linux 自旋spinlock

    背景 由于在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入的概念,只有获取的任务才能够对资源进行访问,由于多线程的核心是CPU的时间分片
    的头像 发表于 09-11 14:36 2080次阅读

    详谈Linux操作系统的三种状态的读写

    读写是另一种实现线程间同步的方式。与互斥量类似,但读写将操作分为读、两种方式,可以多个线程同时占用读模式的
    的头像 发表于 09-27 14:57 3114次阅读

    Linux中的伤害/等待互斥介绍

    序言:近期读Linux 5.15的发布说明,该版本合并了实时机制,当开启配置宏CONFIG_PREEMPT_RT的时候,这些被基于实时互斥的变体替代:mutex、ww_mutex
    的头像 发表于 11-06 17:27 2663次阅读

    使用Linux自旋实现互斥点灯

    自旋最多只能被一个可执行线程持有。如果一个线程试图获得一个已经被持有的自旋,那么该线程将循环等待,然后不断的判断是否能够被成功获取,直到获取
    的头像 发表于 04-13 15:09 770次阅读
    使用<b class='flag-5'>Linux</b>自旋<b class='flag-5'>锁</b>实现互斥点灯

    Linux实例:多线程和互斥到底该如何使用

    最近在多进程和Linux中的各种的文章,总觉得只有文字讲解虽然能够知道多进程和互斥是什么,但是还是不知道到底该怎么用。
    发表于 05-18 14:16 399次阅读
    <b class='flag-5'>Linux</b>实例:多线程和互斥<b class='flag-5'>锁</b>到底该如何使用

    Linux互斥的作用 互斥是什么

    。如果释放互斥时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。 初始化互斥
    的头像 发表于 07-21 11:13 932次阅读

    自旋和互斥的区别有哪些

    之间的区别: 实现方式上的区别:互斥是基于自旋而实现的,所以自旋锁相较于互斥更加底层; 开销上的区别:获取不到互斥
    的头像 发表于 07-21 11:19 9488次阅读

    读写的实现原理规则

    读写 互斥或自旋要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。 读写
    的头像 发表于 07-21 11:21 908次阅读
    <b class='flag-5'>读写</b><b class='flag-5'>锁</b>的实现原理规则

    AQS独占获取

    AQS提供了两种,独占和共享。独占只有一把,同一时间只允许一个线程获得;而共享
    的头像 发表于 10-13 14:51 458次阅读
    AQS独占<b class='flag-5'>锁</b>的<b class='flag-5'>获取</b>

    互斥和自旋的区别 自旋临界区可以被中断吗?

    获得了互斥时,其他线程如果要获取,则必须等待直到该线程释放。互斥的实现通常会利用操作系统提供的原子操作和线程调度机制。当某个线程
    的头像 发表于 11-22 17:41 828次阅读