kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

Linux时间子系统之五:低分辨率定时器的原理和实现

http://blog.csdn.net/DroidPhone/article/details/8051405

利用定时器,我们可以设定在未来的某一时刻,触发一个特定的事件。所谓低分辨率定时器,是指这种定时器的计时单位基于jiffies值的计数,也就是说,它的精度只有1/HZ,假如你的内核配置的HZ是1000,那意味着系统中的低分辨率定时器的精度就是1ms。早期的内核版本中,内核并不支持高精度定时器,理所当然只能使用这种低分辨率定时器,我们有时候把这种基于HZ的定时器机制成为时间轮:time wheel。虽然后来出现了高分辨率定时器,但它只是内核的一个可选配置项,所以直到目前最新的内核版本,这种低分辨率定时器依然被大量地使用着。

1. 定时器的使用方法

在讨论定时器的实现原理之前,我们先看看如何使用定时器。要在内核编程中使用定时器,首先我们要定义一个time_list结构,该结构在include/Linux/timer.h中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct timer_list {
	/* 
	 * All fields that change during normal runtime grouped to the 
	 * same cacheline 
	 */
	struct list_head entry;
	unsigned long expires;
	struct tvec_base *base;

	void (*function)(unsigned long);
	unsigned long data;

	int slack;
		......
};

entry  字段用于把一组定时器组成一个链表,至于内核如何对定时器进行分组,我们会在后面进行解释。

expires  字段指出了该定时器的到期时刻,也就是期望定时器到期时刻的jiffies计数值。

base  每个cpu拥有一个自己的用于管理定时器的tvec_base结构,该字段指向该定时器所属的cpu所对应tvec_base结构。

function  字段是一个函数指针,定时器到期时,系统将会调用该回调函数,用于响应该定时器的到期事件。

data  该字段用于上述回调函数的参数。

slack  对有些对到期时间精度不太敏感的定时器,到期时刻允许适当地延迟一小段时间,该字段用于计算每次延迟的HZ数。

要定义一个timer_list,我们可以使用静态和动态两种办法,静态方法使用DEFINE_TIMER宏:

1
#define DEFINE_TIMER(_name, _function, _expires, _data)

该宏将得到一个名字为name,并分别用function,expires,data参数填充timer_list的相关字段。

如果要使用动态的方法,则可以自己声明一个timer_list结构,然后手动初始化它的各个字段:

1
2
3
4
5
6
struct timer_list timer;
......
init_timer(&timer);
timer.function = _function;
timer.expires = _expires;
timer.data = _data;

要激活一个定时器,我们只要调用add_timer即可:

1
add_timer(&timer);

要修改定时器的到期时间,我们只要调用mod_timer即可:

1
mod_timer(&timer, jiffies+50);

要移除一个定时器,我们只要调用del_timer即可:

1
del_timer(&timer);

定时器系统还提供了以下这些API供我们使用:

1
2
3
4
5
void add_timer_on(struct timer_list *timer, int cpu);  // 在指定的cpu上添加定时器
int mod_timer_pending(struct timer_list *timer, unsigned long expires);  //  只有当timer已经处在激活状态时,才修改timer的到期时刻
int mod_timer_pinned(struct timer_list *timer, unsigned long expires);  //  当
void set_timer_slack(struct timer_list *time, int slack_hz);  //  设定timer允许的到期时刻的最大延迟,用于对精度不敏感的定时器
int del_timer_sync(struct timer_list *timer);  //  如果该timer正在被处理中,则等待timer处理完成才移除该timer

2. 定时器的软件架构

低分辨率定时器是基于HZ来实现的,也就是说,每个tick周期,都有可能有定时器到期,关于tick如何产生,请参考:Linux时间子系统之四:定时器的引擎:clock_event_device。系统中有可能有成百上千个定时器,难道在每个tick中断中遍历一下所有的定时器,检查它们是否到期?内核当然不会使用这么笨的办法,它使用了一个更聪明的办法:按定时器的到期时间对定时器进行分组。因为目前的多核处理器使用越来越广泛,连智能手机的处理器动不动就是4核心,内核对多核处理器有较好的支持,低分辨率定时器在实现时也充分地考虑了多核处理器的支持和优化。为了较好地利用cache line,也为了避免cpu之间的互锁,内核为多核处理器中的每个cpu单独分配了管理定时器的相关数据结构和资源,每个cpu独立地管理属于自己的定时器。

2.1 定时器的分组

首先,内核为每个cpu定义了一个tvec_base结构指针:

1
static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;

tvec_base结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;

running_timer  该字段指向当前cpu正在处理的定时器所对应的timer_list结构。

timer_jiffies  该字段表示当前cpu定时器所经历过的jiffies数,大多数情况下,该值和jiffies计数值相等,当cpu的idle状态连续持续了多个jiffies时间时,当退出idle状态时,jiffies计数值就会大于该字段,在接下来的tick中断后,定时器系统会让该字段的值追赶上jiffies值。

next_timer  该字段指向该cpu下一个即将到期的定时器。

tv1--tv5  这5个字段用于对定时器进行分组,实际上,tv1--tv5都是一个链表数组,其中tv1的数组大小为TVR_SIZE, tv2 tv3 tv4 tv5的数组大小为TVN_SIZE,根据CONFIG_BASE_SMALL配置项的不同,它们有不同的大小:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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];
};

默认情况下,没有使能CONFIG_BASE_SMALL,TVR_SIZE的大小是256,TVN_SIZE的大小则是64,当需要节省内存空间时,也可以使能CONFIG_BASE_SMALL,这时TVR_SIZE的大小是64,TVN_SIZE的大小则是16,以下的讨论我都是基于没有使能CONFIG_BASE_SMALL的情况。当有一个新的定时器要加入时,系统根据定时器到期的jiffies值和timer_jiffies字段的差值来决定该定时器被放入tv1至tv5中的哪一个数组中,最终,系统中所有的定时器的组织结构如下图所示:

图 2.1.1 定时器在系统中的组织结构

2.2 定时器的添加

要加入一个新的定时器,我们可以通过api函数add_timer或mod_timer来完成,最终的工作会交由internal_add_timer函数来处理。该函数按以下步骤进行处理:

计算定时器到期时间和所属cpu的tvec_base结构中的timer_jiffies字段的差值,记为idx;

根据idx的值,选择该定时器应该被放到tv1–tv5中的哪一个链表数组中,可以认为tv1-tv5分别占据一个32位数的不同比特位,tv1占据最低的8位,tv2占据紧接着的6位,然后tv3再占位,以此类推,最高的6位分配给tv5。最终的选择规则如下表所示:

1
2
3
4
5
6
链表数组     idx范围
tv1   0-255(2^8)
tv2   256--16383(2^14)
tv3   16384--1048575(2^20)
tv4   1048576--67108863(2^26)
tv5   67108864--4294967295(2^32)

确定链表数组后,接着要确定把该定时器放入数组中的哪一个链表中,如果时间差idx小于256,按规则要放入tv1中,因为tv1包含了256个链表,所以可以简单地使用timer_list.expires的低8位作为数组的索引下标,把定时器链接到tv1中相应的链表中即可。如果时间差idx的值在256–18383之间,则需要把定时器放入tv2中,同样的,使用timer_list.expires的8–14位作为数组的索引下标,把定时器链接到tv2中相应的链表中,。定时器要加入tv3 tv4 tv5使用同样的原理。经过这样分组后的定时器,在后续的tick事件中,系统可以很方便地定位并取出相应的到期定时器进行处理。以上的讨论都体现在internal_add_timer的代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void internal_add_timer(struct tvec_base *base, struct timer_list *timer)
{
	unsigned long expires = timer->expires;
	unsigned long idx = expires - base->timer_jiffies;
	struct list_head *vec;

	if (idx < TVR_SIZE) {
		int i = expires & TVR_MASK;
		vec = base->tv1.vec + i;
	} else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
		int i = (expires >> TVR_BITS) & TVN_MASK;
		vec = base->tv2.vec + i;
	} else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
		int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;
		vec = base->tv3.vec + i;
	} else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
		int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
		vec = base->tv4.vec + i;
	} else if ((signed long) idx < 0) {
				......
	} else {
				......
		i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
		vec = base->tv5.vec + i;
	}
	list_add_tail(&timer->entry, vec);
}
2.2 定时器的到期处理

经过2.1节的处理后,系统中的定时器按到期时间有规律地放置在tv1–tv5各个链表数组中,其中tv1中放置着在接下来的256个jiffies即将到期的定时器列表,需要注意的是,并不是tv1.vec[0]中放置着马上到期的定时器列表,tv1.vec[1]中放置着将在jiffies+1到期的定时器列表。因为base.timer_jiffies的值一直在随着系统的运行而动态地增加,原则上是每个tick事件会加1,base.timer_jiffies代表者该cpu定时器系统当前时刻,定时器也是动态地加入头256个链表tv1中,按2.1节的讨论,定时器加入tv1中使用的下标索引是定时器到期时间expires的低8位,所以假设当前的base.timer_jiffies值是0x34567826,则马上到期的定时器是在tv1.vec[0x26]中,如果这时候系统加入一个在jiffies值0x34567828到期的定时器,他将会加入到tv1.vec[0x28]中,运行两个tick后,base.timer_jiffies的值会变为0x34567828,很显然,在每次tick事件中,定时器系统只要以base.timer_jiffies的低8位作为索引,取出tv1中相应的链表,里面正好包含了所有在该jiffies值到期的定时器列表。

那什么时候处理tv2–tv5中的定时器?每当base.timer_jiffies的低8位为0值时,这表明base.timer_jiffies的第8-13位有进位发生,这6位正好代表着tv2,这时只要按base.timer_jiffies的第8-13位的值作为下标,移出tv2中对应的定时器链表,然后用internal_add_timer把它们从新加入到定时器系统中来,因为这些定时器一定会在接下来的256个tick期间到期,所以它们肯定会被加入到tv1数组中,这样就完成了tv2往tv1迁移的过程。同样地,当base.timer_jiffies的第8-13位为0时,这表明base.timer_jiffies的第14-19位有进位发生,这6位正好代表着tv3,按base.timer_jiffies的第14-19位的值作为下标,移出tv3中对应的定时器链表,然后用internal_add_timer把它们从新加入到定时器系统中来,显然它们会被加入到tv2中,从而完成tv3到tv2的迁移,tv4,tv5的处理可以以此作类推。具体迁移的代码如下,参数index为事先计算好的高一级tv的需要迁移的数组索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int cascade(struct tvec_base *base, struct tvec *tv, int index)
{
	/* cascade all the timers from tv up one level */
	struct timer_list *timer, *tmp;
	struct list_head tv_list;

	list_replace_init(tv->vec + index, &tv_list);  //  移除需要迁移的链表

	/* 
	 * We are removing _all_ timers from the list, so we 
	 * don't have to detach them individually. 
	 */
	list_for_each_entry_safe(timer, tmp, &tv_list, entry) {
		BUG_ON(tbase_get_base(timer->base) != base);
				//  重新加入到定时器系统中,实际上将会迁移到下一级的tv数组中
		internal_add_timer(base, timer);  
	}

	return index;
}

每个tick事件到来时,内核会在tick定时中断处理期间激活定时器软中断:TIMER_SOFTIRQ,关于软件中断,请参考另一篇博文:Linux中断(interrupt)子系统之五:软件中断(softIRQ。TIMER_SOFTIRQ的执行函数是__run_timers,它实现了本节讨论的逻辑,取出tv1中到期的定时器,执行定时器的回调函数,由此可见,低分辨率定时器的回调函数是执行在软件中断上下文中的,这点在写定时器的回调函数时需要注意。__run_timers的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static inline void __run_timers(struct tvec_base *base)
{
	struct timer_list *timer;

	spin_lock_irq(&base->lock);
		/* 同步jiffies,在NO_HZ情况下,base->timer_jiffies可能落后不止一个tick  */
	while (time_after_eq(jiffies, base->timer_jiffies)) {  
		struct list_head work_list;
		struct list_head *head = &work_list;
				/*  计算到期定时器链表在tv1中的索引  */
		int index = base->timer_jiffies & TVR_MASK;  

		/* 
		 * /*  tv2--tv5定时器列表迁移处理  */
		 */
		if (!index &&
			(!cascade(base, &base->tv2, INDEX(0))) &&              
				(!cascade(base, &base->tv3, INDEX(1))) &&      
					!cascade(base, &base->tv4, INDEX(2)))  
			cascade(base, &base->tv5, INDEX(3));  
				/*  该cpu定时器系统运行时间递增一个tick  */                 
		++base->timer_jiffies;  
				/*  取出到期的定时器链表  */                                       
		list_replace_init(base->tv1.vec + index, &work_list);
				/*  遍历所有的到期定时器  */          
		while (!list_empty(head)) {                                    
			void (*fn)(unsigned long);
			unsigned long data;

			timer = list_first_entry(head, struct timer_list,entry);
			fn = timer->function;
			data = timer->data;

			timer_stats_account_timer(timer);

			base->running_timer = timer;    /*  标记正在处理的定时器  */
			detach_timer(timer, 1);

			spin_unlock_irq(&base->lock);
			call_timer_fn(timer, fn, data);  /*  调用定时器的回调函数  */
			spin_lock_irq(&base->lock);
		}
	}
	base->running_timer = NULL;
	spin_unlock_irq(&base->lock);
}

通过上面的讨论,我们可以发现,内核的低分辨率定时器的实现非常精妙,既实现了大量定时器的管理,又实现了快速的O(1)查找到期定时器的能力,利用巧妙的数组结构,使得只需在间隔256个tick时间才处理一次迁移操作,5个数组就好比是5个齿轮,它们随着base->timer_jifffies的增长而不停地转动,每次只需处理第一个齿轮的某一个齿节,低一级的齿轮转动一圈,高一级的齿轮转动一个齿,同时自动把即将到期的定时器迁移到上一个齿轮中,所以低分辨率定时器通常又被叫做时间轮:time wheel。事实上,它的实现是一个很好的空间换时间软件算法。

3. 定时器软件中断

系统初始化时,start_kernel会调用定时器系统的初始化函数init_timers:

1
2
3
4
5
6
7
8
9
10
11
void __init init_timers(void)
{      
	int err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE, 
				(void *)(long)smp_processor_id());

	init_timer_stats();

	BUG_ON(err != NOTIFY_OK);
	register_cpu_notifier(&timers_nb);  /* 注册cpu notify,以便在hotplug时在cpu之间进行定时器的迁移 */
	open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

可见,open_softirq把run_timer_softirq注册为TIMER_SOFTIRQ的处理函数,另外,当cpu的每个tick事件到来时,在事件处理中断中,update_process_times会被调用,该函数会进一步调用run_local_timers,run_local_timers会触发TIMER_SOFTIRQ软中断:

1
2
3
4
5
void run_local_timers(void)
{
	hrtimer_run_queues();
	raise_softirq(TIMER_SOFTIRQ);
}

TIMER_SOFTIRQ的处理函数是run_timer_softirq:

1
2
3
4
5
6
7
8
9
static void run_timer_softirq(struct softirq_action *h)
{
	struct tvec_base *base = __this_cpu_read(tvec_bases);

	hrtimer_run_pending();

	if (time_after_eq(jiffies, base->timer_jiffies))
		__run_timers(base);
}

好啦,终于看到__run_timers函数了,2.2节已经介绍过,正是这个函数完成了对到期定时器的处理工作,也完成了时间轮的不停转动。

Linux时间子系统之四:定时器的引擎:clock_event_device

http://blog.csdn.net/DroidPhone/article/details/8017604

早期的内核版本中,进程的调度基于一个称之为tick的时钟滴答,通常使用时钟中断来定时地产生tick信号,每次tick定时中断都会进行进程的统计和调度,并对tick进行计数,记录在一个jiffies变量中,定时器的设计也是基于jiffies。这时候的内核代码中,几乎所有关于时钟的操作都是在machine级的代码中实现,很多公共的代码要在每个平台上重复实现。随后,随着通用时钟框架的引入,内核需要支持高精度的定时器,为此,通用时间框架为定时器硬件定义了一个标准的接口:clock_event_device,machine级的代码只要按这个标准接口实现相应的硬件控制功能,剩下的与平台无关的特性则统一由通用时间框架层来实现。

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事件,以完成进程的调度和进程信息统计,负载平衡和时间更新等操作。

2. 时钟事件设备相关数据结构

2.1 struct clock_event_device

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
struct clock_event_device {
	void            (*event_handler)(struct clock_event_device *);
	int         (*set_next_event)(unsigned long evt,
						  struct clock_event_device *);
	int         (*set_next_ktime)(ktime_t expires,
						  struct clock_event_device *);
	ktime_t         next_event;
	u64         max_delta_ns;
	u64         min_delta_ns;
	u32         mult;
	u32         shift;
	enum clock_event_mode   mode;
	unsigned int        features;
	unsigned long       retries;

	void            (*broadcast)(const struct cpumask *mask);
	void            (*set_mode)(enum clock_event_mode mode,
						struct clock_event_device *);
	unsigned long       min_delta_ticks;
	unsigned long       max_delta_ticks;

	const char      *name;
	int         rating;
	int         irq;
	const struct cpumask    *cpumask;
	struct list_head    list;
} ____cacheline_aligned;

event_handler  该字段是一个回调函数指针,通常由通用框架层设置,在时间中断到来时,machine底层的的中断服务程序会调用该回调,框架层利用该回调实现对时钟事件的处理。

set_next_event  设置下一次时间触发的时间,使用类似于clocksource的cycle计数值(离现在的cycle差值)作为参数。

set_next_ktime  设置下一次时间触发的时间,直接使用ktime时间作为参数。

max_delta_ns  可设置的最大时间差,单位是纳秒。

min_delta_ns  可设置的最小时间差,单位是纳秒。

mult shift  与clocksource中的类似,只不过是用于把纳秒转换为cycle。

mode  该时钟事件设备的工作模式,两种主要的工作模式分别是:
	CLOCK_EVT_MODE_PERIODIC  周期触发模式,设置后按给定的周期不停地触发事件;
	CLOCK_EVT_MODE_ONESHOT  单次触发模式,只在设置好的触发时刻触发一次;

set_mode  函数指针,用于设置时钟事件设备的工作模式。

rating  表示该设备的精度等级。

list  系统中注册的时钟事件设备用该字段挂在全局链表变量clockevent_devices上。
2.2 全局变量clockevent_devices

系统中所有注册的clock_event_device都会挂在该链表下面,它在kernel/time/clockevents.c中定义:

1
static LIST_HEAD(clockevent_devices);
2.3 全局变量clockevents_chain

通用时间框架初始化时会注册一个通知链(NOTIFIER),当系统中的时钟时间设备的状态发生变化时,利用该通知链通知系统的其它模块。

1
2
/* Notification for clock events */
static RAW_NOTIFIER_HEAD(clockevents_chain);

3. clock_event_device的初始化和注册

每一个machine,都要定义一个自己的machine_desc结构,该结构定义了该machine的一些最基本的特性,其中需要设定一个sys_timer结构指针,machine级的代码负责定义sys_timer结构,sys_timer的声明很简单:

1
2
3
4
5
6
7
8
struct sys_timer {
	void            (*init)(void);
	void            (*suspend)(void);
	void            (*resume)(void);
#ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
	unsigned long       (*offset)(void);
#endif
};

通常,我们至少要定义它的init字段,系统初始化阶段,该init回调会被调用,该init回调函数的主要作用就是完成系统中的clocksource和clock_event_device的硬件初始化工作,以samsung的exynos4为例,在V3.4内核的代码树中,machine_desc的定义如下:

1
2
3
4
5
6
7
8
9
10
11
MACHINE_START(SMDK4412, "SMDK4412")
	/* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
	/* Maintainer: Changhwan Youn <chaos.youn@samsung.com> */
	.atag_offset    = 0x100,
	.init_irq   = exynos4_init_irq,
	.map_io     = smdk4x12_map_io,
	.handle_irq = gic_handle_irq,
	.init_machine   = smdk4x12_machine_init,
	.timer      = &exynos4_timer,
	.restart    = exynos4_restart,
MACHINE_END

定义的sys_timer是exynos4_timer,它的定义和init回调定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void __init exynos4_timer_init(void)
{
	if (soc_is_exynos4210())
		mct_int_type = MCT_INT_SPI;
	else
		mct_int_type = MCT_INT_PPI;

	exynos4_timer_resources();
	exynos4_clocksource_init();
	exynos4_clockevent_init();
}

struct sys_timer exynos4_timer = {
	.init       = exynos4_timer_init,
};

exynos4_clockevent_init函数显然是初始化和注册clock_event_device的合适时机,在这里,它注册了一个rating为250的clock_event_device,并把它指定给cpu0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static struct clock_event_device mct_comp_device = {
	.name       = "mct-comp",
	.features       = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
	.rating     = 250,
	.set_next_event = exynos4_comp_set_next_event,
	.set_mode   = exynos4_comp_set_mode,
};
......
static void exynos4_clockevent_init(void)
{
	clockevents_calc_mult_shift(&mct_comp_device, clk_rate, 5);
		......
	mct_comp_device.cpumask = cpumask_of(0);
	clockevents_register_device(&mct_comp_device);

	setup_irq(EXYNOS4_IRQ_MCT_G0, &mct_comp_event_irq);
}

因为这个阶段其它cpu核尚未开始工作,所以该clock_event_device也只是在启动阶段给系统提供服务,实际上,因为exynos4是一个smp系统,具备2-4个cpu核心,前面说过,smp系统中,通常会使用各个cpu的本地定时器来为每个cpu单独提供时钟事件服务,继续翻阅代码,在系统初始化的后段,kernel_init会被调用,它会调用smp_prepare_cpus,其中会调用percpu_timer_setup函数,在arch/arm/kernel/smp.c中,为每个cpu定义了一个clock_event_device:

1
2
3
4
/* 
 * Timer (local or broadcast) support 
 */
static DEFINE_PER_CPU(struct clock_event_device, percpu_clockevent);

percpu_timer_setup最终会调用exynos4_local_timer_setup函数完成对本地clock_event_device的初始化工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int __cpuinit exynos4_local_timer_setup(struct clock_event_device *evt)
{
	......
	evt->name = mevt->name;
	evt->cpumask = cpumask_of(cpu);
	evt->set_next_event = exynos4_tick_set_next_event;
	evt->set_mode = exynos4_tick_set_mode;
	evt->features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT;
	evt->rating = 450;

	clockevents_calc_mult_shift(evt, clk_rate / (TICK_BASE_CNT + 1), 5);
	......
	clockevents_register_device(evt);
	......
	enable_percpu_irq(EXYNOS_IRQ_MCT_LOCALTIMER, 0);
	......
	return 0;
}

由此可见,每个cpu的本地clock_event_device的rating是450,比启动阶段的250要高,显然,之前注册给cpu0的精度要高,系统会用本地clock_event_device替换掉原来分配给cpu0的clock_event_device,至于怎么替换?我们先停一停,到这里我们一直在讨论machine级别的初始化和注册,让我们回过头来,看看框架层的初始化。在继续之前,让我们看看整个clock_event_device的初始化的调用序列图:

图3.1 clock_event_device的系统初始化

由上面的图示可以看出,框架层的初始化步骤很简单,又start_kernel开始,调用tick_init,它位于kernel/time/tick-common.c中,也只是简单地调用clockevents_register_notifier,同时把类型为notifier_block的tick_notifier作为参数传入,回看2.3节,clockevents_register_notifier注册了一个通知链,这样,当系统中的clock_event_device状态发生变化时(新增,删除,挂起,唤醒等等),tick_notifier中的notifier_call字段中设定的回调函数tick_notify就会被调用。接下来start_kernel调用了time_init函数,该函数通常定义在体系相关的代码中,正如前面所讨论的一样,它主要完成machine级别对时钟系统的初始化工作,最终通过clockevents_register_device注册系统中的时钟事件设备,把每个时钟时间设备挂在clockevent_device全局链表上,最后通过clockevent_do_notify触发框架层事先注册好的通知链,其实就是调用了tick_notify函数,我们主要关注CLOCK_EVT_NOTIFY_ADD通知,其它通知请自行参考代码,下面是tick_notify的简化版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int tick_notify(struct notifier_block *nb, unsigned long reason,
				   void *dev)
{
	switch (reason) {

	case CLOCK_EVT_NOTIFY_ADD:
		return tick_check_new_device(dev);

	case CLOCK_EVT_NOTIFY_BROADCAST_ON:
	case CLOCK_EVT_NOTIFY_BROADCAST_OFF:
	case CLOCK_EVT_NOTIFY_BROADCAST_FORCE:
			......
	case CLOCK_EVT_NOTIFY_BROADCAST_ENTER:
	case CLOCK_EVT_NOTIFY_BROADCAST_EXIT:
			......
	case CLOCK_EVT_NOTIFY_CPU_DYING:
			......
	case CLOCK_EVT_NOTIFY_CPU_DEAD:
			......
	case CLOCK_EVT_NOTIFY_SUSPEND:
			......
	case CLOCK_EVT_NOTIFY_RESUME:
			......
	}

	return NOTIFY_OK;
}

可见,对于新注册的clock_event_device,会发出CLOCK_EVT_NOTIFY_ADD通知,最终会进入函数:tick_check_new_device,这个函数比对当前cpu所使用的与新注册的clock_event_device之间的特性,如果认为新的clock_event_device更好,则会进行切换工作。下一节将会详细的讨论该函数。到这里,每个cpu已经有了自己的clock_event_device,在这以后,框架层的代码会根据内核的配置项(CONFIG_NO_HZ、CONFIG_HIGH_RES_TIMERS),对注册的clock_event_device进行不同的设置,从而为系统的tick和高精度定时器提供服务,这些内容我们留在本系列的后续文章进行讨论。

4. tick_device

当内核没有配置成支持高精度定时器时,系统的tick由tick_device产生,tick_device其实是clock_event_device的简单封装,它内嵌了一个clock_event_device指针和它的工作模式:

1
2
3
4
struct tick_device {
	struct clock_event_device *evtdev;
	enum tick_device_mode mode;
};

在kernel/time/tick-common.c中,定义了一个per-cpu的tick_device全局变量,tick_cpu_device:

1
2
3
4
/* 
 * Tick devices 
 */
DEFINE_PER_CPU(struct tick_device, tick_cpu_device);

前面曾经说过,当machine的代码为每个cpu注册clock_event_device时,通知回调函数tick_notify会被调用,进而进入tick_check_new_device函数,下面让我们看看该函数如何工作,首先,该函数先判断注册的clock_event_device是否可用于本cpu,然后从per-cpu变量中取出本cpu的tick_device:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static int tick_check_new_device(struct clock_event_device *newdev)
{
		......
	cpu = smp_processor_id();
	if (!cpumask_test_cpu(cpu, newdev->cpumask))
		goto out_bc;

	td = &per_cpu(tick_cpu_device, cpu);
	curdev = td->evtdev;

如果不是本地clock_event_device,会做进一步的判断:如果不能把irq绑定到本cpu,则放弃处理,如果本cpu已经有了一个本地clock_event_device,也放弃处理:

	if (!cpumask_equal(newdev->cpumask, cpumask_of(cpu))) {
			   ......
		if (!irq_can_set_affinity(newdev->irq))
			goto out_bc;
			   ......
		if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))
			goto out_bc;
	}

反之,如果本cpu已经有了一个clock_event_device,则根据是否支持单触发模式和它的rating值,决定是否替换原来旧的clock_event_device:

	if (curdev) {
		if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&
			!(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
			goto out_bc;  // 新的不支持单触发,但旧的支持,所以不能替换
		if (curdev->rating >= newdev->rating)
			goto out_bc;  // 旧的比新的精度高,不能替换
	}

在这些判断都通过之后,说明或者来cpu还没有绑定tick_device,或者是新的更好,需要替换:

	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));

上面的tick_setup_device函数负责重新绑定当前cpu的tick_device和新注册的clock_event_device,如果发现是当前cpu第一次注册tick_device,就把它设置为TICKDEV_MODE_PERIODIC模式,如果是替换旧的tick_device,则根据新的tick_device的特性,设置为TICKDEV_MODE_PERIODIC或TICKDEV_MODE_ONESHOT模式。可见,在系统的启动阶段,tick_device是工作在周期触发模式的,直到框架层在合适的时机,才会开启单触发模式,以便支持NO_HZ和HRTIMER。

5. tick事件的处理–最简单的情况

clock_event_device最基本的应用就是实现tick_device,然后给系统定期地产生tick事件,通用时间框架对clock_event_device和tick_device的处理相当复杂,因为涉及配置项:CONFIG_NO_HZ和CONFIG_HIGH_RES_TIMERS的组合,两个配置项就有4种组合,这四种组合的处理都有所不同,所以这里我先只讨论最简单的情况:

1
2
CONFIG_NO_HZ == 0;
CONFIG_HIGH_RES_TIMERS == 0;

在这种配置模式下,我们回到上一节的tick_setup_device函数的最后:

1
2
3
4
if (td->mode == TICKDEV_MODE_PERIODIC)
	tick_setup_periodic(newdev, 0);
else
	tick_setup_oneshot(newdev, handler, next_event);

因为启动期间,第一个注册的tick_device必然工作在TICKDEV_MODE_PERIODIC模式,所以tick_setup_periodic会设置clock_event_device的事件回调字段event_handler为tick_handle_periodic,工作一段时间后,就算有新的支持TICKDEV_MODE_ONESHOT模式的clock_event_device需要替换,再次进入tick_setup_device函数,tick_setup_oneshot的handler参数也是之前设置的tick_handle_periodic函数,所以我们考察tick_handle_periodic即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;

	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);
	}
}

该函数首先调用tick_periodic函数,完成tick事件的所有处理,如果是周期触发模式,处理结束,如果工作在单触发模式,则计算并设置下一次的触发时刻,这里用了一个循环,是为了防止当该函数被调用时,clock_event_device中的计时实际上已经经过了不止一个tick周期,这时候,tick_periodic可能被多次调用,使得jiffies和时间可以被正确地更新。tick_periodic的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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触发调度系统进行进程统计和调度工作;

Linux时间子系统之三:时间的维护者:timekeeper

http://blog.csdn.net/droidphone/article/details/7989566

本系列文章的前两节讨论了用于计时的时钟源:clocksource,以及内核内部时间的一些表示方法,但是对于真实的用户来说,我们感知的是真实世界的真实时间,也就是所谓的墙上时间,clocksource只能提供一个按给定频率不停递增的周期计数,如何把它和真实的墙上时间相关联?本节的内容正是要讨论这一点。

1. 时间的种类

内核管理着多种时间,它们分别是:

1
2
3
4
5
RTC时间
wall time:墙上时间
monotonic time
raw monotonic time
boot time:总启动时间

RTC时间 在PC中,RTC时间又叫CMOS时间,它通常由一个专门的计时硬件来实现,软件可以读取该硬件来获得年月日、时分秒等时间信息,而在嵌入式系统中,有使用专门的RTC芯片,也有直接把RTC集成到Soc芯片中,读取Soc中的某个寄存器即可获取当前时间信息。一般来说,RTC是一种可持续计时的,也就是说,不管系统是否上电,RTC中的时间信息都不会丢失,计时会一直持续进行,硬件上通常使用一个后备电池对RTC硬件进行单独的供电。因为RTC硬件的多样性,开发者需要为每种RTC时钟硬件提供相应的驱动程序,内核和用户空间通过驱动程序访问RTC硬件来获取或设置时间信息。

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

monotonic time 该时间自系统开机后就一直单调地增加,它不像xtime可以因用户的调整时间而产生跳变,不过该时间不计算系统休眠的时间,也就是说,系统休眠时,monotoic时间不会递增。

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

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

1
2
3
4
5
6
时间种 类     精度(统计单位)     访问速度     累计休眠时间     受NTP调整的影响
RTC           低                   慢           Yes              Yes
xtime         高                   快           Yes              Yes
monotonic     高                   快           No               Yes
raw monotonic 高                   快           No               No
boot time     高                   快           Yes              Yes

2. struct timekeeper

内核用timekeeper结构来组织与时间相关的数据,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct timekeeper {  
	struct clocksource *clock;    /* Current clocksource used for timekeeping. */  
	u32 mult;    /* NTP adjusted clock multiplier */  
	int shift;  /* The shift value of the current clocksource. */  
	cycle_t cycle_interval; /* Number of clock cycles in one NTP interval. */  
	u64 xtime_interval; /* Number of clock shifted nano seconds in one NTP interval. */  
	s64 xtime_remainder;    /* shifted nano seconds left over when rounding cycle_interval */  
	u32 raw_interval;   /* Raw nano seconds accumulated per NTP interval. */  
  
	u64 xtime_nsec; /* Clock shifted nano seconds remainder not stored in xtime.tv_nsec. */  
	/* Difference between accumulated time and NTP time in ntp 
	 * shifted nano seconds. */  
	s64 ntp_error;  
	/* Shift conversion between clock shifted nano seconds and 
	 * ntp shifted nano seconds. */  
	int ntp_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 */  
};  

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

内核除了用xtime表示墙上的真实时间外,还维护了另外一个时间:monotonic time,可以把它理解为自系统启动以来所经过的时间,该时间只能单调递增,可以理解为xtime虽然正常情况下也是递增的,但是毕竟用户可以主动向前或向后调整墙上时间,从而修改xtime值。但是monotonic时间不可以往后退,系统启动后只能不断递增。奇怪的是,内核并没有直接定义一个这样的变量来记录monotonic时间,而是定义了一个变量wall_to_monotonic,记录了墙上时间和monotonic时间之间的偏移量,当需要获得monotonic时间时,把xtime和wall_to_monotonic相加即可,因为默认启动时monotonic时间为0,所以实际上wall_to_monotonic的值是一个负数,它和xtime同一时间被初始化,请参考timekeeping_init函数。

计算monotonic时间要去除系统休眠期间花费的时间,内核用total_sleep_time记录休眠的时间,每次休眠醒来后重新累加该时间,并调整wall_to_monotonic的值,使其在系统休眠醒来后,monotonic时间不会发生跳变。因为wall_to_monotonic值被调整。所以如果想获取boot time,需要加入该变量的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void get_monotonic_boottime(struct timespec *ts)  
{  
		......  
	do {  
		seq = read_seqbegin(&timekeeper.lock);  
		*ts = timekeeper.xtime;  
		tomono = timekeeper.wall_to_monotonic;  
		<span style="color:#ff0000;">sleep = timekeeper.total_sleep_time;</span>  
		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);  
}  

raw_time字段用来表示真正的硬件时间,也就是上面所说的raw monotonic time,它不受时间调整的影响,monotonic时间虽然也不受settimeofday的影响,但会受到ntp调整的影响,但是raw_time不受ntp的影响,他真的就是开完机后就单调地递增。xtime、monotonic-time和raw_time可以通过用户空间的clock_gettime函数获得,对应的ID参数分别是 CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_MONOTONIC_RAW。

clock字段则指向了目前timekeeper所使用的时钟源,xtime,monotonic time和raw time都是基于该时钟源进行计时操作,当有新的精度更高的时钟源被注册时,通过timekeeping_notify函数,change_clocksource函数将会被调用,timekeeper.clock字段将会被更新,指向新的clocksource。

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

1
static struct timekeeper timekeeper;  

3. timekeeper的初始化

timekeeper的初始化由timekeeping_init完成,该函数在start_kernel的初始化序列中被调用,timekeeping_init首先从RTC中获取当前时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void __init timekeeping_init(void)  
{  
	struct clocksource *clock;  
	unsigned long flags;  
	struct timespec now, boot;  
  
	read_persistent_clock(&now);  
	read_boot_clock(&boot);  

然后对锁和ntp进行必要的初始化:

	seqlock_init(&timekeeper.lock);  
  
	ntp_init();  

接着获取默认的clocksource,如果平台没有重新实现clocksource_default_clock函数,
默认的clocksource就是基于jiffies的clocksource_jiffies,
然后通过timekeeper_setup_inernals内部函数把timekeeper和clocksource进行关联:

	write_seqlock_irqsave(&timekeeper.lock, flags);  
	clock = clocksource_default_clock();  
	if (clock->enable)  
		clock->enable(clock);  
	timekeeper_setup_internals(clock);  

利用RTC的当前时间,初始化xtime,raw_time,wall_to_monotonic等字段:

	timekeeper.xtime.tv_sec = now.tv_sec;  
	timekeeper.xtime.tv_nsec = now.tv_nsec;  
	timekeeper.raw_time.tv_sec = 0;  
	timekeeper.raw_time.tv_nsec = 0;  
	if (boot.tv_sec == 0 && boot.tv_nsec == 0) {  
		boot.tv_sec = timekeeper.xtime.tv_sec;  
		boot.tv_nsec = timekeeper.xtime.tv_nsec;  
	}  
	set_normalized_timespec(&timekeeper.wall_to_monotonic,  
			-boot.tv_sec, -boot.tv_nsec);  

最后,初始化代表实时时间和monotonic时间之间偏移量的offs_real字段,total_sleep_time字段初始化为0:


	update_rt_offset();  
	timekeeper.total_sleep_time.tv_sec = 0;  
	timekeeper.total_sleep_time.tv_nsec = 0;  
	write_sequnlock_irqrestore(&timekeeper.lock, flags);  

}

xtime字段因为是保存在内存中,系统掉电后无法保存时间信息,所以每次启动时都要通过timekeeping_init从RTC中同步正确的时间信息。其中,read_persistent_clock和read_boot_clock是平台级的函数,分别用于获取RTC硬件时间和启动时的时间,不过值得注意到是,到目前为止(我的代码树基于3.4版本),ARM体系中,只有tegra和omap平台实现了read_persistent_clock函数。如果平台没有实现该函数,内核提供了一个默认的实现:

1
2
3
4
5
void __attribute__((weak)) read_persistent_clock(struct timespec *ts)  
{  
	ts->tv_sec = 0;  
	ts->tv_nsec = 0;  
}  
1
2
3
4
5
void __attribute__((weak)) read_boot_clock(struct timespec *ts)  
{  
	ts->tv_sec = 0;  
	ts->tv_nsec = 0;  
}  

那么,其他ARM平台是如何初始化xtime的?答案就是CONFIG_RTC_HCTOSYS这个内核配置项,打开该配置后,driver/rtc/hctosys.c将会编译到系统中,由rtc_hctosys函数通过do_settimeofday在系统初始化时完成xtime变量的初始化:

1
2
3
4
5
6
7
8
9
10
11
static int __init rtc_hctosys(void)   
{   
		......   
		err = rtc_read_time(rtc, &tm);   
		......  
		rtc_tm_to_time(&tm, &tv.tv_sec);   
		do_settimeofday(&tv);   
		......   
		return err;   
}   
late_initcall(rtc_hctosys);  

4. 时间的更新

xtime一旦初始化完成后,timekeeper就开始独立于RTC,利用自身关联的clocksource进行时间的更新操作,根据内核的配置项的不同,更新时间的操作发生的频度也不尽相同,如果没有配置NO_HZ选项,通常每个tick的定时中断周期,do_timer会被调用一次,相反,如果配置了NO_HZ选项,可能会在好几个tick后,do_timer才会被调用一次,当然传入的参数是本次更新离上一次更新时相隔了多少个tick周期,系统会保证在clocksource的max_idle_ns时间内调用do_timer,以防止clocksource的溢出:

1
2
3
4
5
6
void do_timer(unsigned long ticks)  
{  
	jiffies_64 += ticks;  
	update_wall_time();  
	calc_global_load(ticks);  
}  

在do_timer中,jiffies_64变量被相应地累加,然后在update_wall_time中完成xtime等时间的更新操作,更新时间的核心操作就是读取关联clocksource的计数值,累加到xtime等字段中,其中还设计ntp时间的调整等代码,详细的代码就不贴了。

5. 获取时间

timekeeper提供了一系列的接口用于获取各种时间信息。

1
2
3
4
5
6
7
8
9
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结构