linux-内核锁

原文地址:https://blog.csdn.net/chbgoon/article/details/123495480


1. 铺垫知识

在这里插入图片描述

1.1. 进程指令执行流

代码在CPU上执行的指令数据流,由一系列代码组成。可分为两大类:线程维度和中断维度
在这里插入图片描述

  1. cpu只要上电,就需要不停的执行指令,永不停歇,若无事可做,那就执行空指令,直到下电。cpu类似一个跑道,各个task轮流到跑道上跑。

  2. 用户态视角:cpu执行一个个进程或者线程,一个线程就是一个执行流。内核态视角:CPU调度执行一个个task(对应一个进程或者线程),一个task就是一个执行流。CPU就这样永不疲倦的轮换执行各个task(调度)。8个CPU,同一时刻,最多有8个执行流。

  3. 并发:用户态系统调用的代码编译成so,当so被多个bin链接时,运行时,就可能有多个指令执行流,就有可能分别在cpu0和cpu1上执行。宏观并行,微观串行

  4. 代码是静态的,执行流是动态的,这是两个不同的视角。若只存在一个cpu,那就只有一个执行流,那就不存在微观上的并发,也就不需要锁。但我们往往使用多个cpu,因而有多个执行流,同一个代码可能在两个执行流上执行。如上图。

1.2. 上下文(context)

分类:进程上下文,中断上下文(硬中断上下文、软中断上下文,不可屏蔽中断上下文)
在这里插入图片描述
中断能够打断进程的执行流,无论进程优先级多高,都会被打断。因此原本执行流被打断,CPU转而执行中断的指令流。因此,与上下文对应,执行流也可分为:进程执行流和中断执行流

  1. cpu在用户态运行时,外部中断触发,程序会先陷入内核态,保存上下文后,再执行中断代码。
  2. cpu在内核态运行时,外部中断触发,保存上下文后,执行中断代码。
  3. 中断处理函数(中断上半部)必须要快速执行完成,以便返回继续执行各个进程。中断返回时,会发生调度,原来被打断的进程未必能够得到执行。

假设进程和中断都调用如下函数,那么就有可能出现如下场景:

进程执行流:

  1. 某进程执行完743行后,全局变量enable=1;
  2. 外部触发中断。

程序执行流:

  1. 从744行继续执行,此时enable变成0了。后边就会出错

中断执行流:

  1. 中断处理函数恰好也调用这个函数;
  2. 走了case1的分支,将enable又改成0。
  3. 中断执行完毕返回。

1.3. 抢占

什么是抢占? 一张图展现
在这里插入图片描述
在这里插入图片描述

  1. 抢占可以分为用户态抢占和内核态抢占。抢占时机?(中断、返回用户态、主动schedule())
  2. Linux kernel是抢占式内核。

2. 内核锁基础知识

2.1. 为什么要使用锁?-Why

为了性能,引入了多核。

但多核导致了并发,并发就导致了争抢,为了保证争抢有序,就出现了锁。

备注:

网上关于锁的介绍的文档很多,推荐知乎兰新宇的博客

术道经纬 - 知乎

关于锁的介绍,有一系列文章,个人感觉写的非常不错,我主要从这个博客学习锁的知识。

2.2. 锁保护什么?-What

使用锁,首先要搞清楚使用锁的目的是什么?要保护什么?

锁保护的对象:公共资源。公共资源,从某一方面讲,就是全局变量,或者从堆里面申请的共用内存。

如右图内核代码:

  1. 锁保护的是全局变量zone(从堆上申请的)。
  2. 无法保护局部变量tmp和low,局部变量也不需要保护,想想这是为什么?
  3. 代码段不需要保护,因为它是只读的。

误区:常认为锁保护的是一段代码和变量。

其实:从保护资源的角度,锁只是为了保护全局变量,公共资源是一种更严谨的通用的说法。若非要说“锁也保护了代码”,那也只是在保护全局变量的时候,顺便保护了代码而已。

2.3. 锁是如何保护资源的?–How

在这里插入图片描述
核心原理:根据全局变量的状态来实现锁的控制。

“统一到门口排队,依次获得钥匙”

无进程持锁时,lock=0;

进程0持锁后,lock=1;

进程1再尝试持锁时,由于lock=1,无法获取,只能等待。

因此:lock一定要是全局的,8字节,其实只用1bit即可。Spinlock就充分利用这8个字节,仅用1bit表示锁状态。

衍生出锁的两大功能:同步和保护。

3. 各类锁的介绍

3.1. 锁的种类

锁的分类相关API特点注意点
原子操作:atomicatomic_read(atomic_t * v);
atomic_set(atomic_t * v, int i);
void atomic_add(int i, atomic_t *v);
最小执行单元,未操作完成前,不允许被任何事件打断。
是实现其他锁的基础。
自旋锁:spinlockspin_lock(&lock);
spin_unlock(&lock)
等锁时,在原地打转,所以叫自旋。理解为:等锁急得团团转。
1. 等锁时,占着cpu不释放,不睡眠,关闭调度。
2. 访问临界区就要持锁,无论读还是写?
3. 谁持锁,那就谁来释放
1. 同一把锁,不能被进程上下文和中断上下文一起使用。
2. 临界区内不能调用可能阻塞的函数,如sleep,io操作等
互斥锁:mutex lockmutex_init(&mutex);
mutex_lock(struct mutex *lock)
mutex_unlock(struct mutex *lock)
1. 等锁时,可进入睡眠等待,让出cpu,不需要自旋。
2. 持锁后,也可进入睡眠。
3. 谁持锁,那就谁来释放。
信号量:semaphoreint down (struct semaphore *sem) //加锁
int up (struct semaphore *sem) //释放锁
1. 与mutex有相似之处,等锁或持锁时,可睡眠
2. 可设置资源数为多个,能多个进程都获取锁。
3、A持锁,可以B释放。
共享资源为1时,为二意信号量,就非常接近mutex了。唯一的差异是信号量可以A持锁,B释放锁。
读写锁:rw_lockread_lock(); read_unlock();
write_lock(); write_unlock();
“reader-writer spin lock”,是spin lock衍生出来的。
分为读锁和写锁,适用于读多写少的场景,可以多个进程同时读取,但写时,只能唯一,且写时关闭调度。
看似非常合理,但对“写”不公平。如果读者太多,写者需要等很长时间才能拿到锁。
读写信号量:rw_semaphoredown_read(); up_read() //读锁
down_write();up_write(); //写锁
可视作是rwlock的可睡眠版本“写”一样受到不公平待遇。
顺序锁:seqlockread_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区的处理是核心关键。“当你感觉岁月静好的时候,一定是另外有一个人帮你承担了”

3.2. 原子操作

  1. 最小执行单元,未操作完成前,不允许被任何事件打断。
  2. 是实现其他锁的基础,是基础中的基础。 Spin lock 是实现其他锁的基础,原子操作是实现 spin lock 的基础。
  3. ARM 原子操作采用的方法主要是 LL/SC (Load-Link/Store-Conditional) 。

Arm内存单元上的数据不允许被直接操作,而是必须先放到寄存器中,不能直接对内存进行更改操作。例如,写操作“x=5;”, 需要先将内存的值读取到寄存器内,修改后,再从寄存器写会内存。

原子操作是:通过指令和硬件的配合,保证”x=5;”这一过程不被打断,一次执行完毕。

3.3. Spin lock

3.3.1. 特点

  1. 自旋,性格刚烈,不达目的誓不罢休,一直占着CPU不放,不进入睡眠,关闭了抢占,故也不做调度 。
  2. 无论读写,只要访问临界区,就需要加锁。
  3. 谁持锁,谁释放。
  4. 是实现其他锁的基础,其他锁基本都是基于spin lock衍生出来的。若要理解其他锁,须先理解spinlock

3.3.2. 简述

Linux中spinlock机制发展到现在,其实现方式的大致有3种:

  1. 经典的CAS(Compare And Swap),现在已经不使用,理解经典CAS,有助于理解后面的锁。
  2. Ticket Spinlock,中间态,并未广泛使用,但为qspinlock打下了坚实的基础。
  3. qspinlock,目前Linux中广泛使用的spinlock机制,高通、MTK平台等当然使用此种机制。

锁的实现与CPU架构体系强相关,下面以ARM架构体系为例,说明spinlock的实现。

3.3.3. 详解

3.3.3.1. 经典的CAS(Compare And Swap)

最古老的一种做法是:spinlock用一个整形变量表示,其初始值为1,表示available的状态。当一个CPU(设为CPU A)获得spinlock后,会将该变量的值设为0,之后其他CPU试图获取这个spinlock时,会一直等待,直到CPU A释放spinlock,并将该变量的值设为1。

3.3.3.2. Ticket Spinlock

CAS spinlock存在不足:他是不公平的,一旦锁被释放,下一个获得锁的cpu不确定,有可能后到先得。大家抢,没有排队。

为了解决这种「无序竞争」带来的不公平问题,spinlock的另一种实现方法是采用排队形式的"ticket spinlock"

  • 核心原理:去银行办理业务,需要先取个号,例如号码位5,然后排队等待叫号,此时银行正在给2号办理业务,2号办理完业务后,号码加1,变为3号,直到变为5号,就会轮到自己。
  • 不足:尽管实现了排队,但当有多个cpu等锁时,每个cpu都要通过cache line不停的访问同一块内存,即便是值没有变,也要不停的刷新cache line去读值,浪费功耗。只有一个cpu的刷新是有意义的,其他cpu都是在做无用功。

如果在ticket spinlock的基础上进行一定的修改,让每个CPU不再是等待同一个spinlock变量,而是基于各自不同的per-CPU的变量进行等待,那么每个CPU平时只需要查询自己对应的这个变量所在的本地cache line,仅在这个变量发生变化的时候,才需要读取内存和刷新这条cache line,这样就可以解决上述的这个问题。

要实现类似这样的spinlock的「分身」,其中的一种方法就是使用MCS lock(Ticket Spinlock-MCS)。试图获取一个spinlock的每个CPU,都有一份自己的MCS lock。
在这里插入图片描述

  • 不足:排队问题解决了,功耗问题也解决了。但该机制仍存在不足之处:MCS lock多了一个指针,要多占4(或8)个字节,消耗的存储空间是原来的2-3倍。Spinlock在操作系统中使用非常广泛,数量很大,那 么消耗的内存空间也很大。

那有没有更优的方案呢?

3.3.3.3. qspinlock(queue spinlock)

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调用流程

在这里插入图片描述
在这里插入图片描述
慢速等待路径里面,又实现了排队机制,第一顺位继承人(太子),第二顺位继承人(宫外建府)。又是一堆复杂的代码逻辑。

3.3.4. 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型死锁。
3.3.4.1. spinlock与抢占

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首先要关闭抢占。
一般情况下,我们的代码是不会主动关闭抢占的。

3.3.4.2. Spinlock 与中断
  1. 中断处理函数指的中断上半部,需要快速执行完毕,以便响应下次硬件中断。软中断、tasklet、中断线程化、中断工作队列等都是中断下半部,处理耗时操作。
  2. 中断处理函数要快速执行完毕,若使用锁,那就有可能出现等锁的情况,那就要耗时等待。若在中断处理函数内等待,就会造成cpu资源浪费,cpu空转等待。占着XX,不XX。

在这里插入图片描述

假设一个CPU上的线程Task_1持有了一个spinlock,发生中断后,该CPU转而执行对应的hardirq。如果该hardirq也试图去持有这个spinlock,那么将无法获取成功,导致hardirq无法退出。在hardirq主动退出之前,线程T是无法继续执行以释放spinlock的,最终将导致该CPU上的代码不能继续向前运行,形成死锁(dead lock)
在这里插入图片描述

3.3.4.3. spinlock使用注意点

spinlock保护资源时,临界区应尽可能的短,临界区不能有太耗时的操作

Spinlock的特点是spin,也就是自旋,一直占着CPU,性格刚烈。若是在临界区内耗时太长,则其他任务得不到执行,影响调度,进而影响性能,严重时,甚至会导致watchdog。

案例:手机因watchdog而panic。

  1. cpu卡死的堆栈如右图。
  2. 原因就是xchi_stop()先持spinlock锁,并关闭中断。世界安静了,无人再打扰该线程的执行,但该线程在读取硬件寄存器时,深深的沉睡下去,硬件异常,一直无法返回信号。软件一直等到,超过10s,触发watchdog。

3.3.5. Spinlock的不足及改进

  1. 若是想在临界区内睡眠,spinlock就不能使用。那该如何解决呢?
  2. 访问要保护公共资源就需要解锁。若读多写少,每次读时,也需要加锁,读也要排队等待,浪费资源。那能不能读的时候不加锁,而写时候加锁呢?

为了解决这两个问题,内核社区的大神们付出了很大努力,给出了各种解决方案,各个方案都有各自的优缺点和使用场景。

充分体现了内核工匠们精益求精的精神。
在这里插入图片描述
睡眠相关概念:

  • 系统睡眠:不多解释
  • CPU睡眠:cpu只有掉电才能睡眠,CPU睡眠,系统也就睡眠了。
  • 进程(线程)睡眠:进程让出cpu,不再运行。在内核调用schdeule()后,即进入睡眠。用户态是无法进入睡眠的。通常说的阻塞(block)其实就是进程睡眠了。此处的睡眠就是进程睡眠。

3.4. 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,防止出现竞争。

在这里插入图片描述

Mutex lock问题举例 右图,

  1. 当前task 19722处于等mutex锁状态,将自己挂在了等待队列上,然后调度出去,让出cpu。
  2. 由于:owner = (counter = 0xFFFFFF87A5136041),
    所以:mutex锁已经被其他task持有,当前持有mutex锁的task地址是:0xFFFFFF87A5136040,红色1代表锁已经被持有。
  3. 可以看到wait_list已经有3个等待者了。为什么不是4? 又有几个task竞争锁呢?
  4. 第1066行的spin_unlock()又是什么意思呢?

3.4.1. 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)等。

驱动开发过程中,需要根据实际情况来选择。首先要明确使用锁的目的,要保护什么?还是要同步?

3.4.2. Spinlock 使用举例

以printk(fmt, …)为例:

printk()会调用到vprintk_emit(),为了保证log有序打印到logbuf内,每条日志打印时,都需要持spinlock锁,并关闭中断。

关中断,持spinlock锁。目的:“同步”。
想想,此处能使用mutex吗?

从技术上讲,此处当然可以使用mutex,但是谁又希望自己在打印log的时候被调度出去呢?即便此时有task在持锁打印log,那应该也会很快完成,我就稍等一会呗。因此选用了spinlock。

还是那句话,一定要清楚自己持锁的目的是什么?根据自己的目的才能选择合适的锁。

3.4.3. mutex 使用举例

以电源管理的regular_enable()为例,讲述锁的使用,中间省略了一些次要调用的过程。内核中各个器件都要使用电源,经常是多任务并发,而设置电源电压有时候需要路由到AOP侧。AOP是一个简单的RTOS系统,不可能有多任务对应AP侧的请求,那么。多对一,为什么没有发送条件竞争了?

3.5. rwlock读写锁

rwlock的全称是“reader-writer spin lock”,是有spinlock衍化过来的,和普通的spinlock不同,它对"read"和"write"的操作进行了区分。

  1. 如果当前没有writer,那么多个reader可以同时获取这个rwlock。
  2. 如果当前没有任何的reader,那么一个writer可以获取这个rwlock。

3.5.1. 使用方法

  1. 对于reader,依靠read_lock()和read_unlock()来限定读取一侧的临界区。
    每当一个reader进入临界区,就需要将读取一侧的cnts加1,退出则减1。只有当这个cnts的值为0,writer才可以执行自己的临界区代码
  2. 对于writer,依靠write_lock()和write_unlock()来限定写入一侧的临界区。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.5.2. 不足之处

设想,当有很多读者进入临界区后,若想写,那就必须要等读者读完。这对写来说,不公平。 目前读写锁使用越来越少,新代码基本不在使用这个锁。

3.6. seqlock顺序锁

seqlock是由Stephen Hemminger负责开发,自Linux 2.6版本引入的,其全称是“sequential lock”。相比起rwlock,它进一步解除了reader与writer之间的互斥,只保留了writer与writer之间的互斥。只要没有其他的writer持有这个seqlock(即便当前存在reader持有该seqlock),那么第一个试图获取该seqlock的writer就可以成功地持有。

那么,若正在读时,有写修改,如何保证读的一致性呢?

  • 写开始时,有writter持有seqlock之后,seqcount的值就会加1;
  • 写结束后,writter释放seqlock, seqcount的值再次加1;
  • 读开始时,记录seqcount的值;
  • 读结束后,再次读取seqcount ,若发现本次读取的值和前面读开始时记录的值不同,则说明有写修改,那么,将放弃本次的读操作,重新读取一次。

很显然,seqlock存在不足,若是读的临界区比较长,那么再重来一次,耗时很长,影响效率。

适用场景:读多写少,在Linux中的一个重要应用就是表示时间的jiffies。但整体的使用场景不多。
在这里插入图片描述
在这里插入图片描述

3.7. 信号量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_semaphoresa建议使用spinlock操作抵抗力理函数处理数内,不建议使用
为了区分读写,spinlock衍生出rwlock,同理,semaphore也衍生出了rw_semaphore。

3.7.1. 读写信号量(rw_semaphore)

读写信号量持锁逻辑:

  1. 读时,要判断是否有写锁。
    若有写者持锁,则等待。此时,类似mutex等待。
    若无写者持锁,则获取读锁成功,conut的高8位以上的数加1, count低8位用作记录写相关的标记。
  2. 写时,判断是否有读者和写者持锁。只有没有读者和写者持锁时,才能申请到写锁。
    若有写者持锁,则等待,此时,类似mutex等待。
    若无写者,只有读者,情况就比较复杂。首先置位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();

这里面的实现就是使用了读写信号量。

3.8. RCU锁-read copy update

3.8.1. 核心思想

  1. 若要读取公共资源时,那就尽情的读。
  2. 若要写公共资源,那就将原来的公共资源copy出一份来,修改完成后,替换原来旧的公共资源,旧的公共资源需要释放掉。

3.8.2. 使用场景

  1. 读多写少。
  2. 访问内核链表资源。(内核中存在各种各样的链表,RCU发挥了其用武之地)

3.8.3. 原理与实现

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长时间占用了锁,往往是锁套锁,相互依赖

3.8.4. 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在等外部事件(中断),外部事件一直不来。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值