linux的时钟系统的两大主要功能是计时和定时。计时功能就是指记录或设置当前的系统时间(包括日期),gettimeofday、settimeofday、time、clock_gettime、clock_settime等系统调用与计时相关。
定时功能与定时器相关。设定一个定时器的(定时)时间,设定定时器的回调函数,启动定时器,在(定时)时间到时,定时器的回调函数会被调用。在 Linux内核中主要有两种类型的定时器。一类称为timeout类型,另一类称为timer类型。timeout类型的定时器通常用于检测各种错误条件,例如用于检测网卡收发数据包是否会超时的定时器,IO设备的读写是否会超时的定时器等等。通常情况下这些错误很少发生,因此,使用 timeout 类型的定时器一般在超时之前就会被移除,从而很少产生真正的函数调用和系统开销。总的来说使用 timeout类型的定时器产生的系统开销很小,它是下文提及的timer wheel通常使用的环境。此外,在使用timeout类型定时器的地方往往并不关心超时处理,因此超时精确与否,早0.01秒或者晚0.01秒并不十分重要。timer类型的定时器与timeout类型的定时器正相反,使用 timer类型的定时器往往要求在精确的时钟条件下完成特定的事件,通常是周期性的并且依赖超时机制进行处理。例如设备驱动通常会定时读写设备来进行数据交互。
Linux具有动态tick和高精度定时器hrtimer功能,它们需要具体硬件平台的支持,动态tick功能由CONFIG_NO_HZ宏定义控制,高精度定时器由CONFIG_HIGH_RES_TIMERS宏定义控制。动态tick功能是指系统不需要为调度器周期性产生中断(tick),系统只在需要时才产生中断,提高系统效率,降低系统功耗。hrtimer的精度很高,可以用于realtime和多媒体等需要高精度定时的场景,常规定时器timer的精度不高,不能用于需要高精度定时的场景。本文基于CONFIG_NO_HZ和CONFIG_HIGH_RES_TIMERS都使能的情况进行介绍。
任务调度器(scheduler)使用hrtimer作为其调度定时器,在CPU非空闲时,设置调度hrtimer的到时时间为下一个tick,在hrtimer到时时,触发任务调度。在CPU空闲时,根据timer的超时时间设置调度hrtimer的到时时间。
jiffies是linux当前tick计数,在linux中,一些超时函数的时间单位是tick。linux要求具体的硬件平台提供两个硬件时钟设备,一个是时钟源(clocksource)设备,另一个是时钟事件(clockevent)设备。时钟源设备用于计时;时钟事件设备用于触发时钟事件(产生定时中断),hrtimer通过时钟事件设备实现,它的到时回调函数在时钟事件设备中断服务函数中调用。
系统初始化过程中与时钟系统相关的初始化函数在start_kernel函数中调用,它们的调用顺序:tick_init,init_timers,hrtimers_init,timekeeping_init,time_init,sched_clock_init。
目录(?)[+]
通常OS操作系统都支持Wheel方式,例如Linux、Neclues和vxworks都支持100-200Hz的节拍时钟。通过节拍OS进行时钟刷新以及产生任务调度,而每个硬件节拍就称为tick。
在Linux 2.6.16之前,内核一直使用一种称为timer wheel(定时器轮)的机制来管理时钟。这就是kernel一直采用的基于HZ的timer机制。Timer wheel 的核心数据结构如下所示:
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
struct tvec {
struct list_head vec[TVN_SIZE];
};
struct tvec_root {
struct list_head vec[TVR_SIZE];
};
struct tvec_base {
spinlock_t lock;
struct timer_list *running_timer;
unsigned long timer_jiffies;
unsigned long next_timer;
struct tvec_root tv1; //每一项称为一组
struct tvec tv2;
struct tvec tv3;
struct tvec tv4;
struct tvec tv5;
} ____cacheline_aligned;
在SMP系统中,通过如下方式定义per-cpu变量:
struct tvec_base boot_tvec_bases;
EXPORT_SYMBOL(boot_tvec_bases);
static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;
在内存较小的系统中可以设置CONFIG_BASE_SMALL选项为1来减少内存的使用。
对所有定时器的处理都由update_process_times发起,具体的调用流程如下:
update_process_times
run_local_timers
hrtimer_run_queues
raise_softirq(TIMER_SOFTIRQ)
在init_timers初始化时设置TIMER_SOFTIRQ的软中处理函数为run_timer_softirq,那么在run_timer_softirq将完成对到期的定时器实际的处理工作。其调用流程如下:
run_timer_softirq
__run_timers
一个弊端就是cascade开销过大。在极端的条件下,同时会有多个TV需要进行cascade处理,会产生很大的时延。这也是为什么说timeout类型的定时器是timer wheel的主要应用环境,或者说timer wheel是为timeout类型的定时器优化的。因为timeout类型的定时器的应用场景多是错误条件的检测,这类错误发生的机率很小,通常不到超时就被删除了,因此不会产生 cascade的开销。另一方面,由于timer wheel是建立在 HZ 的基础上的,因此其计时精度无法进一步提高。毕竟一味的通过提高 HZ值来提高计时精度并无意义,结果只能是产生大量的定时中断,增加额外的系统开销。因此,有必要将高精度的 timer 与低精度的 timer 分开,这样既可以确保低精度的 timeout 类型的定时器应用,也便于高精度的 timer 类型定时器的应用。还有一个重要的因素是 timer wheel 的实现与 jiffies 的耦合性太强,非常不便于扩展。因此,自从2.6.16开始, hrtimer这个新的timer子系统被加入到内核中。
在kernel启动的时候完成对Linux Wheel的核心数据结构的初始化工作。具体的初始化流程为
start_kernel
init_timers
timer_cpu_notify
init_timers_cpu
init_timer_stats
register_cpu_notifier
open_softirq
目录(?)[+]
系统中可以提供一定精度的计时设备都可以作为时钟源设备。如x86构架里的TSC、HPET、ACPI PM-Timer、PIT等。但是不同的时钟源提供的时钟精度是不一样的。像TSC、HPET等时钟源既支持高精度模式(high-resolution mode)也支持低精度模式(low-resolution mode),而PIT只能支持低精度模式。此外,时钟源的计时都是单调递增的(monotonically),如果时钟源的计时出现翻转(即返回到0值),很容易造成计时错误,内核的一个patch(commit id: ff69f2)就是处理这类问题的一个很好示例。时钟源作为系统时钟的提供者,在可靠并且可用的前提下精度越高越好。在 Linux中不同的时钟源有不同的rating,有更高 rating的时钟源会优先被系统使用,如图 2 所示。
系统中可以触发one-shot(单次)或者周期性中断的设备都可以作为时钟事件设备。如 HPET、CPU Local APIC Timer等。HPET比较特别,它既可以做时钟源设备也可以做时钟事件设备。时钟事件设备的类型分为全局和per-CPU两种类型。全局的时钟事件设备虽然附属于某一个特定的CPU上,但是完成的是系统相关的工作,例如完成系统的tick更新;per-CPU的时钟事件设备主要完成 Local CPU上的一些功能,例如对在当前 CPU上运行进程的时间统计,profile,设置 Local CPU上的下一次事件中断等。和时钟源设备的实现类似,时钟事件设备也通过 rating来区分优先级关系。
Tick device提供时钟事件的连续流,各个事件定期触发。Tick device其实是时钟事件设备的一个 wrapper,因此tick device也有one-shot和周期性这两种中断触发模式。每注册一个时钟事件设备,这个设备会自动被注册为一个tick device。全局的tick device用来更新诸如jiffies这样的全局信息,per-CPU的tick device则用来更新每个CPU相关的特定信息。 Broadcast
Broadcast的出现是为了应对这样一种情况:假定CPU使用Local APIC Timer作为 per-CPU的tick device,但是某些特定的CPU如 Intel的Westmere之前的CPU)在进入C3+的状态时Local APIC Timer也会同时停止工作,进入睡眠状态。在这种情形下broadcast可以替代Local APIC Timer继续完成统计进程的执行时间等有关操作。本质上broadcast是发送一个IPI(Inter-processor interrupt)中断给其他所有的CPU,当目标CPU收到这个IPI中断后就会调用原先Local APIC Timer正常工作时的中断处理函数,从而实现了同样的功能。目前主要在 x86 以及 MIPS下会用到 broadcast功能(补充:在ARM Cortex-A9上也可以使用)。
Timekeeping(可以理解为时间测量或者计时)是内核时间管理的一个核心组成部分。没有 Timekeeping,就无法更新系统时间,维持系统“心跳”。 GTOD 是一个通用的框架,用来实现诸如设置系统时间do_gettimeofday或者修改系统时间do_settimeofday等工作。这些功能的实现都依赖于系统的clocksource设备。
为了实现以上功能,Linux实现了多种与时间相关但用于不同目的的数据结构。
Ø struct timespec
struct timespec {
__kernel_time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
timespec精度是纳秒。它用来保存从00:00:00 GMT, 1 January 1970开始经过的时间。内核使用全局变量xtime来记录这一信息,这就是通常所说的“Wall Time”或者“Real Time”。与此对应的是“System Time”。System Time是一个单调递增的时间,每次系统启动时从0开始计时。
Ø struct timeval
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
timeval精度是微秒。timeval主要用来指定一段时间间隔。
Ø ktime
typedef union {
s64 tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
struct {
# ifdef __BIG_ENDIAN
s32 sec, nsec;
# else
s32 nsec, sec;
# endif
} tv;
#endif
}ktime_t;
ktime_t是hrtimer主要使用的时间结构。无论使用哪种体系结构,ktime_t始终保持64bit的精度,并且考虑了大小端的影响。
Ø cycle_t;
typedef u64 cycle_t;
cycle_t是从时钟源设备中读取的时钟类型。
为了管理这些不同的时间结构,Linux实现了一系列辅助函数来完成相互间的转换。
ktime_to_timespec,ktime_to_timeval,ktime_to_ns/ktime_to_us,反过来有诸如 ns_to_ktime 等类似的函数。
timeval_to_ns,timespec_to_ns,反过来有诸如 ns_to_timeval等类似的函数。
timeval_to_jiffies,timespec_to_jiffies,msecs_to_jiffies, usecs_to_jiffies, clock_t_to_jiffies反过来有诸如 ns_to_timeval 等类似的函数。
clocksource_cyc2ns / cyclecounter_cyc2ns
时钟源设备和时钟事件设备的引入,将原本放在各个体系结构中重复实现的冗余代码封装到各自的抽象层中,这样做不但消除了原来timer wheel与内核其他模块的紧耦合性,更重要的是系统可以在运行状态动态更换时钟源设备和时钟事件设备而不影响系统正常使用,譬如当 CPU 由于睡眠要关闭当前使用的时钟源设备或者时钟事件设备时系统可以平滑的切换到其他仍处于工作状态的设备上。Timekeeping/GTOD在使用时钟源设备的基础上也采用类似的封装实现了体系结构的无关性和通用性。hrtimer则可以通过timekeeping提供的接口完成定时器的更新,通过时钟事件设备提供的事件机制,完成对timer的管理。在图2中还有一个重要的模块就是tick device的抽象,尤其是dynamic tick。Dynamic tick的出现是为了能在系统空闲时通过停止tick的运行以达到降低CPU功耗的目的。使用dynamic tick的系统,只有在有实际工作时才会产生tick,否则tick是处于停止状态。
hrtimer是建立在per-CPU时钟事件设备上的,对于一个SMP系统,如果只有全局的时钟事件设备,hrtimer无法工作。因为如果没有per-CPU时钟事件设备,时钟中断发生时系统必须产生必要的IPI中断来通知其他CPU完成相应的工作,而过多的IPI中断会带来很大的系统开销,这样会令使用hrtimer的代价太大,不如不用。为了支持hrtimer,内核需要配置CONFIG_HIGH_RES_TIMERS=y。
hrtimer有两种工作模式:低精度模式(low-resolution mode)与高精度模式(high-resolution mode)。虽然hrtimer子系统是为高精度的timer准备的,但是系统可能在运行过程中动态切换到不同精度的时钟源设备,因此,hrtimer必须能够在低精度模式与高精度模式下自由切换。由于低精度模式是建立在高精度模式之上的,因此即便系统只支持低精度模式,部分支持高精度模式的代码仍然会编译到内核当中。
在介绍具体的实现之前,先介绍一下相关的数据结构
Ø struct hrtimer
/**
* struct hrtimer - the basic hrtimer structure
* @node: timerqueue node, which also manages node.
* @_softexpires: the absolute earliest expiry time of the hrtimer.
* The time which was given as expiry time when the timer
* was armed.
* @function: timer expiry callback function
* @base: pointer to the timer base (per cpu and per clock)
* @start_site: timer statistics field to store the site where the timer
* was started
* The hrtimer structure must be initialized by hrtimer_init()
*/
struct hrtimer {
struct timerqueue_node node;
ktime_t _softexpires;
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
unsigned long state;
#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
};
hrtimer_clock_base
/**
* struct hrtimer_clock_base - the timer base for a specific clock
* @cpu_base: per cpu clock base
* @index: clock type index for per_cpu support when moving a
* timer to a base on another cpu.
* @clockid: clock id for per_cpu support
* @active: red black tree root node for the active timers
* @resolution: the resolution of the clock, in nanoseconds
* @get_time: function to retrieve the current time of the clock
* @softirq_time: the time when running the hrtimer queue in the softirq
* @offset: offset of this clock to the monotonic base
*/
struct hrtimer_clock_base {
struct hrtimer_cpu_base *cpu_base;
int index;
clockid_t clockid;
struct timerqueue_head active;
ktime_t resolution;
ktime_t (*get_time)(void);
ktime_t softirq_time;
ktime_t offset;
};
Ø struct hrtimer_cpu_base
/*
* struct hrtimer_cpu_base - the per cpu clock bases
* @lock: lock protecting the base and associated clock bases
* and timers
* @active_bases: Bitfield to mark bases with active timers
* @expires_next: absolute time of the next event which was scheduled
* via clock_set_next_event()
* @hres_active: State of high resolution mode
* @hang_detected: The last hrtimer interrupt detected a hang
* @nr_events: Total number of hrtimer interrupt events
* @nr_retries: Total number of hrtimer interrupt retries
* @nr_hangs: Total number of hrtimer interrupt hangs
* @max_hang_time: Maximum time spent in hrtimer_interrupt
* @clock_base: array of clock bases for this cpu
*/
struct hrtimer_cpu_base {
raw_spinlock_t lock;
unsigned long active_bases;
#ifdef CONFIG_HIGH_RES_TIMERS
ktime_t expires_next;
int hres_active;
int hang_detected;
unsigned long nr_events;
unsigned long nr_retries;
unsigned long nr_hangs;
ktime_t max_hang_time;
#endif
struct hrtimer_clock_base clock_base[HRTIMER_MAX_CLOCK_BASES];
};
使用DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases)定义hrtimer_bases,可以管理挂在每一个CPU上的所有hrtimer。每个CPU上的timer list不再使用timer wheel中多级链表的实现方式,而是采用了红黑树(Red-Black Tree)来进行管理。hrtimer_bases的定义如下所示:
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) =
{
.clock_base =
{
{
.index = CLOCK_REALTIME,
.get_time = &ktime_get_real,
.resolution = KTIME_LOW_RES,
},
{
.index = CLOCK_MONOTONIC,
.get_time = &ktime_get,
.resolution = KTIME_LOW_RES,
},
}
};
图3为 hrtimer如何通过 hrtimer_bases 来管理 hrtimer
每个hrtimer_bases都包含两个clock_base,一个是CLOCK_REALTIME类型的,另一个是CLOCK_MONOTONIC类型的。hrtimer可以选择其中之一来设置timer的expire time, 可以是实际的时间 , 也可以是相对系统运行的时间。在hrtimer_run_queues的处理中,首先要通过hrtimer_bases找到正在执行当前中断的 CPU相关联的clock_base,然后逐个检查每个clock_base上挂的timer是否超时。由于timer在添加到clock_base上时使用了红黑树,最早超时的timer被放到树的最左侧,因此寻找超时timer的过程非常迅速,找到的所有超时timer会被逐一处理。
目录(?)[+]
在低精度模式下,hrtimer的核心处理函数是 hrtimer_run_queues,每一次 tick中断都要执行一次(在tick的中断处理函数中调用update_process_times)。这个函数的调用流程为:
update_process_times
run_local_timers
hrtimer_run_queues
raise_softirq(TIMER_SOFTIRQ)
其中hrtimer_run_queues是对到期的高精度定时器hrtimer的处理。
void hrtimer_run_queues(void)
{
struct timerqueue_node *node;
struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);
struct hrtimer_clock_base *base;
int index, gettime = 1;
//当hrtimer使能高精度时,该函数相当于空函数,不做任何处理直接返回。
if (hrtimer_hres_active())
return;
for (index = 0; index < HRTIMER_MAX_CLOCK_BASES; index++) {
base = &cpu_base->clock_base[index];
if (!timerqueue_getnext(&base->active))
continue;
if (gettime) {
hrtimer_get_softirq_time(cpu_base);
gettime = 0;
}
raw_spin_lock(&cpu_base->lock);
while ((node = timerqueue_getnext(&base->active))) {
struct hrtimer *timer;
timer = container_of(node, struct hrtimer, node);
if (base->softirq_time.tv64 <=
hrtimer_get_expires_tv64(timer))
break;
//移除hrtimer并运行hrtimer的处理函数,更新hrtimer的状态
__run_hrtimer(timer, &base->softirq_time);
}
raw_spin_unlock(&cpu_base->lock);
}
}
可以看出:在未配置高精度模式时,hrtimer的到期由函数hrtimer_run_queues检查。hrtimer_run_queues是在run_local_timers中被调用,而run_local_timers又是在系统时钟中断中被调用。从这里可以看出,与传统的使用时间轮算法的定时器一样,hrtimer在未配置高精度模式时采用了在每一个系统时钟中断中轮循的方式来判断hrtimer是否到期,因此,这里的定时精度为时钟中断轮循的时间间隔。在函数hrtimer_run_queues的开始处,会执行一项检查:
if (hrtimer_hres_active())
return;
所以在配置高精度模式后,这里的hrtimer_run_queues函数相当于空函数,会直接返回。
配置了高精度模式之后,hrtimer的到期由clock_event设备的产生的硬中断处理来调用hrtimer_interrupt函数。注意这里不再采用传统的轮循方式判断定时器是否到期,而是通过设置clock_event_device的中断,在第一个到期的定时器超时的时间点触发一个中断来执行超时操作。所以,这里的定时精度由clock_event_device的计时精度决定。
由于刚启动时没有特别重要的任务要做,因此默认是进入低精度+周期tick的工作模式,之后会根据硬件的配置(如硬件上是否支持高精度timer)和软件的配置(如是否通过命令行参数或者内核配置使能了高精度timer等特性)进行切换。切换过程的发起函数为run_timer_softirq,该函数被TIMER_SOFTIRQ软中断触发。其具体的流程为
run_timer_softirq
hrtimer_run_pending
tick_check_oneshot_change (在这里可能会切换到NOHZ模式,在后面进行分析)
hrtimer_switch_to_hres
在update_process_times中,除了处理处于低精度模式的hrtimer外,还要唤醒 IRQ0的 softIRQ(TIMER_SOFTIRQ)以便执行timer wheel的代码。由于hrtimer子系统的加入,在IRQ0的softIRQ中,还需要通过hrtimer_run_pending检查是否可以将hrtimer切换到高精度模式:
hrtimer 进行精度切换的处理函数
void hrtimer_run_pending(void)
{
if (hrtimer_hres_active())
return;
if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
hrtimer_switch_to_hres();
}
每一次触发IRQ0的softIRQ都需要检查一次是否可以将hrtimer切换到高精度,显然是十分低效的,希望将来有更好的方法不用每次都进行检查。
如果可以将hrtimer切换到高精度模式,则调用hrtimer_switch_to_hres函数进行切换。hrtimer切换到高精度模式的核心函数
static int hrtimer_switch_to_hres(void)
{
int cpu = smp_processor_id();
struct hrtimer_cpu_base *base = &per_cpu(hrtimer_bases, cpu);
unsigned long flags;
if (base->hres_active)
return 1;
local_irq_save(flags);
if (tick_init_highres()) {
local_irq_restore(flags);
printk(KERN_WARNING "Could not switch to high resolution "
"mode on CPU %d\n", cpu);
return 0;
}
base->hres_active = 1;
base->clock_base[CLOCK_REALTIME].resolution = KTIME_HIGH_RES;
base->clock_base[CLOCK_MONOTONIC].resolution = KTIME_HIGH_RES;
tick_setup_sched_timer();
/* "Retrigger" the interrupt to get things going */
retrigger_next_event(NULL);
local_irq_restore(flags);
return 1;
}
在这个函数中,首先使用tick_init_highres更新与原来的tick device绑定的时钟事件设备的event handler,例如将在低精度模式下的工作函数tick_handle_periodic或者tick_handle_ periodic_broadcast换成hrtimer_interrupt(它是hrtimer在高精度模式下的timer中断处理函数),同时将tick device的触发模式变为one-shot,即单次触发模式,这是使用dynamic tick或者hrtimer时tick device的工作模式。tick_init_highres通过调用tick_switch_to_oneshot函数来完成上述工作。
具体的代码如下:
int tick_switch_to_oneshot(void (*handler)(struct clock_event_device *))
{
struct tick_device *td = &__get_cpu_var(tick_cpu_device);
struct clock_event_device *dev = td->evtdev;
//都成立时
if (!dev || !(dev->features & CLOCK_EVT_FEAT_ONESHOT) ||
!tick_device_is_functional(dev)) {
printk(KERN_INFO "Clockevents: "
"could not switch to one-shot mode:");
if (!dev) {
printk(" no tick device\n");
} else {
if (!tick_device_is_functional(dev))
printk(" %s is not functional.\n", dev->name);
else
printk(" %s does not support one-shot mode.\n",
dev->name);
}
return -EINVAL;
}
td->mode = TICKDEV_MODE_ONESHOT;
dev->event_handler = handler;
clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT);
tick_broadcast_switch_to_oneshot();
return 0;
}
由于dynamic tick可以随时停止和开始,以不规律的速度产生tick,因此支持one-shot模式的时钟事件设备是必须的;对于hrtimer,由于hrtimer采用事件机制驱动timer前进,因此使用one-shot的触发模式也是顺理成章的。不过这样一来,原本tick device每次执行中断时需要完成的周期性任务如更新jiffies / wall time (do_timer)以及更新process的使用时间(update_process_times)等工作在切换到高精度模式之后就没有了,因此在执行完tick_init_highres之后紧接着会调用tick_setup_sched_timer函数来完成这部分设置工作。
下面我们就来看一下,中断处理函数hrtimer_interrupt
void hrtimer_interrupt(struct clock_event_device *dev)
{
struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);
ktime_t expires_next, now, entry_time, delta;
int i, retries = 0;
BUG_ON(!cpu_base->hres_active);
cpu_base->nr_events++;
dev->next_event.tv64 = KTIME_MAX;
//保存进入中断处理的时间
entry_time = now = ktime_get();
retry:
expires_next.tv64 = KTIME_MAX;
raw_spin_lock(&cpu_base->lock);
/*
* We set expires_next to KTIME_MAX here with cpu_base->lock
* held to prevent that a timer is enqueued in our queue via
* the migration code. This does not affect enqueueing of
* timers which run their callback and need to be requeued on
* this CPU.
*/
cpu_base->expires_next.tv64 = KTIME_MAX;
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
struct hrtimer_clock_base *base;
struct timerqueue_node *node;
ktime_t basenow;
//active_bases的每一位表示一个CPU是否存在激活的hrtimer
if (!(cpu_base->active_bases & (1 << i)))
continue;
base = cpu_base->clock_base + i;
basenow = ktime_add(now, base->offset);
while ((node = timerqueue_getnext(&base->active))) {
struct hrtimer *timer;
timer = container_of(node, struct hrtimer, node);
//如果basenow小于最早到期的hrtimer的时间,意味着没有hrtimer到期
if (basenow.tv64 < hrtimer_get_softexpires_tv64(timer)) {
ktime_t expires;
expires = ktime_sub(hrtimer_get_expires(timer),
base->offset);
if (expires.tv64 < expires_next.tv64)
expires_next = expires;
break;
}
__run_hrtimer(timer, &basenow);
}
}
/*
* Store the new expiry value so the migration code can verify
* against it.
*/
cpu_base->expires_next = expires_next;
raw_spin_unlock(&cpu_base->lock);
/* Reprogramming necessary ? */
if (expires_next.tv64 == KTIME_MAX ||
!tick_program_event(expires_next, 0)) {
cpu_base->hang_detected = 0;
return;
}
//时钟已经到期,由于一些时间不能及时处理
now = ktime_get();
cpu_base->nr_retries++;
if (++retries < 3)
goto retry;
/*
* Give the system a chance to do something else than looping
* here. We stored the entry time, so we know exactly how long
* we spent here. We schedule the next event this amount of
* time away.
*/
cpu_base->nr_hangs++;
cpu_base->hang_detected = 1;
delta = ktime_sub(now, entry_time);
if (delta.tv64 > cpu_base->max_hang_time.tv64)
cpu_base->max_hang_time = delta;
/*
* Limit it to a sensible value as we enforce a longer
* delay. Give the CPU at least 100ms to catch up.
*/
if (delta.tv64 > 100 * NSEC_PER_MSEC)
expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);
else
expires_next = ktime_add(now, delta);
tick_program_event(expires_next, 1);
printk_once(KERN_WARNING "hrtimer: interrupt took %llu ns\n",
ktime_to_ns(delta));
}
这个函数的逻辑相对比较复杂,遍历每个时钟基,判断时钟是否到期,如果有到期的时钟就处理;如果hrtimer还没有到期,则计算下次到期的时间并根据需要确认是否编程。
if (expires_next.tv64 == KTIME_MAX ||
!tick_program_event(expires_next, 0)) {
cpu_base->hang_detected = 0;
return;
}
if语句判断的两个条件表示两种情况,第一种是expires_next.tv64 == KTIME_MAX成立,表示完成对到期的定时器的处理,正常结束;第二种是expires_next.tv64 == KTIME_MAX不成立,需要重启编程tick_program_event(expires_next, 0),编程成功后返回。
当上述的两种情况都不成立,即expires_next.tv64 == KTIME_MAX不成立且tick_program_event(expires_next, 0)编程不成功(主要是由于expires_nex是过去的时间点)。则会跳到retry进行重试,三次不成功后,则设置相应的状态并重新编程退出。
下面就看一下hrtimer_interrupt的核心处理函数__run_hrtimer。该函数的主要功能是:移除到期的hrtimer,执行hrtimer的回调函数并更新hrtimer的状态。
static void __run_hrtimer(struct hrtimer *timer, ktime_t *now)
{
struct hrtimer_clock_base *base = timer->base;
struct hrtimer_cpu_base *cpu_base = base->cpu_base;
enum hrtimer_restart (*fn)(struct hrtimer *);
int restart;
WARN_ON(!irqs_disabled());
debug_deactivate(timer);
__remove_hrtimer(timer, base, HRTIMER_STATE_CALLBACK, 0);
timer_stats_account_hrtimer(timer);
fn = timer->function;
/*
* Because we run timers from hardirq context, there is no chance
* they get migrated to another cpu, therefore its safe to unlock
* the timer base.
*/
raw_spin_unlock(&cpu_base->lock);
trace_hrtimer_expire_entry(timer, now);
restart = fn(timer);
trace_hrtimer_expire_exit(timer);
raw_spin_lock(&cpu_base->lock);
/*
* Note: We clear the CALLBACK bit after enqueue_hrtimer and
* we do not reprogramm the event hardware. Happens either in
* hrtimer_start_range_ns() or in hrtimer_interrupt()
*/
if (restart != HRTIMER_NORESTART) {
BUG_ON(timer->state != HRTIMER_STATE_CALLBACK);
enqueue_hrtimer(timer, base);
}
WARN_ON_ONCE(!(timer->state & HRTIMER_STATE_CALLBACK));
timer->state &= ~HRTIMER_STATE_CALLBACK;
}
在一个支持hrtimer高精度模式并使能了dynamic tick的系统中,在系统的时钟源已经切换到支持高精度的时钟源后的第一次发生IRQ0的软中断时hrtimer就会进行从低精度到高精度的切换,然后再进一步切换到NOHZ模式。
负责启动系统的CPU的hrtimer的初始化也是在start_kernel是进行的,其具体流程如下:
start_kernel
hrtimers_init
hrtimer_cpu_notify
init_hrtimers_cpu
register_cpu_notify
opensoftirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq)
首先,分析一下hrtimers_init的代码,其主要完成的初始化当前cpu的hrtimer的相关数据结构,把hrtimer_nb通知块注册到cpu_chain通知链,并根据内核配置CONFIG_HIGH_RES_TIMERS决定是否打开HRTIMER_SOFTIRQ软中断。
void __init hrtimers_init(void)
{
hrtimer_cpu_notify(&hrtimers_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id());
register_cpu_notifier(&hrtimers_nb);
#ifdef CONFIG_HIGH_RES_TIMERS
open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq);
#endif
}
hrtimer相关的数据结构(cpu base和clockbase)都是在hrtimer_cpu_notify中调用init_hrtimers_cpu来完成的,具体代码如下:
static void __cpuinitinit_hrtimers_cpu(int cpu)
{
//获取per-cup变量hrtimer_base
struct hrtimer_cpu_base *cpu_base = &per_cpu(hrtimer_bases, cpu);
int i;
raw_spin_lock_init(&cpu_base->lock);
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
cpu_base->clock_base[i].cpu_base = cpu_base;
//初始化红黑树和timerqueue
timerqueue_init_head(&cpu_base->clock_base[i].active);
}
//完成cpu_base的初始化,即设置cpu_base的下次事件的到期时间为KTIME_MAX
//并设置cpu_base中的域hres_active为0,表示现在处于低精度模式。
hrtimer_init_hres(cpu_base);
}
目录(?)[+]
在dynamic tick引入之前,内核一直使用周期性的基于HZ的tick。传统的tick机制在系统进入空闲状态时仍然会产生周期性的中断,这种频繁的中断迫使CPU无法进入更深的睡眠。如果放开这个限制,在系统进入空闲时停止tick,有工作时恢复tick,实现完全自由的,根据需要产生tick的机制,可以使CPU获得更多的睡眠机会以及更深的睡眠,从而进一步节能。dynamic tick的出现,就是为彻底替换掉周期性的tick机制而产生的。周期性运行的tick机制需要完成诸如进程时间片的计算,更新profile,协助CPU进行负载均衡等诸多工作,这些工作dynamic tick都提供了相应的模拟机制来完成。
从上文中可知内核时钟子系统支持低精度和高精度两种模式,因此dynamic tick也必须有两套对应的处理机制。
其核心数据结构为:
/**
* struct tick_sched - sched tick emulation and no idle tick control/stats
* @sched_timer: hrtimer to schedule the periodic tick in high
* resolution mode
* @idle_tick: Store the last idle tick expiry time when the tick
* timer is modified for idle sleeps. This is necessary
* to resume the tick timer operation in the timeline
* when the CPU returns from idle
* @tick_stopped: Indicator that the idle tick has been stopped
* @idle_jiffies: jiffies at the entry to idle for idle time accounting
* @idle_calls: Total number of idle calls
* @idle_sleeps: Number of idle calls, where the sched tick was stopped
* @idle_entrytime: Time when the idle call was entered
* @idle_waketime: Time when the idle was interrupted
* @idle_exittime: Time when the idle state was left
* @idle_sleeptime: Sum of the time slept in idle with sched tick stopped
* @iowait_sleeptime: Sum of the time slept in idle with sched tick stopped, with IO outstanding
* @sleep_length: Duration of the current idle sleep
* @do_timer_lst: CPU was the last one doing do_timer before going idle
*/
struct tick_sched {
struct hrtimer sched_timer;
unsigned long check_clocks;
enum tick_nohz_mode nohz_mode;
ktime_t idle_tick;
int inidle;
int tick_stopped;
unsigned long idle_jiffies;
unsigned long idle_calls;
unsigned long idle_sleeps;
int idle_active;
ktime_t idle_entrytime;
ktime_t idle_waketime;
ktime_t idle_exittime;
ktime_t idle_sleeptime;
ktime_t iowait_sleeptime;
ktime_t sleep_length;
unsigned long last_jiffies;
unsigned long next_jiffies;
ktime_t idle_expires;
int do_timer_last;
};
/*
* Per cpu nohz control structure
*/
static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);
在低精度模式下,每次tick都会触发TIMER_SOFTIRQ软中断,软中断处理函数run_time_softirq这个函数里可能使得时钟模式切换到NOHZ模式。切换过程如下:
run_timer_softirq
hrtimer_run_pending
tick_check_oneshot_change
tick_nohz_switch_to_nohz();
tick_switch_to_oneshot(tick_nohz_handler)
发生上述调用流程的前提是没有设置CONFIG_HIGH_RES_TIMERS选项,即没有启用高精度模式但是内核使能了NOHZ模式。
低精度模式下dynamic tick的核心处理函数tick_nohz_handler,其核心处理函数下所示。
static void tick_nohz_handler(struct clock_event_device *dev)
{
struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
struct pt_regs *regs = get_irq_regs();
int cpu = smp_processor_id();
ktime_t now = ktime_get();
dev->next_event.tv64 = KTIME_MAX;
if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
tick_do_timer_cpu = cpu;
/* Check, if the jiffies need an update */
if (tick_do_timer_cpu == cpu)
tick_do_update_jiffies64(now);
/*
* When we are idle and the tick is stopped, we have to touch
* the watchdog as we might not schedule for a really long
* time. This happens on complete idle SMP systems while
* waiting on the login prompt. We also increment the "start
* of idle" jiffy stamp so the idle accounting adjustment we
* do when we go busy again does not account too much ticks.
*/
if (ts->tick_stopped) {
touch_softlockup_watchdog();
ts->idle_jiffies++;
}
update_process_times(user_mode(regs));
profile_tick(CPU_PROFILING);
//设置下次超时事件
while (tick_nohz_reprogram(ts, now)) {
now = ktime_get();
tick_do_update_jiffies64(now);
}
}
在这个函数中首先模拟周期性tick device完成类似的工作:如果当前CPU负责全局tick device的工作,则更新jiffies,同时完成对本地CPU的进程时间统计等工作。如果当前tick device在此之前已经处于停止状态,为了防止tick停止时间过长造成 watchdog超时,从而引发soft-lockdep的错误,需要通过调用touch_softlockup_watchdog复位软件看门狗防止其溢出。正如代码中注释所描述,这种情况有可能出现在启动完毕后完全空闲等待登录的SMP系统上。最后需要设置下一次tick的超时时间。如果tick_nohz_reprogram执行时间超过了一个jiffy,会导致设置的下一次超时时间已经过期,因此需要重新设置,相应的也需要再次更新jiffies。这里虽然设置了下一次的超时事件,但是由于系统空闲时会停止tick,因此下一次的超时事件可能发生,也可能不发生。这也正是dynamic tick根本特性。
其具体的流程为:
hrtimer_switch_to_hres();
tick_init_highres
tick_switch_to_oneshot(hrtimer_interrupt);
tick_setup_sched_timer();
ts->sched_timer.function = tick_sched_timer;
高精度NOHZ模式下的核心处理函数是tick_sched_timer,具体实现如下:
/*
* We rearm the timer until we get disabled by the idle code.
* Called with interrupts disabled and timer->base->cpu_base->lock held.
*/
static enum hrtimer_restarttick_sched_timer(struct hrtimer *timer)
{
struct tick_sched *ts =
container_of(timer, struct tick_sched, sched_timer);
struct pt_regs *regs = get_irq_regs();
ktime_t now = ktime_get();
int cpu = smp_processor_id();
#ifdef CONFIG_NO_HZ
/*
* Check if the do_timer duty was dropped. We don't care about
* concurrency: This happens only when the cpu in charge went
* into a long sleep. If two cpus happen to assign themself to
* this duty, then the jiffies update is still serialized by
* xtime_lock.
*/
if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
tick_do_timer_cpu = cpu;
#endif
/* Check, if the jiffies need an update */
if (tick_do_timer_cpu == cpu)
tick_do_update_jiffies64(now);
/*
* Do not call, when we are not in irq context and have
* no valid regs pointer
*/
if (regs) {
/*
* When we are idle and the tick is stopped, we have to touch
* the watchdog as we might not schedule for a really long
* time. This happens on complete idle SMP systems while
* waiting on the login prompt. We also increment the "start of
* idle" jiffy stamp so the idle accounting adjustment we do
* when we go busy again does not account too much ticks.
*/
if (ts->tick_stopped) {
touch_softlockup_watchdog();
ts->idle_jiffies++;
}
update_process_times(user_mode(regs));
profile_tick(CPU_PROFILING);
}
//设置下次超时事件
hrtimer_forward(timer, now, tick_period);
return HRTIMER_RESTART;
}
从hrtimer高精度模式下模拟周期运行的tick device的简化实现中可以看到,在高精度模式下tick_sched_timer用来模拟周期性tick device的功能。需要注意的是tick_sched_timer又是在hrtimer_interrupt中调用的。dynamic tick的实现也使用了这个函数。这是因为hrtimer在高精度模式时必须使用one-shot模式的tick device,这也同时符合dynamic tick的要求。虽然使用同样的函数,表面上都会触发周期性的 tick中断,但是使用dynamic tick的系统在空闲时会停止tick工作,因此tick中断不会是周期产生的。
当CPU进入空闲时是最好的启动dynamic tick机制时机,停止tick;反之在CPU从空闲中恢复到工作状态时,则可以停止dynamic tick,如下所示:
CPU 在 idle时 dynamic tick 的启动/停止设置
void cpu_idle(void)
{
. . . .
while (1) {
tick_nohz_stop_sched_tick(1);
while (!need_resched()) {
. . . .
}
tick_nohz_restart_sched_tick();
}
在freerun_clocksource_init函数中向系统注册新的时钟源,但没有切换时钟源。
freerun_clocksource_init
clocksource_mmio_init
clocksource_register_hz
__clocksource_register_scale
clocksource_select // finished_booting没有被设置为1,在这里是直接返回
在clocksource_done_booting函数中将finished_booting置1,接着调用clocksource_select切换系统的时钟源。
clocksource_done_booting
clocksource_select
timekeeping_notify(curr_clocksource);
stop_machine(change_clocksource, clock, NULL);
__stop_machine(fn, data, cpus);
当调用clocksource_done_booting时,系统会通过clocksource_select选择系统中最好的时钟源作为系统的时钟源。下面分析一下clocksource_select的代码:
static void clocksource_select(void)
{
struct clocksource *best, *cs;
if (!finished_booting || list_empty(&clocksource_list))
return;
/* First clocksource on the list has the best rating. */
best = list_first_entry(&clocksource_list, struct clocksource, list);
/* Check for the override clocksource. */
list_for_each_entry(cs, &clocksource_list, list) {
if (strcmp(cs->name, override_name) != 0)
continue;
/*
* Check to make sure we don't switch to a non-highres
* capable clocksource if the tick code is in oneshot
* mode (highres or nohz)
*/
if (!(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES) &&
tick_oneshot_mode_active()) {
/* Override clocksource cannot be used. */
printk(KERN_WARNING "Override clocksource %s is not "
"HRT compatible. Cannot switch while in "
"HRT/NOHZ mode\n", cs->name);
override_name[0] = 0;
} else
/* Override clocksource can be used. */
best = cs;
break;
}
if (curr_clocksource != best) {
printk(KERN_INFO "Switching to clocksource %s\n", best->name);
curr_clocksource = best;
timekeeping_notify(curr_clocksource);
}
}
/*
* clocksource_done_booting - Called near the end of core bootup
*
* Hack to avoid lots of clocksource churn at boot time.
* We use fs_initcall because we want this to start before
* device_initcall but after subsys_initcall.
*/
static int __init clocksource_done_booting(void)
{
mutex_lock(&clocksource_mutex);
curr_clocksource = clocksource_default_clock();
mutex_unlock(&clocksource_mutex);
finished_booting = 1;
/*
* Run the watchdog first to eliminate unstable clock sources
*/
clocksource_watchdog_kthread(NULL);
mutex_lock(&clocksource_mutex);
clocksource_select();
mutex_unlock(&clocksource_mutex);
return 0;
}
fs_initcall(clocksource_done_booting);
__stop_machine实际上调用change_clocksource完成了对时钟源的切换,具体的代码如下:
static inline int __stop_machine(int (*fn)(void *), void *data,
const struct cpumask *cpus)
{
int ret;
local_irq_disable();
//调用的函数是实际是change_clocksource(clock);
ret = fn(data);
local_irq_enable();
return ret;
}
从而切换到我们注册的时钟上。