目录:
一、铺垫知识
1
、指令执行流
2
、上下文
3
、抢占
二、内核锁基础知识
1
、为什么要用锁?
why
2
、锁保护什么?
what
3
、锁是如何保护资源的?
How
三、各类锁的介绍
1
、原子操作
2
、
spinlock
3
、
mutex
4
、
…
进程指令执行流
代码在
CPU
上执行的指令数据流,由一系列代码组成。可分为两大类:
线程维度和中断维度
1
、
cpu
只要上电,就需要不停的执行指令,永不停歇,若无事可做,那就执行空指令,直到下电。
cpu
类似一个跑道,各个
task
轮流到跑道上跑。
2
、用户态视角:
cpu
执行一个个进程或者线程,
一个线程就是一个执行流
。内核态视角:
CPU
调度执行一个个
task
(对应一个进程或者线程),
一个
task
就是一个执行流
。
CPU
就这样永不疲倦的轮换执行各个
task
(调度)。
8
个
CPU
,同一时刻,最多有
8
个执行流。
3
、
并发
:用户态系统调用的代码编译成
so
,当
so
被多个
bin
链接时,运行时,就可能有多个指令执行流,就有可能分别在
cpu0
和
cpu1
上执行。
宏观并行,微观串行。
4
、代码是静态的,执行流是动态的,这是两个不同的视角。
若只存在一个
cpu
,那就只有一个执行流
,那就不存在微观上的并发,也就不需要锁。但我们往往使用多个
cpu
,因而有多个执行流,同一个代码可能在两个执行流上执行。如上图。
上下文
(context)
分类:进程上下文,中断上下文(硬中断上下文、软中断上下文,不可屏蔽中断上下文)
中断能够打断进程的执行流,无论进程优先级多高,都会被打断。因此原本执行流被打断,
CPU
转而执行中断的指令流。因此,与上下文对应,执行流也可分为:
进程执行流和中断执行流
1
、
cpu
在用户态运行时,外部中断触发,程序会先陷入内核态,保存上下文后,再执行中断代码。
2
、
cpu
在内核态运行时,外部中断触发,保存上下文后,执行中断代码。
3
、中断处理函数(中断上半部)必须要快速执行完成,以便返回继续执行各个进程。中断返回时,会发生调度,原来被打断的进程未必能够得到执行。
假设进程和中断都调用如下函数,那么就有可能出现如下场景:
进程执行流:
1
、某进程执行完
743
行后,全局变量
enable=1
;
2
、外部触发中断。
程序执行流:
7
、从
744
行继续执行,此时
enable
变成
0
了。后边就会出错
中断执行流:
3
、中断处理函数恰好也调用这个函数;
4
、走了
case1
的分支,将
enable
又改成
0
。
5
、中断执行完毕返回。
抢占
什么是抢占? 一张图展现
1
、抢占可以分为用户态抢占和内核态抢占。抢占时机?(中断、返回用户态、主动
schedule
())
2
、
Linux kernel
是抢占式内核。
为什么要使用锁?
-Why
为了性能,引入了多核。
但多核导致了并发,并发就导致了争抢,为了保证争抢有序,就出现了锁。
备注:
网上关于锁的介绍的文档很多,推荐知乎兰新宇的博客
关于锁的介绍,有一系列文章,个人感觉写的非常不错,我主要从这个博客学习锁的知识。
锁保护什么?
-What
锁保护的对象:公共资源。公共资源,从某一方面讲,就是全局变量,或者从堆里面申请的共用内存。
如右图内核代码:
1
、锁保护的是全局变量
zone
(从堆上申请的)。
2
、无法保护局部变量
tmp
和
low
,局部变量也不需要保护,想想这是为什么?
3
、代码段不需要保护,因为它是只读的。
误区
:常认为锁保护的是一段代码和变量。
其实
:从保护资源的角度,锁只是为了保护全局变量,公共资源是一种更严谨的通用的说法。若非要说“锁也保护了代码”,那也只是在保护全局变量的时候,顺便保护了代码而已。
使用锁,首先要搞清楚使用锁的目的是什么?要保护什么?
锁是如何保护资源的?
–How
核心原理:根据全局变量的状态来实现锁的控制。
“统一到门口排队,依次获得钥匙”
无进程持锁时,
lock=0
;
进程
0
持锁后,
lock=1
;
进程
1
再尝试持锁时,由于
lock=1
,无法获取,只能等待。
因此:
lock
一定要是全局的
,
8
字节,
其实只用
1bit
即可
。
Spinlock
就充分利用这
8
个字节,仅用
1bit
表示锁状态。
衍生出锁的两大功能:
同步
和保护。
锁的种类
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
原子操作
最小执行单元,未操作完成前,不允许被任何事件打断。
是实现其他锁的基础,是基础中的基础。
Spin lock
是实现其他锁的基础,原子操作是实现
spin lock
的基础。
3、ARM
原子操作采用的方法主要是
LL/SC
(Load-Link/Store-Conditional)
。
Arm
内存单元上的数据不允许被直接操作,而是必须先放到寄存器中,不能直接对内存进行更改操作。例如,写操作“
x=5
;”
,
需要先将内存的值读取到寄存器内,修改后,再从寄存器写会内存。
原子操作是:通过指令和硬件的配合,保证
”x=5;”
这一过程不被打断,一次执行完毕。
Spin lock
特点:
1
、自旋,
性格刚烈
,不达目的誓不罢休,一直占着
CPU
不放,不进入睡眠,关闭了抢占,故也不做调度 。
2
、无论读写,只要访问临界区,就需要加锁。
3
、谁持锁,谁释放。
4
、是实现其他锁的基础,其他锁基本都是基于
spin lock
衍生出来的。若要理解其他锁,须先理解
spinlock
简述:
Linux
中
spinlock
机制发展到现在,其实现方式的大致有
3
种:
一、经典的
CAS
(
Compare And Swap
),现在已经不使用,理解经典
CAS
,有助于理解后面的锁。
二、
Ticket Spinlock
,
中间态,并未广泛使用,但为
qspinlock
打下了坚实的基础。
三、
qspinlock
,目前
Linux
中广泛使用的
spinlock
机制,高通、
MTK
平台等当然使用此种机制。
锁的实现与
CPU
架构体系强相关,下面以
ARM
架构体系为例,说明
spinlock
的实现。
详解:
一、经典的
CAS
(
Compare And Swap
):
最古老的一种做法是:
spinlock
用一个整形变量表示
,其初始值为
1
,表示
available
的状态。当一个
CPU
(设为
CPU A
)获得
spinlock
后,会将该变量的值设为
0
,之后其他
CPU
试图获取这个
spinlock
时,会一直等待,直到
CPU A
释放
spinlock
,并将该变量的值设为
1
。
二、
Ticket Spinlock
:
CAS spinlock
存在不足:他是不公平的,一旦锁被释放,下一个获得锁的
cpu
不确定,有可能后到先得。大家都抢,没有排队。
为了解决这种「无序竞争」带来的不公平问题,
spinlock
的另一种实现方法是采用排队形式的
“ticket spinlock”
。
核心原理:
去银行办理业务,需要先取个号,例如号码位
5
,然后排队等待叫号,此时银行正在给
2
号办理业务,
2
号办理完业务后,号码加
1
,变为
3
号,直到变为
5
号,就会轮到自己。
Linux
版本的实现如下:
不足:
1
、尽管实现了排队,但当有多个
cpu
等锁时,每个
cpu
都要通过
cache line
不停的访问同一块内存,即便是值没有变,也要不停的刷新
cache line
去读值,浪费功耗。只有一个
cpu
的刷新是有意义的,其他
cpu
都是在做无用功。
二、
Ticket Spinlock-MCS
实现方法:
如果在
ticket spinlock
的基础上进行一定的修改,让每个
CPU
不再是等待同一个
spinlock
变量,而是基于各自不同的
per-CPU
的变量进行等待,那么每个
CPU
平时只需要查询自己对应的这个变量所在的本地
cache line
,仅在这个变量发生变化的时候,才需要读取内存和刷新这条
cache line
,这样就可以解决上述的这个问题。
要实现类似这样的
spinlock
的「分身」,其中的一种方法就是使用
MCS lock
。试图获取一个
spinlock
的每个
CPU
,都有一份自己的
MCS lock
。
不足:
排队问题解决了,功耗问题也解决了。但该机制仍存在不足之处:
MCS lock
多了一个指针,要多占
4
(或者
8
)个字节,消耗的存储空间是原来的
2-3
倍。
Spinlock
在操作系统中使用非常广泛,数量很大,那么消耗的内存空间也很大。
那有没有更优的方案呢?
qspinlock
(
queue spinlock
)
三、
qspinlock
: (
queue spin lock
)
qspinlock
是目前被广泛使用的
spinlock
的实现方式,我们正在使用的就是这种机制。同时解决了排队、功耗和内存消耗的问题。
qspinlock
的实现比较复杂,若要深入理解,建议读兰新宇的博客:
Linux中的spinlock机制[三] – qspinlock – 知乎
以下内容是从他的博客上
copy
的。
qspinlock
1
、第一个
cpu
获得锁后,三元数组
(0,0,1)
(皇帝登基,住在皇宫。 )
2
、第二个
cpu
排队等锁,
(0,1,1)
(太子排队,住在东宫,仍属皇宫。 )
3
、第三个
cpu
排队等锁,
(0,1,1)
,
tail
指向
per
cpu
的
mcs
node
,
tail
的值为等待
cpu
的编号,若
tail=5
,代表第
5
个
cpu
正在等待锁。
(皇宫外建府邸。 )
spinlock API
调用流程
慢速等待路径里面,又实现了排队机制,第一顺位继承人
(
太子
)
,第二顺位继承人(宫外建府)。又是一堆复杂的代码逻辑。
spinlock
死锁问题举例
Raw_lock
状态分析:
Locked =1
;
//
说明申请锁时,锁已经被其他线程持有,只能等。
Pending =1
;
//
说明申请锁时,前面已经有线程排队,本次申请只能是第二顺位以后的申请,前面有
1
个线程已经持续,至少有
1
个线程在排队等待持续。
Locked_pending
= 0x0101; //
就是
count
的前半段,联合体的另一种表达方式,表示的仍然是
locked
和
pending
的状态。
tail =0x10; //
这
8bit
用于表示已经持锁的
cpu
和其所处的上下文,算法是:高
6bit
表示持锁的
cpu
,低
2bit
表示当前所属的上下文
context
,(
Linux
中一共有
4
种
context
,分别是
task,
softirq
,
hardirq
和
nmi
)。高
6bit
的二进制是:
000100
,值为
4
,
4-1
表示当前持锁的
cpu
,即
cpu3
当前正在持锁。
当前进程运行在
cpu3
上,申请持锁的函数
也正运行在
cpu3
上。
AA
型死锁。
spinlock
与抢占
特点:
自旋,
性格刚烈
,不达目的誓不罢休,一直占着
CPU
不放,不进入睡眠,关闭了抢占,故也不做调度 。
那么,如何理解上面的意思?
普通
API
:
spin_lock
()/
spin_unlock
()
普通
API
建立的临界区,还是能被中断打断的,还能不能更霸道一些?当然可以。
关中断
API
:
spin_lock_irq
()/
spin_unlock_irq
()
spin_lock_irqsave
()/
spin_unlock_irqstore
()
Spinlock
霸占
CPU
不释放的情况举例
长时间占着
CPU
不释放,不参与调度。只有在关闭抢占的情况下会发生。
而
spinlock
首先要关闭抢占。
一般情况下,我们的代码是不会主动关闭抢占的。
Spinlock
与中断
是我,不建议使用spinlock1
、中断处理函数内,不建议使用spinlock1
、中
断
1、中断处理函数指的中断上半部,需要快速执行完毕,以便响应下次硬件中断。软中断、
tasklet
、中断线程化、中断工作队列等都是中断下半部,处理耗时操作。
2
、中断处理函数要快速执行完毕,若使用锁,那就有可能出现等锁的情况,那就要耗时等待。若在中断处理函数内等待,就会造成
cpu
资源浪费,
cpu
空转等待。占着
XX
,不
XX
。
假设一个
CPU
上的线程
Task_1
持有了一个
spinlock
,发生中断后,该
CPU
转而执行对应的
hardirq
。如果该
hardirq
也试图去持有这个
spinlock
,那么将无法获取成功,导致
hardirq
无法退出。在
hardirq
主动退出之前,线程
T
是无法继续执行以释放
spinlock
的,最终将导致该
CPU
上的代码不能继续向前运行,形成死锁(
dead lock
)
spinlock
使用注意点
spinlock保护资源时,临界区应尽可能的短,临界区不能有太耗时的操作
Spinlock
的特点是
spin
,也就是自旋,一直占着
CPU
,
性格刚烈
。若是在临界区内耗时太长,则其他任务得不到执行,影响调度,进而影响性能,严重时,甚至会导致
watchdog
。
案例:手机因
watchdog
而
panic
。
1
、
cpu
卡死的堆栈如右图。
2
、原因就是
xchi_stop
()
先持
spinlock
锁,并关闭中断。世界安静了,无人再打扰该线程的执行,但该线程在读取硬件寄存器时,深深的沉睡下去,硬件异常,一直无法返回信号。软件一直等到,超过
10s
,触发
watchdog
。
Spinlock
的不足及改进
1
、若是想在临界区内睡眠,
spinlock
就不能使用。那该如何解决呢?
2
、访问要保护公共资源就需要解锁。若读多写少,每次读时,也需要加锁,读也要排队等待,浪费资源。那能不能读的时候不加锁,而写时候加锁呢?
为了解决这两个问题,内核社区的大神们付出了很大努力,给出了各种解决方案,各个方案都有各自的优缺点和使用场景。
充分体现了内核工匠们精益求精的精神。
睡眠相关概念:
系统睡眠
:不多解释
CPU
睡眠
:
cpu
只有掉电才能睡眠,
CPU
睡眠,系统也就睡眠了。
进程
(
线程
)
睡眠
:进程让出
cpu
,不再运行。在内核调用
schdeule
()
后,即进入睡眠。用户态是无法进入睡眠的。通常说的阻塞
(block)
其实就是进程睡眠了。此处的睡眠就是进程睡眠。
Mutex lock
spinlock
临界区内不能进入睡眠,为了实现临界区内也能睡眠的功能,内核社区开发出
mutex lock
,也叫互斥锁
1
、
task
首次持锁时,要先判断
owner
的低
4bit
是否为
0
,若为
0
,直接获得锁,并将第
0bit
置位
1
,代表已经有
task
持锁。同时将该
task
地址记录高
60bit
。
2
、若低
4bit
不为零,就比较复杂了,就有乐观等待和慢速等待路径两种情况了,为了容易理解,暂时先这样理解:第二个申请锁的
task
会将自己挂到
mutex
锁的等待队列上,然后
task
进入睡眠。因此,
mutex
允许睡眠。
3
、
spinlock
wait_lock
此处是为了保护竞争
mutex
锁的
owner
,防止出现竞争。
临界区应尽可能的短,临界区内不能有太耗时的操作。ock保护资源时,临界区应尽可能的短,临界区内不
Mutex lock
问题举例
右图,
1
、当前
task 19722
处于等
mutex
锁状态,将自己挂在了等待队列上,然后调度出去,让出
cpu
。
2
、由于:
owner = (
counter = 0xFFFFFF87A513604
1
),
所以:
mutex
锁已经被其他
task
持有,
当前持有
mutex
锁的
task
地址是
:
0xFFFFFF87A5136040
,红色
1
代表锁已经被持有。
3
、可以看到
wait_list
已经有
3
个等待者了。为什么不是
4
? 又有几个
task
竞争锁呢?
4
、第
1066
行的
spin_unlock
()
又是什么意思呢?
Mutex lock
& spinlock
混合类型:
spinlock
临界区不能嵌套
mutex
,否则死机。
mutex
可视作是
spinlock
的可睡眠版本,等锁时,同样是线程无法继续向前执行,但:
1
、
spinlock
是
“spin”
,导致该
CPU
上无法发生线程切换。
临界区内不能调用可能阻塞的函数
,例如:不能调用
sleep(),
copy_from_user
()
或者
kmalloc
(GFP_KERNEL)
等。
2
、而
mutex
是
“block”
,阻塞,
可以发生线程切换
,让所在
CPU
上执行其他线程。阻塞既可以发生在线程试图获取
mutex
时,也可以发生在线程持有
mutex
时。临界区内可以调用
sleep(),
copy_from_user
()
或者
kmalloc
(GFP_KERNEL)
等。
驱动开发过程中,需要根据实际情况来选择。
首先要明确使用锁的目的,要保护什么?还是要同步?
Spinlock
使用举例
以
printk
(
fmt
, …)
为例
:
printk
()
会调用到
vprintk_emit
()
,为了保证
log
有序打印到
logbuf
内,每条日志打印时,都需要持
spinlock
锁,并关闭中断。
关中断,持
spinlock
锁。目的:“同步”。
想想,此处能使用
mutex
吗?
从技术上讲,此处当然可以使用
mutex
,但是谁又希望自己在打印
log
的时候被调度出去呢?即便此时有
task
在持锁打印
log
,那应该也会很快完成,我就稍等一会呗。因此选用了
spinlock
。
还是那句话,一定要清楚自己持锁的目的是什么?根据自己的目的才能选择合适的锁。
mutex
使用举例
以电源管理的regular_enable()为例,讲述锁的使用,中间省略了一些次要调用的过程。内核中各个器件都要使用电源,经常是多任务并发,而设置电源电压有时候需要路由到AOP侧。AOP是一个简单的RTOS系统,不可能有多任务对应AP侧的请求,那么。多对一,为什么没有发送条件竞争了?
rwlock
读写锁
rwlock
的全称是
“reader-writer spin lock”
,是有
spinlock
衍化过来的,和普通的
spinlock
不同,它对
“read”
和
“write”
的操作进行了区分。
1
、如果当前没有
writer
,那么多个
reader
可以同时获取这个
rwlock
。
2
、如果当前没有任何的
reader
,那么一个
writer
可以获取这个
rwlock
。
使用方法:
1
、对于
reader
,依靠
read_lock
()
和
read_unlock
()
来限定读取一侧的临界区
每当一个
reader
进入临界区,就需要将读取一侧的
cnts
加
1
,退出则减
1
。只有当这个
cnts
的值为
0
,
writer
才可以执行自己的临界区代码
2
、对于
writer
,依靠
write_lock
()
和
write_unlock
()
来限定写入一侧的临界区。
不足之处:
设想,当有很多读者进入临界区后,若想写,那就必须要等读者读完。这对写来说,不公平。 目前读写锁使用越来越少,新代码基本不在使用这个锁。
seqlock
顺序锁
seqlock是由Stephen Hemminger负责开发,自Linux 2.6版本引入的,其全称是“sequential lock”。相比起rwlock,它进一步解除了reader与writer之间的互斥,
只保留了
writer
与
writer
之间的互斥
。只要没有其他的writer持有这个seqlock(即便当前存在reader持有该seqlock),那么第一个试图获取该seqlock的writer就可以成功地持有。
那么,若正在读时,有写修改,如何保证读的一致性呢?
l
写开始时,有
writter
持有
seqlock
之后,
seqcount
的值就会加
1
;
l
写结束后,
writter
释放
seqlock
,
seqcount
的值再次加
1
;
l
读开始时,记录
seqcount
的值;
l
读结束后,再次读取
seqcount
,若发现本次读取的值和前面读开始时记录的值不同,则说明有写修改,那么,将放弃本次的读操作,
重新读取一次。
l
很显然,
seqlock
存在不足,若是读的临界区比较长,那么再重来一次,耗时很长,影响效率。
适用场景:读多写少,在
Linux
中的一个重要应用就是表示时间的
jiffies
。但整体的使用场景不多。
信号量
semaphore
不管是mutex还是spinlock,都限定了某一时刻只有一个线程可以获得临界区资源,但在某些场景下,临界区资源允许多个线程同时访问,这时就可以使用semaphore。Linux中semaphore的定义同mutex很相似,都是包含一个保护该结构体的spinlock和一个等待队列”wait_list”。
semaphore是没有”owner”的,它只需要一个标识共享资源数目的”count”,因而也被称为counting semaphore。
只要semaphore对应的“count”的值大于0,线程获取semaphore就可以成功,但它会使可进入临界区的线程数目减少(对应“count”值减1),所以有两个API:
down(); //加锁,counter减1,花钱。
up(); //释放锁,counter加1,挣钱。
semaphore是没有“owner”的,故有一显著特点:
A task
加的锁,
B task
可以释放锁,这是与
spinlock
和
mutex
的显著区别。
Linux
内核主要是使用了信号量的这个特点。
(内核一般将
counter
设置为
1
,即二义信号量。 类似
mutex
,但又有区别)
读写信号量
rw_semaphore
sa建议使用spinlock操作抵抗力理函数
处理数内,不建议使用
为了区分读写,
spinlock
衍生出
rwlock
,同理,
semaphore
也衍生出了
rw_semaphore。
读写信号量(rw_semaphore)持锁逻辑:
1
、读时,要判断是否有写锁
。
l若有写者持锁,则等待。此时,类似mutex等待。
l若无写者持锁,则获取读锁成功,conut的高8位以上的数加1, count低8位用作记录写相关的标记。
2
、写时,判断是否有读者和写者持锁
。
只有没有读者和写者持锁时,才能申请到写锁。
l若有写者持锁,则等待,此时,类似mutex等待。
l若无写者,只有读者,情况就比较复杂。首先置位count的bit1,阻止后面的读者再次持读锁。然后进入等待期,等待前面的读者完成读操作。
持有写锁后,owner设置为写者,记录当前谁持有写锁。
关于
owner
:
”
owner
“用于表示获得rwsem的reader或writer信息。当一个writer线程获得rwsem后,它就会将自己的”task_struct”指针填入”owner”,直到释放时才清除。如果排在等待队列首位的线程是一个reader,那么队列中将可能有多个reader被唤醒来获取这个rwsem,”owner”填入的将是最后一个获得rwsem的reader线程的”task_struct”指针。
读写信号量使用还是比较多的,尤其常见与内存管理相关,核心数据结构
mm_struct
的成员
mmap_lock
就是读写信号量。在
binder
调用时,必定要申请内存,频繁调用
mmap_read_lock
();
这里面的实现就是使用了读写信号量。
RCU
锁
-read copy update
【
核心思想
】
:
1
、若要读取公共资源时,那就尽情的读。
2
、若要写公共资源,那就将原来的公共资源
copy
出一份来,修改完成后,替换原来旧的公共资源,旧的公共资源需要释放掉。
【
使用场景
】
:
1
、读多写少。
2
、访问内核链表资源。(内核中存在各种各样的链表,
RCU
发挥了其用武之地)
RCU 锁的原理比较简单,但其实现原来非常复杂,涉及到的知识非常广泛,涉及到中断、进程切换、tick时钟、RCU软中断、内核rcu的gp专有线程、RCU tree的各个数据结构。大家若有兴趣研究RCU锁,推荐如下博客:
https://zhuanlan.zhihu.com/p/89439043
https://zhuanlan.zhihu.com/p/374902282
关于RCU锁,有一句话想说:
当你感觉岁月静好的时候,一定是另外有一个人帮你承担了
。
reader 是轻松了,但对于 RCU 中的一个 writer 来说(包括 updater 和 reclaimer),需要申请新的内存空间,向内核注册回调函数,以及进行数据的更新操作,同时还要考虑与其他 writer 之间的互斥问题,开销较大。为了减少writer的负担,又引入了专有线程。最终的实现非常的复杂,主要还是为了解决“
什么时候释放、谁来释放
” 的问题。
RCU锁我们最常遇到的问题是RCU stall,直接原因是在指定的时间(21s)内没有释放旧内存。总结了一下,基本就是如下两种情况:
1
、持
rcu
读锁的
task
无法得到执行
,即无法获取cpu资源。这类问题需要分析调度信息,看看为什么task无法调度到,往往是由于高通优先级的task占用了cpu资源,低优先级的task无法调度,例如中断风暴,RT task太多等。
2
、持
rcu
读锁的
task A
能够执行,但在
RCU
临界区内又需要等待其他锁,
A
长时间无法出
RCU
临界区,
这种情况也会发生rcu stall,需要恢复堆栈分析那个task长时间占用了锁,往往是锁套锁,相互依赖
RCU
锁
–GP(Grace Period)
GP:常翻译成宽限期,RCU之所以实现复杂,主要是围绕GP展开的。
RCU实现了读写并行,那么就有一种情况:
1、updater正在更新数据,此时有reader需要读取数据,reader是可以读取旧数据的。
2、updater更新完毕数据以后,reader仍然还在读,这时,updater是不能释放旧内存的,那怎么办呢?
3、此时,updater调用synchronize_rcu()或call_rcu(),做好标记,声明进入GP。
等
old reader
读取完成后,再释放旧内存。
GP开始的时候,容易判定,那么GP退出,如何判定呢?
若是有多个reader,那就需要这些reader都得退出临界区,GP才能结束,那又如何判断这些reader都退出临界区了呢?
这些判定,涉及调度、中断、多cpu协同等方面的配合,实现复杂。
因此,GP的时间长度不定,若GP持续时间太长,超过21s,则触发RCU Stall。
例如:
1、有一个reader迟迟得不到调度,不能运行,GP就会一直持续,21s后stall
2、reader发生死锁,无法执行。
3、reader在等外部事件(中断),外部事件一直不来。
RCU
锁
-read copy update
最后,贴一张网上的RCU锁的图,看懂这个图,也就理解了RCU锁的实现机制。
dsdssa建议使用spinlock操作抵抗力
理函数
处理数内,不建议使用spinlock内,不建议使用spinlock