hrtimer代码阅读

hrtimer代码阅读

nanosleep系统调用

这里的解释参见man手册。

名字

nanosleep – 高分辨率的休眠

概要

#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);

glibc 的功能测试宏要求(请参阅 feature_test_macros(7)):

 nanosleep():_POSIX_C_SOURCE >= 199309L

描述

nanosleep() 暂停调用线程的执行,直到至少在 *req 中指定的时间已经过去,或者传递触发调用线程中处理程序调用或终止进程的信号。

如果调用被信号处理程序中断,则 nanosleep() 返回 -1,将 errno 设置为 EINTR,并将剩余时间写入 rem 指向的结构中,除非 rem 为 NULL。 然后可以使用 *rem 的值再次调用 nanosleep() 并完成指定的暂停(但请参阅 NOTES)。

结构 timespec 用于以纳秒精度指定时间间隔。 它的定义如下:

struct timespec {
    time_t tv_sec;        /* seconds */
    long   tv_nsec;       /* nanoseconds */
};

纳秒字段的值必须在 0 到 999999999 的范围内。

与 sleep(3) 和 usleep(3) 相比,nanosleep() 具有以下优点: 为指定睡眠间隔提供了更高的分辨率; POSIX.1 明确规定它不与信号交互; 它使恢复被信号处理程序中断的睡眠的任务更容易。

返回值

在请求的时间间隔内成功休眠后,nanosleep() 返回 0。如果调用被信号处理程序中断或遇到错误,则返回 -1,并设置 errno 以指示错误。

错误码

EFAULT从用户空间复制信息的问题。

EINTR暂停已被传递给线程的信号中断。 剩余的睡眠时间已写入 *rem 以便线程可以轻松地再次调用 nanosleep() 并继续暂停。

EINVALtv_nsec 字段中的值不在 0 到 999999999 的范围内或 tv_sec 为负。

注意事项

如果 req 中指定的间隔不是基础时钟粒度的精确倍数(请参阅 time(7)),则该间隔将向上取整为下一个倍数。 此外,在睡眠完成后,在 CPU 再次空闲以再次执行调用线程之前可能仍然存在延迟。

如果调用在被信号中断后反复重新启动, nanosleep() 睡眠相对间隔的事实可能会出现问题,因为调用中断和重新启动之间的时间会导致睡眠最终完成的时间漂移。 这个问题可以通过使用带有绝对时间值的clock_nanosleep(2) 来避免。

POSIX.1 指定 nanosleep() 应该根据 CLOCK_REALTIME 时钟测量时间。 但是,Linux 使用 CLOCK_MONOTONIC 时钟来测量时间。 这可能无关紧要,因为 clock_settime(2) 的 POSIX.1 规范说 CLOCK_REALTIME 中的不连续更改不应影响 nanosleep():

通过clock_settime(2) 设置CLOCK_REALTIME 时钟的值对被阻塞等待基于该时钟的相对时间服务的线程没有影响,包括nanosleep() 函数; … 因此,当请求的相对间隔过去时,这些时间服务将到期,与时钟的新值或旧值无关。

旧行为
为了支持需要更精确暂停的应用程序(例如,为了控制一些对时间要求严格的硬件),nanosleep() 将通过忙等待以微秒精度从实际调度的线程调用时处理长达 2 毫秒的暂停。 - 时间策略,如 SCHED_FIFO 或 SCHED_RR。 这个特殊的扩展在内核 2.5.39 中被删除,因此在当前的 2.4 内核中仍然存在,但不在 2.6 内核中。

这个也翻译完了,就看看内核是怎么实现这个系统调用的吧。

nanosleep内核的实现

nanosleep()的内核实现如下所示,拷贝到内核空间后先对输入的时间检查一下是否合法,然后调用hrtimer_nanosleep()函数,高精度定时器的模式是HRTIMER_MODE_REL相对时间,选用CLOCK_MONOTONIC单调递增的时钟源。

在这里插入图片描述

接下来,就是走进hrtimer高精度定时器的世界。

hrtimer高精度定时器

hrtimer相关数据结构

在这里,可以先看一张图,了解一下高精度定时器的数据结构:

在这里插入图片描述

左上角的hrtimer_sleeper结构体一般会在栈上声明,然后里面包含一个struct hrtimer结构体和一个struct task_struct结构体指针task,这个task一般是指向调用hrtimer_nanosleep()的进程。

struct hrtimer结构体里面数据这里就关注三个成员,一个是node结构体,是一个红黑树节点结构体struct timerqueue_node,用于加入到红黑树的结构体中,这里是struct timerqueue_headbase指针指向高精度定时器的时钟源struct hrtimer_clock_basehrtimer_nanosleep()的第四个参数就是用来选择不同的时钟源的,这里选择的是CLOCK_MONOTONIC类型的。function()函数指针指向hrtimer_wakeup()函数,用来唤醒进程。

struct hrtimer_clock_base结构体的active成员则是当前定时器的红黑树的根,里面包含当前定时器源下的所有要定时的红黑树数据。

至于struct hrtimer_clock_base结构体与struct hrtimer_cpu_base的关系,也可以看一下下面这个图:hrtimer_bases是一个每CPU变量,存放着struct hrtimer_cpu_base结构体,里面有四种类型的时钟源struct hrtimer_clock_base,每一种struct hrtimer_clock_base都指向着不同的get_time()函数。

在这里插入图片描述

函数流程阅读

在看完相关数据结构后,可以开始看一下代码的流程了。

hrtimer_nanosleep()函数大概可以划分为以下几个部分:

  • 初始化定时器(选择定时器源、设置定时时间)
  • 执行延时动作
  • 延时过程被中断后为重新重新发起系统调用做准备

hrtimer_nanosleep()入口处在栈上声明了一个struct hrtimer_sleeper结构体,然后获取当前进程的松弛时间,如果当前进程是deadlinerealtime进程,那么不允许有松弛时间。将clockidmode交给hrtimer_init_on_stack(),对刚刚声明的struct hrtimer_sleeper结构体进行选择时钟源。将slack松弛时间输入hrtimer_set_expires_range_ns(),设置当前定时的软过期时间和硬过期时间,同样,如果当前进程是deadlinerealtime进程,那么软过期时间和硬过期时间一致。接下来是指向真正的延时动作。如果返回结果是非0的,表明延时结束,直接退出到out;否则,当前延时过程可能被信号打断了,延时没有完成,需要把剩余的时间更新到rmtp给用户,更新到restart_block方便重新发起系统调用。

在这里插入图片描述

初始化定时器源

hrtimer_init_on_stack()函数直接调用__hrtimer_init(),无需多言。

在这里插入图片描述

__hrtimer_init()函数对hrtimer结构体里面的数据清零,获取本地CPU的hrtimer_cpu_base结构体,再根据clock_id,获取本地CPUhrtimer_cpu_base结构体的时钟源ID,然后保存到hrtimer结构体里,并初始化hrtimer结构体的红黑树节点。

在这里插入图片描述

及诶下了就是设置定时器的定时到期时间,软到期时间设置在_softexpires成员变量,硬过期时间设置在红黑树节点的expires里面。

在这里插入图片描述

执行延时操作

执行延时操作在do_nanosleep()函数里面,

在这里插入图片描述

设置回调函数

初始化hrtimer完成回调和唤醒任务的是hrtimer_init_sleeper(),这个也很清晰:就是设置hrtimerfunction成员变量和sleepertask成员变量。

在这里插入图片描述

唤醒回调函数

然后是定时任务完成回调函数如下:

在这里插入图片描述

接下来就是hrtimer_start_expires()要准备启动高精度定时器了,这里再重新计算了一下软过期,硬过期和松弛时间,然后传递给hrtimer_start_range_ns()函数处理。

在这里插入图片描述

hrtimer_start_range_ns()先获取hrtimer_clock_baseraw_spinlock_t,然后移除

在这里插入图片描述

移除定时器

接下来看一下移除定时器的过程。remove_hrtimer()先检查一下当前定时器是否已经在队列中,如果不在队列中,那就不需要执行删除的动作了。如果当前定时器是是属于本地CPU的话,可能需要对下一次的定时时间进行调整,这里就只设置reprogram1,然后交给__remove_hrtimer()执行下一步操作。

在这里插入图片描述

为什么要这么做,在代码中也有解释:

当高分辨率模式处于活动状态且定时器在当前 CPU 上时,移除定时器并强制重新编程。 如果我们移除另一个 CPU 上的计时器,则跳过重新编程。 该 CPU 上的中断事件被触发,并在中断处理程序中进行重新编程。 这是一种罕见的情况,比 smp call消耗更小。

__remove_hrtimer()开始再次判断一下定时器是否在队列中,如果在的话,将定时器从红黑树中删除,如果红黑树中已经为空,那么将此CPU这个类型的定时器源标记不活动状态。然后执行hrtimer_force_reprogram()重新编写下一次定时事件。

在这里插入图片描述

这个函数的开头注释道:

当定时器是下一个到期时,高分辨率定时器模式重新编程时钟事件设备。 调用者可以通过将重新编程设置为零来禁用此功能。 这很有用,当上下文无论如何都进行重新编程时(例如定时器中断)

函数里还提到:

注意:如果 reprogram 为 false,我们不会更新 cpu_base->next_timer。 当我们删除远程 CPU 上的第一个计时器时会发生这种情况。 没有坏处,因为我们从不取消引用 cpu_base->next_timer。 因此,如果同一个计时器再次入队,可能会发生的最糟糕的事情是稍后在远程 cpu 上对 hrtimer_force_reprogram() 进行多余的调用。

为什么不需要将定时器重新入队,可以看hrtimer_start_range_ns()函数中,还有一个enqueue_hrtimer()的过程。

重新编程定时事件

hrtimer_force_reprogram()重新编写定时器事件的开始出有比较多的时间对比过程,就直接看图的注释吧。__hrtimer_get_next_event()获取当前CPU上hrtimer_clock_base的最快到期的时间,然后交由tick_program_event()去编写下一个定时器事件的触发。

在这里插入图片描述

关于hang_detected的注释,这里暂不过多关注:

如果在最后一个定时器中断中检测到挂起,那么我们将在硬件中保持挂起延迟处于活动状态。 我们希望系统取得进展。 这也可以防止出现以下情况:

  • T1 从现在起 50 毫秒到期
  • T2 从现在起 5 秒到期

T1 被删除,因此调用此代码并将从现在开始将硬件重新编程为 5 秒。 由于设置了hang_detected,此后的任何hrtimer_start 都不会重新编程硬件。 所以我们会有效地阻止所有计时器,直到 T2 事件触发。

__hrtimer_get_next_event()获取当前CPU上hrtimer_clock_base的最快到期的时间就是遍历hrtimer_cpu_baseclock_base,然后获取红黑树的最快到期的过程,中间伴随着更新hrtimer_cpu_base的下一个要定时的定时器指针。

在这里插入图片描述

至于tick_program_event()函数以及之后的内容,这里就只列一下大概的流程图好了,更多详细的内容可能要在其他的章节补上。

tick_program_event()clockevents_program_event()
    	↘ dev->set_next_event()arch_timer_set_next_event_phys()set_next_event()
                	↘ 写 ARMv8 的通用定时器寄存器
重新入队,编写下一次定时事件

重新入队的过程也比较简单,就是加入红黑树即可。

在这里插入图片描述

而编写下一次定时事件,实际也要根据条件判断是否满足需求:在判断了各种要求后,再调用tick_program_event()函数,流程上与重新编程定时器事件一样。

在这里插入图片描述

在这里插入图片描述

调度回来后取消定时器

再回过头看do_nanosleep()的部分,在执行freezable_schedule()调度出去;等待再次调度回来时,调用hrtimer_cancel()函数来取消此定时器。

hrtimer_cancel()这里先调用hrtimer_try_to_cancel()尝试取消定时器,如果返回值大于等于0,可以执行返回,否则执行一下内存屏障的动作。

在这里插入图片描述

hrtimer_try_to_cancel()函数默认返回值是-1,表明当前定时器正在运行,如果正在运行,那么调用此函数的hrtimer_cancel()将会一直for循环;否则执行remove_hrtimer()函数尝试将此定时器移除。具体的可以看上面的移除定时器章节。

在这里插入图片描述

此时的do_nanosleep()在没有信号打断的情况下,会不断地执行循环,尝试重新编程下一个定时事件->调度出去->调度回来后尝试移除定时事件->尝试重新编程下一个定时事件。陷入无限循环中。。。

中断唤醒

这个时候,久违的中断唤醒,就来了。如果没有人来唤醒这个循环,那么就是无限月读了。

高精度定时器的中断处理函数是hrtimer_interrupt(),这个是在哪里设置的呢?又是谁来掉用这个函数的呢?那就看下面的流程了。

在设备启动过程中,会有这么一个步骤:这里就是设置了hrtimer_interrupt作为clock_event_device时钟事件设备的事件处理函数。

tick_init_highres()tick_switch_to_oneshot(hrtimer_interrupt);

arm_arch_timer代码阅读时,可以看到正常的一个中断处理流程:

arch_timer_handler_phys()timer_handler()
    	↘ evt->event_handler()hrtimer_interrupt()

这个时候就是hrtimer_interrupt()出场的时候了。

hrtimer_interrupt()持有锁后,先表明此时正值高精度定时器的中断中,然后执行__hrtimer_run_queues(),找下一个几点到期的定时时间,将in_hrtirq置零表明即将退出高精度定时中断,然后解锁。重新编程下一次定时器事件,如果正常编程了下一次定时器事件,退出;否则,执行后面的异常检测环境。

在这里插入图片描述

重复检测三次,看是否是超时超时,重新给此定时器设置定时事件,报出警告信息。
在这里插入图片描述

异常的原因解释:

由于以下原因,下一个计时器已过期:

  • 追踪
  • 持久的回调
  • 在 VM 中运行时被安排离开

我们需要防止我们在 hrtimer 中断例程中永远循环。 我们给它 3 次尝试以避免对某些虚假事件反应过度。
获取基锁以更新偏移量和检索当前时间。

__hrtimer_run_queues()遍历在活动的时钟源,将到期的定时器执行__run_hrtimer(),去执行对应的回调函数。

在这里插入图片描述

__run_hrtimer()用底层的内存屏障计数,将整个函数划分三个阶段。

在这里插入图片描述

__run_hrtimer() 中的 write_seqcount_barrier()s 将事物分成 3 个不同的部分:

  • 排队:计时器正在排队
  • 回调:正在运行计时器
  • 发布:计时器处于非活动状态或(重新)排队

在读取方面,我们确保从同一部分观察 timer->state 和 cpu_base->running,如果我们查看时有任何更改,我们会重试。
这包括 timer->base 更改,因为仅靠序列号是不够的。

序列号是必需的,否则如果读取端被多次连续的 __run_hrtimer() 调用弄脏,我们仍然可以观察到假阴性。

这里上锁,执行回调函数hrtimer_wakeup(),如果需要再次执行,将此定时器加入到红黑树中,不需要重新编写下一个定时器事件。

在这里插入图片描述

至此,延时操作告一段落了。

被中断后的收尾动作

restart_block的相关东西,这里暂时没有了解很多,暂时不看了,收尾结束~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值