Linux 时钟系统漫游

缘由:

在开发KVM虚拟机过程中,不可避免的会跟虚拟机时钟打交道

目前存在的问题是:一旦出现多个虚拟机运行在同一个CPU上,造成足够多的负载时,由于调度不及时导致虚拟机时钟中断的频率将会变慢

在x86上的虚拟机貌似没有这个问题,借此机会打算梳理一下整个Linux 时钟系统,也不一定能解决,但是至少可以理一下思路

时钟贯穿整个Linux 执行周期,千丝万缕,我们就从最直观的时钟中断切入,循序渐进,看看Linux 时钟的演化过程(本篇不涉及动态时钟,后续有机会可以继续分析)。

时钟中断:

1. 时钟中断到来时,系统首先就是要更新墙上时间:update_wall_time

与时钟中断和墙上时钟紧密相关的一个变量是jiffies。这个变量记录着系统启动后产生了多少次时钟中断,所以每个中断来后自增。

与时钟中断相关的重要配置是CONFIG_HZ,这个是编译时配置文件决定,在我这里CONFIG_HZ=256表示一秒钟jiffies增加256次。

但系统时钟并不用jiffies来更新,在update_wall_time函数内使用clock->read(clock)读取时钟源,并更新系统时钟xtime。这个时钟源往往是之前注册好的主板上恒定的硬件时钟

内核中很多变量都需要用jiffies计算,而不是人所需要的xtime,所以对内核来说jiffies比xtime更有用。

2. 在每个CPU上更新统计信息:account_process_tick

这部分工作在account_process_tick完成,可以看出这里会更新进程运行时间,以及统计当前tick下处于用户态还是核心态

3. 唤醒定时器和软中断:update_process_times

这里涉及高精度定时器的切换,后面会详细介绍

总的来说,如果内核发现有高精度的定时器则在这里进行切换为高精度定时器,并使用高精度定时器设置中断

如果没有则触发低精度定时器,这里调用__hrtimer_run_queues对到期的定时器进行处理,以及raise_softirq处理软中断

一个r4k时钟源的初始化过程

以上都是由tick_periodic触发,也是低精度定时器的内容。低精度定时器其实就是以HZ为单位的定时器,在系统初始化时均使用的是低精度定时器

然而低精度定时器的最初注册却可能是高精度时钟源,这是两个概念。

以r4k高精度时钟的初始化r4k_clockevent_init为例,其精度为cd->rating = 300 算高精度了,它注册了一个irqaction,并且注册了clockevents

irqaction:主要是清除中断,并调用r4k时钟源的event_handler,使得每个中断时都调用。但在r4k的init这里没有赋值。

clockevents:这是一个基于时钟源的时钟事件,使用clockevents_register_device最终调用tick_setup_device,这里将dev->event_handler 设置为tick_handle_periodic,

而tick_handle_periodic的主要任务就是clockevents_program_event,它是一个r4k时钟源的set_next_event过程的封装,而set_next_event是设置下个到期时间。

可以看到这些都是在低精度下运行,也就是说初始化的这些动作都必须在时钟中断到来时触发。

那么什么时候低精度会转化为高精度,高精度有什么特殊之处呢?

高精度定时器(hres)的起步

前面提到,时钟中断触发的update_process_times会切换高精度定时器,我们看看它是怎么切换的:

在update_process_times调用的run_local_timers内,调用hrtimer_run_queues,首先就判断是不是hres,如果是则什么也不做,不是则看能否切换,能切换则调用hrtimer_switch_to_hres切换

它做了三件事:tick_init_highres、tick_setup_sched_timer、retrigger_next_event

tick_init_highres:重置高精度定时器的触发函数hrtimer_interrupt,设置方式是用tick_switch_to_oneshot,将dev->event_handler = handler,这是刚刚触发中断时的irqaction需要调用的函数

这个其实就是说,之后每次时钟到期都会触发hrtimer_interrupt,hrtimer_interrupt我们在后面进行分析

tick_setup_sched_timer:低精度时钟转换为高精度时钟后,仍然需要保证每个HZ的时钟中断,称为周期时钟仿真。可以想象,所有低精度时钟触发的功能这里都要实现一次。

最简单粗暴的解决方案就是设置一个高精度定时器,每个时钟周期触发一次,事实上内核就是这么干的。使用了一个专门的结构tick_sched,其回调函数就是最开始的时钟中断所说的更新jiffies和统计进程功能

这就保证了高精度时钟和低精度时钟功能一致,只是这里最后更新一下高精度定时器,设置下一个定时周期,而不再触发定时器的软中断。

retrigger_next_event:高精度定时器刚刚切换,这里对其重新编程用以激活下一次的定时器。

hres的使用

至此便完成了高精度定时器的切换工作,用户在使用的时候两步即可完成:

hrtimer_init:初始化定时器,主要是模式和函数。结合KVM,将高精度定时器触发函数设为唤醒进程并注入中断,便可保证虚拟机切出后执行该函数

hrtimer_start:就是高精度定时器的定时功能了,hres的实现是红黑树,这里最终会调用timerqueue_add插入一个红黑树节点。

hres的触发:hrtimer_interrupt

前文提到高精度定时器将dev->event_handler切换成了hrtimer_interrupt,保证每个中断都会调用该函数,这是一个高精度时钟的关键函数

关中断执行

首先__hrtimer_run_queues,从红黑树取得定时器(timerqueue_getnext),取下操作是O(1)的。如果未到期则返回

然后调用__run_hrtimer,首先__remove_hrtimer取消定时器,然后执行定时器的用户函数restart = fn(timer);,再次循环得到下一个定时器

然后计算出下一个到期时间,使用tick_program_event对其编程。

这些都是容易想到的部分,然而后面还有一些超时处理,原文如下:

/*

The next timer was already expired due to:

tracing

long lasting callbacks

being scheduled away when running in a VM 

*/

这意思就是说我们最后一步定时的时候,定时的时间已经越过了,这里对其反复进行处理3次,尝试进行触发

这注释的最后一条跟虚拟机直接相关:被调度走了而超时,这里就是一条很明显的处理虚拟机时钟延迟的线索

如果3次还是不够,则最后设置一个时间并返回

于是Linux时钟便由低精度进化成了高精度定时器,可以看到其实大部分框架处理的都是定时器部分,真正的墙上human时钟却很少

猜你喜欢

转载自www.cnblogs.com/jkserge/p/10679977.html