linux-内核锁

  • Post author:
  • Post category:linux



目录:



一、铺垫知识


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


表示锁状态。



衍生出锁的两大功能:




同步




和保护。



锁的种类



锁的分类



相关




API



特点



注意点


原子操作:


atomic


atomic_read


(


atomic_t


* v);


atomic_set


(


atomic_t


* v, int


i


);


void


atomic_add


(int


i


,


atomic_t


*v);


最小执行单元,未操作完成前,不允许被任何事件打断。



是实现其他锁的基础。


自旋锁:


spinlock


spin_lock


(&lock);


spin_unlock


(&lock)


等锁时,在原地打转,所以叫自旋。理解为:等锁急得团团转。


1


、等锁时,占着


cpu


不释放,不睡眠,关闭调度。


2


、访问临界区就要持锁,无论读还是写?


3


、谁持锁,那就谁来释放


1


、同一把锁,不能被进程上下文和中断上下文一起使用。


2


、临界区内不能调用可能阻塞的函数,如


sleep





io


操作等


互斥锁:


mutex lock


mutex_init


(&mutex);


mutex_lock


(struct mutex *lock)


mutex_unlock


(struct mutex *lock)


1


、等锁时,可进入睡眠等待,让出


cpu


,不需要自旋。


2


、持锁后,也可进入睡眠。


3


、谁持锁,那就谁来释放。


信号量:


s


emaphore


int down (struct semaphore *


sem


) //


加锁


int up (struct semaphore *


sem


)  //


释放锁


1


、与


mutex


有相似之处,等锁或持锁时,可睡眠


2


、可设置资源数为多个,能多个进程都获取锁。


3





A


持锁,可以


B


释放。


共享资源为


1


时,为二意信号量,就非常接近


mutex


了。唯一的差异是信号量可以


A


持锁,


B


释放锁。


读写锁


rw_lock


read_lock


();


read_unlock


();


write_lock


();


write_unlock


();


“reader-writer spin lock”


,是


spin lock


衍生出来的。


分为读锁和写锁,适用于读多写少的场景,可以多个进程同时读取,但写时,只能唯一,且写时关闭调度。


看似非常合理,但对“写”不公平。如果读者太多,写者需要等很长时间才能拿到锁。


读写信号量


rw_semaphore


down_read


();


up_read


()    //


读锁


down_write


();


up_write


();  //


写锁


可视作是


rwlock






可睡眠版本


“写”一样受到不公平待遇。


顺序锁


seqlock


read_seqlock


()


read_sequnlock


()


write_seqlock


()


write_sequnlock


()


读写锁的进一步完善,诞生了顺序写。相比起


rwlock


,它进一步解除了


reader





writer


之间的互斥,只保留了


writer





writer


之间的互斥。


reader


在读取一个共享变量之前,需要先读取一下


sequence number


的值。读取变量之后,


reader


需要再次读取一下


sequence number


的值,并和读取之前的


sequence number


的值进行比较,看是否相等,若不等,再重新读取一次。


也有其不足之处


,


使用场景也很少。逐渐被


RCU


取代


RCU




Read copy update


主要有


5


大接口函数


内核现在非常流行的锁,使用量逐年增加,主要保护链表。



读写可同时进行。



核心思想:先创建一个旧数据的


copy


,然后


writer


更新这个


copy


,最后再用新的数据替换掉旧的数据


GP


区的处理是核心关键。“当你感觉岁月静好的时候,一定是另外有一个人帮你承担了”



原子操作

1、

最小执行单元,未操作完成前,不允许被任何事件打断。
2、


是实现其他锁的基础,是基础中的基础。




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



版权声明:本文为chbgoon原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。