Linux读写锁逻辑解析

news2024/11/24 9:33:27

一、Linux为何会引入读写锁?

除了mutex,在linux内核中,还有一个经常用到的睡眠锁就是rw semaphore(后文简称为rwsem),它到底和mutex有什么不同呢?为何会有rw semaphore?无他,仅仅是为了增加内核的并发,从而增加性能而已。Mutex严格的限制只有一个thread可以进入临界区,但是实际应用中,有些场景对共享资源的访问可以严格区分读和写的,并且是读多写少,这时候,其实多个读的thread同时进入临界区是OK的,使用mutex则限制一个线程进入临界区,从而导致性能的下降。

本文会描述linux5.15.81中读写锁的数据结构和逻辑过程。

二、如何抽象读写锁的数据结构?

下图可以抽象rwsem相关的数据结构:

cca1a8c16bc9c61d9bc2fa55571f22ec.png

一个rwsem对象需要记录两种数据:

  1. 读写锁的状态信息

  2. 和该读写锁相关的任务信息

我们先看看读写锁的状态。读写锁状态字需要分别记录读锁和写锁的状态:由于多个reader可以同时处于临界区,所以对于reader-owned的场景,读锁状态变成了一个counter,来记录临界区内reader的数量,counter等于0表示读锁为空锁状态。对于writer,其行为和互斥锁一致,因此其写锁状态和mutex一样,仍然使用一个bit表示。

和读写相关的任务有两类,一类是已经持锁的线程(即在临界区的线程),另外一类是无法持锁而需要等待的任务。对于writer持锁情况,由于排他性,我们很清楚的知道是哪个task持锁,那么一个task struct指针就足够了记录owner了。然而对于读侧可以多个reader进入临界区,那么owner们需要组成一个队列才可以记录每一个临界区的reader。不过在实际的rwsem实现中,由于跟踪owner们开销比较大,因此也是用一个task struct指针指向其一。具体linux代码是这样处理的:reader进入的时候会设置owner task,但是离开读临界区并不会清除task指针。这样,实际上对于读,owner task应该表示该任务曾经拥有该锁,并不表示是目前持锁的owner task,也有可能已经离开临界区,甚至该任务已经销毁。

如果持锁失败,无法进入临界区,我们有两种选择:

  1. 乐观自旋

  2. 挂入等待队列

两种选择各有优点和缺点,总结如下:

7ecde1573713de5dc4cf051d424303e3.png

在5.15的内核中,只有在write持锁路径上有乐观自旋的操作,reader路径没有,只有偷锁的操作。当乐观自旋失败后就会挂入等待队列,阻塞当前线程。(乐观自旋功能有一个很有意思的发展过程,从开始支持writer的乐观自旋,到支持全场景的乐观自旋,然后又回到最初,有兴趣可以查阅内核的patch了解详情)

在了解了rwsem的基本概念之后,我们一起来看看struct rw_semaphore数据结构,其成员描述如下:

f84ee5a0fbf4f321fdc3578684d1925e.png

45fa719a3d57d36e3d0fb096f0e387f4.png

由于是sleep lock,我们需要把等待的任务挂入队列。在内核中,struct rwsem_waiter用来抽象等待rwsem的任务,其成员描述如下:

fc282c9187727f14061afa4848ef0f95.png

三、Rwsem外部接口API为何?

Rwsem模块的外部接口API如下:

1255e7adaa229c8ed9a7b39b27849599.png

6e847444e7e03b0ef10b92e7f6a3668c.png

四、尝试获取读锁

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

7bb80ef28ea63641328725d3bd05204b.png

  1. tmp的初始值设定为RWSEM_UNLOCKED_VALUE(0值),因此第一次循环是为当前是空锁而做的优化:如果当前的sem->count等于0,那么给sem->count赋值RWSEM_READER_BIAS,标记持锁成功,然后设定owner返回1即可。

  2. 如果快速获取空锁不成功,这时候tmp已经赋值(等于sem->count),不再是0值了。通过对当前sem->count的值可以判断是否是可以进入临界区。持读锁失败的情况包括:

9e8e0a45841a5e82441466f6e434aeb5.png

如果判断可以进入读临界区(临界区仅有reader并且没有writer等待的场景),那么重新进入循环,如果sem->count保持不变,那么可以持锁成功,给进入临界区的reader数目加一,并设置owner task和reader持锁标记(non-spinnable比特保持不变)。如果这期间有其他线程插入修改了count值,那么需要再次判断是否能持读锁,重复上面的循环。如果判断不可以进入临界区,退出循环,持锁失败。

五、获取读锁

Reader获取读锁的代码主要在__down_read_common函数中,如下:

4330412403a77b0795fe01adcbbf55f7.png

  1. 快速路径

rwsem_read_trylock是快速路径,代码如下:

722b80673617bfaa8c0a9a8da4b1e37f.png

  1. reader直接会给sem->count加RWSEM_READER_BIAS来增加读临界区的线程个数,当然这有可能失败,那么就进入慢速路径(需要回退错误增加读临界区线程数量)。如果恰好能够进入临界区,那么就直接设定owner返回即可。注意:这里*cntp保存了atomic add之后的新值。rwsem_down_read_slowpath会使用这个新值作为参数。

  2. 当reader的数量过多(以至于都溢出了)的时候,需要禁止乐观自旋。

  3. 这里是持锁成功的路径。RWSEM_READ_FAILED_MASK上一节已经解释,这里不再赘述。这里需要注意的是rwsem_set_reader_owned函数中flag的设定,由于reader进入临界区,因此RWSEM_READER_OWNED也需要设定。RWSEM_RD_NONSPINNABLE标记保持不变。

在快速路径中,有两种常见的情况会持锁成功:一种是空锁,另外一种是没有任何waiter等待的纯reader并发。

2.慢速路径

如果快速路径持锁失败,那么进入慢速路径。慢速路径代码比较长,我们分段解析。首先是防止等待队列中waiter任务饿死的代码:

69c0a7e081004ae5726a6718897f18a5.png

如果当前的锁被reader持有(至少有一个reader在临界区),那么不再乐观偷锁而是直接进行挂等待队列的操作。为何怎么做呢?因为需要在饿死waiter和reader吞吐量上进行平衡。一方面,连续的reader持续偷锁的话会饿死等待队列上的任务。另外,在唤醒路径上,被唤醒的top reader会顺便将队列中的若干(不大于256个)reader也同时唤醒,以便增加rwsem的吞吐量。所以这里的reader直接挂入队列,累计多个reader以便可以批量唤醒。

Reader偷锁的场景主要发生在唤醒top waiter的过程中,这时候临界区没有线程,被唤醒的reader或者writer也没有持锁(writer需要被调度到CPU上执行之后才会试图持锁,高负载的场景下,锁被偷的概率比较大,reader是唤醒后立刻持锁,被偷的几率小一点)。具体乐观偷锁(optimistic lock stealing)的代码如下:

2f996af15ed8c10b689dbad0f4dc935f.png

  1. 所谓偷锁就是不乐观自旋(要有排队),不管先来后到,直接获取锁。允许偷锁的场景是这样的:临界区没有writer持锁,也没有设置handoff,正在唤醒top waiter的过程中,并且有任务在等待队列的情况。这时候进入慢速路径的reader可以先于top waiter唤醒之前把锁偷走。需要特别说明的是:这时候reader counter已经加一,还是尽量让reader偷锁成功,否则还需要回退。

  2. 当前线程获得了读锁,需要设置owner,毕竟它是临界区的新客

  3. 如果偷锁成功并且它是临界区第一个reader,那么它还会把等待队列中的reader都唤醒(前提是top waiter不是writer),带领大家一起往前冲(这里会打破FIFO的顺序,惩罚了队列中的writer)。具体是通过rwsem_mark_wake来标记唤醒的reader,然后通过wake_up_q将reader唤醒并进入读临界区。为了减低对等待中的writer线程的影响,这时候对reader的并发是受限的,最多可以唤醒MAX_READERS_WAKEUP个reader。

如果偷锁不成功,当前的reader还是需要进入阻塞状态:

549941fd8c60a546f924938c3cc8d37c.png

  1. 准备好挂入等待队列的rwsem waiter数据,需要特别说明的是这里的timeout时间:目前手机平台的HZ设置的是250,也就是说在触发handoff机制之前waiter需要至少在队列中等待一个tick(4ms)的时间。这里的timeout是指handoff timeout,为了防止偷锁或者自旋导致等待队列中的top waiter有一个长时间的持锁延迟。在timeout时间内,乐观偷锁或者自旋可以顺利进行,但是一旦超时就会设定handoff标记,乐观偷锁或者自旋被禁止,锁的所有权需要递交给等待队列中的top waiter。

  2. 如果目前等待队列为空,那么要做一些额外的处理。例如入队之前肯定给安排上RWSEM_FLAG_WAITERS这个标记。

  3. 当然,在入队之前还要垂死挣扎一下(等待队列为空的时候逻辑简单一些,不需要唤醒队列上的wait),看看是不是当前有机可乘,如果是这样,那么就顺势而为,直接持锁成功,而且counter都已经准备好了,前面已经加一了。

  4. 等待队列非空的时候,逻辑稍微负载一点。调用rwsem_add_waiter函数即可以把当前任务挂入等待队列尾部。这时候也需要把之前武断增加的counter给修正回来了(adjustment初始化为-RWSEM_READER_BIAS)。如果是第一个waiter,也顺便设置了RWSEM_FLAG_WAITERS标记。

在当前线程进入阻塞之前,我们需要进行试图持锁的动作(上面是空队列场景检查,这里的逻辑稍微复杂一点,由于已经入队,这里需要调用rwsem_mark_wake函数来完成阻塞后唤醒的动作),毕竟这时候可能恰好owner离开临界区,变成空锁。

fcf77bd4151ade5e99e32b2601e9b1a8.png

  1. 如果这时候发现锁的owner恰好都离开了临界区,那么我们是需要执行唤醒top waiter操作的,唤醒之前需要清除禁止乐观自旋的标记,毕竟目前临界区没有任何线程。

  2. 除了上面说的场景需要唤醒,在reader持锁并且我们是队列中的第一个waiter的时候,也需要唤醒的动作(唤醒自己)。

阻塞部分的代码逻辑如下:

f79b5179398535ddf7fb329ab1f345d2.png

  1. 在rwsem_mark_wake函数中我们会唤醒reader并将其等待对象的task成员(waiter.task)设置为NULL。因此,这里如果发现waiter.task等于NULL,那么说明是该线程被正常唤醒,那么从阻塞状态返回,持锁成功。

  2. 如果在该线程阻塞的时候,有其他任务发送信号给该线程,那么就持锁失败退出。如果已经被唤醒,同时又收到信号,这时候需要首先完成唤醒,持锁成功,然后在其他的合适点再处理该信号。当然,大部分的rwsem都是D状态,也就不需要处理信号了。

  3. 进入阻塞状态,让调度器选择next task

六、释放读锁

释放读锁的代码逻辑主要在__up_read函数中,如下:

eefbaea5541f2865f69da357e1d1fa6d.png

需要强调的是:这里仅仅是减去了读临界区的counter计数,并没有清除owner中的task pointer。此外,当等待队列有waiter并且没有writer或者reader在临界区的时候,我们会调用rwsem_wake来唤醒等待队列的线程。因为临界区已经没有线程,所以需要清除nonspinable标记。唤醒的动作主要是通过rwsem_mark_wake和wake_up_q来完成的,wake_up_q比较简单,我们就不赘述了,主要看看rwsem_mark_wake的逻辑。

我们首先给出wake type的解释:

781a33788b8b5af0cd6bf16784d90174.png

在RWSEM_WAKE_READERS场景中,多个reader被唤醒,并且当前很可能是空锁状态,为了防止writer抢锁,因此会先让top waiter持有读锁,然后慢慢处理后续。RWSEM_WAKE_READ_OWNED则没有这个顾虑,因为唤醒者已经持有读锁。

在释放读锁的场景中,rwsem_mark_wake使用的是RWSEM_WAKE_ANY参数,具体的代码如下:

3ac0f5311908cf110e9110df35403bca.png

这段代码是处理top waiter是writer的逻辑。这时候,如果wake type是RWSEM_WAKE_ANY,即不关心唤醒的是reader还是writer,只要唤醒等待队列头部的waiter就好。如果top waiter是writer,我们只需要将这个writer唤醒即可,不需要修改锁的状态,出队等操作,这些都是在唤醒之后完成。如果wake type是其他两种类型(都是唤醒reader的),那么就直接返回。也就是说在rwsem_mark_wake想要唤醒reader的场景中,如果top waiter是writer,那么将不会唤醒任何reader线程。如果top waiter是reader的话,那么基本上是需要唤醒一组reader了。

a9ce162f81427b579c34bfb25b0d748d.png

  1. 执行到这里,我们需要唤醒等待队列头部的若干reader线程去持锁。由于writer有可能会在这个阶段偷锁,因此,这里我们会先让top waiter(reader)持锁,然后再慢慢去计算到底需要唤醒多少个reader并将其唤醒。如果当前线程已经持有了读锁(wake type的类型是RWSEM_WAKE_READ_OWNED),则不需要提前持锁,直接越过这部分的逻辑即可。

  2. 如果的确发生了writer通过乐观自旋偷锁,那么我们需要检查设置handoff的条件。如果reader被writer阻塞太久,那么我们设定handoff标记,要求rwsem的writer停止通过乐观自旋偷锁,将锁的所有权转交给top waiter(reader)

  3. 上面已经向rwsem的count增加reader计数,这里把owner也设定上(flag也同步安排,这里non-spinnable bit保持不变)。随后top waiter的reader会唤醒若干队列中的non top reader,但是它们都不配拥有名字。

读锁已经安排的妥妥的了,下面就是慢慢唤醒等待队列的reader了。我们通过两步来完成唤醒:

  1. 将等待队列中的reader摘下放入到一个单独的列表中(wlist),同时对reader进行计数。后续这个计数会写入rwsem 的reader counte域。

  2. 对于wlist中的每一个waiter对象(reader任务),清除waiter->task并将它们放入wake_q以便稍后被唤醒。

我们先看第一轮计算唤醒reader个数的计数:

58d3e6226fb024cfd506bbb1e81e7980.png

  1. 对于rwsem,其公平性是区分读写的。对于读,如果top waiter是reader,那么所有的reader都可以进入临界区,不管reader在队列中的顺序。对于writer,我们要确保其公平性,我们要按照writer在队列中的顺序依次持锁。根据上面的原则,我们会略过队列中的writer,将尽量多的reader唤醒并进入临界区

  2. 唤醒数量不能大于256,否则会饿死writer

  3. 根据唤醒的reader数量计算count调整值

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

1a719d55588f1bfd2005c6419743859a.png

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

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

  3. 完成sem->count的调整

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

b022238d442c07f3e5485218191bb18b.png

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

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

七、尝试获取写锁

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

b455a4fdb863b34cf25c032a2fe4219b.png

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函数中,如下:

f0f9350469caf65f90d490f016691099.png

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

7ca912076909b3005126d1ba68760e92.png

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

8901bc745e496760f048cf2b2c049c3c.png

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

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

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

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

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

284ec93f0e70dee21aceb782b992c3c3.png

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

  2. 有pending的信号,异常路径退出

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

  4. 进入阻塞状态

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

rwsem_try_write_lock代码如下:

577b59fdd3adbf8ef588a9c8a36f73af.png

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

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

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

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

c70dc132fe6fed76cbb64c86a60766bb.png

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

九、释放写锁

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

十、乐观自旋的条件

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

e6ddd770c3bc27279ce76ef86dfad82f.png

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

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

  3. 如果该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有效),详细的代码逻辑如下:

7ef6d29fa77fb0bf6d61fa52192133a5.png

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

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

b577f1df119ec9893abc01343884856d.png

只有明确的知道当前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不断自旋去持锁:

55f0c39044fd7ef5079f0b4beb64cb3a.png

1cb20c1af736513894fe1a965f09bb22.png

  1. 对于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掉,自旋失败,下面就准备挂入等待队列了。

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

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

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

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

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

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

  2. 锁是空闲的并且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的场景如下:

762be09c2d6b506f18f8042cc4bd5465.png

2.清除handoff标记

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

a2c489ea74de3bca45f1f43ad8b16551.png

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

2eac75288961d8dd422813f7e7990f53.png

十四、结论

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

参考文献:

  1. linux-5.15.81内核源代码

  2. linux-5.15.81\Documentation\locking\*

649674fafbc646e7339711c02414ee83.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章 | 精选教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/575164.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在Flask中构建API接口

重定向行为 斜杠 以下两个路由的不同之处在于是否使用尾部的斜杠。 第一个路由的URL尾部有一个斜杠,看起来就像一个文件夹,访问一个没有斜杠结尾的URL时,Flask会自动进行重定向,在结尾加上一个斜杠。 第二个路由的URL没有尾部…

CVPR 2023 | 风格迁移论文3篇简读,视觉AIGC系列

CAP-VSTNet: Content Affinity Preserved Versatile Style Transfer 内容相似度损失(包括特征和像素相似度)是逼真和视频风格迁移中出现伪影的主要问题。本文提出了一个名为CAP-VSTNet的新框架,包括一个新的可逆残差网络(reversib…

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权)

目录 概述 微信登录接口说明 关于获取微信用户的信息 前端代码(uni-app) 后端代码(SpringBoot) 配置文件:application.yml 配置文件:Pom.xml 类:WeChatModel 类:WeChatSe…

《面试1v1》JVM内存模型

聊聊 JVM 内存模型 我是 javapub,一名 Markdown 程序员从👨‍💻,八股文种子选手。 面试官: 你好,请问你对JVM内存模型有了解吗? 候选人: 当然,JVM内存模型是Java程序运…

【计算机视觉 | 目标检测】术语理解5:Split Shuffle Block、Group Shuffle Block 和复杂非结构化室内场景

文章目录 一、Split Shuffle Block二、Group Shuffle Block三、复杂非结构化室内场景 一、Split Shuffle Block Split Shuffle Block(分割混洗块)是一种用于深度学习模型的基础组件,旨在增强模型的表征能力和学习能力。该概念最常用于图像分…

3D图像双线性插值

文章目录 前言结论说明:公式 测试 前言 看了一下2d图像的双线性插值的理论,基本上都是在原图上找到对应的浮点坐标 p f p_f pf​后,将以 p f p_f pf​外围的4个点进行计算。计算的方法类似于二维直线方程的理论,但是写成了权重的…

《低代码指南》——维格云低代码管理系统解决方案,成倍降低开发成本

目录 典型场景介绍 一、采购管理 二、产品BOM管理 三、成本核算管理 “我之前是打算自己去开发ERP系统,大概要用上八九个月时间,而且还不是很稳定。但现在用维格云,我们一个人做个一两个月,就可以做到很稳定了。因此,即使需要付出一些学习成本,但无代码的确能极大的帮…

自动驾驶系统中摄像头相对地面的在线标定

文章:Online Camera-to-ground Calibration for Autonomous Driving 作者:Binbin Li, Xinyu Du, Yao Hu, Hao Yu, and Wende Zhang 编辑:点云PCL 欢迎各位加入知识星球,获取PDF论文,欢迎转发朋友圈。文章仅做学术分享&…

记录--前端小票打印、网页打印

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一、小票打印 目前市面上的小票打印机大多采用的打印指令集为ESC/POS指令,它可以使用ASCII码、十进制、十六进制来控制打印,我们可以使用它来控制字体大小、打印排版、字体加粗…

如何用 ChatGPT 做数据进阶可视化?(三维交互图与动图视频)

你只需输入数据和需求,结果自然来。 自动可视化 在《如何用 ChatGPT 帮你自动分析数据?》这篇文章里,我已经为你介绍过 Code Interpreter 。它是 ChatGPT 的一个模式,目前还在 alpha 测试阶段。 Code Interpreter 可以接收文件输入…

Android 图片编码之必备技能

在进行 Android 开发时,不可避免地会接触到许多图片格式,例如 JPEG、PNG 等。就以 JPEG 格式为例,它是一种有损压缩模式,使用 YCbCr 的颜色空间来保存色彩信息。当需要在屏幕上显示图片时,会将 JPEG 数据解码成 RGB 进…

淘宝用户体验分析方法论

本专题共10篇内容,包含淘宝APP基础链路过去一年在用户体验数据科学领域(包括商详、物流、性能、消息、客服、旅程等)一些探索和实践经验,本文为该专题第一篇。 在商详页基于用户动线和VOC挖掘用户决策因子带来浏览体验提升&#x…

chatgpt赋能python:Python扫描IP段的简介

Python 扫描 IP 段的简介 Python 是一种广泛应用于数据科学、机器学习、Web 开发等领域的高级编程语言。作为一种通用编程语言,Python 也可以应用于网络安全领域。其中,Python 可以用于扫描 IP 段的网络安全工具开发。 Python 扫描 IP 段 Python 扫描…

5.27下周黄金行情走势预测及开盘操作策略

近期有哪些消息面影响黄金走势?下周黄金多空该如何研判? ​黄金消息面解析:周五(5月26日)黄金大幅下跌,主要受到美国数据影响,美国公布的4月PCE和耐用品订单数据向好,再次强化市场对美联储的鹰派押注。现货…

软件测试之自动化测试【webdriver API】

目录 一、webdriver API 1.元素的定位 2.操作测试对象 3.添加等待 3.1 sleep 强制等待 3.2 隐式等待 3.3 显式等待 4.打印信息 5.浏览器的操作 5.1 浏览器的前进和后退 5.2 浏览器滚动条操作 5.3 浏览器最大化及设置浏览器宽、高 6.键盘按键 7. 鼠标事件 8.定位…

chatgpt赋能python:Python找零-让你的生活更轻松

Python 找零 - 让你的生活更轻松 在我们日常生活中,找零是一个很常见的问题。无论是在超市买东西、给朋友拿钱、或者是做商业交易,都需要进行找零操作。而使用 Python 编程语言,可以让这个问题更加简单易懂,让我们来一起学习 Pyt…

Python中的布尔类型以及布尔值介绍

什么是布尔类型? 布尔类型是一种逻辑类型,它只有两个取值:True(真)和False(假)。在Python中,True和False是内置的布尔类型常量,用于表示真和假的状态。 布尔运算符 在P…

一场九年前的“出发”:奠基多模态,逐鹿大模型

原创:谭婧 全球AI大模型的技术路线,没有多少秘密,就那几条路线,一只手都数得过来。 而举世闻名的GPT-4浑身上下都是秘密。 这两件事并不矛盾。为什么呢? 这就好比,回答“如何制造一台光刻机?”。…

Yolov5/Yolov7涨点技巧:MobileViT移动端轻量通用视觉transformer,MobileViTAttention助力小目标检测,涨点显著

1. MobileViT介绍 论文:https://arxiv.org/abs/2110.02178 现有博客都是将MobileViT作为backbone引入Yolov5,因此存在的问题点是训练显存要求巨大,本文引入自注意力的Vision Transformer(ViTs):MobileViTAttention MobileViT是一种基于Transformers的轻量级模型,它可以用于…

chatgpt赋能python:Python操作手机:SEO指南

Python 操作手机:SEO 指南 在移动设备占据互联网用户市场大头的今天,应用程序的互动变得越来越受欢迎。这就需要我们在开发和优化网站时将手机端无缝集成到我们的计划中。使用 Python 语言可以有效地实现此目标,本文将探讨如何使用 Python 操…