首页 > 其他分享 >RCU-2——RCU初探

RCU-2——RCU初探

时间:2023-04-27 20:44:58浏览次数:38  
标签:宽限期 head 函数 RCU 初探 hlist rcu

一、RCU简介

RCU(Read-Copy-Update)的意思是读-复制-更新,是根据原理命名的。写者修改对象的逻辑是: 首先复制一个副本,然后更新这个副本,最后使用新的对象替换旧的对象。在写者执行复制更新的时候读者可以读数据。

写者删除对象,必须要等到所有读者结束访问,才能执行销毁操作。RCU的关键技术在于如何判断所有读者已经完成访问。等待所有读者访问结束的时间称为宽限期(grace period)。

RCU的优点是读者没有任何同步开销:不需要获取任何锁,不需要执行原子指令(除Alpha以外),不需要执行内存屏障。但是写者的开销比较大,写者需要复制被修改的对象,延迟对象的释放。写者与写者之间必须要使用锁互斥。

RCU的第一个版本称为经典RCU,在 Linux-2.5.43 中引入。目前内核支持3种RCU: 不可抢占RCU(RCU-sched)、加速版不可抢占RCU(RCU-bh,bh是bottom half的缩写,下半部)、可抢占RCU(RCU-preempt)。

RCU根据数据结构可分为2种:
(1) 树型RCU(tree RCU): 也称为基于树的分层RCU,为拥有几百个或几千个处理器的大型系统设计,但小型系统也能适配的很好。配置宏是 CONFIG_TREE_RCU。
(2) 微型RCU(tiny RCU): 为不需要实时响应的单处理器系统设计。配置宏 CONFIG_TINY_RCU。


二、技术原理

1. RCU术语

(1) 读端临界区(Read-Side Criticial Section): 读者访问RCU保护对象的代码区域。

(2) 静止状态(Quiescent State): 处理器没有读RCU保护的对象,也可理解为读者静止,没有访问临界区。

(3) 候选静止状态(Candidate Quiescent State): 读端临界区以外的所有点都是候选静止状态。

(4) 可被观察的静止状态(Observed Quiescent State): 一部分候选静止状态可以很容易被观察到,因为它们与内核的某个事件相关联,通过内核事件就可以观察到。把这些容易观察到的静止状态称为可被观察的静止转状态。内核的RCU使用的静止状态是可被观察的静止状态,不可抢占RCU通过事件“调度器调度进程”观察到静止状态,加速版不可抢占RCU通过事件“执行完软中断”观察到静止状态。

(5) 宽限期(Grace Period): 等待所有处理器上的读者退出临界区是时间称为宽限期。如果所有处理器都至少经历了一次静止状态,那么当前宽限期结束,新的宽限期开始。当前宽限期结束的时候,写者在当前宽限期以及以前注册的销毁对象的回调函数就可以安全地执行了。

宽限期分为正常宽限期(Normal Grace Period) 和加速宽限期(Expedited Grace Period)。调用函数 synchronize_rcu_expedited() 等待宽限期结束的时候,会向其它处理器发送IPI中断请求,强制产生静止状态。使宽限期快速结束,我们把强制结束的宽限期称为加速宽限期。

RCU需要使用位图标记哪些处理器经历了一个静止状态。在宽限期开始的时候,在位图中为所有处理器设置对应的位,如果一个处理器经历了一个静止状态,就把自己的位从位图中清除,处理器修改位图的时候必须要使用自旋锁保护。如果处理器很多,对自旋锁的竞争很激烈,会导致系统性能很差。
为了解决性能问题,基于树(Tree)的分层RCU采用了类似淘汰赛的原理:把处理器分组,当一个处理器经历了一个静止状态时,把自己的位从分组的位图中清除,这就只需要竞争分组的自旋锁即可。当分组中的最后一个处理器经历了静止状态时,代表分组从上一层的位图中清除位,这个操作只需要竞争上一层分组的自旋锁。


三、经典RCU

若不关心使用的RCU是可抢占RCU还是不可抢占RCU,应该使用经典RCU编程接口。最初经典RCU是不可抢占RCU,后来实现了可抢占RCU,经典RCU的意思发生了变化,如果内核编译了可抢占RCU(开启了CONFIG_PREEMPT_RCU),那么经典RCU编程接口被实现为可抢占RCU,否则被实现为不可抢占RCU。

1. 读者

读者使用 rcu_read_lock() 标记进入读端临界区,使用 rcu_read_unlock() 标记退出读端临界区。读端临界区可以嵌套。
在读端临界区应该使用 rcu_dereference(p) 来访问指针,这个宏封装了数据依赖的屏障,即只有Alpha处理器依赖的读内存屏障。

2. 写者

写者可以使用下面4个函数修改对象:

(1) 使用函数 synchronize_rcu() 等待宽限期结束,即所有读者退出读端临界区,然后写者执行下一步操作。这个函数可能休眠。

(2) 使用函数 synchronize_rcu_expedited() 等待宽限期结束。和函数 synchronize_rcu() 的区别是:该函数会向其它处理器发送IPI中断请求,强制宽限期快速结束。我们把强制快速结束的宽限期称为加速宽限期(expedited grace period),把没有强制快速结束的宽限期称为正常宽限期(normal grace period)。

(3) 使用函数 call_rcu(head, func) 注册延后执行的回调函数,把回调函数添加到RCU回调函数链表中,立即返回,不会阻塞。函数原型:

void call_rcu(struct rcu_head *head, rcu_callback_t func) //rcu/tree.c

struct callback_head { //include/linux/types.h
    struct callback_head *next;
    void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));

#define rcu_head callback_head

(4) 使用 rcu_barrier() 等待使用 call_rcu() 注册的所有回调函数执行完。这个函数可能会休眠。######### -----------------------------------------------


四、不可抢占RCU(RCU-sched)

不可抢占RCU是指进程在读端临界区不允许被抢占。最初经典RCU是不可抢占RCU,后来引入了可抢占RCU,所以 Linux-2.6.12 为不可抢占RCU设计了一套专用变成接口。

如果我们的需求是“无论内核是否编译了可抢占RCU,都要使用不可抢占RCU”,那么应该使用不可抢占RCU专用的编程接口:

读者使用 rcu_read_lock_sched() 标记进入读端临界区,使用函数 rcu_read_unlock_sched() 标记退出读端临界区。读端临界区可以嵌套。

在读端临界区里面应该使用 rcu_dereference_sched(p) 访问指针,这个宏封装了数据依赖的屏障,即只有Alpha处理器需要的读内存屏障。

写者可以使用下面4个函数:

(1) 使用函数 synchronize_sched() 等待宽限期结束,即所有读者退出读端临界区,然后执行下一步操作,这个函数可能休眠。(注:在5.10内核上已经不存在这个函数了,原4.19上使用这个函数的位置都被 synchronize_rcu() 给替换了。)

(2) 使用函数 synchronize_sched_expedited() 等待宽限期结束,和 synchronize_sched() 的区别是该函数会向其它处理器发送IPI中断请求,强制宽限期快速结束。(注:在5.10内核上已经不存在这个函数了,原4.19对这个函数也没有使用位置,也替换为 synchronize_rcu_expedited() ?)

(3) 使用函数 call_rcu_sched() 注册延后执行的回调函数,把回调函数添加到回调函数链表中后立即返回,不会阻塞。(注:在5.10内核上已经不存在这个函数了,原4.19上使用这个函数的位置都被call_rcu() 给替换了。)

(4) 使用函数 rcu_barrier_sched() 等待使用 call_rcu_sched() 注册的所有回调函数执行完,可能休眠。(注:在5.10内核上已经不存在这个函数了,原4.19上使用这个函数的位置都被rcu_barrier() 给替换了。)


五、加速版不可抢占RCU(RCU-bh)

加速版不可抢占RCU在 linux-2.6.9 中引入,是针对不可抢占RCU的改进,在软中断很多的情况下可以缩短宽限期。

内核中的网络栈在软中断中处理报文,若攻击者疯狂发送报文攻击设备,导致被攻击的设备大部分时间执行软中断,宽限期会变得很长,导致大量延后的销毁操作没有被执行,可能导致内存耗尽。为了应对这种分布式拒绝服务攻击,RCU-bh把执行完软中断看做处理器退出读端临界区的标志,缩短宽限期。

读者使用 rcu_read_lock_bh() 标记进入读端临界区,使用函数 rcu_read_unlock_bh() 标记退出读端临界区。读端临界区可以嵌套。在读端临界区里面应该使用宏 rcu_dereference_bh(p) 访问指针, 这个宏封装了数据依赖的屏障,即只有Alpha处理器需要的读内存屏障。

写者可以使用下面4个函数:

(1) 使用函数 synchronize_rcu_bh() 等待宽限期结束,即所有读者退出读端临界区,然后执行下一步操作,这个函数可能休眠。(注:在5.10内核上已经不存在这个函数了,原4.19上使用这个函数的位置都被 synchronize_rcu() 给替换了。)

(2) 使用函数 synchronize_rcu_bh_expedited() 等待宽限期结束,和 synchronize_rcu_bh() 的区别是该函数会向其它处理器发送IPI中断请求,强制宽限期快速结束。(注:在5.10内核上已经不存在这个函数了,原4.19对这个函数也没有使用位置,也替换为 synchronize_rcu_expedited() ?)

(3) 使用函数 call_rcu_bh() 注册延后执行的回调函数,把回调函数添加到回调函数链表中后立即返回,不会阻塞。(注:在5.10内核上已经不存在这个函数了,原4.19上使用这个函数的位置都被call_rcu() 给替换了。)

(4) 使用函数 rcu_barrier_bh() 等待使用 call_rcu_bh() 注册的所有回调函数执行完,可能休眠。(注:在5.10内核上已经不存在这个函数了,原4.19上使用这个函数的位置都被 rcu_barrier() 给替换了。)

看来这三类RCU在5.10内核上只是读端不同,写端保持一致了。


六、可抢占RCU(RCU-preempt)

可抢占RCU也称为实时RCU,在 Linux-2.6.26 中引入。可抢占RCU允许进程在读端临界区被其它进程抢占,编译时需要开启 CONFIG_PREEMPT_RCU。


七、RCU使用场景

1. RCU最常见的使用场合是保护大多数时候是读的双向链表。内核也实现了链表操作的RCU版本,这些操作封装了内存屏障。内核实现了四种双向链表:

(1) 链表 list_head,常用操作:

list_for_each_entry_rcu(pos, head, member, cond...) //宏,封装了只有Alpha处理器依赖的内存屏障
void list_add_rcu(struct list_head *new, struct list_head *head) //添加到链表首
void list_add_tail_rcu(struct list_head *new, struct list_head *head) //添加到链表尾
void list_del_rcu(struct list_head *entry)
void list_replace_rcu(struct list_head *old, struct list_head *new)

(2) 链表 hlist, 和上面 list_head 比,优势是头部节点只有一个指针,节省内存。其常用操作如下:

hlist_for_each_entry_rcu(pos, head, member, cond...)
void hlist_add_head_rcu(struct hlist_node * n, struct hlist_head * h)
void hlist_add_tail_rcu(struct hlist_node * n, struct hlist_head * h)
void hlist_del_rcu(struct hlist_node * n)
void hlist_replace_rcu(struct hlist_node * old, struct hlist_node * new)

(3) 链表 hlist_nulls 是 hlist 的变体,区别是 hlist 的结束符是个NULL指针,而练表 hlist_nulls 的结束符是 (1UL|(((long)value)<<1))), 低位是1,value 是嵌入的值,比如散列桶的索引。

hlist_nulls_for_each_entry_rcu(tpos, pos, head, member)
void hlist_nulls_add_head_rcu(struct hlist_nulls_node * n, struct hlist_nulls_head * h)
void hlist_nulls_add_tail_rcu(struct hlist_nulls_node * n, struct hlist_nulls_head * h)
void hlist_nulls_del_rcu(struct hlist_nulls_node * n)

(4) 链表 hlist_bl,它是 hlist 的变体,链表头节点的最低位作为基于位的自旋锁保护链表。其常用操作如下:

hlist_bl_for_each_entry_rcu(tpos, pos, head, member)
void hlist_bl_add_head_rcu(struct hlist_bl_node * n, struct hlist_bl_head * h)
void hlist_bl_del_rcu(struct hlist_bl_node * n)

 

标签:宽限期,head,函数,RCU,初探,hlist,rcu
From: https://www.cnblogs.com/hellokitty2/p/17360174.html

相关文章

  • C++ linux epoll并发服务器模型初探
    socket通讯流程图最简单的可以通讯的C++服务器端代码:#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<arpa/inet.h>#include<netinet/in.h>#defineSERV_PORT8000intmain(void){intlfd......
  • vulfocus初探01--搭个玩玩
    vulfocus初探01--搭个玩玩0X10搭建环境平台:debian1164位docker0X20vulfocus介绍vulfocus是一个基于docker的漏洞模拟平台,完全开源,网址GitHub-fofapro/vulfocus:......
  • 初探Java框架
    (一)什么是Java框架?1.概念Java框架就是一些类和接口的集合,通过这些类和接口协调来完成一系列的程序实现。框架又叫做开发中的半成品,它不能提供整个WEB应用程序的所有东西,但是有了框架,我们就可以集中精力进行业务逻辑的开发而不用去关心它的技术实现以及一些辅助的业务逻辑。说白......
  • 第五讲 Weldentity分布式身份解决方案、智能合约初探
    什么是智能合约1996年,NickSzabo在文章《SmartContracts:BuildingBlocksForDigitalMarkets》中提出了智能合约的概念所谓“合约”,就是条文、合同一类的东西,里面记录了发生的条件与对应执行的条款,以支持确权等操作;所谓”智能”,就意味着自动化、可编程。所以,智能合约就是......
  • 夜莺初探三·Categraf采集器
    夜莺初探三·Categraf采集器前言github仓库文档中对Categraf有很详细的介绍,简单重复一下就是:支持多种数据格式的remote_write;All-in-one的设计理念,指标采集只需要一个agent完成,也计划支持日志和调用链路的数据采集;Go编写,依赖少,容易分发和安装;内置一些监控大盘和告警规则,用户可......
  • 初探富文本之OT协同算法
    初探富文本之OT协同算法OT的英文全称是OperationalTransformation,是一种处理协同编辑的算法。当前OT算法用的比较多的地方就是富文本编辑器领域了,常用于作为实现文档协同的底层算法,支持多个用户同时编辑文档,不会因为用户并发修改导致冲突,而导致结果不一致甚至数据丢失的问题。描述......
  • 响应式编程初探
    响应式响应式系统(ReactiveSystem)具有以下特质:即时响应性(Responsive)、回弹性(Resilient)、弹性(Elastic)以及消息驱动(MessageDriven)响应式系统更加灵活,松耦合,可伸缩即时响应性只要有可能, 系统就会及时地做出响应。即时响应是可用性和实用性的基石,而更加重要的是,即时响应意......
  • Android中GC—初探-1—触发时机和条件
    一、GC简介Java对象的创建由Allocator负责,回收由Collector负责。从AndroidO开始,对于前台应用默认的GCCollector是CC(ConcurrentCopying)Collector,与之相匹配的Allocator则是Region-basedBumpPointerAllocator(withTLAB)。二、前台应用GC何时触发1.GC触......
  • cpp结构体的sizeof涉及到的字节对齐问题代码-内存初探
    #include<iostream>usingnamespacestd;intmain(){structS1{chara;//占8个,因为要和double对齐doubleb;//占8个字节};cout<<sizeof(S1)<<endl;//16/*内存地址排列方式[00,01,02,03,04,05,06,07,08,09,1......
  • 夜莺监控V6初探
    目标客户用产品可能是功能设计好,也可能是因为响应快稳定可靠,例如滴滴用不了用高德,券商app故障受罚,微信凌晨服务崩溃,所以稳定性建设工作价值是保障客户体验,避免资损,社会负面舆论。故障生命周期处理围绕故障生命周期,在整个故障定位体系,可分为故障开始前,预案阶段,做量化分析找到潜......