全部博文(10)
分类: LINUX
2014-07-21 13:53:27
Linux Timer 定时器
0. Notes
以下所有的东西都是基于linux-3.14.1版本,其实自从2.6.xx版起,就已经有了这套高精度Timer架构,不同版本的差别不大。下面所有的叙述中,代码尽量少堆砌,相关函数或结构或宏的定义可以参见源码。有几个概念需要先明确,内核的Timer架构分为低精度timer架构和高精度timer架构。低精度timer架构又可以叫做经典的timer架构,指的是高精度timer架构出现之前的那套东西。经典的timer架构就是采用周期性的tick来驱动jiffies,在每个时钟中断中去运行到期的低精度定时器timer_list。现在低精度timer架构基本被抛弃,但还是可以用,当然不建议去用,基本上只有在编译内核时去掉了高精度timer的支持时,才会在内核运行时用到低精度timer架构。高精度timer架构已经完全包含了低精度timer架构的所有功能。所以现在即便不开启高精度timer功能以及不使用高精度定时器hrtimer进行定时方面的操作,低精度timer的周期性tick功能以及经典定时器timer_list的运作都是在高精度timer全新的代码架构下运行的。无论是否在内核参数中开启了高精度timer,只要编译时选中了支持高精度timer架构,那么高精度timer的架构代码部分就编译进了内核,用于支持低精度timer以及高精度timer以及周期性tick以及NO_HZ动态时钟。
所以,下面的所有东西都是基于高精度timer架构。
1. Tick-时钟滴答
从低精度timer架构的周期性tick来看,一个tick在内核中表示一个时钟中断的到来,或者相邻两次时钟中断之间的时间间隔。内核有一个宏HZ,HZ的值就是每秒钟时钟中断发生的次数,也就是每秒钟产生的tick次数。时钟中断由谁产生?时钟中断是由专门的“定时器”硬件产生的,时钟中断就是“定时器”硬件产生的普通的IRQ中断,没有什么特别的。任何的arm平台都有“定时器”硬件,“定时器”硬件又称为时钟源,这个时钟源或许是SOC内部的或者是外部的。时钟中断如何产生?时钟源是一种具有自动单调递增功能的计数器。例如,在imx53平台中,时钟源就是一个32位的计数器,时钟源的输入时钟频率是50MHz,也就是计数器每隔1/50M=20ns自动加1。计数器从0一直加到溢出需要大约2^32*20ns=1.43分钟。可以通过编程设置时钟源的中断到期的计数值,这样在计数器的计数值增加至设置的计数值后,就产了IRQ。
从高精度timer架构来看,时钟中断的产生和低精度架构下没有任何不同,都是通过设置时钟源的到期计数值,到期后产生时钟中断;在时钟中断中再次设置下一次的到期计数值,就这样驱动了时钟中断的不断产生和jiffies的更新以及系统的正常运作。
有一点非常混乱,定时器相关的东西在内核中叫做时钟,例如上面的时钟中断;而系统中用于产生时序信号的那个硬件相关的东西也叫做时钟,例如各种模块的需要的不同频率的时钟信号。总之,注意就可以了。
2. Jiffies
内核中有 jiffies和jiffies_64,它们的地址是一样的,只是数据长度不一样,jiffies_64 保证了即使在1000HZ的高频tick情况下发生溢出也要上亿年。内核中大量存在的是jiffies而不是jiffies_64,内核在访问jiffies时,实际访问的是jiffies_64的低32位。jiffies可以用作低精粒度的延时,例如wait_event_interruptible_timeout的超时设置基于jiffies。在做超时判断时候可以利用内核提供的函数而不用担心溢出问题,time_after()、time_before()、time_after_eq()、time_before_eq() 。
jiffies值的更新是在时钟中断中,但其实现在已经不使用时钟中断来更新jiffies了(本质上还是通过时钟源的IRQ中断,只是架构或者方式不一样了,只是说法不一样了罢了),而是通过高精度timer架构来模拟周期性的时钟中断行为。
更具体一点,jiffies的更新发生在timekeeping.c的do_timer()函数中,do_timer()又在3处被调用,分别是:
tick-common.c 的tick_periodic();//在高精度timer没有开启时通过高精度timer架构模拟周期性tick
tick-sched.c的tick_do_update_jiffies64();//已经开启高精度timer
timekeeping.c的xtime_update();//在arm/kernel/time.c的timer_tick()中调用,timer_tick()则在经典timer架构中被调用,现在几乎不用了。
3. Clocksource-时钟源
struct clocksource就是对时钟源硬件的抽象。.read用于从计数器中读出当前的计数值。.cycle_last表示上一次读到的计数值。.mult和.shift用于时间间隔和计数值之间的转换。.rating表示时钟源的精度。clocksource_enqueue()用于把clocksource添加到全局的clocksource_list链表中,链表中的clocksource按照精度rating大小排序。clocksource_register()用于向内核中注册一个struct clocksource时钟源,一般来说,一个系统至少得注册一个时钟源。
一句话,clocksource就是一个不断飞奔的n位计数器,从0到溢出再回到0再到溢出,永不停止。计数器每隔1/freq加1。freq是时钟源硬件的输入时钟信号的频率,由时钟信号发生器/PLL/晶振之类的东西共同作用产生。.read读取的正是当前的计数器中的数值,通过计算连续两次读到计数值,就可以计算出走了多长时间。
4. clock_event_device-时钟事件设备
clock_event_device 是比clocksource更高层的一种抽象,它表示了一种具有事件产生能力的虚拟设备。产生什么事件?就是产生时钟中断啊!clock_event_device 可以通过. set_next_event设置时钟源计数器的到期时间(计数值),到期后就产生时钟中断,在时钟中断IRQ函数中执行.event_handler。也就是说,时钟源clocksource在将来什么时候产生中断,以及产生中断后需要干什么事情都是由clock_event_device来决定的。clock_event_device的.event_handler根据当前的状况设置为 tick_handle_periodic()周期性中断/ tick_nohz_handler()动态中断 /tick_handle_oneshot_broadcast()。
5. Timer_list-低精度定时器在高精度架构中的处理
timer_list是经典的定时器使用方法,即便现在使用了高精度timer架构,timer_list依然可以很ok的使用。timer_list的到期判断依赖于低精度的周期性的clock_event_device,每次在clock_event_device的中断函数中,会检查系统中所有注册的低精度定时器,查看是否有到时的定时器,如果有,则调用该定时器的回调函数。在高精度timer架构中处理低精度timer_list的两个独立的过程为:
tick_handle_periodic()->tick_periodic()->update_process_times()->run_local_timers()->raise_softirq(TIMER_SOFTIRQ)[run_timer_softirq()]-__run_timers()中进行低精度定时器的到期判断和回调,用于周期性tick。
tick_nohz_handler()->tick_sched_handle->update_process_times()->run_local_timers()->raise_softirq(TIMER_SOFTIRQ)[run_timer_softirq()]-__run_timers(),用于NO_HZ动态时钟启用时。
6. 时钟源clocksource
6.1 clocksource_register()
struct clocksource {
cycle_t (*read)(struct clocksource *cs);
cycle_t cycle_last;
cycle_t mask;
u32 mult;
u32 shift;
u64 max_idle_ns;
u32 maxadj;
const char *name;
struct list_head list;
int rating;
……
}
struct clocksource就是对时钟源硬件的抽象。read用于从计数器中读出当前的计数值。cycle_last表示上一次读到的计数值。mult和shift用于时间间隔和计数值之间的转换。rating表示时钟源的精度。
clocksource_enqueue()用于把clocksource添加到全局的clocksource_list链表中,链表中的clocksource按照精度rating大小排序。
clocksource_select()
7. 高精度时钟模拟的周期性tick/jiffies
低精度定时器的超时时间单位是jiffies,而高精度定时器的超时时间单位是ns,两者显然是数量级的差别。看起来,在imx53上还难以实现高精度定时器,因为计数器每隔20ns才自动加1,计数器的精度是20ns,不过凑合着也可以,把定时器的超时时间设置为20ns的倍数就可以了。或许可以修改GPT的输入时钟频率,这样计数器的精度可以达到2ns级别,不知道这样是否可以。
高精度时钟是在低精度时钟状态下通过以下调用链切换进入的,
tick_handle_periodic()->tick_periodic()->update_process_times()->run_local_timers()->raise_softirq(TIMER_SOFTIRQ)[run_timer_softirq()]->hrtimer_run_pending()->hrtimer_switch_to_hres()。
切换后,高精度时钟架构模拟了一个tick产生模式,其实就是注册一个struct hrtimer,设置它的超时时间,到时间后就调用模拟的tick回调函数tick_sched_timer(),这里对jiffies_64进行更新,不过不是简单的加1,可能是过了多个tick。最后使用hrtimer_forward()把这个tick_sched.sched_timer的超时时间后移,以便在将来某个时间再次到期执行tick_sched_timer()。在tick_sched_timer()有2个重要的函数:tick_sched_do_timer()用于更新jiffies_64以及决定下一次到期的可能值;tick_sched_handle()用于update_process_times(),update_process_times()又会回调低精度时钟,所以在高精度时钟开启时,低精度时钟依然是可以使用的。只不过低精度定时器的到期时间是难以得到保障的,因为高精度定时器架构不会为低精度定时器设置中断,只能是某一次高精度定时器到期了或者是tick事件(也是高精度定时器)到来了,检查刚好有个低精度定时器的超时时间已经过了,于是就调用这个低精度定时器的回调函数。
8. 高精度时钟架构如何组织多个hrtimer
多个hrtimer按照红黑树来组织。红黑树是per-cpu的,每个cpu对应一棵树。每个cpu有一个tick_device,这个tick_device的超时时间只设置为本cpu的红黑树中最近将要到期的hrtimer的超时时间,而不是对每一个hrtimer都设置一个tick_device。所以很明显了。一句话,一个cpu对应一个tick_device,一个cpu对应一个hrtimer红黑树,tick_device的超时时间设置为hrtimer红黑树中超时时间最近的那个hrtimer的超时时间。
9. Timer工作模式转换
8.1 低精度Periodic切换à低精度NO_HZ模式/低精度Periodic切换à高精度(NO_HZ或Periodic)模式
update_process_times()->run_local_timers()->run_timer_softirq()->hrtimer_run_pending():在低精度timer模式下,用于判定是否可以切换到低精度timer的NO_HZ模式以及是否可以切换到高精度timer模式。在高精度timer模式下,hrtimer_run_pending()什么事情也不做。hrtimer_run_pending()先进行几个条件测试,若满足高精度timer切换条件、highres内核参数是on,则切换到高精度timer模式;若满足高精度模式切换条件但highres内核参数是off,就切换到低精度NO_HZ模式。highres内核参数是用来指示内核是否开启高精度timer功能。
在hrtimer_run_pending()àhrtimer_switch_to_hres()中切换到高精度timer模式。在hrtimer_run_pending()àhrtimer_switch_to_hres()àtick_setup_sched_timer()中根据内核参数nohz(tick_nohz_enabled)决定是否启用高精度NO_HZ模式。也就是说,如果在启用高精度模式的时候没有开启NO_HZ模式的话,那么以后就再也没有机会开启NO_HZ模式了,直到系统重启。更进一步说,高精度timer模式下满足开启NO_HZ的一切条件,只是策略问题。
10. NO_HZ-动态时钟
NO_HZ模式并不是在任何时候都禁用周期性tick,只是在系统空闲而仅仅有idle进程的时候才会关闭周期性的tick,这样明显可以减小系统的负载甚至能耗;而在系统中有其他进程或者中断运行时,tick依然是周期性的产生。可以知道NO_HZ模式的实现需要idle进程支持的。什么时候关闭周期性tick呢?那就是在idle进程中,在idle进程的循环cpu_idle_loop()中,第一条语句就是tick_nohz_idle_enter(),也就是判断是否可以禁止周期性的tick,如果可以就禁止。而在idle进程的循环跳出之后,也就是idle进程即将要结束运行之前,会使用tick_nohz_idle_exit()来重新启用周期性的tick。
idle进程会被中断或者其他进程打断抢占,在系统中断过程的代码中irq_enter()àtick_irq_enter()恢复周期性tick以及得到正确的jiffies值