From 3d4d9664e816e5a4011607e70ffe9f5e4f0bf761 Mon Sep 17 00:00:00 2001 From: "kyrie.xi" <11186799+jingwei-xi@user.noreply.gitee.com> Date: Sun, 3 Jul 2022 12:02:49 +0000 Subject: [PATCH] =?UTF-8?q?add=20articles/20220703-riscv-linux-swtimer.md.?= =?UTF-8?q?=20=E4=BF=AE=E6=94=B9=E7=82=B9=E5=A6=82=E4=B8=8B=EF=BC=9A=201.?= =?UTF-8?q?=20=E6=A2=B3=E7=90=86=E6=96=87=E7=AB=A0=E5=B1=82=E6=AC=A1?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=9B=202.=20=E6=A0=B9=E6=8D=AE=E5=90=B4?= =?UTF-8?q?=E8=80=81=E5=B8=88=E7=9A=84review=20comment=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- articles/20220703-riscv-linux-swtimer.md | 127 +++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 articles/20220703-riscv-linux-swtimer.md diff --git a/articles/20220703-riscv-linux-swtimer.md b/articles/20220703-riscv-linux-swtimer.md new file mode 100644 index 0000000..689cb1f --- /dev/null +++ b/articles/20220703-riscv-linux-swtimer.md @@ -0,0 +1,127 @@ +Author: kyrie.xi <709150653@qq.com>
+Date: 2022/07/03
+Revisor: lzufalcon
+Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-Linux) + +# RISC-V Linux SwTimer 分析 + +## 一.背景简介 + +在软件开发中经常会使用到与时间相关的概念,例如获取当前的时间对某一事件进行打戳;或设置某个定时任务,在时间到时执行该函数;而且内核也会通过定时的相关记录来驱动进程切换。 +总结下来,我们需要的与时间相关的功能主要有两大类: +1.获取时间; +2.定时任务; + +想获取以上两大功能,需要软件与硬件相结合;硬件部分包括时钟和定时器电路;时钟电路用于获取当前的时间(如 RTC,属于单独供电,即使 CPU 断电也会持续记录该时间);而不同的定时器电路用于支持在不同场景下的时间精度要求,大体有如下几种: + +* 时间戳计数器 TSC ( Time Stamp Counter ); +* 可编程间隔定时器 PIT ( Programmable Interval Timer ); +* 高精度事件定时器 HPET ( High Precision Event Timer ); +* 电源管理定时器 ACPI ( Advanced Configuration and Power Management Interface ); + +## 二.定时器与软件定时器分类 + +定时器主要分为如下两大类: +1. 硬件定时器:由芯片本身提供的定时器电路,一般基于固定频率振荡器和计数器的硬件电路组成,可以按照固定的,预先定义的频率向 CPU 发出中断;优点是精度高,但是受限于硬件限制,硬件定时器个数较少; +2. 软件定时器:基于硬件定时器设计的软件定时功能,理论上数量不受限制,但是精度较低,必须是硬件定时器的整数倍; + +按照定时器触发方式分为: +1. 单次触发方式 ( oneshot ):该类型定时器创建后只会触发一次定时器事件,执行一次超时函数后会自动销毁,想要重新定时,需要再设置一次寄存器值; +2. 周期触发方式 ( periodic ):创建后会按照设定的超时时间周期执行,每次超时后会自动加载超时时间到寄存器,除非我们主动停掉; + +按照超时处理函数的执行环境可以分为: +1. 中断上下文:要求处理函数简短,不要使用可以导致处理函数休眠的操作,实时性较高,响应迅速; +2. 进程上下文:需要重新创建一个任务来执行,可以进行休眠或其他操作,但实时性会较低; + +## 三.软件定时器的设计 + +首先介绍 sw timer 结构体: +主要有超时处理函数及其参数,超时时间(相对时间,即从现在起还有多久会超时的 timeout 时间); +![输入图片说明](../%E5%9B%BE%E7%89%871.jpg) + +### 1.初始化 + +初始化 timer_list 结构体数组,将定时器的超时处理函数和其参数初始为 NULL;因为在上电复位时,CPU 硬件会设置 mtime(64 位的 real-time 计数器,系统保证其会按照固定的频率递增)为 0 ,但不会设置 mtimecmp( time compare register,每个 hart 会有一个该寄存器,上电复位时,系统不负责将其初始化)的初值,因此需要调用 ~timer_load()~ 函数加载 TIMER_INTERVAL 到 CLINT_MTIMECMP 寄存器内;接下来需要打开全局中断并将 MIE_MTIE 寄存器置 1; +![输入图片说明](../%E5%9B%BE%E7%89%872.jpg) + +### 2.创建定时器任务 + +创建一个软件定时器,传入超时处理函数地址及其参数,并设置超时时间 timeout;将新增的定时器加入到 timer_list 数组为空的位置(从数组元素为 0 的地址开始逐次寻找),将传入的参数加入到该位置处的 timer 结构体中; +![输入图片说明](../%E5%9B%BE%E7%89%873.jpg) + +### 3.删除定时器任务 + +传入待删除的 timer,在 timer_list 中按顺序查找待删除的 timer,将其超时处理函数及其参数设置为 NULL; +![输入图片说明](../%E5%9B%BE%E7%89%874.jpg) + +### 4.中断处理程序 + +每次当系统的 tick 中断发生时,会调用 ~timer_handler~ 函数; +![输入图片说明](../%E5%9B%BE%E7%89%875.jpg) + +~timer_handler()~ 函数会先调用 ~timer_check()~ 函数,该函数会按顺序检查 timer 的超时时间是否到达(因为 timer_list 是按照超时时间进行排序的,如果不是这种排序的话,那么 check 函数会有不同机制),如果已到则执行该 timer 的超时处理函数,执行结束后会清除该 timer 的超时处理函数及其参数; +![输入图片说明](../%E5%9B%BE%E7%89%876.jpg) + +然后会重新调用 ~timer_load()~ 函数加载 TIMER_INTERVAL 到 CLINT_MTIMECMP 寄存器内;然后会触发一次调度 ~schedule()~。 + +## 四.软件定时器的优化 + +当创建一个新的定时器任务时,需要将其加入到 timer_list 进行管理;上文有介绍的一种方式是通过链表的方式进行管理,当需要对定时器进行插入或是执行其超时函数时,其时间复杂度是 O(n); +基于排序链表的定时器任务使用一个链表来管理所有的定时器,所以当链表数目越来越多时,效率就会越来越低;这种算法对大量的定时器任务进行管理时,其效率会非常低;当前典型的定时器管理算法有:时间轮,最小堆,红黑树,跳转表等,本文主要介绍 Linux 内核采用的时间轮算法( timer wheel )。 + +### 1.时间轮的概念 + +时间轮是一种基于哈希表的概念来管理定时器任务,将定时器存到不同的链表中,每个链表中定时器任务是无序的;类比于钟表的秒针,分针和时针的概念;单级时间轮分为 N 个槽(类似于秒针有 60 个槽),每个槽上对应一个定时器链表,该链表上可以挂载多个定时器任务,该链表上的所有定时器 timeout 时间相同,因此链表里的定时器无须排序。时间轮经过系统时间 Ti(可以理解为时间轮的粒度)会移动一个槽,这个粒度值取决于时间轮的具体实现,可以是系统的一个时钟时间。 +![输入图片说明](../%E5%9B%BE%E7%89%877.png) + +假设时间轮开始移动的时间为 Ts,如果想创建一个超时时间为 Tto 的定时器任务,该定时器会放到那个槽上呢?可以按照如下公式计算: +$$ (Tto - Ts) / Ti {\%} N $$ +那这样会有一个问题,假设时间轮分为 6 个槽,每个槽所表示的时间为 1 ms,那么超时时间为 8 ms 和 2 ms 的任务会放在一个定时器链表中, 8 ms 的超时任务在时间为 2 ms 时就已经被执行了,这样肯定是不行的; +解决办法很简单,加大时间轮槽数 N,但是这样会急剧增加内存的消耗,更好的方法是采用多级时间轮的方法; +第一级时间轮转动 N 个系统时间后,会自动跳到第二级时间轮上(类似于秒针转动一圈 60 秒之后,分针会转动一格); 第二级的时间轮每个槽上也对应有一个定时器链表,这样就可以表示更长的超时定时任务; + +假设第二级时间轮有 M 个槽,那么两级时间轮可以表示的最长超时时间为:$ (N * M) * Ti $,假设这个两级时间轮的槽数均可以用二进制表示,即 $N = 2^n$ ,$M = 2^m$ ,那么这个可以记录的最长超时时间可以表示为 $2^(n+m) * Ti$ ; + +### 2. Linux 时间轮表示 +Linux 采用 5 级时间轮,并且使用 32 bit 来表示,每级时间轮所能表示的超时时间长度分别占 32 bit 的低 8 位(256),次 6 位(64),次 6 位(64),次 6 位(64),高 6 位(64),如下表一所示: +表一: +tv5 | tv4 | tv3 | tv2 | tv1 +-------|-----------|----------|-----------|----------- +6 bit | 6 bit | 6 bit | 6 bit | 8 bit + +同时,每级时间轮的粒度 Ti 均不同,分别为 1 ms,256 ms,256 * 64 ms,256 * 64 * 64 ms,256 * 64 * 64 * 64 ms; + +每级时间轮所能表示的超时时间如下表二,单位为 1 ms: + +表二: +时间轮 | 可表示超时时间长度 +tv1 | 0~255(2^8 - 1) +tv2 | 256~16383(2^14 - 1) +tv3 | 16384~1048575(2^20 - 1) +tv4 | 1048576~ 67108863(2^26 - 1) +tv5 | 67108864~4294967295(2^32 - 1) + +### 3. 如何插入一个定时器任务? + +定时器超时时间 Tto 和当前时间 Tc 做差值,记为 Tdelta; +根据 Tdelta 的值将定时器放到确定的时间轮 tvn 上(参照表二),再根据定时器的超时时间相对值 Tdelta 与对应的 tvn 所占的 bit 位置(对应表一)做按位与计算得到值作为数组下标,将定时器挂入到该链表上; + +举例子来加深下理解: +假设当前时间为 Tc = 1, Tto = 3, Tdelta = Tto - Tc = 3 - 1 = 2 < 256, 因此会将该定时器放入 tv1 中,而 Tdelta & 0xFF ( 低 8 位 ) = 2 ( 取低 8 位对应的值:2 ),因此将该定时器加入到 tv1 中数组下标为 2 的链表中; + +假设当前时间为 Tc = 1,Tto = 262,Tdelta = Tto - Tc = 262 - 1 = 261 在 { 256, 16383 } 区间,因此会将该定时器放入 tv2 中,而 Tdelta & 0x3F00 ( 次 6 位 ) = 0x100 (取 8 ~ 13 位对应的值:1),因此将该定时器加入到 tv2 中数组下标为 1 的链表中; + +假设当前时间为 Tc = 1,Tto = 514,Tdelta = Tto - Tc = 514 - 1 = 513 在 { 256,16383 } 区间,因此会将该定时器放入 tv2 中,而 Tdelta & 0x3F00 ( 次 6 位 ) = 0x200 ( 取8 ~ 13位对应的值:2 ),因此将该定时器加入到 tv2 中数组下标为 2 的链表中; + +### 4. 如何检查定时器任务到期? + +假设当前时间 Tc = 0x87654321, 因此下一个检查时刻为 0x87654322,如果 tv1.array[0x22] 链表非空,那么在下一时刻需要执行 tv1.array[0x22] 上的所有定时器任务的超时函数;如果 Tc 增加到 0x87654300,低 8 位为空,这时需要移出 tv2 时间轮,并根据 tv2 上的定时器超时时间将其重新加入到定时器系统,完成了 tv2 时间轮向 tv1 时间轮的迁移,因为在下一个256 ms tv2 上的定时器一定会超时,这保证了 tv1 时间轮一直有数据;如果当 Tc 的第 8 ~ 13位为 0 时,表明 tv2 对 tv3 有进位,这时需要移出 tv3 轮对应的定时器链表并将其重新加入到定时器系统中,根据定时器超时时间将其放入 tv2 时间轮;按照上述步骤依次检查 tv4 和 tv5 时间轮。 + +小结 + +本文介绍了软件定时器的的相关概念及其使用方法,并对 Linux 采用的软件定时器时间轮管理算法进行了简单分析。下一篇文章,会对当前常见的定时器管理算法进行分析与比较。 + +参考文献 +* https://tinylab.org/lwn-646950/ +* https://github.com/riscv/riscv-platform-specs/blob/main/riscv-platform-spec.adoc/ +* https://gitee.com/tinylab/riscv-linux/tree/master/refs -- Gitee