12

vesperW · 1月8日

Linux内核并发与同步机制解读(arm64)下

作者 | Aaron
来源 | 内核工匠(ID:Linux-Tech)

上章内容请点击Linux内核并发与同步机制解读(arm64)上

概述

从浅到深,逐步分析各种同步机制的功能。

5.读写信号量-rw_semaphore 

5.1原理介绍

rw_semaphore的乐观自旋、handoff机制与mutex的对应机制代码实现略微不同,但是这些机制的思想是完全一样的,因此本章不再赘述乐观自旋、handoff的基本原理,如有需要,请阅读mutex章节。下面仍然通过问答的形式,来深度理解锁的设计原理。

5.1.1为什么要实现读写信号量

在内存管理中,大多数情况下是在读取数据,少数情况下修改数据,读取数据并不会对临界区数据造成影响,这样是可以实现并行读,串行写,从而提升效率。    
image.png

5.1.2为什么早前的kernel版本,writer不乐观自旋呢 

Writer持锁的情况比较简单,因为writer是互斥的,我们可以认为持锁的writer很快就会释放锁,因此reader乐观自旋与mutex的乐观自旋没有区别。

Reader持锁场景比较复杂,writer要等所有reader都释放了才能持锁。如果多个reader持锁,有一个reader因为调度等原因较长时间不能释放锁,writer的忙等就很不值得。

后面的kernel版本,在writer乐观自旋等待reader的场景,加了个timer来管控,忙等超时后就不在等待,这样就实现了writer的乐观自旋。

引入问题:加重偷锁问题

偷锁主要产生在writer持锁的情况下,waiter_list里有reader与writer,如下下图:    

image.png

假设没有乐观自旋各自进入临界区的顺序如上图,及公平进入临界区。乐观自旋后,如下图:

image.png

大家都在忙等,最终谁能进入临界区,完全靠运气,破坏了公平性,极端情况下可能某个waiter(主要是writer)长时间进入不了临界区而饿死。

乐观自旋场景分析:

5.1.3Writer1持锁,writer2申请锁

image.png

Writer2:乐观自旋超时后,把自己加入到waiter队列中,同时唤醒第一个waiter,该场景就是他自己,这个唤醒其实是无效的,因为此时writer2还未睡眠。接下来writer2就会进入真正的睡眠等待状态了。

writer3:由于乐观自旋之前并没有判断waiter标记位,所以writer3也会进入乐观自旋。

Reader1:由于乐观自旋之前并没有判断waiter标记位,所以Reader1也会进入乐观自旋。

由于乐观自旋之前并没有判断waiter标记位,不管是哪种场景,新来的waiter都会乐观自旋。

5.1.4为什么会有handoff机制

思考:加入乐观自旋后谁吃亏?

Writer吃亏。首先这个锁的应用场景,本身就是reader比writer多。其次,reader一旦持锁,它会把其他reader都唤醒。而writer必须排队进入临界区,所以这个机制对wait_list 上第一个writer很不公平。怎么来解决这个问题呢?    

如果writer等的超时了,就把自己标记为handoff(继承责的候选人),系统看到有继承者,就禁止乐观自旋

5.1.5谁会唤醒睡眠中的writer?

image.png

5.1.6为什么如果ower是reader,该owner不可信?

Reader是“一人得道鸡犬升天”新来的reader抢到锁后,会唤醒wait_list上所有reader,owner到底该设置成谁呢?没有合理办法来确认唤醒的先后顺序,也没有办法确认reader释放锁的顺序。因此reader ower指向的task不可信,除非wait_list上从来没有加入过reader。    

image.png

从代码流程看唤醒后也没有去设置owner

5.2代码实现 

5.2.1关键数据结构  

5.2.1.1struct rw_semaphore    

image.png

5.2.1.1.1Count  

image.png

5.2.1.1.2owner  

image.png

5.2.1.2struct rwsem_waiter    

image.png

5.2.1.2.1Timeout  

Waiter等待超时时间,如果waiter是wait_list上第一个waiter,超时时间到,就会标记count的handoff位。

5.2.1.2.2last_rowner  

这个变量的功能,从源码中没有理解其用意。

5.2.2关键函数接口

5.2.2.1down_read

down_read->__down_read

image.png

  • rwsem_read_trylock

image.png

  • rwsem_down_read_slowpath

640.jpeg
640 (1).jpeg

5.2.2.2Reader乐观自旋流程    

image.png

5.2.2.3down_write  

down_write->rwsem_down_write_slowpath    

640 (2).jpeg
640 (3).jpeg

5.2.2.4Writer乐观自旋流程  

image.png

总结:经过以上的分析,读写锁的机制是偏向reader的,writer经常会是系统扶贫的对象(handoff机制)

5.3应用场景

公共数据读多写少的场景。

5.4思考

5.4.1有没有可能把reader持锁都记录下来,方便定位锁引发的问题  
5.4.2如何唤醒等待队列的读者比较合理  

假设一种场景来拓宽读者思考该问题的思路。    

image.png

假设wait_list上的排队如上图,运行在ARM64 8核上。Reader0一次唤醒256个reader,writer1要等256个reader全部完成,才能拿到锁。8个核处理256个reader任务,再加上CPU处于重载(还有系统其他任务),这256个任务可能多数都加入了runqueue里得不到CPU执行,这可能坑死writer1.这种场景如果只唤醒reader1-reader8,显然对writer1比较友好。

6.percpu-rwsem

6.1背景介绍

场景模拟:ARM64 8核处理器。256个readers在临界区,一个writer在wait_list上。这256个readers任意一个状态的变化都要去修改count值,这就意味着要刷新256次全局变量。对于其他7个CPU核来说,共享内存中的值发生了变化,那么当前cpu cache L1的值会被标记为无效。当CPU要再使用这个值时,必须从shmem中重新加载后使用,无法直接使用cache中的值,cache必须刷新后再使用,这就导致操作路径变长,自然耗费的时间也就变长了。这就造成了严重的内存颠簸问题。有没有更好的方法来解决这个问题,让性能变得更好?先对读写锁的特性来波总结,看看有没有发现。    

读写锁的特性:reader一般要远比writer多,也就是说CPU大部分的时间是在处理reader。

Reader修改count的黄色部分,writer修改紫色部分,只有waiter才会冲突,waiter很明显是跟writer绑定在一起的。

image.png

根据这些特性,我们来设定优化场景。

优化场景1:readers在临界区,wait_list空

优化场景2:readers在临界区,writer在wait_list上

场景3:writer在临界区,readers在wait_list上。无法优化。

假设256个reader,平均分给8个CPU,每个cpu执行32个任务,每个cpu把count值复制个副本仅限自己使用。每个cpu独自执行8个任务,都执行完了,再刷新全局的count值或者由waiter自己计算一下8个cpu的任务是否都执行完了。 256次全局刷新,就变成了一次全局刷新。这个优化的机制就是percpu-rwsem。

6.2代码实战

6.2.1关键数据结构  

6.2.1.1struct percpu_rw_semaphore  

image.png

rw_semaphore与percpu_rw_semaphore进行数据结构比较,变化非常大。

1.乐观自旋没有了

2.上面分析了ower如果是reader,那么owner是没有意义的,这里直接改成了writer

3.新增加了原子变量block,用来标记writer已经申请过这把锁了,但可能还没有申请到,也可能已经申请到了。Block也会用于reader申请锁时中速路径的判断,,如果block=0,中速路径直接获取到锁。

4.新增加了rss,这是一个RCU变量,由于reader申请锁快速路径的判断。主要目的是用来实现reader与writer的交换去原子化。

6.2.2关键接口

6.2.2.1percpu_down_read

image.png

6.2.2.2percpu_up_read

image.png

6.2.2.3percpu_down_write

image.png

这里的writer申请锁的逻辑比较简单,percpu_down_write必须要等待所有readers都离开了临界区,才能退出该函数。

6.2.2.4percpu_up_write

image.png

6.3思考

6.3.1.1取消乐观自旋会不会影响性能?    

乐观自旋本来是用来解决writer与reader切换过程时不知道对方下一刻的状态,产生的性能问题。percpu-rwsem并没有新的机制来解决这个问题,因此直接去掉,会对性能造成影响。

6.3.1.2是否会存在偷锁情况?  

严重的偷锁问题主要是乐观自旋机制引入的,当前实现的percpu-rwsem不会引入严重的偷锁问题,但同样存在低概率的偷锁问题,但这不影响系统的性能与稳定性。

6.3.1.3为什么要用RCU 变量rss来做reader申请锁快速路径的判断条件?  

percpu-rwsem设计的核心思想就是去原子化,通过去原子化来提升性能。判断rss如同判断本地变量一样。在rwsem一章节中我们分析,rwsem遗留了内存颠簸的问题,如果单从内存颠簸的优化来看RCU变量rss并无法优化这一点。

6.3.1.4percpu-rwsem是否可以完全替换rwsem?  

通过上面的分析,可以看到percpu-rwsem与rwsem各有优缺点percpu-rwsem解决了内存颠簸问题,但是没有乐观自旋机制。

作者:本文作者 Aaron,首发于公众号“内核工匠”(ID:Linux-Tech),分享Linux内核相关黑科技、技术文章、技术资讯和精选教程,欢迎关注。

来源:OPPO内核工匠

推荐阅读

欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
2889
内容数
282
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息