RCU-1——内核文档翻译——Data-Structures.rst
翻译:kernel-5.10\Documentation\RCU\Design\Data-Structures\Data-Structures.rst
================================================ =
TREE_RCU 数据结构导览 [LWN.net]
================================================ =
2016 年 12 月 18 日
本文由 Paul E. McKenney 提供
介绍
============
本文档描述了 RCU 的主要数据结构以及它们之间的关系。
数据结构关系
============================
RCU 出于所有意图和目的是一个大型状态机,其数据结构以允许 RCU 读者极快地执行的方式维护状态,同时还以高效且极具可扩展性的方式处理更新程序请求的 RCU 宽限期 .
RCU updaters 的效率和可扩展性主要由组合树提供,如下所示:
.. 内核图:: BigTreeClassicRCU.svg
此图显示了一个包含“rcu_node”结构树的封闭“rcu_state”结构。 ``rcu_node`` 树的每个叶节点最多有 16 个 ``rcu_data`` 结构与之关联,因此有 ``NR_CPUS`` 数量的 ``rcu_data`` 结构,
每个可能的 CPU 一个。 如果需要,此结构会在启动时进行调整,以处理 ``nr_cpu_ids`` 远小于 ``NR_CPUs`` 的常见情况。 例如,许多 Linux 发行版设置了“NR_CPUs=4096”,这导致了一个
三级“rcu_node”树。 如果实际硬件只有 16 个 CPU,RCU 会在启动时自行调整,导致只有一个节点的 ``rcu_node`` 树。
此组合树的目的是允许高效且可扩展地处理每个 CPU 事件,例如静态、dyntick-idle 转换和 CPU 热插拔操作。
静态由每个 CPU 的“rcu_data”结构记录,和其他事件由叶级 ``rcu_node`` 结构记录。
所有这些事件都在树的每一层进行组合,直到最后在树的根“rcu_node”结构中完成宽限期。
一旦每个 CPU(或者,在 ``CONFIG_PREEMPT_RCU`` 的情况下,任务)已经通过静止状态,就可以在根节点完成宽限期。 一旦宽限期结束,该事实的记录就会沿着树向下传播。
从图中可以看出,在64位系统上,一棵64个叶子的两级树可以容纳1024个CPU,根部的扇出为64,叶子的扇出为16。
| **小测验**: |
+------------------------------------------------ ----------------------+
| 为什么叶子的扇出不是 64? |
+------------------------------------------------ ----------------------+
| **回答**:
因为影响叶级 ``rcu_node`` 结构的事件类型多于树的更上层。 因此,如果叶 ``rcu_node`` 结构的扇出为 64,则对这些结构的``->structures`` 的争用变得过多。 在各种系统上进行的实验表明,
16 的扇出适用于 ``rcu_node`` 树的叶子。
当然,具有数百或数千个 CPU 的系统的进一步经验可能会证明非叶 ``rcu_node`` 结构的扇出也必须减少。 当证明有必要时,可以很容易地进行这种减少。 同时,如果您正在使用这样的系统并在非
叶``rcu_node``结构上遇到争用问题,您可以使用``CONFIG_RCU_FANOUT``内核配置参数根据需要减少非叶扇出。
为具有强大 NUMA 特性的系统构建的内核可能还需要调整“CONFIG_RCU_FANOUT”,以便“rcu_node”结构的域与硬件边界对齐。 然而,到目前为止还没有这个必要。
如果您的系统有超过 1,024 个 CPU(或在 32 位系统上超过 512 个 CPU),那么 RCU 会自动向树中添加更多层级。 例如,如果你足够疯狂地构建一个具有 65,536 个 CPU 的 64 位系统,RCU 将配置
``rcu_node`` 树如下:
.. 内核图:: HugeTreeClassicRCU.svg
RCU 目前允许最多四级树,在 64 位系统上最多可容纳 4,194,304 个 CPU,但在 32 位系统上只能容纳 524,288 个 CPU。 另一方面,您可以将 ``CONFIG_RCU_FANOUT`` 和 ``CONFIG_RCU_FANOUT_LEAF``
设置为小至 2,这将导致使用 4 级树的 16-CPU 测试。 这对于在小型测试机器上测试大型系统功能很有用。
这种多级组合树使我们能够获得分区的大部分性能和可伸缩性优势,即使 RCU 宽限期检测本质上是一个全局操作。 这里的技巧是,只有最后一个 CPU 将静止状态报告给给定的 ``rcu_node`` 结构需要前
进到树的下一层的 ``rcu_node`` 结构。 这意味着在叶级 ``rcu_node`` 结构中,16 个访问中只有一个会在树上进行。 对于内部的 ``rcu_node`` 结构,情况更加极端:64 次访问中只有一次会在树上进
行。 因为绝大多数 CPU 没有在树中向上移动,所以锁争用在树中大致保持不变。 无论系统中有多少个 CPU,每个宽限期最多 64 个静态报告将一直进行到根 ``rcu_node`` 结构,从而确保该根 ``rcu_node``
结构上的锁争用仍然处于可接受的低水平。
实际上,组合树就像一个大减震器,无论系统负载水平如何,都可以在所有树级别控制锁争用。
RCU updaters 通过注册 RCU 回调来等待正常的宽限期,可以直接通过 ``call_rcu()`` 或间接通过 ``synchronize_rcu()`` 或其朋友函数。 RCU 回调由 ``rcu_head`` 结构表示,它们在等待宽限期结束时在
``rcu_data`` 结构上排队,如下图所示:
.. 内核图:: BigTreePreemptRCUBHdyntickCB.svg
此图显示了 ``TREE_RCU`` 和 ``PREEMPT_RCU`` 的主要数据结构之间的关系。 较小的数据结构将与使用它们的算法一起引入。
注意上图中的每个数据结构都有自己的同步:
#. 每个 ``rcu_state`` 结构都有一个锁和一个互斥量,一些字段由相应的根 ``rcu_node`` 结构的锁保护。
#. 每个 ``rcu_node`` 结构都有一个自旋锁。
#. ``rcu_data`` 中的字段是相应 CPU 私有的,尽管有一些字段可以被其他 CPU 读取和写入。
重要的是要注意,不同的数据结构在任何给定时间对 RCU 的状态可能有非常不同的想法。 举一个例子,对给定 RCU 宽限期开始或结束的意识通过数据结构缓慢传播。 这种缓慢的传播对于 RCU 具有良好
的读取端性能是绝对必要的。 如果您觉得这种割裂的实现方式很陌生,一个有用的技巧是将这些数据结构的每个实例都视为不同的人,每个人对现实的看法通常略有不同。
这些数据结构的一般作用如下:
#. ``rcu_state``:该结构形成``rcu_node``和``rcu_data``结构之间的互连,跟踪宽限期,作为CPU热插拔事件孤立的回调的短期存储库,维护``rcu_barrier()`` 状态,跟踪加速宽限期状态,并在宽限
期延长太长时维护用于强制静态状态的状态,
#. ``rcu_node``:这个结构形成了组合树,它将静止状态信息从叶子传播到根,并且还将宽限期信息从根传播到叶子。它提供宽限期状态的本地副本,以允许以同步方式访问此信息,而不会受到全局锁定强加的
可伸缩性限制。 在 ``CONFIG_PREEMPT_RCU`` 内核中,它管理在当前 RCU 读端临界区中阻塞的任务列表。 在 ``CONFIG_PREEMPT_RCU`` 和 ``CONFIG_RCU_BOOST`` 中,它管理每 ``rcu_node`` 的优先级提
升内核线程(kthreads)和状态。 最后,它记录 CPU 热插拔状态以确定在给定的宽限期内应忽略哪些 CPU。
#. ``rcu_data``:这个每 CPU 的结构是静态检测和 RCU 回调排队的重点。 它还跟踪它与相应的叶``rcu_node``结构的关系,以允许更有效地将静态状态传播到``rcu_node``组合树上。 与 rcu_node 结构
一样,它提供了宽限期信息的本地副本,以允许从相应的 CPU 中访问此信息,而不需要任何同步操作。 最后,该结构记录了相应 CPU 过去的 dyntick-idle 状态并跟踪统计信息。
#. ``rcu_head``:这个结构代表RCU回调,并且是RCU用户分配和管理的唯一结构。 ``rcu_head`` 结构通常嵌入在受 RCU 保护的数据结构中。
如果您希望从本文中了解 RCU 的数据结构如何相关的一般概念,那么您已经完成了。 否则,以下各节将提供有关 ``rcu_state``、``rcu_node`` 和 ``rcu_data`` 数据结构的更多详细信息。
一、``rcu_state`` 结构
~~~~~~~~~~~~~~~~~~~~~~~~~~~
``rcu_state`` 结构是表示系统中 RCU 状态的基本结构。 这个结构形成了 ``rcu_node`` 和 ``rcu_data`` 结构之间的互连,跟踪宽限期,包含用于与 CPU 热插拔事件同步的锁,并维护用于在宽限期延长太
长时强制进入静止状态的状态 ,
在以下部分中,将单独或成组地讨论一些 rcu_state 结构的字段。 更专业的领域涵盖在它们的使用讨论中。
1.1. 与 rcu_node 和 rcu_data 结构的关系
'''''''''''''''''''''''''''''''''''''''''''''''
``rcu_state`` 结构的这一部分声明如下:
::
1 struct rcu_node node[NUM_RCU_NODES];
2 struct rcu_node *level[NUM_RCU_LVLS + 1];
3 struct rcu_data __percpu *rda;
| **小测验**: |
+------------------------------------------------ ----------------------+
| 等一下! 你说 ``rcu_node`` 结构形成了一棵树,但它们被声明为平面数组! 是什么赋予了?
+------------------------------------------------ ----------------------+
| **回答**: |
+------------------------------------------------ ----------------------+
| 树被布置在数组中。 数组中的第一个节点是头,数组中的下一组节点是头节点的子节点,依此类推,直到数组中的最后一组节点是叶子。 请参阅下图以了解其工作原理。
``rcu_node`` 树嵌入到``->node[]`` 数组中,如下图所示:
.. 内核图:: TreeMapping.svg
这种映射的一个有趣的结果是树的广度优先遍历被实现为数组的简单线性扫描,这实际上是 ``rcu_for_each_node_breadth_first()`` 宏所做的。 该宏在宽限期的开始和结束时使用。
``->level`` 数组的每个条目都引用树的相应级别上的第一个 ``rcu_node`` 结构,例如,如下所示:
.. 内核图:: TreeMappingLevel.svg
数组的第零个 :sup:`th` 元素引用根 ``rcu_node`` 结构,第一个元素引用根 ``rcu_node`` 的第一个子节点,最后第二个元素引用第一个叶子 `` rcu_node``结构。
不管怎样,如果你把树画成树形而不是数组形,就很容易画出平面表示:
.. 内核图:: TreeLevel.svg
最后,``->rda`` 字段引用一个指向相应 CPU 的``rcu_data`` 结构的每 CPU 指针。
一旦初始化完成,所有这些字段都是常量,因此不需要保护。
1.2. 宽限期跟踪
''''''''''''''''''''
``rcu_state`` 结构的这一部分声明如下:
::
1 unsigned long gp_seq;
RCU 宽限期是被编号的,``->gp_seq`` 字段包含当前宽限期序列号。 低两位是当前宽限期的状态,可以为 0 表示尚未开始,也可以为 1 表示进行中。换句话说,如果 ``->gp_seq`` 的低两位为零,则 RCU 空闲。
底部两位中的任何其他值都表示有东西坏了。 该字段受根 ``rcu_node`` 结构的 ``->lock`` 字段保护。
``rcu_node`` 和 ``rcu_data`` 结构中也有 ``->gp_seq`` 字段。 ``rcu_state`` 结构中的字段表示最新值,并且比较其他结构中的字段,以便以分布式方式检测宽限期的开始和结束。
值从 ``rcu_state`` 流向 ``rcu_node``(从根到叶的树)到 ``rcu_data``。
各种各样的
'''''''''''''
``rcu_state`` 结构的这一部分声明如下:
::
1 unsigned long gp_max;
2 char abbr;
3 char *name;
``->gp_max`` 字段以 jiffies 为单位跟踪最长宽限期的持续时间。 它受根 ``rcu_node`` 的 ``->lock`` 保护。
``->name`` 和 ``->abbr`` 字段区分抢占式 RCU(“rcu_preempt”和“p”)和非抢占式 RCU(“rcu_sched”和“s”)。 这些字段用于诊断和跟踪目的。
二、``rcu_node`` 结构
~~~~~~~~~~~~~~~~~~~~~~~~~~
``rcu_node`` 结构形成了组合树,它将静止状态信息从叶子传播到根,并将宽限期信息从根向下传播到叶子。 它们提供宽限期状态的本地副本,以允许以同步方式访问此信息,而不
会受到全局锁定强加的可伸缩性限制。 在 ``CONFIG_PREEMPT_RCU`` 内核中,它们管理在当前 RCU 读端临界区中阻塞的任务列表。在 ``CONFIG_PREEMPT_RCU`` 和 ``CONFIG_RCU_BOOST`` 中,
它们管理每个 ``rcu_node`` 优先级提升内核线程(kthreads)和状态。 最后,他们记录 CPU 热插拔状态以确定在给定的宽限期内应忽略哪些 CPU。
``rcu_node`` 结构的字段将在以下部分单独和成组讨论。
2.1. 连接到组合树
''''''''''''''''''''''''''
``rcu_node`` 结构的这一部分声明如下:
::
1 struct rcu_node *parent;
2 u8 level;
3 u8 grpnum;
4 unsigned long grpmask;
5 int grplo;
6 int grphi;
``->parent`` 指针引用树中上一层的``rcu_node``,对于根``rcu_node`` 为``NULL``。 RCU 实现大量使用此字段将静止状态推到树上。 ``->level`` 字段给出了树中的级别,根为零级,
其子级为一级,依此类推。 ``->grpnum`` 字段给出了该节点在其父节点的子节点中的位置,因此该数字在 32 位系统上可以介于 0 到 31 之间,在 64 位系统上可以介于 0 到 63 之间。
``->level`` 和 ``->grpnum`` 字段仅在初始化和tracing期间使用。 ``->grpmask`` 字段是 ``->grpnum`` 的对应位掩码,因此总是只有一位设置。此掩码用于清除其父级位掩码中与此
``rcu_node`` 结构对应的位,稍后将对此进行描述。 最后,``->grplo`` 和``->grphi`` 字段分别包含此 ``rcu_node`` 结构服务的编号最低和最高的 CPU。
所有这些字段都是常量,因此不需要任何同步。
2.2. 同步
'''''''''''''''
``rcu_node`` 结构的这个字段声明如下:
::
1 raw_spinlock_t lock;
除非另有说明,否则此字段用于保护此结构中的其余字段。 也就是说,出于跟踪目的,无需锁定即可访问此结构中的所有字段。 是的,这可能会导致混乱的trace,但一些混乱trace比被 heisenbugged
淘汰好。
.. _grace-period-tracking-1:
2.3. 宽限期跟踪
''''''''''''''''''''
``rcu_node`` 结构的这一部分声明如下:
::
1 unsigned long gp_seq;
2 unsigned long gp_seq_needed;
``rcu_node`` 结构的``->gp_seq`` 字段是``rcu_state`` 结构中同名字段的对应项。 他们每个人都可能落后于他们的 ``rcu_state`` 一步。 如果给定 ``rcu_node`` 结构的 ``->gp_seq`` 字
段的底部两位为零,则此 ``rcu_node`` 结构认为 RCU 空闲。
每个“rcu_node”结构的“>gp_seq”字段在每个宽限期的开始和结束时更新。
``->gp_seq_needed`` 字段记录相应 ``rcu_node`` 结构看到的最远的未来宽限期请求。 当 ``->gp_seq`` 字段的值等于或超过 ``->gp_seq_needed`` 字段的值时,请求被认为已完成。
| **小测验**: |
+------------------------------------------------ ----------------------+
| 假设这个 ``rcu_node`` 结构很长时间没有看到请求。 ``->gp_seq`` 字段的 wrapping 不会导致问题吗? |
+------------------------------------------------ ----------------------+
| **回答**: |
+------------------------------------------------ ----------------------+
| 不会,因为如果 ``->gp_seq_needed`` 字段滞后于 ``->gp_seq`` 字段,``->gp_seq_needed`` 字段将在宽限期结束时更新。 因此,模算术比较总是会得到正确的答案,即使有回绕也是如此。 |
2.4. 静态跟踪
'''''''''''''''''''''''
这些字段管理静态在组合树上的传播。
``rcu_node`` 结构的这一部分具有如下字段:
::
1 unsigned long qsmask;
2 unsigned long expmask;
3 unsigned long qsmaskinit;
4 unsigned long expmaskinit;
``->qsmask`` 字段跟踪此 ``rcu_node`` 结构的哪些子结构仍需要报告当前正常宽限期的静止状态。 这样的孩子在其相应位中的值为 1。 请注意,叶 ``rcu_node`` 结构应该被视为具有 ``rcu_data``
结构作为它们的子结构。 类似地,``->expmask`` 字段跟踪此 ``rcu_node`` 结构的哪些子结构仍需要报告当前加速宽限期的静止状态。 加速宽限期与正常宽限期具有相同的概念属性,但加速实施接受
极端的 CPU 开销以获得更低的宽限期延迟,例如,消耗几十微秒的 CPU 时间来减少持续时间从几毫秒到几十微秒的宽限期。 ``->qsmaskinit`` 字段跟踪这个 ``rcu_node`` 结构的哪个子结构覆盖了至
少一个在线 CPU。 此掩码用于初始化``->qsmask``,``->expmaskinit`` 用于初始化``->expmask`` 以及正常和加速宽限期的开始。
| **小测验**: |
+------------------------------------------------ ----------------------+
| 为什么这些位掩码受锁保护? 拜托,你没听说过原子指令吗??? |
+------------------------------------------------ ----------------------+
| **回答**: |
+------------------------------------------------ ----------------------+
| 无锁宽限期计算! 如此诱人的可能性! 但是请考虑以下事件序列:
|
| #. CPU 0 已经处于 dyntick-idle 模式很长一段时间了。 当它醒来时,它注意到当前的 RCU 宽限期需要它报告,所以它设置一个标志,调度时钟中断将找到它。
| #. 与此同时,CPU 1 正在运行“force_quiescent_state()”,并注意到 CPU 0 一直处于 dyntick 空闲模式,这符合扩展静止状态的条件。
| #. CPU 0 的调度时钟中断在 RCU 读端临界区的中间触发,并注意到 RCU 核心需要一些东西,因此开始 RCU softirq 处理。
| #. CPU 0 的 softirq 处理程序执行并准备好向 ``rcu_node`` 树报告其静止状态。
| #. 但是 CPU 1 先发制人,完成了当前的宽限期并开始了新的宽限期。
| #. CPU 0 现在报告错误宽限期的静止状态。 该宽限期现在可能会在 RCU 读端临界区之前结束。 如果发生这种情况,灾难就会接踵而至。
|
| 所以锁定是绝对需要的,以便协调位的清除与更新 ``->gp_seq`` 中的宽限期序列号。
2.5. 阻塞任务管理
''''''''''''''''''''''
``PREEMPT_RCU`` 允许任务在其 RCU 读端临界区中被抢占,并且必须显式跟踪这些任务。 跟踪它们的确切原因和方式的详细信息将在有关 RCU 读取端处理的另一篇文章中介绍。 现在,知道 ``rcu_node``
结构跟踪它们就足够了。
::
1 struct list_head blkd_tasks;
2 struct list_head *gp_tasks;
3 struct list_head *exp_tasks;
4 bool wait_blkd_tasks;
``->blkd_tasks`` 字段是阻塞和抢占任务列表的列表头。 当任务在 RCU 读端临界区内进行上下文切换时,它们的 ``task_struct`` 结构被排入队列(通过 ``task_struct`` 的 ``->rcu_node_entry`` 字段)
到 ``-> blkd_tasks`` 中,它是叶``rcu_node``结构的列表,对应于执行传出上下文切换的CPU。 当这些任务稍后退出它们的 RCU 读端临界区时,它们将自己从列表中移除。 因此这个列表是时间倒序的,所以如
果其中一个任务阻塞了当前的宽限期,那么所有后续任务也必须阻塞同一个宽限期。 因此,指向此列表的单个指针足以跟踪所有阻塞给定宽限期的任务。 对于正常的宽限期,该指针存储在 ``->gp_tasks`` 中,
对于加速的宽限期存储在 ``->exp_tasks`` 中。 如果没有进行中的宽限期或者没有阻止宽限期完成的阻塞任务,最后两个字段为“NULL”。 如果这两个指针中的任何一个正在引用一个从``->blkd_tasks``列表中
删除自身的任务,那么该任务必须将指针推进到列表中的下一个任务,或者将指针设置为``NULL``如果列表中没有后续任务。
例如,假设任务 T1、T2 和 T3 都 hard-affinitied 到系统中编号最大的 CPU 上。 然后,如果任务 T1 阻塞在 RCU 读端临界区,然后加速宽限期开始,然后任务 T2 阻塞在 RCU 读端临界区,然后正常宽限期开
始,最后任务 3 阻塞在 RCU 读端临界区,那么最后一个叶子 ``rcu_node`` 结构的阻塞任务列表的状态将如下所示:
.. 内核图:: blkd_task.svg
任务 T1 阻塞了两个宽限期,任务 T2 仅阻塞了正常宽限期,任务 T3 没有阻塞任何宽限期。 请注意,这些任务不会在恢复执行后立即从该列表中删除。 它们将保留在列表中,直到它们执行最外层的
``rcu_read_unlock()`` 结束它们的 RCU 读端临界区。
``->wait_blkd_tasks`` 字段指示当前宽限期是否正在等待阻塞的任务。
2.6. 调整 ``rcu_node`` 数组的大小
'''''''''''''''''''''''''''
``rcu_node`` 数组通过一系列 C 预处理器表达式确定大小,如下所示:
::
1 #ifdef CONFIG_RCU_FANOUT
2 #define RCU_FANOUT CONFIG_RCU_FANOUT
3 #else
4 # ifdef CONFIG_64BIT
5 # define RCU_FANOUT 64
6 # else
7 # define RCU_FANOUT 32
8 # endif
9 #endif
10
11 #ifdef CONFIG_RCU_FANOUT_LEAF
12 #define RCU_FANOUT_LEAF CONFIG_RCU_FANOUT_LEAF
13 #else
14 # ifdef CONFIG_64BIT
15 # define RCU_FANOUT_LEAF 64
16 # else
17 # define RCU_FANOUT_LEAF 32
18 # endif
19 #endif
20
21 #define RCU_FANOUT_1 (RCU_FANOUT_LEAF)
22 #define RCU_FANOUT_2 (RCU_FANOUT_1 * RCU_FANOUT)
23 #define RCU_FANOUT_3 (RCU_FANOUT_2 * RCU_FANOUT)
24 #define RCU_FANOUT_4 (RCU_FANOUT_3 * RCU_FANOUT)
25
26 #if NR_CPUS <= RCU_FANOUT_1
27 # define RCU_NUM_LVLS 1
28 # define NUM_RCU_LVL_0 1
29 # define NUM_RCU_NODES NUM_RCU_LVL_0
30 # define NUM_RCU_LVL_INIT { NUM_RCU_LVL_0 }
31 # define RCU_NODE_NAME_INIT { "rcu_node_0" }
32 # define RCU_FQS_NAME_INIT { "rcu_node_fqs_0" }
33 # define RCU_EXP_NAME_INIT { "rcu_node_exp_0" }
34 #elif NR_CPUS <= RCU_FANOUT_2
35 # define RCU_NUM_LVLS 2
36 # define NUM_RCU_LVL_0 1
37 # define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)
38 # define NUM_RCU_NODES (NUM_RCU_LVL_0 + NUM_RCU_LVL_1)
39 # define NUM_RCU_LVL_INIT { NUM_RCU_LVL_0, NUM_RCU_LVL_1 }
40 # define RCU_NODE_NAME_INIT { "rcu_node_0", "rcu_node_1" }
41 # define RCU_FQS_NAME_INIT { "rcu_node_fqs_0", "rcu_node_fqs_1" }
42 # define RCU_EXP_NAME_INIT { "rcu_node_exp_0", "rcu_node_exp_1" }
43 #elif NR_CPUS <= RCU_FANOUT_3
44 # define RCU_NUM_LVLS 3
45 # define NUM_RCU_LVL_0 1
46 # define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_2)
47 # define NUM_RCU_LVL_2 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)
48 # define NUM_RCU_NODES (NUM_RCU_LVL_0 + NUM_RCU_LVL_1 + NUM_RCU_LVL_2)
49 # define NUM_RCU_LVL_INIT { NUM_RCU_LVL_0, NUM_RCU_LVL_1, NUM_RCU_LVL_2 }
50 # define RCU_NODE_NAME_INIT { "rcu_node_0", "rcu_node_1", "rcu_node_2" }
51 # define RCU_FQS_NAME_INIT { "rcu_node_fqs_0", "rcu_node_fqs_1", "rcu_node_fqs_2" }
52 # define RCU_EXP_NAME_INIT { "rcu_node_exp_0", "rcu_node_exp_1", "rcu_node_exp_2" }
53 #elif NR_CPUS <= RCU_FANOUT_4
54 # define RCU_NUM_LVLS 4
55 # define NUM_RCU_LVL_0 1
56 # define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_3)
57 # define NUM_RCU_LVL_2 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_2)
58 # define NUM_RCU_LVL_3 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)
59 # define NUM_RCU_NODES (NUM_RCU_LVL_0 + NUM_RCU_LVL_1 + NUM_RCU_LVL_2 + NUM_RCU_LVL_3)
60 # define NUM_RCU_LVL_INIT { NUM_RCU_LVL_0, NUM_RCU_LVL_1, NUM_RCU_LVL_2, NUM_RCU_LVL_3 }
61 # define RCU_NODE_NAME_INIT { "rcu_node_0", "rcu_node_1", "rcu_node_2", "rcu_node_3" }
62 # define RCU_FQS_NAME_INIT { "rcu_node_fqs_0", "rcu_node_fqs_1", "rcu_node_fqs_2", "rcu_node_fqs_3" }
63 # define RCU_EXP_NAME_INIT { "rcu_node_exp_0", "rcu_node_exp_1", "rcu_node_exp_2", "rcu_node_exp_3" }
64 #else
65 # error "CONFIG_RCU_FANOUT insufficient for NR_CPUS"
66 #endif
``rcu_node`` 结构中的最大层数目前限制为四,如第 21-24 行和后续“if”语句的结构所指定。 对于 32 位系统,这允许 16*32*32*32=524,288 个 CPU,至少在未来几年应该足够了。 对于 64 位系统,
16*64*64*64=4,194,304 个 CPU 是允许的,这应该可以让我们度过未来十年左右的时间。 这个四级树还允许使用 ``CONFIG_RCU_FANOUT=8`` 构建的内核支持多达 4096 个 CPU,这在每个插槽有 8 个
CPU 的非常大的系统中可能很有用(但请注意,还没有人显示任何可测量的性能 由于未对齐的套接字和 ``rcu_node`` 边界导致的降级)。 此外,使用完整的四级 ``rcu_node`` 树构建内核可以更好地
测试 RCU 的组合树代码。
``RCU_FANOUT`` 符号控制在 ``rcu_node`` 树的每个非叶级别上允许有多少孩子。 如果没有指定 ``CONFIG_RCU_FANOUT`` Kconfig 选项,它是根据系统的字大小设置的,这也是 Kconfig 默认值。
``RCU_FANOUT_LEAF`` 符号控制每个叶``rcu_node`` 结构处理多少 CPU。 经验表明,允许给定的叶子“rcu_node”结构处理 64 个 CPU,如 64 位系统上“->qsmask”字段中的位数所允许的那样,会导致对
叶子的过度争用 ``rcu_node`` 结构' `->lock`` 字段。 因此,给定 CONFIG_RCU_FANOUT_LEAF 的默认值,每个叶子 rcu_node 结构的 CPU 数量限制为 16。 如果未指定 ``CONFIG_RCU_FANOUT_LEAF``,
则选择的值基于系统的字大小,就像 ``CONFIG_RCU_FANOUT`` 一样。 第 11-19 行执行此计算。
第 21-24 行分别计算单级(包含单个 ``rcu_node`` 结构)、二级、三级和四级 ``rcu_node`` 树支持的最大 CPU 数量, 给定 RCU_FANOUT 和 RCU_FANOUT_LEAF 指定的扇出。 这些 CPU 数量分别保留
在“RCU_FANOUT_1”、“RCU_FANOUT_2”、“RCU_FANOUT_3”和“RCU_FANOUT_4”C 预处理器变量中。
这些变量用于控制跨越 26-66 行的 C 预处理器“#if”语句,该语句计算树的每个级别所需的“rcu_node”结构的数量,以及所需的级别数量。 层数由第 27、35、44 和 54 行放置在“NUM_RCU_LVLS”C 预处
理器变量中。树的最顶层的“rcu_node”结构的数量始终正好是一个,并且 该值无条件地放入第 28、36、45 和 55 行的“NUM_RCU_LVL_0”中。“rcu_node”树的其余级别(如果有)是通过将最大 CPU 数量
除以 从当前级别向下舍入的级别数支持的扇出。 此计算由第 37、46-47 和 56-58 行执行。 第 31-33、40-42、50-52 和 62-63 行为 lockdep 锁类名称创建初始值设定项。 最后,如果 CPU 的最大数
量对于指定的扇出太大,则第 64-66 行会产生错误。
三、``rcu_segcblist`` 结构
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3.1. ``rcu_segcblist`` 结构维护一个分段的回调列表,如下所示:
::
1 #define RCU_DONE_TAIL 0
2 #define RCU_WAIT_TAIL 1
3 #define RCU_NEXT_READY_TAIL 2
4 #define RCU_NEXT_TAIL 3
5 #define RCU_CBLIST_NSEGS 4
6
7 struct rcu_segcblist {
8 struct rcu_head *head;
9 struct rcu_head **tails[RCU_CBLIST_NSEGS];
10 unsigned long gp_seq[RCU_CBLIST_NSEGS];
11 long len;
12 long len_lazy;
13 };
分段如下:
#. ``RCU_DONE_TAIL``:宽限期已过的回调。 这些回调已准备好被调用。
#. ``RCU_WAIT_TAIL``:正在等待当前宽限期的回调。 请注意,不同的 CPU 可能对哪个宽限期是当前有不同的想法,见 ``->gp_seq`` 字段。
#. ``RCU_NEXT_READY_TAIL``:等待下一个宽限期开始的回调。
#. ``RCU_NEXT_TAIL``:尚未与宽限期关联的回调。
``->head`` 指针引用第一个回调,或者如果列表不包含回调(这*不* 等于空)则为 ``NULL``。 ``->tails[]`` 数组的每个元素引用列表相应段中最后一个回调的``->next`` 指针,或者如果该段是列
表的``->head`` 指针 并且之前的所有段都是空的。 如果相应的段为空但之前的某个段不为空,则数组元素与其前身相同。 较旧的回调更靠近列表的头部,新的回调添加在尾部。 ``->head`` 指针、
``->tails[]`` 数组和回调之间的关系如下图所示:
.. 内核图:: nxtlist.svg
在此图中,``->head`` 指针引用列表中的第一个 RCU 回调。 ``->tails[RCU_DONE_TAIL]`` 数组元素引用``->head`` 指针本身,表明没有任何回调准备好调用。 ``->tails[RCU_WAIT_TAIL]`` 数组元素引用回
调 CB 2 的 ``->next`` 指针,这表明 CB 1 和 CB 2 都在等待当前宽限期,给出或接受可能的分歧 哪个宽限期是当前的宽限期。 ``->tails[RCU_NEXT_READY_TAIL]`` 数组元素引用与``->tails[RCU_WAIT_TAIL]``
相同的 RCU 回调,这表明没有回调等待下一个 RCU 宽限期。 ``->tails[RCU_NEXT_TAIL]`` 数组元素引用 CB 4 的``->next`` 指针,表示所有剩余的 RCU 回调尚未被分配 RCU 宽限期。 请注意,
``->tails[RCU_NEXT_TAIL]`` 数组元素始终引用最后一个 RCU 回调的``->next`` 指针,除非回调列表为空,在这种情况下它引用``->head`` 指针 .
``->tails[RCU_NEXT_TAIL]`` 数组元素还有一个重要的特殊情况:当此列表*禁用*时,它可以是 ``NULL``。 当相应的 CPU 处于离线状态或当相应的 CPU 的回调被卸载到 kthread 时,列表将被禁用,这两种情
况都在别处描述。
随着宽限期的推进,CPU 将它们的回调从“RCU_NEXT_TAIL”推进到“RCU_NEXT_READY_TAIL”到“RCU_WAIT_TAIL”到“RCU_DONE_TAIL”列表段。
``->gp_seq[]`` 数组记录了与列表段对应的宽限期编号。 这就是允许不同的 CPU 对哪个是当前宽限期有不同的想法,同时仍然避免过早调用它们的回调。 特别是,这允许长时间空闲的 CPU 确定它们的哪些回调
已准备好在重新唤醒后调用。
``->len`` 计数``->head`` 中回调的数量,而``->len_lazy`` 包含已知仅释放内存的那些回调的数量,其调用因此可以安全地推迟。
.. 重要的::
``->len`` 字段决定是否存在与此 ``rcu_segcblist`` 结构关联的回调,*不是* ``->head`` 指针。 这样做的原因是所有准备好调用的回调(即那些在 ``RCU_DONE_TAIL`` 段中的回调)在回调调用时间
(``rcu_do_batch``)被一次性全部提取出来,因此 ` 如果 ``rcu_segcblist`` 中没有未完成的回调,`->head`` 可以设置为 NULL。 如果 allback 调用必须被推迟,例如,因为一个高优先级进程刚刚在这个 CPU
上醒来,那么剩余的回调将被放回 ``RCU_DONE_TAIL`` 段并且 ``->head`` 再次指向段的开始。 简而言之,head 字段可以短暂地为“NULL”,即使 CPU 一直存在回调。 因此,测试 ``->head`` 指针是否为 ``NULL``
是不合适的。
相反,``->len`` 和 ``->len_lazy`` 计数仅在调用相应的回调后才进行调整。 这意味着只有当 rcu_segcblist 结构确实没有回调时,->len 计数才为零。 当然,``->len`` 计数的 off-CPU 采样需要小心使用适
当的同步,例如内存屏障。 这种同步可能有点微妙,特别是在 ``rcu_barrier()`` 的情况下。
四、``rcu_data`` 结构
~~~~~~~~~~~~~~~~~~~~~~~~~~
``rcu_data`` 维护 RCU 子系统的每个 CPU 状态。 除非另有说明,否则只能从相应的 CPU(和tracing)访问此结构中的字段。 此结构是静态检测和 RCU 回调排队的重点。 它还跟踪它与相应的叶``rcu_node``结
构的关系,以允许更有效地将静态状态传播到``rcu_node``组合树上。 与 rcu_node 结构一样,它提供了宽限期信息的本地副本,以允许从相应的 CPU 中免费同步访问此信息。 最后,该结构记录了相应 CPU 过去
的 dyntick-idle 状态并跟踪统计信息。
``rcu_data`` 结构的字段将在以下部分中单独和成组讨论。
4.1. 连接到其他数据结构
'''''''''''''''''''''''''''''''''
``rcu_data`` 结构的这一部分声明如下:
::
1 int cpu;
2 struct rcu_node *mynode;
3 unsigned long grpmask;
4 bool beenonline;
``->cpu`` 字字段是相应 CPU 的编号,``->mynode`` 字段引用相应的 ``rcu_node`` 结构。 ``->mynode`` 用于在组合树上传播静止状态。 这两个字段是常量,因此不需要同步。
``->grpmask`` 字段表示``->mynode->qsmask`` 中与此``rcu_data`` 结构对应的位,在传播静止状态时也会使用。 ``->beenonline`` 标志在相应的 CPU 上线时设置,这意味着 debugfs 跟踪不需要转储任何未设
置此标志的 ``rcu_data`` 结构。
4.2. 静态和宽限期跟踪
'''''''''''''''''''''''''''''''''''''''
``rcu_data`` 结构的这一部分声明如下:
::
1 unsigned long gp_seq;
2 unsigned long gp_seq_needed;
3 bool cpu_no_qs;
4 bool core_needs_qs;
5 bool gpwrap;
``->gp_seq`` 字段与``rcu_state`` 和``rcu_node`` 结构中的同名字段对应。 ``->gp_seq_needed`` 字段是 rcu_node 结构中同名字段的对应部分。 它们可能每个都落后于它们的 ``rcu_node`` 对应物,但在
``CONFIG_NO_HZ_IDLE`` 和 ``CONFIG_NO_HZ_FULL`` 内核可以任意落后于 dyntick-idle 模式下的 CPU(但一旦退出 dyntick-idle 模式,这些计数器会赶上)。 如果给定的 ``rcu_data`` 结构的 ``->gp_seq``
的低两位为零,那么这个 ``rcu_data`` 结构认为 RCU 是空闲的。
| **小测验**: |
+------------------------------------------------ ----------------------+
| 所有这些宽限期数字的复制只会造成巨大的混乱。 为什么不只保留一个全局序列号并完成它???
+------------------------------------------------ ----------------------+
| **回答**: |
+------------------------------------------------ ----------------------+
| 因为如果只有一个全局序列号,就需要一个全局锁来允许安全地访问和更新它。 如果我们不打算拥有一个全局锁,我们需要在每个节点的基础上仔细管理数字。 回想一下之前快速测验的答案,将之前采样的静止
状态应用于错误的宽限期的后果非常严重。
``->cpu_no_qs`` 标志表示 CPU 尚未通过静止状态,而``->core_needs_qs`` 标志表示 RCU 内核需要来自相应 CPU 的静止状态。 ``->gpwrap`` 字段表明相应的 CPU 已经保持空闲太久以至于 ``gp_seq`` 计数器
有溢出的危险,这将导致 CPU 在下次从空闲退出时忽略其计数器的值。
4.3. RCU 回调处理
''''''''''''''''''''
在没有 CPU 热插拔事件的情况下,RCU 回调由注册它们的同一个 CPU 调用。 这完全是一种缓存位置优化:回调可以并且确实在注册它们的 CPU 之外的 CPU 上被调用。 毕竟,如果注册给定回调的 CPU 在回调可以
被调用之前已经离线,那么真的没有其他选择。
``rcu_data`` 结构的这一部分声明如下:
::
1 struct rcu_segcblist cblist;
2 long qlen_last_fqs_check;
3 unsigned long n_cbs_invoked;
4 unsigned long n_nocbs_invoked;
5 unsigned long n_cbs_orphaned;
6 unsigned long n_cbs_adopted;
7 unsigned long n_force_qs_snap;
8 long blimit;
``->cblist`` 结构是前面描述的分段回调列表。 只要 CPU 注意到另一个 RCU 宽限期已经完成,它就会在其 ``rcu_data`` 结构中推进回调。 CPU 通过注意到其“rcu_data”结构的“->gp_seq”字段的值与其叶
“rcu_node”结构的值不同来检测 RCU 宽限期的完成。回想一下,每个 ``rcu_node`` 结构的 ``->gp_seq`` 字段在每个宽限期的开始和结束时更新。
当回调列表变得过长时,``->qlen_last_fqs_check`` 和``->n_force_qs_snap`` 协调来自``call_rcu()`` 和其朋友函数的静态强制。
``->n_cbs_invoked``、``->n_cbs_orphaned`` 和 ``->n_cbs_adopted`` 字段计算调用的回调数,当此 CPU 离线时发送到其他 CPU,并在其他 CPU 离线时从其他 CPU 接收。``->n_nocbs_invoked``
在 CPU 的回调被卸载到 kthread 时使用。
最后,``->blimit`` 计数器是在给定时间可以调用的 RCU 回调的最大数量。
4.4. Dyntick-Idle 处理
''''''''''''''''''''
``rcu_data`` 结构的这一部分声明如下:
::
1 int dynticks_snap;
2 unsigned long dynticks_fqs;
``->dynticks_snap`` 字段用于在强制静止状态时拍摄相应 CPU 的 dyntick-idle 状态的快照,因此可以从其他 CPU 访问。 最后,``->dynticks_fqs`` 字段用于统计此CPU 被确定为dyntick-idle 状态的次数,
用于跟踪和调试。
rcu_data 结构的这一部分声明如下:
::
1 long dynticks_nesting;
2 long dynticks_nmi_nesting;
3 atomic_t dynticks;
4 bool rcu_need_heavy_qs;
5 bool rcu_urgent_qs;
rcu_data 结构中的这些字段维护相应 CPU 的 per-CPU dyntick-idle 状态。 除非另有说明,否则只能从相应的 CPU(和tracing)访问这些字段。
``->dynticks_nesting`` 字段计算进程执行的嵌套深度,因此在正常情况下该计数器的值为零或一。 NMI、irq 和跟踪器由“->dynticks_nmi_nesting”字段计算。 由于无法屏蔽 NMI,因此必须使用 Andy Lutomirski
提供的算法仔细更改此变量。 idle 的初始转换加 1,嵌套转换加 2,因此嵌套级别 5 由 ``->dynticks_nmi_nesting`` 值 9 表示。 因此,这个计数器可以被认为是计算除了进程级转换之外,这个 CPU 不能进入
dyntick-idle 模式的原因的数量。
然而,事实证明,当在非空闲内核上下文中运行时,Linux 内核完全能够进入永不退出的中断处理程序,反之亦然。 因此,每当 ``->dynticks_nesting`` 字段从零递增时,``->dynticks_nmi_nesting`` 字段被设置
为一个大的正数,每当 ``->dynticks_nesting`` 字段减少到 零,``->dynticks_nmi_nesting`` 字段设置为零。 假设错误嵌套中断的数量不足以使计数器溢出,每次相应的 CPU 从进程上下文进入空闲循环时,这种
方法都会纠正 ``->dynticks_nmi_nesting`` 字段。
``->dynticks`` 字段计算相应的 CPU 进出 dyntick-idle 模式或用户模式的转换次数,因此当 CPU 处于 dyntick-idle 模式或用户模式时,该计数器的值为偶数,否则为奇数 . 用户模式自适应滴答支持需要计算
进/出用户模式的转换(参见 timers/NO_HZ.txt)。
``->rcu_need_heavy_qs`` 字段用于记录 RCU 核心代码真的很想从相应的 CPU 看到一个静止状态,以至于它愿意调用重量级的 dyntick-counter 操作 . 此标志由 RCU 的上下文切换和 ``cond_resched()`` 代码检查,
它们提供暂时的空闲逗留作为响应。
最后,``->rcu_urgent_qs`` 字段用于记录 RCU 核心代码真的希望从相应的 CPU 看到静止状态这一事实,其他各种字段表明 RCU 多么想要这种静止状态。 此标志由 RCU 的上下文切换路径
(``rcu_note_context_switch``)和 cond_resched 代码检查。
| **小测验**: |
+------------------------------------------------ ----------------------+
| 为什么不简单地将 ``->dynticks_nesting`` 和 ``->dynticks_nmi_nesting`` 计数器组合成一个计数器,只计算相应 CPU 非空闲的原因数量? |
+------------------------------------------------ ----------------------+
| **回答**: |
+------------------------------------------------ ----------------------+
| 因为在存在其处理程序永远不会返回的中断以及设法从虚构中断返回的处理程序的情况下,这将失败。
一些特殊用途的构建存在其他字段,并将单独讨论。
五、``rcu_head`` 结构
~~~~~~~~~~~~~~~~~~~~~~~~~~
每个 ``rcu_head`` 结构代表一个 RCU 回调。 这些结构通常嵌入在受 RCU 保护的数据结构中,其算法使用异步宽限期。 相反,当使用阻塞等待 RCU 宽限期的算法时,RCU 用户不需要提供“rcu_head”结构。
``rcu_head`` 结构具有如下字段:
::
1 struct rcu_head *next;
2 void (*func)(struct rcu_head *head);
``->next`` 字段用于将 ``rcu_data`` 结构中的列表中的 ``rcu_head`` 结构链接在一起。 ``->func`` 字段是一个指向函数的指针,当回调准备好被调用时,这个函数被传递一个指向 ``rcu_head`` 结构的
指针。 但是,``kfree_rcu()`` 使用``->func`` 字段来记录``rcu_head`` 结构在封闭的受 RCU 保护的数据结构中的偏移量。
这两个字段都由 RCU 在内部使用。 从 RCU 用户的角度来看,这个结构是一个不透明的“cookie”。
| **小测验**: |
+------------------------------------------------ ----------------------+
| 鉴于回调函数``->func`` 被传递了一个指向``rcu_head`` 结构的指针,该函数应该如何找到封闭的受 RCU 保护的数据结构的开头?
+------------------------------------------------ ----------------------+
| **回答**: |
+------------------------------------------------ ----------------------+
| 实际上,每种类型的受 RCU 保护的数据结构都有一个单独的回调函数。 因此,回调函数可以使用 Linux 内核中的“container_of()”宏(或其他软件环境中的其他指针操作工具)来查找封闭结构的开头。
``task_struct`` 结构中的 RCU 特定字段
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~
``CONFIG_PREEMPT_RCU`` 实现在 ``task_struct`` 结构中使用了一些额外的字段:
::
1 #ifdef CONFIG_PREEMPT_RCU
2 int rcu_read_lock_nesting;
3 union rcu_special rcu_read_unlock_special;
4 struct list_head rcu_node_entry;
5 struct rcu_node *rcu_blocked_node;
6 #endif /* #ifdef CONFIG_PREEMPT_RCU */
7 #ifdef CONFIG_TASKS_RCU
8 unsigned long rcu_tasks_nvcsw;
9 bool rcu_tasks_holdout;
10 struct list_head rcu_tasks_holdout_list;
11 int rcu_tasks_idle_cpu;
12 #endif /* #ifdef CONFIG_TASKS_RCU */
``->rcu_read_lock_nesting`` 字段记录了 RCU 读端临界区的嵌套级别,
``->rcu_read_unlock_special`` 字段是一个位掩码,记录了需要 ``rcu_read_unlock()`` 做额外操作的特殊条件。
``->rcu_node_entry`` 字段用于形成在可抢占 RCU 读端临界区内阻塞的任务列表,
``->rcu_blocked_node`` 字段引用 ``rcu_node`` 结构,该任务为其列表的成员,或者如果它没有被阻塞在可抢占的 RCU 读端临界区内,则为 ``NULL``。
``->rcu_tasks_nvcsw`` 字段跟踪该任务在当前任务-RCU 宽限期开始时经历的自愿上下文切换次数,
``->rcu_tasks_holdout`` 如果当前 task-RCU 宽限期正在等待此任务则设置,
``->rcu_tasks_holdout_list`` 是将此任务排入 holdout 列表的列表元素,
``->rcu_tasks_idle_cpu`` 跟踪此空闲任务正在运行的 CPU,但前提是该任务当前正在运行 ,也就是说,CPU 当前是否处于空闲状态。
六、 访问函数
~~~~~~~~~~~~~~~~~~
以下清单显示了``rcu_get_root()``、``rcu_for_each_node_breadth_first`` 和``rcu_for_each_leaf_node()`` 函数和宏:
::
1 static struct rcu_node *rcu_get_root(struct rcu_state *rsp)
2 {
3 return &rsp->node[0];
4 }
5
6 #define rcu_for_each_node_breadth_first(rsp, rnp) \
7 for ((rnp) = &(rsp)->node[0]; \
8 (rnp) < &(rsp)->node[NUM_RCU_NODES]; (rnp)++)
9
10 #define rcu_for_each_leaf_node(rsp, rnp) \
11 for ((rnp) = (rsp)->level[NUM_RCU_LVLS - 1]; \
12 (rnp) < &(rsp)->node[NUM_RCU_NODES]; (rnp)++)
``rcu_get_root()`` 只是返回指向指定``rcu_state`` 结构的``->node[]`` 数组的第一个元素的指针,这是根``rcu_node`` 结构。
如前所述,``rcu_for_each_node_breadth_first()`` 宏利用``rcu_state`` 结构的``->node[]`` 数组中的``rcu_node`` 结构的布局,执行广度优先 遍历只需按顺序遍历数组即可。 类似地,
``rcu_for_each_leaf_node()`` 宏只遍历数组的最后一部分,因此只遍历叶``rcu_node`` 结构。
| **小测验**: |
+------------------------------------------------ ----------------------+
| 如果 ``rcu_node`` 树只包含一个节点,``rcu_for_each_leaf_node()`` 会做什么? |
+------------------------------------------------ ----------------------+
| **回答**: |
+------------------------------------------------ ----------------------+
| 在单节点的情况下,``rcu_for_each_leaf_node()`` 遍历单个节点。
概括
~~~~~~~
因此 RCU 的状态由 ``rcu_state`` 结构表示,其中包含 ``rcu_node`` 和 ``rcu_data`` 结构的组合树。 最后,在 ``CONFIG_NO_HZ_IDLE`` 内核中,每个 CPU 的 dyntick-idle 状态由 ``rcu_data``
结构中与 dynticks 相关的字段跟踪。 如果您做到了这一点,那么您已经做好了阅读本系列其他文章中的代码演练的准备。
致谢
~~~~~~~~~~~~~~~
我要感谢 Cyrill Gorcunov、Mathieu Desnoyers、Dhaval Giani、Paul Turner、Abhishek Srivastava、Matt Kowalczyk 和 Serge Hallyn 帮助我使这份文件更易于阅读。
法律声明
~~~~~~~~~~~~~~~
本作品仅代表作者观点,不代表 IBM 观点。
Linux 是 Linus Torvalds 的注册商标。
其他公司、产品和服务名称可能是其他公司的商标或服务标记。
posted on 2022-12-22 20:59 Hello-World3 阅读(446) 评论(0) 编辑 收藏 举报