Read the fucking source code!
--By 鲁迅A picture is worth a thousand words.
--By 高尔基说明:html
我会假设你已经看过了《Linux RCU原理剖析(一)-初窥门径》node
本文将进一步去探索下RCU
背后的机制。数组
Grace Period
继续贴出《Linux RCU原理剖析(一)-初窥门径》
中的图片:安全
Grace Period
,中文叫作宽限期,从Removal
到Reclamation
,中间就隔了一个宽限期;Quiescent Status
;Quiescent Status
),而且判断宽限期是否结束。来一张图:数据结构
Quiescent Status
Quiescent Status
,用于描述处理器的执行状态。当某个CPU正在访问RCU保护的临界区时,认为是活动的状态,而当它离开了临界区后,则认为它是静止的状态。当全部的CPU都至少经历过一次QS后,宽限期将结束并触发回收工做。异步
用户模式
或者idle模式
,则代表CPU离开了临界区;cpumask
去记录CPU经历静止状态,在经典RCU(Classic RCU
)实现中,因为使用了全局的cpumask
位图,当CPU数量很大时锁争用会带来很大开销(GP开始时设置对应位,GP结束时清除对应位),所以也促成了Tree RCU
的诞生;Tree RCU
以树形分层来组织CPU,将CPU分组,本小组的CPU争用同一个锁,当本小组的某个CPU经历了一个静止状态QS后,将其对应的位从位图清除,若是该小组最后一个CPU经历完静止状态QS后,代表该小组所有经历了CPU的QS状态,那么将上一层对应该组的位从位图清除;struct rcu_state
,struct rcu_node
,struct rcu_data
;图来了:ide
struct rcu_state
:用于描述RCU的全局状态,它负责组织树状层级结构,系统中支持不一样类型的RCU状态:rcu_sched_state
, rcu_bh_state
,rcu_preempt_state
;struct rcu_node
:Tree RCU
中的组织节点;struct rcu_data
:用于描述处理器的RCU状态,每一个CPU都维护一个数据,它归属于某一个struct rcu_node
,struct rcu_data
检测静止状态并进行处理,对应的CPU进行RCU回调,__percpu
的定义也减小了同步的开销;看到这种描述,若是仍是在懵逼的状态,那么再来一张拓扑图,让真相更白一点:函数
struct rcu_node
来组成,这些节点在struct rcu_state
结构中是放置在数组中的,因为struct rcu_node
结构有父节点指针,所以能够构造树形;CPU0/CPU1
就不须要和CPU6/CPU7
去争用锁了,逐级以淘汰赛的形式向上;
关键点来了:Tree RCU使用rcu_node节点来构造层级结构,进而管理静止状态Quiescent State和宽限期Grace Period,静止状态信息QS是从每一个CPU的rcu_data往上传递到根节点的,而宽限期GP信息是经过根节点从上往下传递的,当每一个CPU经历过一次QS状态后,宽限期结束
工具
关键字段仍是有必要介绍一下的,不然岂不是耍流氓?性能
struct rcu_state { struct rcu_node node[NUM_RCU_NODES]; // rcu_node节点数组,组织成层级树状 struct rcu_node *level[RCU_NUM_LVLS + 1]; //指向每层的首个rcu_node节点,数组加1是为了消除编译告警 struct rcu_data __percpu *rda; //指向每一个CPU的rcu_data实例 call_rcu_func_t call; //指向特定RCU类型的call_rcu函数:call_rcu_sched, call_rcu_bh等 int ncpus; // 处理器数量 unsigned long gpnum; //当前宽限期编号,gpnum > completed,代表正处在宽限期内 unsigned long completed; //上一个结束的宽限期编号,若是与gpnum相等,代表RCU空闲 ... unsigned long gp_max; //最长的宽限期时间,jiffies ... } /* * Definition for node within the RCU grace-period-detection hierarchy. */ struct rcu_node { raw_spinlock_t __private lock; //保护本节点的自旋锁 unsigned long gpnum; //本节点宽限期编号,等于或小于根节点的gpnum unsigned long completed; //本节点上一个结束的宽限期编号,等于或小于根节点的completed unsigned long qsmask; //QS状态位图,某位为1,表明对应的成员没有经历QS状态 unsigned long qsmaskinit; //正常宽限期开始时,QS状态的初始值 ... int grplo; //该分组的CPU最小编号 int grphi; //该分组的CPU最大编号 u8 grpnum; //该分组在上一层分组里的编号 u8 level; //在树中的层级,Root为0 ... struct rcu_node *parent; //指向父节点 } /* Per-CPU data for read-copy update. */ struct rcu_data { unsigned long completed; //本CPU看到的已结束的宽限期编号 unsigned long gpnum; //本CPU看到的最高宽限期编号 union rcu_noqs cpu_no_qs; //记录本CPU是否经历QS状态 bool core_need_qs; //RCU须要本CPU上报QS状态 unsigned long grpmask; //本CPU在分组的位图中的掩码 struct rcu_segcblist; //回调函数链表,用于存放call_rcu注册的延后执行的回调函数 ... }
从《Linux RCU原理剖析(一)-初窥门径》的示例中,咱们看到了RCU的写端调用了synchronize_rcu/call_rcu
两种类型的接口,事实上Linux内核提供了三种不一样类型的RCU,所以也对应了相应形式的接口。
来张图:
RCU
写者,能够经过两种方式来等待宽限期的结束,一种是调用同步接口等待宽限期结束,一种是异步接口等待宽限期结束后再进行回调处理,分别如上图的左右两侧所示;wait_for_completion
睡眠等待操做,而且会将wakeme_after_rcu
回调函数传递给异步接口,当宽限期结束后,在异步接口中回调了wakeme_after_rcu
进行唤醒处理;rcu_read_lock/rcu_read_unlock
来界定区域,在读端临界区能够被其余进程抢占;(RCU-sched)
:rcu_read_lock_sched/rcu_read_unlock_sched
来界定区域,在读端临界区不容许其余进程抢占;(RCU-bh)
:rcu_read_lock_bh/rcu_read_unlock_bh
来界定区域,在读端临界区禁止软中断;__call_rcu
接口,它是接口实现的关键,因此接下来分析下这个函数了;__call_rcu
函数的调用流程以下:
__call_rcu
函数,第一个功能是注册回调函数,而回调的函数的维护是在rcu_data
结构中的struct rcu_segcblist cblist
字段中;rcu_accelerate_cbs/rcu_advance_cbs
,实现中都是经过操做struct rcu_segcblist
结构,来完成回调函数的移动处理等;__call_rcu
函数第二个功能是判断是否须要开启新的宽限期GP;链表的维护关系以下图所示:
那么经过__call_rcu
注册的这些回调函数在哪里调用呢?答案是在RCU_SOFTIRQ
软中断中:
invoke_rcu_core
时,在该函数中调用raise_softirq
接口,从而触发软中断回调函数rcu_process_callbacks
的执行;rcu_process_callbacks
中会调用rcu_gp_kthread_wake
唤醒内核线程,最终会在rcu_gp_kthread
线程中执行;rcu_do_batch
函数中执行,其中有两种执行方式:1)若是不支持优先级继承的话,直接调用便可;2)支持优先级继承,在把回调的工做放置在rcu_cpu_kthread
内核线程中,其中内核为每一个CPU都建立了一个rcu_cpu_kthread
内核线程;既然涉及到宽限期GP的操做,都放到了rcu_gp_kthread
内核线程中了,那么来看看这个内核线程的逻辑操做吧:
rcu_preempt_state, rcu_bh_state, rcu_sched_state
建立了内核线程rcu_gp_kthread
;rcu_gp_kthread
内核线程主要完成三个工做:1)建立新的宽限期GP;2)等待强制静止状态,设置超时,提早唤醒说明全部处理器通过了静止状态;3)宽限期结束处理。其中,前边两个操做都是经过睡眠等待在某个条件上。很显然,对这种状态的检测一般都是周期性的进行,放置在时钟中断处理中就是情理之中了:
rcu_sched/rcu_bh
类型的RCU中,当检测CPU处于用户模式或处于idle
线程中,说明当前CPU已经离开了临界区,经历了一个QS静止状态,对于rcu_bh
的RCU,若是没有出去softirq
上下文中,也代表CPU经历了QS静止状态;rcu_pending
知足条件的状况下,触发软中断的执行,rcu_process_callbacks
将会被调用;rcu_process_callbacks
回调函数中,对宽限期进行判断,并对静止状态逐级上报,若是整个树状结构都经历了静止状态,那就代表了宽限期的结束,从而唤醒内核线程去处理;rcu_pending
函数中,rcu_pending->__rcu_pending->check_cpu_stall->print_cpu_stall
的流程中,会去判断是否有CPU stall的问题,这个在内核中有文档专门来描述,再也不分析了;若是要观察整个状态机的变化,跟踪一下trace_rcu_grace_period
接口的记录就能发现:
/* * Tracepoint for grace-period events. Takes a string identifying the * RCU flavor, the grace-period number, and a string identifying the * grace-period-related event as follows: * * "AccReadyCB": CPU acclerates new callbacks to RCU_NEXT_READY_TAIL. * "AccWaitCB": CPU accelerates new callbacks to RCU_WAIT_TAIL. * "newreq": Request a new grace period. * "start": Start a grace period. * "cpustart": CPU first notices a grace-period start. * "cpuqs": CPU passes through a quiescent state. * "cpuonl": CPU comes online. * "cpuofl": CPU goes offline. * "reqwait": GP kthread sleeps waiting for grace-period request. * "reqwaitsig": GP kthread awakened by signal from reqwait state. * "fqswait": GP kthread waiting until time to force quiescent states. * "fqsstart": GP kthread starts forcing quiescent states. * "fqsend": GP kthread done forcing quiescent states. * "fqswaitsig": GP kthread awakened by signal from fqswait state. * "end": End a grace period. * "cpuend": CPU first notices a grace-period end. */
大致流程以下:
RCU_SOFTIRQ
和内核线程rcu_gp_kthread
的动态运行及交互等;rcu_state, rcu_node, rcu_data
组织成树状结构来维护,此外回调函数是经过rcu_data
中的分段链表来批处理,至于这些结构中相关字段的处理(好比gpnum, completed
字段的设置来判断宽限期阶段等),以及链表的节点移动等,都没有进一步去分析跟进了;渐入佳境篇
就此打住,是否还会有登堂入室篇
呢?想啥呢,歇歇吧。
Verification of the Tree-Based Hierarchical Read-Copy Update in the Linux Kernel
Documentation/RCU
What is RCU, Fundamentally?
What is RCU? Part 2: Usage
RCU part 3: the RCU API
Introduction to RCU
欢迎关注公众号: