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 函数1
2
3
4
5
6
7
8
| 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 函数1
2
3
4
5
6
| 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 函数1
2
3
4
5
6
7
| 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定义1
2
3
4
5
6
| 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 函数1
2
3
4
5
6
| 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 函数1
2
3
4
5
6
7
8
| 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 ):
- 更新 jiffies_64
- 更新 xtimer 和当前时钟源信息等
- 根据 tick 计算 avenrun 负载
- 局部处理(每个 CPU 都要运行):
- 根据当前在用户态还是核心态,统计当前进程的时间:用户态时间还是核心态时间
- 唤醒 TIMER_SOFTIRQ 软中断
- 唤醒 RCU 软中断
- 调用 scheduler_tick (更新进程时间片等等操作,更多内容参见参考文献)
- profile_tick 函数调用
以上就介绍完了硬件时钟的处理过程,下面来看软件时钟。 |