全部博文(165)
分类: LINUX
2012-06-13 10:41:50
1 计算机系统中的计时器
在计算机系统中存在着许多硬件计时器,例如 Real Timer Clock ( RTC )、Time Stamp Counter ( TSC ) 和 Programmable Interval Timer ( PIT ) 等等。
这部分内容不是本文的中点,这里仅仅简单介绍几种,更多内容参见参考文献:
Real Timer Clock ( RTC ):
独立于整个计算机系统(例如: CPU 和其他 chip )
内核利用其获取系统当前时间和日期
Time Stamp Counter ( TSC ):
从 Pentium 起,提供一个寄存器 TSC,用来累计每一次外部振荡器产生的时钟信号
通过指令 rdtsc 访问这个寄存器
比起 PIT,TSC 可以提供更精确的时间测量
Programmable Interval Timer ( PIT ):
时间测量设备
内核使用的产生时钟中断的设备,产生的时钟中断依赖于硬件的体系结构,慢的为 10 ms 一次,快的为 1 ms 一次
High Precision Event Timer ( HPET ):
PIT 和 RTC 的替代者,和之前的计时器相比,HPET 提供了更高的时钟频率(至少10 MHz )以及更宽的计数器宽度(64位)
一个 HPET 包括了一个固定频率的数值增加的计数器以及3到32个独立的计时器,这每一个计时器有包涵了一个比较器和一个寄存器(保存一个数值,表示触发中断的时机)。每一个比较器都比较计数器中的数值和寄存器中的数值,当这两个数值相等时,将产生一个中断
2 硬件时钟处理
这里所说的硬件时钟处理特指的是硬件计时器时钟中断的处理过程。
2.1 数据结构
和硬件计时器(本文又称作硬件时钟,区别于软件时钟)相关的数据结构主要有两个:
struct clocksource :对硬件设备的抽象,描述时钟源信息
struct clock_event_device :时钟的事件信息,包括当硬件时钟中断发生时要执行那些操作(实际上保存了相应函数的指针)。本文将该结构称作为“时钟事件设备”。
上述两个结构内核源代码中有较详细的注解,分别位于文件 clocksource.h 和 clockchips.h 中。需要特别注意的是结构 clock_event_device 的成员 event_handler ,它指定了当硬件时钟中断发生时,内核应该执行那些操作,也就是真正的时钟中断处理函数。 在2.3节“时钟初始化”部分会介绍它真正指向哪个函数。
Linux 内核维护了两个链表,分别了系统中所有时钟源的信息和时钟事件设备的信息。这两个链表的表头在内核中分别是 clocksource_list 和 clockevent_devices 。图2-1显示了这两个链表。
图2-1 时钟源链表和时钟事件链表
2.2 通知链技术( notification chain )
在时钟处理这部分中,内核用到了所谓的“通知链( notification chain )”技术。所以在介绍时钟处理过程之前先来了解下“通知链”技术。
在 Linux 内核中,各个子系统之间有很强的相互关系,一些被一个子系统生成或者被探测到的事件,很可能是另一个或者多个子系统感兴趣的,也就是说这个事件的获取者必 须能够通知所有对该事件感兴趣的子系统,并且还需要这种通知机制具有一定的通用性。基于这些, Linux 内核引入了“通知链”技术。
2.2.1 数据结构:
通知链有四种类型,
原子通知链( Atomic notifier chains ):通知链元素的回调函数(当事件发生时要执行的函数)只能在中断上下文中运行,不允许阻塞
可阻塞通知链( Blocking notifier chains ):通知链元素的回调函数在进程上下文中运行,允许阻塞
原始通知链( Raw notifier chains ):对通知链元素的回调函数没有任何限制,所有锁和保护机制都由调用者维护
SRCU 通知链( SRCU notifier chains ):可阻塞通知链的一种变体
所以对应了四种通知链头结构:
struct atomic_notifier_head :原子通知链的链头
struct blocking_notifier_head :可阻塞通知链的链头
struct raw_notifier_head :原始通知链的链头
struct srcu_notifier_head : SRCU 通知链的链头
通知链元素的类型:
struct notifier_block :通知链中的元素,记录了当发出通知时,应该执行的操作(即回调函数)
链头中保存着指向元素链表的指针。通知链元素结构则保存着回调函数的类型以及优先级,参见 notifier.h 文件。
2.2.2 运作机制
通知链的运作机制包括两个角色:
被通知者:对某一事件感兴趣一方。定义了当事件发生时,相应的处理函数,即回调函数。但需要事先将其注册到通知链中(被通知者注册的动作就是在通知链中增加一项)。
通知者:事件的通知者。当检测到某事件,或者本身产生事件时,通知所有对该事件感兴趣的一方事件发生。他定义了一个通知链,其中保存了每一个被通知者对事件的处理函数(回调函数)。通知这个过程实际上就是遍历通知链中的每一项,然后调用相应的事件处理函数。
包括以下过程:
通知者定义通知链
被通知者向通知链中注册回调函数
当事件发生时,通知者发出通知(执行通知链中所有元素的回调函数)
整个过程可以看作是“发布——订阅”模型(参见参考资料)
被通知者调用 notifier_chain_register 函数注册回调函数,该函数按照优先级将回调函数加入到通知链中 。注销回调函数则使用 notifier_chain_unregister 函数,即将回调函数从通知链中删除。2.2.1节讲述的4种通知链各有相应的注册和注销函数,但是他们最终都是调用上述两个函数完成注册和注销功能的。有 兴趣的读者可以自行查阅内核代码。
通知者调用 notifier_call_chain 函数通知事件的到达,这个函数会遍历通知链中所有的元素,然后依次调用每一个的回调函数(即完成通知动作)。2.2.1节讲述的4种通知链也都有其对应的 通知函数,这些函数也都是最终调用 notifier_call_chain 函数完成事件的通知。
更多关于通知链的内容,参见参考文献。
由以上的叙述,“通知链”技术可以概括为:事件的被通知者将事件发生时应该执行的操作通过函数指针方式保存在链表(通知链)中,然后当事件发生时通知者依次执行链表中每一个元素的回调函数完成通知。
2.3 时钟初始化
内核初始化部分( start_kernel 函数)和时钟相关的过程主要有以下几个:
tick_init()
init_timers()
hrtimers_init()
time_init()
其中函数 hrtimers_init() 和高精度时钟相关(本文暂不介绍这部分内容)。下面将详细介绍剩下三个函数。
2.3.1 tick_init 函数
函数 tick_init() 很简单,调用 clockevents_register_notifier 函数向 clockevents_chain 通知链注册元素: tick_notifier。这个元素的回调函数指明了当时钟事件设备信息发生变化(例如新加入一个时钟事件设备等等)时,应该执行的操作,该回调函数为 tick_notify (参见2.4节)。
2.3.2 init_timers 函数
注:本文中所有代码均来自于Linux2.6.25 源代码
函数 init_timers() 的实现如清单2-1(省略了部分和
主要功能无关的内容,以后代码同样方式处理)
清单2-1 init_timers 函数
void __init init_timers(void){ int err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE, (void *)(long)smp_processor_id()); …… register_cpu_notifier(&timers_nb); open_softirq(TIMER_SOFTIRQ,run_timer_softirq, NULL);}
代码解释:
初始化本 CPU 上的软件时钟相关的数据结构,参见3.2节
向 cpu_chain 通知链注册元素 timers_nb ,该元素的回调函数用于初始化指定 CPU 上的软件时钟相关的数据结构
初始化时钟的软中断处理函数
2.3.3 time_init 函数
函数 time_init 的实现如清单2-2
清单2-2 time_init 函数
void __init time_init(void){ …… init_tsc_clocksource(); late_time_init = choose_time_init();}
函数 init_tsc_clocksource 初始化 tsc 时钟源。choose_time_init 实际是函数 hpet_time_init ,其代码清单2-3
清单2-3 hpet_time_init 函数
void __init hpet_time_init(void){ if (!hpet_enable()) setup_pit_timer(); setup_irq(0, &irq0);}
函数 hpet_enable 检测系统是否可以使用 hpet 时钟,如果可以则初始化 hpet 时钟。否则初始化 pit 时钟。最后设置硬件时钟发生时的处理函数(参见2.4节)。
初始化硬件时钟这个过程主要包括以下两个过程(参见 hpet_enable 的实现):
初始化时钟源信息( struct clocksource 类型的变量),并将其添加到时钟源链表中,即 clocksource_list 链表(参见图2-1)。
初始化时钟事件设备信息( struct clock_event_device 类型的变量),并向通知链 clockevents_chain 发布通知:一个时钟事件设备要被添加到系统中。在通知(执行回调函数)结束后,该时钟事件设备被添加到时钟事件设备链表中,即 clockevent_devices 链表(参见图2-1)。有关通知链的内容参见2.2节。
需要注意的是在初始化时钟事件设备时,全局变量 global_clock_event 被赋予了相应的值。该变量保存着系统中当前正在使用的时钟事件设备(保存了系统当前使用的硬件时钟中断发生时,要执行的中断处理函数的指针)。
2.4 硬件时钟处理过程
由2.3.3可知硬件时钟中断的处理函数保存在静态变量 irq0 中,其定义如清单2-4
清单2-4 变量irq0定义
static struct irqaction irq0 = { .handler = timer_event_interrupt, .flags = IRQF_DISABLED | IRQF_IRQPOLL | IRQF_NOBALANCING, .mask = CPU_MASK_NONE, .name = "timer"};
由定义可知:函数 timer_event_interrupt 为时钟中断处理函数,其定义如清单2-5
清单2-5 timer_event_interrupt 函数
static irqreturn_t timer_event_interrupt(int irq, void *dev_id){ add_pda(irq0_irqs, 1); global_clock_event->event_handler(global_clock_event); return IRQ_HANDLED;}
从代码中可以看出:函数 timer_event_interrupt 实际上调用的是 global_clock_event 变量的 event_handler 成员。那 event_handler 成员指向哪里呢?
为了说明这个问题,不妨假设系统中使用的是 hpet 时钟。由2.3.3节可知 global_clock_event 指向 hpet 时钟事件设备( hpet_clockevent )。查看 hpet_enable 函数的代码并没有发现有对 event_handler 成员的赋值。所以继续查看时钟事件设备加入事件的处理函数 tick_notify ,该函数记录了当时钟事件设备发生变化(例如,新时钟事件设备的加入)时,执行那些操作(参见2.3.1节),代码如清单2-6
清单2-6 tick_notify 函数
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); …… return NOTIFY_OK;}
由代码可知:对于新加入时钟事件设备这个事件,将会调用函数 tick_check_new_device 。顺着该函数的调用序列向下查找。tick_set_periodic_handler 函数将时钟事件设备的 event_handler 成员赋值为 tick_handle_periodic 函数的地址。由此可知,函数 tick_handle_periodic 为硬件时钟中断发生时,真正的运行函数。
函数 tick_handle_periodic 的处理过程分成了以下两个部分:
全局处理:整个系统中的信息处理
局部处理:局部于本地 CPU 的处理
总结一下,一次时钟中断发生后, OS 主要执行的操作( tick_handle_periodic ):
全局处理(仅在一个 CPU 上运行):
更新 jiffies_64
更新 xtimer 和当前时钟源信息等
根据 tick 计算 avenrun 负载
局部处理(每个 CPU 都要运行):
根据当前在用户态还是核心态,统计当前进程的时间:用户态时间还是核心态时间
唤醒 TIMER_SOFTIRQ 软中断
唤醒 RCU 软中断
调用 scheduler_tick (更新进程时间片等等操作,更多内容参见参考文献)
profile_tick 函数调用
以上就介绍完了硬件时钟的处理过程,下面来看软件时钟。
3 软件时钟处理
这里所说“软件时钟”指的是软件定时器( Software Timers ),是一个软件上的概念,是建立在硬件时钟基础之上的。它记录了未来某一时刻要执行的操作(函数),并使得当这一时刻真正到来时,这些操作(函数)能够被 按时执行。举个例子说明:它就像生活中的闹铃,给闹铃设定振铃时间(未来的某一时间)后,当时间(相当于硬件时钟)更新到这个振铃时间后,闹铃就会振铃。 这个振铃时间好比软件时钟的到期时间,振铃这个动作好比软件时钟到期后要执行的函数,而闹铃时间更新好比硬件时钟的更新。
实现软件时钟原理也比较简单:每一次硬件时钟中断到达时,内核更新的 jiffies ,然后将其和软件时钟的到期时间进行比较。如果 jiffies 等于或者大于软件时钟的到期时间,内核就执行软件时钟指定的函数。
接下来的几节会详细介绍 Linux2.6.25 是怎么实现软件时钟的。
3.1 相关数据结构
struct timer_list :软件时钟,记录了软件时钟的到期时间以及到期后要执行的操作。具体的成员以及含义见表3-1。
struct tvec_base :用于组织、管理软件时钟的结构。在 SMP 系统中,每个 CPU 有一个。具体的成员以及含义参见表3-2。
表3-1 struct timer_list 主要成员
域名 类型 描述
entry struct list_head 所在的链表
expires unsigned long 到期时间,以 tick 为单位
function void (*)(unsigned long) 回调函数,到期后执行的操作
data unsigned long 回调函数的参数
base struct tvec_base * 记录该软件时钟所在的 struct tvec_base 变量
注:一个 tick 表示的时间长度为两次硬件时钟中断发生时的时间间隔
表3-2 struct tvec_base 类型的成员
域名 类型 描述
lock spinlock_t 用于同步操作
running_timer struct timer_list * 正在处理的软件时钟
timer_jiffies unsigned long 当前正在处理的软件时钟到期时间
tv1 struct tvec_root 保存了到期时间从 timer_jiffies 到 timer_jiffies + 之间(包括边缘值)的所有软件时钟
tv2 struct tvec 保存了到期时间从 timer_jiffies + 到 timer_jiffies +之间(包括边缘值)的 所有软件时钟
tv3 struct tvec 保存了到期时间从 timer_jiffies +到 timer_jiffies +之间(包括边缘值)的所有软件时钟
tv4 struct tvec 保存了到期时间从 timer_jiffies +到 timer_jiffies +之间(包括边缘值)的所有软件时钟
tv5 struct tvec 保存了到期时间从 timer_jiffies +到 timer_jiffies +之间(包括边缘值)的所有软件时钟
其中 tv1 的类型为 struct tvec_root ,tv 2~ tv 5的类型为 struct tvec ,清单3-1显示它们的定义
清单3-1 struct tvec_root 和 struct tvec 的定义
struct tvec { struct list_head vec[TVN_SIZE];};struct tvec_root { struct list_head vec[TVR_SIZE];};
可见它们实际上就是类型为 struct list_head 的数组,其中 TVN_SIZE 和 TVR_SIZE 在系统没有配置宏 CONFIG_BASE_SMALL 时分别被定义为64和256。
3.2 数据结构之间的关系
图3-1显示了以上数据结构之间的关系:
从图中可以清楚地看出:软件时钟( struct timer_list ,在图中由 timer 表示)以双向链表( struct list_head )的形式,按照它们的到期时间保存相应的桶( tv1~tv5 )中。tv1 中保存了相对于 timer_jiffies 下256个 tick 时间内到期的所有软件时钟; tv2 中保存了相对于 timer_jiffies 下256*64个 tick 时间内到期的所有软件时钟; tv3 中保存了相对于 timer_jiffies 下256*64*64个 tick 时间内到期的所有软件时钟; tv4 中保存了相对于 timer_jiffies 下256*64*64*64个 tick 时间内到期的所有软件时钟; tv5 中保存了相对于 timer_jiffies 下256*64*64*64*64个 tick 时间内到期的所有软件时钟。具体的说,从静态的角度看,假设 timer_jiffies 为0,那么 tv1[0] 保存着当前到期(到期时间等于 timer_jiffies )的软件时钟(需要马上被处理), tv1[1] 保存着下一个 tick 到达时,到期的所有软件时钟, tv1[n] (0<= n <=255)保存着下 n 个 tick 到达时,到期的所有软件时钟。而 tv2[0] 则保存着下256到511个 tick 之间到期所有软件时钟, tv2[1] 保存着下512到767个 tick 之间到期的所有软件时钟, tv2[n] (0<= n <=63)保存着下256*(n+1)到256*(n+2)-1个 tick 之间到达的所有软件时钟。 tv3~tv5 依次类推。
注:一个tick的长度指的是两次硬件时钟中断发生之间的时间间隔
从上面的说明中可以看出:软件时钟是按照其到期时间相对于当前正在处理的软件时钟的到期时间( timer_jiffies 的数值)保存在 struct tvec_base 变量中的。而且这个到期时间的最大相对值(到期时间 - timer_jiffies )为 0xffffffffUL ( tv5 最后一个元素能够表示的相对到期时间的最大值)。
还需要注意的是软件时钟的处理是局部于 CPU 的,所以在 SMP 系统中每一个 CPU 都保存一个类型为 struct tvec_base 的变量,用来组织、管理本 CPU 的软件时钟。从图中也可以看出 struct tvec_base 变量是 per-CPU 的(关于 per-CPU 的变量原理和使用参见参考资料)。
由于以后的讲解经常要提到每个 CPU 相关的 struct tvec_base 变量,所以为了方便,称保存软件时钟的 struct tvec_base 变量为该软件时钟的 base ,或称 CPU 的 base 。