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() 并继续暂停。
EINVAL
tv_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_head
。base
指针指向高精度定时器的时钟源struct hrtimer_clock_base
,hrtimer_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
结构体,然后获取当前进程的松弛时间,如果当前进程是deadline
或realtime
进程,那么不允许有松弛时间。将clockid
和mode
交给hrtimer_init_on_stack()
,对刚刚声明的struct hrtimer_sleeper
结构体进行选择时钟源。将slack
松弛时间输入hrtimer_set_expires_range_ns()
,设置当前定时的软过期时间和硬过期时间,同样,如果当前进程是deadline
或realtime
进程,那么软过期时间和硬过期时间一致。接下来是指向真正的延时动作。如果返回结果是非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()
,这个也很清晰:就是设置hrtimer
的function
成员变量和sleeper
的task
成员变量。
唤醒回调函数
然后是定时任务完成回调函数如下:
接下来就是hrtimer_start_expires()
要准备启动高精度定时器了,这里再重新计算了一下软过期,硬过期和松弛时间,然后传递给hrtimer_start_range_ns()
函数处理。
hrtimer_start_range_ns()
先获取hrtimer_clock_base
的raw_spinlock_t
,然后移除
移除定时器
接下来看一下移除定时器的过程。remove_hrtimer()
先检查一下当前定时器是否已经在队列中,如果不在队列中,那就不需要执行删除的动作了。如果当前定时器是是属于本地CPU的话,可能需要对下一次的定时时间进行调整,这里就只设置reprogram
为1
,然后交给__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_base
的clock_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
的相关东西,这里暂时没有了解很多,暂时不看了,收尾结束~~~