linux时间子系统

来源:互联网 发布:淘宝大布包 编辑:程序博客网 时间:2024/05/29 08:40
1、clocksource
一台设备的硬件时钟源(事实上不一定是“硬件”时钟源)通常有多个,并且多种多样,各有特点。按照层次结构思想,对功能相同,特性和使用方法不同的东西,进行软件抽象,向上层提供统一的服务和使用方式。这样上层就不用关心硬件之间的差异,也不用关心新硬件的适配问题。

1.1 struct clocksourse

struct clocksource {
/*
* Hotpath data, fits in a single cache line when the
* clocksource itself is cacheline aligned.
*/
cycle_t (*read)(struct clocksource *cs);
cycle_t cycle_last;
cycle_t mask;
u32 mult;
u32 shift;
u64 max_idle_ns;
u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
struct arch_clocksource_data archdata;
#endif
 
const char *name;
struct list_head list;
int rating;
int (*enable)(struct clocksource *cs);
void (*disable)(struct clocksource *cs);
unsigned long flags;
void (*suspend)(struct clocksource *cs);
void (*resume)(struct clocksource *cs);
 
/* private: */
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
/* Watchdog related data, used by the framework */
struct list_head wd_list;
cycle_t cs_last;
cycle_t wd_last;
#endif
} ____cacheline_aligned;
重要字段的解释
1)rating:时钟源的精度
每种时钟源的精度不同,每个时钟源的精度由驱动它的时钟频率决定,比如一个由10MHz时钟驱动的时钟源,他的精度就是100nS。clocksource结构中有一个rating字段,代表着该时钟源的精度范围,它的取值范围如下:
  • 1--99: 不适合于用作实际的时钟源,只用于启动过程或用于测试;
  • 100--199:基本可用,可用作真实的时钟源,但不推荐;
  • 200--299:精度较好,可用作真实的时钟源;
  • 300--399:很好,精确的时钟源;
  • 400--499:理想的时钟源,如有可能就必须选择它作为时钟源;

2)  read回调函数

时钟源本身不会产生中断,要获得时钟源的当前计数,只能通过主动调用它的read回调函数来获得当前的计数值,注意这里只能获得计数值,也就是所谓的cycle,要获得相应的时间,必须要借助clocksource的mult和shift字段进行转换计算。

3)  mult和shift字段

因为从clocksource中读到的值是一个cycle计数值,要转换为时间,我们必须要知道驱动clocksource的时钟频率F,一个简单的计算就可以完成:

t = cycle/F;

可是clocksource并没有保存时钟的频率F,因为使用上面的公式进行计算,需要使用浮点运算,这在内核中是不允许的,因此,内核使用了另外一个变通的办法,根据时钟的频率和期望的精度,事先计算出两个辅助常数mult和shift,然后使用以下公式进行cycle和t的转换:

t = (cycle * mult) >> shift;

只要我们保证:

F = (1 << shift) / mult;

内核内部使用64位进行该转换计算:
static inline s64 clocksource_cyc2ns(cycle_t cycles, u32 mult, u32 shift)
{
return ((u64) cycles * mult) >> shift;
}
从转换精度考虑,mult的值是越大越好,但是为了计算过程不发生溢出,mult的值又不能取得过大。为此内核假设cycle计数值被转换后的最大时间值:10分钟(600秒),主要的考虑是CPU进入IDLE状态后,时间信息不会被更新,只要在10分钟内退出IDLE,clocksource的cycle计数值就可以被正确地转换为相应的时间,然后系统的时间信息可以被正确地更新。当然最后的结果不一定是10分钟,它由clocksource_max_deferment进行计算,并保存max_idle_ns字段中,tickless的代码要考虑这个值,以防止在NO_HZ配置环境下,系统保持IDLE状态的时间过长。在这样,由10分钟这个假设的时间值,我们可以推算出合适的mult和shift值。

1.2 clocksource的注册和初始化

一个clocksource需要通过clocksource_register_hz()函数将自己注册进内核。这一般发生在系统自举阶段(必须有的clocksource),硬件加载时由驱动负责注册(可选clocksource)。
// include/linux/clocksource.h
static inline int clocksource_register_hz(struct clocksource *cs, u32 hz)
{
return __clocksource_register_scale(cs, 1, hz);
}
// kernel/time/clocksource.c
/**
* __clocksource_register_scale - Used to install new clocksources
* @cs: clocksource to be registered
* @scale: Scale factor multiplied against freq to get clocksource hz
* @freq: clocksource frequency (cycles per second) divided by scale
*
* Returns -EBUSY if registration fails, zero otherwise.
*
* This *SHOULD NOT* be called directly! Please use the
* clocksource_register_hz() or clocksource_register_khz helper functions.
*/
int __clocksource_register_scale(struct clocksource *cs, u32 scale, u32 freq)
{
 
/* Initialize mult/shift and max_idle_ns(该clocksource可接受的最大IDLE时间) */
__clocksource_updatefreq_scale(cs, scale, freq);
 
/* Add clocksource to the clcoksource list */
mutex_lock(&clocksource_mutex);
/* 按clocksource的rating的大小,把该clocksource按顺序挂在全局链表clocksource_list上,
* rating值越大,在链表上的位置越靠前
*/
clocksource_enqueue(cs);
clocksource_enqueue_watchdog(cs);
clocksource_select();//触发clocksource选择,看是否要更换timekeeping系统使用的clocksource
mutex_unlock(&clocksource_mutex);
return 0;
}
EXPORT_SYMBOL_GPL(__clocksource_register_scale);

1.3 clocksource的建立过程

在系统的启动阶段,内核注册了一个基于jiffies的clocksource
// kernel/time/jiffies.c
struct clocksource clocksource_jiffies = {
.name = "jiffies",
.rating = 1, /* lowest valid rating,精度1/HZ秒*/
.read = jiffies_read,
.mask = 0xffffffff, /*32bits*/
.mult = NSEC_PER_JIFFY << JIFFIES_SHIFT, /* details above */
.shift = JIFFIES_SHIFT,
};
......
static int __init init_jiffies_clocksource(void)
{
return clocksource_register(&clocksource_jiffies);
}
core_initcall(init_jiffies_clocksource);
/* 如果平台的代码没有提供定制的clocksource_default_clock函数,它将返回该clocksource: */
struct clocksource * __init __weak clocksource_default_clock(void) 
{
return &clocksource_jiffies;
}


1.4 clocksource watchdog

系统中可能同时会注册对个clocksource,各个clocksource的精度和稳定性各不相同,为了筛选这些注册的clocksource,内核启用了一个定时器用于监控这些clocksource的性能,定时器的周期设为0.5秒:
#define WATCHDOG_INTERVAL (HZ >> 1)
#define WATCHDOG_THRESHOLD (NSEC_PER_SEC >> 4)
当有新的clocksource被注册时,除了会挂在全局链表clocksource_list外,还会同时挂在一个watchdog链表上:watchdog_list。定时器周期性地(0.5秒)检查watchdog_list上的clocksource,WATCHDOG_THRESHOLD的值定义为0.0625秒,如果在0.5秒内,clocksource的偏差大于这个值就表示这个clocksource是不稳定的,定时器的回调函数通过clocksource_watchdog_kthread线程标记该clocksource,并把它的rate修改为0,表示精度极差。



2 内核中表示时间的单位和结构

2.1 jiffies

内核用jiffies变量记录系统启动以来经过的时钟滴答数
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

在32位的系统上,jiffies是一个32位的无符号数,系统每过1/HZ秒,jiffies的值就会加1,最终该变量可能会溢出,所以内核同时又定义了一个64位的变量jiffies_64,链接的脚本保证jiffies变量和jiffies_64变量的内存地址是相同的,通常,我们可以直接访问jiffies变量,但是要获得jiffies_64变量,必须通过辅助函数get_jiffies_64来实现。jiffies是内核的低精度定时器的计时单位,所以内核配置的HZ数决定了低精度定时器的精度,如果HZ数被设定为1000,那么,低精度定时器(timer_list)的精度就是1ms=1/1000秒。
因为jiffies变量可能存在溢出的问题,所以在用基于jiffies进行比较时,应该使用以下辅助宏来实现:
time_after(a,b)
time_before(a,b)
time_after_eq(a,b)
time_before_eq(a,b)
time_in_range(a,b,c)
同时,内核还提供了一些辅助函数用于jiffies和毫秒以及纳秒之间的转换:
unsigned int jiffies_to_msecs(const unsigned long j);
unsigned int jiffies_to_usecs(const unsigned long j);
unsigned long msecs_to_jiffies(const unsigned int m);
unsigned long usecs_to_jiffies(const unsigned int u);

2.2  struct timeval

struct timeval {
__kernel_time_ttv_sec;/* seconds */
__kernel_suseconds_ttv_usec;/* microseconds */
};
__kernel_time_t__kernel_suseconds_t 实际上都是long型的整数。gettimeofday和settimeofday使用timeval作为时间单位。
转换函数:
static inline int timeval_compare(const struct timeval *lhs, const struct timeval *rhs);
static inline s64 timeval_to_ns(const struct timeval *tv);
extern struct timeval ns_to_timeval(const s64 nsec);
unsigned long timeval_to_jiffies(const struct timeval *value);
void jiffies_to_timeval(const unsigned long jiffies, struct timeval *value);

2.3  struct timespec

timekeeper中的xtime字段用timespec作为时间单位。
struct timespec {
__kernel_time_ttv_sec;/* seconds */
longtv_nsec;/* nanoseconds */
};
转换函数:
static inline int timespec_equal(const struct timespec *a, const struct timespec *b);
static inline int timespec_compare(const struct timespec *lhs, const struct timespec *rhs);
extern unsigned long mktime(const unsigned int year, const unsigned int mon,
const unsigned int day, const unsigned int hour,
const unsigned int min, const unsigned int sec);
extern void set_normalized_timespec(struct timespec *ts, time_t sec, s64 nsec);
static inline struct timespec timespec_add(struct timespec lhs,struct timespec rhs);
static inline struct timespec timespec_sub(struct timespec lhs,struct timespec rhs);
 
static inline s64 timespec_to_ns(const struct timespec *ts);
extern struct timespec ns_to_timespec(const s64 nsec);
static __always_inline void timespec_add_ns(struct timespec *a, u64 ns);
 
unsigned long timespec_to_jiffies(const struct timespec *value);
void jiffies_to_timespec(const unsigned long jiffies, struct timespec *value);

2.4.  struct ktime

linux的通用时间架构用ktime来表示时间,高精度定时器通常用ktime作为计时单位
/* 这个定义兼容了32bit 64bit以及big-little endian系统
* 64位的系统可以直接访问tv64字段,单位是纳秒,32位的系统则被拆分为两个字段:sec和nsec,
*/
union ktime {
s64tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
struct {
# ifdef __BIG_ENDIAN
s32sec, nsec;
# else
s32nsec, sec;
# endif
} tv;
#endif
};
函数:
ktime_t ktime_set(const long secs, const unsigned long nsecs);
ktime_t ktime_sub(const ktime_t lhs, const ktime_t rhs);
ktime_t ktime_add(const ktime_t add1, const ktime_t add2);
ktime_t ktime_add_ns(const ktime_t kt, u64 nsec);
ktime_t ktime_sub_ns(const ktime_t kt, u64 nsec);
ktime_t timespec_to_ktime(const struct timespec ts);
ktime_t timeval_to_ktime(const struct timeval tv);
struct timespec ktime_to_timespec(const ktime_t kt);
struct timeval ktime_to_timeval(const ktime_t kt);
s64 ktime_to_ns(const ktime_t kt);
int ktime_equal(const ktime_t cmp1, const ktime_t cmp2);
s64 ktime_to_us(const ktime_t kt);
s64 ktime_to_ms(const ktime_t kt);
ktime_t ns_to_ktime(u64 ns);


3 timekeeper

3.1 用户态或内核的其它部分对时间的要求各不相同,因此内核维护着多种时间:

1)xtime  xtime和RTC时间一样,都是人们日常所使用的墙上时间,只是RTC时间的精度通常比较低,大多数情况下只能达到毫秒级别的精度,如果是使用外部的RTC芯片,访问速度也比较慢,为此,内核维护了另外一个wall time时间:xtime,取决于用于对xtime计时的clocksource,它的精度甚至可以达到纳秒级别,因为xtime实际上是一个内存中的变量,它的访问速度非常快,内核大部分时间都是使用xtime来获得当前时间信息。xtime记录的是自1970年1月1日0时到当前时刻所经历的纳秒数。

xtime虽然正常情况下是递增的,但是毕竟用户可以通过stime()和settimeofday()系统调用主动向前或向后调整墙上时间,从而修改xtime值。

2)monotonic time  可以把它理解为自系统启动以来所经过的时间,该时间只能单调递增,并且不计入休眠时间,也就是说,系统休眠时monotoic时间不会递增,因此休眠前后时间是连续的,平缓的。

3)boot time  与monotonic时间相同,会累加上系统休眠的时间,它代表着系统上电后的总时间。

xtime,monotonic time和 boot time都要受NTP时间调整的影响,用户态NTP程序可能要求内核每tick更新时间时走快点或走慢点,以此来渐进NTP认为是准确的网络上的时间。

4)raw monotonic time  该时间与monotonic时间类似,也是单调递增的时间,唯一的不同是:raw monotonic time“更纯净”,表示真正的硬件时间,它不受NTP时间调整的影响,它代表着系统独立时钟硬件对时间的统计。

时间种类精度累计休眠时间受NTP调整的影响xtimetimespec(纳秒)yesyesmonotonictimespec(纳秒)noyesraw monotonictimespec(纳秒)nonoboot timetimespec(纳秒)yesyes

xtime、monotonic-time和raw monotonic time可以通过用户空间的clock_gettime函数获得,对应的ID参数分别是 CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_MONOTONIC_RAW。

3.2 struct timekeeper

早期的内核版本中,xtime、wall_to_monotonic、raw_time其实是定义为全局静态变量,到我目前的版本(V3.4.10),这几个变量被移入到了timekeeper结构中,现在只需维护一个timekeeper全局静态变量即可:
// kernel/time/timekeeping.c
static struct timekeeper timekeeper;

// include/linux/timekeeper_internal.h
struct timekeeper {
struct clocksource *clock; /* Current clocksource used for timekeeping. */
u32mult; /* NTP adjusted clock multiplier */
intshift;/* The shift value of the current clocksource. */
cycle_t cycle_interval;/* Number of clock cycles in one NTP interval. */
u64xtime_interval;/* Number of clock shifted nano seconds in one NTP interval. */
s64xtime_remainder;/* shifted nano seconds left over when rounding cycle_interval */
u32raw_interval;/* Raw nano seconds accumulated per NTP interval. */
 
u64xtime_nsec;/* Clock shifted nano seconds remainder not stored in xtime.tv_nsec. */
/* Difference between accumulated time and NTP time in ntp
* shifted nano seconds. */
s64ntp_error;
/* Shift conversion between clock shifted nano seconds and
* ntp shifted nano seconds. */
intntp_error_shift;
 
struct timespec xtime;/* The current time */
 
struct timespec wall_to_monotonic;
struct timespec total_sleep_time;/* time spent in suspend */
struct timespec raw_time;/* The raw monotonic time for the CLOCK_MONOTONIC_RAW posix clock. */
 
ktime_t offs_real;/* Offset clock monotonic -> clock realtime */
 
ktime_t offs_boot;/* Offset clock monotonic -> clock boottime */
 
seqlock_t lock;/* Seqlock for all timekeeper values */
};

1)  clock字段则指向了目前timekeeper所使用的时钟源,当有新的精度更高的时钟源被注册时,通过timekeeping_notify函数,change_clocksource函数将会被调用,timekeeper.clock字段将会被更新,指向新的clocksource

2)xtime字段就是上面所说的墙上时间,它是一个timespec结构的变量,它记录了自1970年1月1日以来所经过的时间,因为是timespec结构,所以它的精度可以达到纳秒级,当然那要取决于系统的硬件是否支持这一精度。

3)monotonic time,可以把它理解为自系统启动以来所经过的时间,不计入休眠时间。奇怪的是,内核并没有直接定义一个这样的变量来记录monotonic时间,而是定义了一个变量wall_to_monotonic,记录了墙上时间和monotonic时间之间的偏移量,当需要获得monotonic时间时,把xtime和wall_to_monotonic相加即可,因为默认启动时monotonic时间为0,所以实际上wall_to_monotonic的值是一个负数,它和xtime同一时间被初始化,请参考timekeeping_init函数。

total_sleep_time记录休眠的时间,每次休眠醒来后重新累加该时间,,并调整wall_to_monotonic的值,使其在系统休眠醒来后,monotonic时间不会发生跳变

4) boot time = xtime + wall_to_monotonic+ total_sleep_time
void get_monotonic_boottime(struct timespec *ts)
{
......
do {
seq = read_seqbegin(&timekeeper.lock);
*ts = timekeeper.xtime;
tomono = timekeeper.wall_to_monotonic;
sleep = timekeeper.total_sleep_time;
nsecs = timekeeping_get_ns();
 
} while (read_seqretry(&timekeeper.lock, seq));
 
set_normalized_timespec(ts, ts->tv_sec + tomono.tv_sec + sleep.tv_sec,
ts->tv_nsec + tomono.tv_nsec + sleep.tv_nsec + nsecs);
}

5)raw_time 就是raw monotonic time。不受网络时间的影响,由自身硬件决定。


3.3 timekeeper的初始化

内核自举时由start_kernel()做初始化工作,其中包括timekeeping_init
/*
* timekeeping_init - Initializes the clocksource and common timekeeping values
*/
void __init timekeeping_init(void)
{
struct timekeeper *tk = &timekeeper;
struct clocksource *clock;
unsigned long flags;
struct timespec now, boot, tmp;
 
read_persistent_clock(&now); //从RTC中获取当前时间
 
if (!timespec_valid_strict(&now)) {
pr_warn("WARNING: Persistent clock returned invalid value!\n"
" Check your CMOS/BIOS settings.\n");
now.tv_sec = 0;
now.tv_nsec = 0;
} else if (now.tv_sec || now.tv_nsec)
persistent_clock_exist = true;
 
read_boot_clock(&boot);
if (!timespec_valid_strict(&boot)) {
pr_warn("WARNING: Boot clock returned invalid value!\n"
" Check your CMOS/BIOS settings.\n");
boot.tv_sec = 0;
boot.tv_nsec = 0;
}
 
raw_spin_lock_irqsave(&timekeeper_lock, flags);
write_seqcount_begin(&timekeeper_seq);
ntp_init();
 
clock = clocksource_default_clock();//获取默认的clocksource,如果平台没有特殊指定,那就是jiffies
if (clock->enable)
clock->enable(clock);
tk_setup_internals(tk, clock);//把timekeeper和clocksource进行关联
 
//利用从RTC中读到的当前时间初始化timekeeper中的各种时间
tk_set_xtime(tk, &now);
tk->raw_time.tv_sec = 0;
tk->raw_time.tv_nsec = 0;
if (boot.tv_sec == 0 && boot.tv_nsec == 0)
boot = tk_xtime(tk);
 
set_normalized_timespec(&tmp, -boot.tv_sec, -boot.tv_nsec);
tk_set_wall_to_mono(tk, tmp);
 
tmp.tv_sec = 0;
tmp.tv_nsec = 0;
tk_set_sleep_time(tk, tmp);
 
memcpy(&shadow_timekeeper, &timekeeper, sizeof(timekeeper));
 
write_seqcount_end(&timekeeper_seq);
raw_spin_unlock_irqrestore(&timekeeper_lock, flags);
}

3.4 时间的更新

xtime一旦初始化完成后,timekeeper就开始独立于RTC,利用自身关联的clocksource进行时间的更新操作,通常每个tick的定时中断周期,do_timer会被调用一次。但是,如果配置了NO_HZ选项,可能会在好几个tick后,do_timer才会被调用一次,当然传入的参数是本次更新离上一次更新时相隔了多少个tick周期,系统会保证在clocksource的max_idle_ns时间内调用do_timer,以防止clocksource的溢出:
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
update_wall_time();//更新墙上时间
calc_global_load(ticks);
}

3.5获取时间的接口

  • void getboottime(struct timespec *ts);    获取系统启动时刻的实时时间
  • void get_monotonic_boottime(struct timespec *ts);     获取系统启动以来所经过的时间,包含休眠时间
  • ktime_t ktime_get_boottime(void);   获取系统启动以来所经过的c时间,包含休眠时间,返回ktime类型
  • ktime_t ktime_get(void);    获取系统启动以来所经过的c时间,不包含休眠时间,返回ktime类型
  • void ktime_get_ts(struct timespec *ts) ;   获取系统启动以来所经过的c时间,不包含休眠时间,返回timespec结构
  • unsigned long get_seconds(void);    返回xtime中的秒计数值
  • struct timespec current_kernel_time(void);    返回内核最后一次更新的xtime时间,不累计最后一次更新至今clocksource的计数值
  • void getnstimeofday(struct timespec *ts);    获取当前时间,返回timespec结构
  • void do_gettimeofday(struct timeval *tv);    获取当前时间,返回timeval结构



小结:

目前为止,我们看到的架构大概是这样的:
HFMZ8KIT_5Z}$C)LV%R8ALS.png

clocksource作为各个时钟源的软件抽象,为上层提供统一的视图,timekeeper基于clocksource来维护系统时间。用户态或内核其它部分需要用到时间时,通过接口询问timekeeper来获得当前时间。除此之外,时间子系统还需要哪些功能呢?

对,时钟中断,硬件时钟源定期产生中断,内核据此完成一些周期性的任务,如进程调度和上面讲到的时间更新。这部分现在的架构和2.6中的经典架构又有什么不同呢?

4 clock_event_device

4.1 架构

与维护时间的clocksource类似,对产生中断的时钟源设备,也使用了软件抽象,这样可以实现更好的层间隔离,上层与平台无关,底层只需实现统一的接口,这个接口就是clock_event_device。
clocksource不能被编程,没有产生事件的能力,它主要被用于timekeeper来实现对真实时间进行精确的统计,而clock_event_device则是可编程的,它可以工作在周期触发或单次触发模式,系统可以对它进行编程,以确定下一次事件触发的时间,clock_event_device主要用于实现普通定时器和高精度定时器,同时也用于产生tick事件,供给进程调度子系统使用。
时钟事件设备与通用时间框架中的其他模块的关系如下图所示:


  • 与clocksource一样,系统中可以存在多个clock_event_device,系统会根据它们的精度和能力,选择合适的clock_event_device对系统提供时钟事件服务。在smp系统中,为了减少处理器间的通信开销,基本上每个cpu都会具备一个属于自己的本地clock_event_device,独立地为该cpu提供时钟事件服务,smp中的每个cpu基于本地的clock_event_device,建立自己的tick_device,普通定时器和高精度定时器。
  • 在软件架构上看,clock_event_device被分为了两层,与硬件相关的被放在了machine层,而与硬件无关的通用代码则被集中到了通用时间框架层,这符合内核对软件的设计需求,平台的开发者只需实现平台相关的接口即可,无需关注复杂的上层时间框架。
  • tick_device是基于clock_event_device的进一步封装,用于代替原有的时钟滴答中断,给内核提供tick事件,以完成进程的调度和进程信息统计,负载平衡和时间更新等操作。


4.2 struct clock_event_device

clock_event_devic它代表着一个时钟硬件设备,该设备就好像是一个具有事件触发能力(通常就是指中断)的clocksource,它不停地计数,当计数值达到预先编程设定的数值那一刻,会引发一个时钟事件中断,继而触发该设备的事件处理回调函数,以完成对时钟事件的处理。

struct clock_event_device {
/* 这个回调函数指针通常由通用框架层设置,在时间中断到来时,machine底层的的中断服务程序会调用该回调,框架层利用该回调实现对时钟事件的处理。 */
void(*event_handler)(struct clock_event_device *);
/* 设置下一次时间触发的时间,使用类似于clocksource的cycle计数值(离现在的cycle差值)作为参数。 */
int(*set_next_event)(unsigned long evt,
struct clock_event_device *);
/* 设置下一次时间触发的时间,直接使用ktime时间作为参数。 */
int(*set_next_ktime)(ktime_t expires,
struct clock_event_device *);
ktime_tnext_event;
u64max_delta_ns;//可设置的最大时间差,单位是纳秒。
u64min_delta_ns;//可设置的最小时间差,单位是纳秒。
/* mult shift 与clocksource中的类似,只不过是用于把纳秒转换为cycle。*/
u32mult;
u32shift;
/* 该时钟事件设备的工作模式,两种主要的工作模式分别是:
* CLOCK_EVT_MODE_PERIODIC 周期触发模式,设置后按给定的周期不停地触发事件;
* CLOCK_EVT_MODE_ONESHOT 单次触发模式,只在设置好的触发时刻触发一次;
*/
enum clock_event_modemode;
unsigned intfeatures;
unsigned longretries;
 
void(*broadcast)(const struct cpumask *mask);
/* 函数指针,用于设置时钟事件设备的工作模式。 */
void(*set_mode)(enum clock_event_mode mode,
struct clock_event_device *);
unsigned longmin_delta_ticks;
unsigned longmax_delta_ticks;
 
const char*name;
intrating;//该设备的精度等级。
intirq;
const struct cpumask*cpumask;
struct list_headlist; //系统中注册的所有clock_event_device用该字段挂在全局链表变量clockevent_devices上。
} ____cacheline_aligned;


系统中注册的所有clock_event_device用该字段挂在全局链表变量clockevent_devices上。
static LIST_HEAD(clockevent_devices);
通用时间框架初始化时会注册一个通知链(NOTIFIER),当系统中的时钟时间设备的状态发生变化时,利用该通知链通知系统的其它模块。
/* Notification for clock events */
static RAW_NOTIFIER_HEAD(clockevents_chain);


5 tick

clock_event_device最基本的应用就是实现tick_device,然后给系统定期地产生tick事件

5.1 tick_device

内核的心跳——周期性时钟中断由tick_device产生,tick_device其实是clock_event_device的简单封装,它内嵌了一个clock_event_device指针和它的工作模式:
struct tick_device {
struct clock_event_device *evtdev;
enum tick_device_mode mode;
};

当machine的代码为每个cpu注册clock_event_device时,通知回调函数tick_notify会被调用,进而进入tick_check_new_device函数
/*
* Check, if the new registered device should be used.
*/
static int tick_check_new_device(struct clock_event_device *newdev)
{
struct clock_event_device *curdev;
struct tick_device *td;
int cpu, ret = NOTIFY_OK;
unsigned long flags;
 
raw_spin_lock_irqsave(&tick_device_lock, flags);
 
cpu = smp_processor_id();
if (!cpumask_test_cpu(cpu, newdev->cpumask))//判断注册的clock_event_device是否可用于本cpu
goto out_bc;
 
td = &per_cpu(tick_cpu_device, cpu);//从per-cpu变量中取出本cpu原来的的tick_device
curdev = td->evtdev;
 
/* cpu local device ? */
if (!cpumask_equal(newdev->cpumask, cpumask_of(cpu))) {
 
/*
* If the cpu affinity of the device interrupt can not
* be set, ignore it.
*/
if (!irq_can_set_affinity(newdev->irq))
goto out_bc;
 
/*
* If we have a cpu local device already, do not replace it
* by a non cpu local device
*/
if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))
goto out_bc;
}
 
/*
* If we have an active device, then check the rating and the oneshot
* feature.以决定是否替换原来旧的clock_event_device
*/
if (curdev) {
/*
* Prefer one shot capable devices !
*/
if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&
!(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
goto out_bc;// 新的不支持单触发,但旧的支持,所以不能替换
/*
* Check the rating
*/
if (curdev->rating >= newdev->rating)
goto out_bc;// 旧的比新的精度高,不能替换
}
 
/*
* Replace the eventually existing device by the new
* device. If the current device is the broadcast device, do
* not give it back to the clockevents layer !
* 通过所有检查,表明新的更好,就需要进行替换
*/
if (tick_is_broadcast_device(curdev)) {
clockevents_shutdown(curdev);
curdev = NULL;
}
clockevents_exchange_device(curdev, newdev);
tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
tick_oneshot_notify();
 
raw_spin_unlock_irqrestore(&tick_device_lock, flags);
return NOTIFY_STOP;
 
out_bc:
/*
* Can the new device be used as a broadcast device ?
*/
if (tick_check_broadcast_device(newdev))
ret = NOTIFY_STOP;
 
raw_spin_unlock_irqrestore(&tick_device_lock, flags);
 
return ret;
}
在系统的启动阶段,tick_device是工作在周期触发模式的,直到框架层在合适的时机,才会依配置决定是否开启单触发模式,以便支持NO_HZ和HRTIMER。
 


5.2 tick事件的处理--最简单的情况

clock_event_device最基本的应用就是实现tick_device,然后给系统定期地产生tick事件,通用时间框架对clock_event_device和tick_device的处理相当复杂,因为涉及配置项:CONFIG_NO_HZ和CONFIG_HIGH_RES_TIMERS的组合,两个配置项就有4种组合,这四种组合的处理都有所不同。
在上一节最后的的tick_setup_device函数中
if (td->mode == TICKDEV_MODE_PERIODIC)
tick_setup_periodic(newdev, 0);
else
tick_setup_oneshot(newdev, handler, next_event);
设置中断处理函数,还记得struct clock_event_device的event_handler字段么。
如在TICKDEV_MODE_PERIODIC配置下的中断处理函数为tick_handle_periodic
void tick_handle_periodic(struct clock_event_device *dev)
{
int cpu = smp_processor_id();
ktime_t next;
 
tick_periodic(cpu);
 
if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
return;
 
/* 如果工作在单触发模式,则计算并设置下一次的触发时刻,这里用了一个循环,是为了防止当该函数被调用时,clock_event_device中的计时实际上已经经过了不止一个tick周期,这时候,tick_periodic可能被多次调用,使得jiffies和时间可以被正确地更新。 */
next = ktime_add(dev->next_event, tick_period);
for (;;) {
if (!clockevents_program_event(dev, next, false))
return;
if (timekeeping_valid_for_hres())
tick_periodic(cpu);
next = ktime_add(next, tick_period);
}
}
static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
write_seqlock(&xtime_lock);
 
/* Keep track of the next tick event */
tick_next_period = ktime_add(tick_next_period, tick_period);
 
do_timer(1);
write_sequnlock(&xtime_lock);
}
 
update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}

如果当前cpu负责更新时间,则通过do_timer进行以下操作:
  • 更新jiffies_64变量;
  • 更新墙上时钟;
  • 每10个tick,更新一次cpu的负载信息;
调用update_peocess_times,完成以下事情:
  • 更新进程的时间统计信息;
  • 触发TIMER_SOFTIRQ软件中断,以便系统处理传统的低分辨率定时器;
  • 检查rcu的callback;
  • 通过scheduler_tick触发调度系统进行进程统计和调度工作;


6 低精度定时器

所谓低分辨率定时器,是指这种定时器的计时单位基于jiffies值的计数,也就是说,它的精度只有1/HZ,假如你的内核配置的HZ是1000,那意味着系统中的低分辨率定时器的精度就是1ms。
内核在时钟中断处理程序会执行update_process_times()函数,该函数随即调用run_local_timers()函数
/*
* Called by the local, per-CPU timer interrupt on SMP.
*/
void run_local_timers(void)
{
hrtimer_run_queues();
raise_softirq(TIMER_SOFTIRQ);//执行定时器软中断
}
软件定时器作为软件中断在下半部上下文中执行。


7 高精度定时器

而随着内核的不断演进,大牛们已经对这种低分辨率定时器的精度不再满足,而且,硬件也在不断地发展,系统中的定时器硬件的精度也越来越高,这也给高分辨率定时器的出现创造了条件。内核从2.6.16开始加入了高精度定时器架构。在实现方式上,内核的高分辨率定时器的实现代码几乎没有借用低分辨率定时器的数据结构和代码,内核文档给出的解释主要有以下几点:

  • 低分辨率定时器的代码和jiffies的关系太过紧密,并且默认按32位进行设计,并且它的代码已经经过长时间的优化,目前的使用也是没有任何错误,如果硬要基于它来实现高分辨率定时器,势必会打破原有的时间轮概念,并且会引入一大堆#if--#else判断;
  • 虽然大部分时间里,时间轮可以实现O(1)时间复杂度,但是当有进位发生时,不可预测的O(N)定时器级联迁移时间,这对于低分辨率定时器来说问题不大,可是它大大地影响了定时器的精度;
  • 低分辨率定时器几乎是为“超时”而设计的,并为此对它进行了大量的优化,对于这些以“超时”为目的而使用定时器,它们大多数期望在超时到来之前获得正确的结果,然后删除定时器,精确时间并不是它们主要的目的,例如网络通信、设备IO等等。

为此,内核为高精度定时器重新设计了一套软件架构,它可以为我们提供纳秒级的定时精度,以满足对精确时间有迫切需求的应用程序或内核驱动,例如多媒体应用,音频设备的驱动程序等等。以下的讨论用hrtimer(high resolution timer)表示高精度定时器。

7.1 组织hrtimer

对于高分辨率定时器,我们期望组织它们的数据结构至少具备以下条件:

  • 稳定而且快速的查找能力;
  • 快速地插入和删除定时器的能力;
  • 排序功能;
使用红黑树(rbtree)来组织hrtimer,随着系统的运行,hrtimer不停地被创建和销毁,新的hrtimer按顺序被插入到红黑树中,树的最左边的节点就是最快到期的定时器,内核用一个hrtimer结构来表示一个高精度定时器:
struct hrtimer {
struct timerqueue_nodenode;//红黑树节点的简单封装
ktime_t_softexpires;//软超时时间
enum hrtimer_restart(*function)(struct hrtimer *);//超时函数,返回值决定是否需要重新激活
struct hrtimer_clock_base*base;
unsigned longstate;
......
};
超时函数的返回值:
enum hrtimer_restart {
HRTIMER_NORESTART,/* Timer is not restarted */
HRTIMER_RESTART,/* Timer must be restarted */
};
state字段用于表示hrtimer当前的状态,有几下几种位组合:
#define HRTIMER_STATE_INACTIVE0x00 // 定时器未激活
#define HRTIMER_STATE_ENQUEUED0x01 // 定时器已经被排入红黑树中
#define HRTIMER_STATE_CALLBACK0x02 // 定时器的回调函数正在被调用
#define HRTIMER_STATE_MIGRATE0x04 // 定时器正在CPU之间做迁移
hrtimer的到期时间可以基于以下几种时间基准系统:
enum hrtimer_base_type {
HRTIMER_BASE_MONOTONIC, // 单调递增的monotonic时间,不包含休眠时间
HRTIMER_BASE_REALTIME, // 平常使用的墙上真实时间
HRTIMER_BASE_BOOTTIME, // 单调递增的boottime,包含休眠时间
HRTIMER_MAX_CLOCK_BASES, // 用于后续数组的定义
};
和低分辨率定时器一样,出于效率和上锁的考虑,每个cpu单独管理属于自己的hrtimer,为此,专门定义了一个结构hrtimer_cpu_base:
struct hrtimer_cpu_base {
......
ktime_t expires_next;//所有时间基准中最先到期的高精度定时器
......
struct hrtimer_clock_baseclock_base[HRTIMER_MAX_CLOCK_BASES];//每种时间基准一棵红黑树
};
其中
struct hrtimer_clock_base {
struct hrtimer_cpu_base*cpu_base; // 指向所属cpu的hrtimer_cpu_base结构
......
struct timerqueue_headactive; // 红黑树根,包含了所有使用该时间基准系统的hrtimer
ktime_tresolution; // 时间基准系统的分辨率
ktime_t(*get_time)(void); // 获取该基准系统的时间函数
ktime_tsoftirq_time;// 当用jiffies
ktime_toffset; //
};
红黑树:
struct timerqueue_node {
struct rb_node node; // 红黑树的节点
ktime_t expires; // 硬过期时间
};
 
struct timerqueue_head {
struct rb_root head; // 红黑树的根节点
struct timerqueue_node *next; // 该红黑树中最早到期的节点,也就是最左下的节点
};
hrtimer的到期时间的范围是:hrtimer._soft_expires到hrtimer.node.expires之间任意时间到期。有了这个范围可以选择,定时器系统可以让范围接近的多个定时器在同一时刻同时到期,这种设计可以避免进程频繁地被hrtimer进行唤醒。


7.2 hrtimer如何运转

hrtimer的实现需要一定的硬件基础,它的实现依赖于timekeeper和clock_event_device,系统hrtimer启用时,clock_event_device被设置成工作于CLOCK_EVT_MODE_ONESHOT单触发模式,中断回调函数会被修改为:hrtimer_interrupt。hrtimer系统需要通过timekeeper获取当前的时间,计算与到期时间的差值,并根据该差值,设定该cpu的tick_device(clock_event_device)的下一次的到期时间,时间一到,在clock_event_device的事件回调函数中处理到期的hrtimer。

7.3 hrtimer使用API


//首先我们需要定义一个hrtimer结构的实例
struct hrtimer my_hrtimer
/* 然后用hrtimer_init函数对它进行初始化
* which_clock可以是CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME中的一种,
* mode则可以是相对时间HRTIMER_MODE_REL,也可以是绝对时间HRTIMER_MODE_ABS。
*/
void hrtimer_init(struct hrtimer *timer, clockid_t which_clock,
enum hrtimer_mode mode);
//设定回调函数
timer.function = hr_callback;
//如果定时器无需指定一个到期范围,可以在设定回调函数后直接使用hrtimer_start激活该定时器:
int hrtimer_start(struct hrtimer *timer, ktime_t tim,
const enum hrtimer_mode mode);
//如果需要指定到期范围,则可以使用hrtimer_start_range_ns激活定时器:
hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
unsigned long range_ns, const enum hrtimer_mode mode);
//要取消一个hrtimer,使用hrtimer_cancel:
int hrtimer_cancel(struct hrtimer *timer);
//推后定时器的到期时间:
extern u64
hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
 
/* Forward a hrtimer so it expires after the hrtimer's current now */
static inline u64 hrtimer_forward_now(struct hrtimer *timer,
ktime_t interval)
{
return hrtimer_forward(timer, timer->base->get_time(), interval);
}
//以下几个函数用于获取定时器的当前状态:
static inline int hrtimer_active(const struct hrtimer *timer)
{
return timer->state != HRTIMER_STATE_INACTIVE;
}
 
static inline int hrtimer_is_queued(struct hrtimer *timer)
{
return timer->state & HRTIMER_STATE_ENQUEUED;
}
 
static inline int hrtimer_callback_running(struct hrtimer *timer)
{
return timer->state & HRTIMER_STATE_CALLBACK;
}


7.4 hrtimer的到期处理

高精度定时器系统有3个入口可以对到期定时器进行处理,它们分别是:

  • 没有切换到高精度模式时,在每个jiffie的tick事件中断中进行查询和处理;
  • 在HRTIMER_SOFTIRQ软中断中进行查询和处理;
  • 切换到高精度模式后,在每个clock_event_device的到期事件中断中进行查询和处理;
1)低精度模式  因为系统并不是一开始就会支持高精度模式,而是在系统启动后的某个阶段,等待所有的条件都满足后,才会切换到高精度模式,当系统还没有切换到高精度模式时,所有的高精度定时器运行在低精度模式下,在每个jiffie的tick事件中断中进行到期定时器的查询和处理,显然这时候的精度和低分辨率定时器是一样的(HZ级别)。低精度模式下,每个tick事件中断中,hrtimer_run_queues函数会被调用,由它完成定时器的到期处理。hrtimer_run_queues首先判断目前高精度模式是否已经启用,如果已经切换到了高精度模式,什么也不做,直接返回:

2)高精度模式  切换到高精度模式后,原来给cpu提供tick事件的tick_device(clock_event_device)会被高精度定时器系统接管,它的中断事件回调函数被设置为hrtimer_interrupt,红黑树中最左下的节点的定时器的到期时间被编程到该clock_event_device中,这样每次clock_event_device的中断意味着至少有一个高精度定时器到期。另外,当timekeeper系统中的时间需要修正,或者clock_event_device的到期事件时间被重新编程时,系统会发出HRTIMER_SOFTIRQ软中断,软中断的处理函数run_hrtimer_softirq最终也会调用hrtimer_interrupt函数对到期定时器进行处理,所以在这里我们只要讨论hrtimer_interrupt函数的实现即可。

hrtimer_interrupt函数的前半部分和低精度模式下的hrtimer_run_queues函数完成相同的事情:它用一个for循环遍历各个时间基准系统,查询每个hrtimer_clock_base对应红黑树的左下节点,判断它的时间是否到期,如果到期,通过__run_hrtimer函数,对到期定时器进行处理,在处理完所有到期定时器后,下一个到期定时器的到期时间保存在变量expires_next中,接下来的工作就是把这个到期时间编程到tick_device中。

7.5 切换到高精度模式

尽管内核配置成支持高精度定时器,但并不是一开始就工作于高精度模式,系统在启动的开始阶段,还是按照传统的模式在运行:tick_device按HZ频率定期地产生tick事件,这时的hrtimer工作在低分辨率模式,到期事件在每个tick事件中断中由hrtimer_run_queues函数处理,同时,在低分辨率定时器(时间轮)的软件中断TIMER_SOFTIRQ中,hrtimer_run_pending会被调用,系统在这个函数中判断系统的条件是否满足切换到高精度模式,如果条件满足,则会切换至高分辨率模式,另外提一下,NO_HZ模式也是在该函数中判断并切换。

切换至高分辨率模式由函数hrtimer_switch_to_hres()完成,
/*
* Switch to high resolution mode
*/
static int hrtimer_switch_to_hres(void)
{
int i, 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);
/* tick_init_highres()函数把tick_device切换到CLOCK_EVT_FEAT_ONESHOT模式,同时把clock_event_device的回调handler设置为hrtimer_interrupt,这样设置以后,tick_device的中断回调将由hrtimer_interrupt接管 */
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;
}
/* 设置hres_active标志,以表明高精度模式已经切换,然后把3个时间基准系统的resolution字段设为KTIME_HIGH_RES */
base->hres_active = 1;
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++)
base->clock_base[i].resolution = KTIME_HIGH_RES;
 
/* 因为tick_device被高精度定时器接管,它将不会再提供原有的tick事件机制,所以需要由高精度定时器系统模拟一个tick事件设备,继续为系统提供tick事件能力 */
tick_setup_sched_timer();
/* "Retrigger" the interrupt to get things going */
retrigger_next_event(NULL);
local_irq_restore(flags);
return 1;
}
当系统切换到高精度模式后,tick_device被高精度定时器系统接管,不再定期地产生tick事件,我们知道,到目前的版本为止(V3.10),内核还没有彻底废除jiffies机制,系统还是依赖定期到来的tick事件,供进程调度系统和时间更新等操作,大量存在的低精度定时器也仍然依赖于jiffies的计数,所以,尽管tick_device被接管,高精度定时器系统还是要想办法继续提供定期的tick事件。为了达到这一目的,内核使用了一个取巧的办法:既然高精度模式已经启用,可以定义一个hrtimer,把它的到期时间设定为一个jiffy的时间,当这个hrtimer到期时,在这个hrtimer的到期回调函数(tick_sched_timer()函数)中,进行和原来的tick_device同样的操作(tick_handle_periodic()函数,然后把该hrtimer的到期时间顺延一个jiffy周期,如此反复循环,完美地模拟了原有tick_device的功能。


8 动态时钟框架(CONFIG_NO_HZ、tickless)

在前面章节的讨论中,我们一直基于一个假设:Linux中的时钟事件都是由一个周期时钟提供,不管系统中的clock_event_device是工作于周期触发模式,还是工作于单触发模式,也不管定时器系统是工作于低分辨率模式,还是高精度模式,内核都竭尽所能,用不同的方式提供周期时钟,以产生定期的tick事件,tick事件或者用于全局的时间管理(jiffies和时间的更新),或者用于本地cpu的进程统计、时间轮定时器框架等等。周期性时钟虽然简单有效,但是也带来了一些缺点,尤其在系统的功耗上,因为就算系统目前无事可做,也必须定期地发出时钟事件,激活系统。为此,内核的开发者提出了动态时钟这一概念,我们可以通过内核的配置项CONFIG_NO_HZ来激活特性。有时候这一特性也被叫做tickless,不过还是把它称呼为动态时钟比较合适,因为并不是真的没有tick事件了,只是在系统无事所做的idle阶段,我们可以通过停止周期时钟来达到降低系统功耗的目的,只要有进程处于活动状态,时钟事件依然会被周期性地发出。

在动态时钟正确工作之前,系统需要切换至动态时钟模式,而要切换至动态时钟模式,需要一些前提条件,最主要的一条就是cpu的时钟事件设备必须要支持单触发模式,当条件满足时,系统切换至动态时钟模式,接着,由idle进程决定是否可以停止周期时钟,退出idle进程时则需要恢复周期时钟。


8.1 struct tick_sched

使用一个高精度定时器来模拟传统的周期时钟或者系统配置为动态时钟时,核心数据结构为struct tick_sched
struct tick_sched {
struct hrtimersched_timer;//在高精度模式下,模拟周期时钟的一个hrtimer
unsigned longcheck_clocks;//用于实现clock_event_device和clocksource的异步通知机制,帮助系统切换至高精度模式或者是动态时钟模式。
/* 保存动态时钟的工作模式,取值:
* NOHZ_MODE_INACTIVE 系统动态时钟尚未激活
* NOHZ_MODE_LOWRES 系统工作于低分辨率模式下的动态时钟
* NOHZ_MODE_HIGHRES 系统工作于高精度模式下的动态时钟
* 基于低分辨率和高精度模式下,动态时钟的实现稍有不同,
*/
enum tick_nohz_modenohz_mode;
ktime_tidle_tick;//该字段用于保存停止周期时钟是的内核时间,当退出idle时要恢复周期时钟,需要使用该时间,以保持系统中时间线(jiffies)的正确性
intinidle;
inttick_stopped;//该字段用于表明idle状态的周期时钟已经停止。
unsigned longidle_jiffies;//系统进入idle时的jiffies值,用于信息统计。
unsigned longidle_calls;//系统进入idle的统计次数。
unsigned longidle_sleeps;//系统进入idle且成功停掉周期时钟的次数。
intidle_active;//表明目前系统是否处于idle状态中。
ktime_tidle_entrytime;//系统进入idle的时刻。
ktime_tidle_waketime;// idle状态被打断的时刻。
ktime_tidle_exittime;// 系统退出idle的时刻。
ktime_tidle_sleeptime;//累计各次idle中停止周期时钟的总时间。
ktime_tiowait_sleeptime;
ktime_tsleep_length;//本次idle中停止周期时钟的时间。
unsigned longlast_jiffies;//系统中最后一次周期时钟的jiffies值。
unsigned longnext_jiffies;//预计下一次周期时钟的jiffies。
ktime_tidle_expires;//进入idle后,下一个最先到期的定时器时刻。
intdo_timer_last;
};

8.2 低分辨率下的动态时钟

首先切换到nohz模式:系统工作于周期时钟模式,定期地发出tick事件中断,tick事件中断触发定时器软中断:TIMER_SOFTIRQ,执行软中断处理函数run_timer_softirq,run_timer_softirq调用hrtimer_run_pending函数:
void hrtimer_run_pending(void)
{
if (hrtimer_hres_active())
return;
......
if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))//不支持hrtimer,切换到NOHZ_MODE_LOWRES模式
hrtimer_switch_to_hres();//支持hrtimer,切换到NOHZ_MODE_HIGHRES模式
}

如果不支持高精度定时器,在tick_check_oneshot_change()中,把tick_device的工作模式设置为单触发模式,并把它的中断事件回调函数置换为tick_nohz_handler,接着把tick_sched结构中的模式字段设置为NOHZ_MODE_LOWRES

当切换至低分辨率动态时钟模式后,tick_device的事件中断处理函数会被设置为tick_nohz_handler,总体来说,它和周期时钟模式的事件处理函数tick_handle_periodic所完成的工作大致类似:更新时间、更新jiffies计数值、调用update_process_time更新进程信息和触发定时器软中断等等,最后重新编程tick_device,使得它在下一个正确的tick时刻再次触发本函数。


8.3 高精度模式下的动态时钟

高精度模式和低分辨率模式类似,区别在于
1) 在hrtimer_run_pending()函数中,是是切换到低分辨率模式还是切换到高分辨率模式;
2)如果是NOHZ_MODE_HIGHRES,是用hrtimer模拟tick,则对tick_sched结构的sched_timer定时器进行设置,如果是NOHZ_MODE_LOWRES,则直接对tick_device编程设置下一个触发时间。



8.4 动态时钟:停止周期tick时钟事件

开启动态时钟模式后,周期时钟的开启和关闭由idle进程控制,idle进程内最终是一个循环,循环的一开始通过tick_nohz_idle_enter检测是否允许关闭周期时钟若干时间,然后进入低功耗的idle模式,当有中断事件使得cpu退出低功耗idle模式后,判断是否有新的进程被激活从而需要重新调度,如果需要则通过tick_nohz_idle_exit重新启用周期时钟,然后重新进行进程调度,等待下一次idle的发生,我们可以用下图来表示:


 停止周期时钟的时机在tick_nohz_idle_enter函数中,它把主要的工作交由tick_nohz_stop_sched_tick函数来完成。这时候既然idle进程在运行,说明系统中的其他进程都在等待某种事件,系统处于无事所做的状态,唯一要处理的就是中断,并且除了定时器的时间中断,产生周期时钟是没有必要的,我们可以据此推算出周期时钟可以停止的tick数,然后重新对tick_device进行编程,使得在最早一个定时器到期前都不会产生周期时钟,实际上,tick_nohz_stop_sched_tick还做了一些限制:当下一个定时器的到期时间与当前jiffies值只相差1时,不会停止周期时钟,当定时器的到期时间与当前的jiffies值相差的时间大于timekeeper允许的最大idle时间时,则下一个tick时刻被设置timekeeper允许的最大idle时间,这主要是为了防止太长时间不去更新timekeeper中的系统时间,有可能导致clocksource的溢出问题。
还有一个情况:idle设置的第一个定时器到期后,会把tick_device编程为紧跟着的下一个tick周期的时刻被触发,如果刚才的定时器处理后,并没有激活新的进程,我们的期望是周期时钟可以用下一个新的定时器重新计算可以停止的时间,而不是下一个tick时刻。对这个问题的解决是定时中断处理完后irq_exit()中加入判断。

当在idle进程中停止周期时钟后,在某一时刻,有新的进程被激活,在重新调度前,tick_nohz_idle_exit会被调用,该函数负责恢复被停止的周期时钟。


8.5 动态时钟对中断的影响

在进入和退出中断时,因为动态时钟的关系,中断系统需要作出一些配合。先说中断发生于周期时钟停止期间,如果不做任何处理,中断服务程序中如果要访问jiffies计数值,可能得到一个滞后的jiffies值,因为正常状态下,jiffies值会在恢复周期时钟时正确地更新,所以,为了防止这种情况发生,在进入中断的irq_enter期间,需要更新jiffies数值。
退出定时器中断时,可能不应该把tick_device编程为下一个tick周期,需要额外判断。这在前面已经讲过。


参考资料
博客http://blog.csdn.net/droidphone/article/category/1263459
《linux内核设计与实现》
《深入理解linux内核》
0 0
原创粉丝点击