Linux 定时器和时间管理

关于时间,内核需要做的两件事是:

  • 保存当前的时间和日期。
  • 维持定时器。

硬件和电路

实时时钟(RTC)

实时时钟(Real Time Clock, RTC)是用来持久存放系统时间的设备,即时系统关闭后, 它也可以依靠主板上的电池保持系统的计时。RTC能在IRQ8上发出周期性的中断。系统启动 时,内核通过读取RTC来初始化墙上时间,改时间存放在xtime变量中。

时间戳计数器(TSC)

所有80x86处理器都包含一条CLK输入引线,它接受外部振荡器的时钟信号,另外还包含一 个计数器,它在每个时钟信号到来时加1. 该计数器就是64位的时间戳计数器 (Time Stamp Counter TSC)。

系统定时器

各种体系结构中的定时器的实现不尽相同,但根本思想都是:提供一种周期性触发中断机 制。可以简单地理解为一个可编程的硬件设备,它能以某个可变的频率发出电信号,作为 时钟中断的中断源。

可编程间隔定时器(PIT)

可编程间隔定时器(Programmable Interval Timer PIT)以内核确定的一个固定频率, 发出一个特殊的中断,即时钟中断(timer interrupt)。Linux启动时会给PC的第一个PIT 进行编程,以某个频率发出时钟中断,两个中断间的时间间隔叫做一个节拍(tick)。

CPU本地定时器

80x86处理器的本地APIC中还提供了另一种定时测量设备:CPU本地定时器。 与PIT的区别是:本地APIC定时器只把中断发送给自己的处理器,而PIT产生一个全局性中 断,系统中的任一CPU都可以对其处理。

高精度事件定时器(HPET)

HPET(High Precision Event Timer)俗称高精度定时器,可以提供更高精度的时间测量, 可以通过配置来取代PIT。

ACPI电源管理定时器(ACPI Power Management Timer, ACPI PMT)

ACPI PMT也是一个定时器,主要是用于CPU降频或降压来省电的情况。

内核中实现计时的数据结构和原理

前面说过,计算机可能有多种不同的计时器(timer),在系统初始化的时候,内核会选择 合适的计时器作为中断源。

内核中时间相关的概念

节拍率(tick rate)

系统定时器以某种频率自行触发(经常被称为hitting或popping)时钟中断,该频率可以 编程预定,称为节拍率。节拍率是通过静态预处理定义的,即HZ值,内核在 <asm/param.h>中定义了这个值。

高HZ的优势是定时器更准,依赖定时执行的系统调用(如poll和epoll)精度更高,对资源 消耗等值的测量有更精细的解析度,进程抢占更准确。高HZ的劣势就是过高的时钟中断意 味着系统负担较重。

节拍(tick)

两次连续时钟中断的间隔时间。

墙上时间(wall time)

实际时间。墙上时间存放于变量xtime中<kernel/time/timekeeping.c>. 从用户空间获 取墙上时间的主要接口是gettimeofday(), 在内核中对应系统调用为 sys_gettimeofday(), 本质上就是读取xtime的内容。xtime的数据结构中存放着自1970 年1月1日以来的秒数,以及自上一秒开始的纳秒数。

jiffies

全局变量jiffies用来记录系统自启动以来产生的节拍的总数。64位系统中该变量等同于 jiffies_64.

动态定时器

动态定时器或内核定时器,是内核提供的一个机制,让一些工作可以在一个指定时间之后 运行。定时器的数据结构是struct timer_list,代码在 这里

可以使用内核提供的一组接口来创建、修改和删除定时器。每个定时器的数据结构内都有 一个字段表明这个定时器的到期时间。所有定时器都是以链表的形式存放在一起,为了让 内核寻找到超时的定时器,内核将定时器按它们的超时时间划分为5组。

定时器通过TIMER_SOFTIRQ软中断执行。

时钟中断处理程序

IRQ0表示system timer,IRQ8表示RTC timer,IRQ239就表示本地APIC时钟中断。

利用时钟中断处理的工作

利用时钟中断处理的工作包括:

  • 更新系统运行时间和实际时间。
  • 在smp系统上,均衡调度程序中各处理器上的运行队列。如果运行队列不均衡的话, 尽量使它们均衡。
  • 进行进程调度。
  • 运行超时的动态定时器。
  • 更新资源消耗和处理器时间的统计值。

在多处理器系统中,所有通用的工作(比如运行超时的动态定时器,系统时间的更新等) ,是通过全局定时器(PIT或HPET等)中断触发的。而与每个CPU相关的工作(比如统计进 程消耗的时间以及资源统计等),是由每个CPU的本地定时器进行触发的。

时钟中断处理程序的主要执行过程

  • 获得xtime_lock锁(对jiffies_64和xtime进行保护)。
  • 需要是应答或重新设置系统时钟。
  • 周期性地使用墙上时间更新RTC。
  • 调用体系结构无关的例程tick_periodic():
    • 给fiffies_64变量加1.
    • 更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
    • 执行已经到期的动态定时器。
    • 对当前进程的时间片数值进行更新。
    • 更新墙上时间(该事件存放在xtime变量中)。
    • 计算平均负载值。

中断处理程序的实现

tick_periodic()的代码在 这里

/*
 * Periodic tick
 */
static void tick_periodic(int cpu)
{
        if (tick_do_timer_cpu == cpu) {
                write_seqlock(&jiffies_lock);

                /* Keep track of the next tick event */
                tick_next_period = ktime_add(tick_next_period, tick_period);

                do_timer(1);
                write_sequnlock(&jiffies_lock);
        }

        update_process_times(user_mode(get_irq_regs()));
        profile_tick(CPU_PROFILING);
}

其中:

1

调用update_wall_time()来更新墙上时间。

2

调用calc_global_load()来计算平均负载,代码在 这里

3

调用update_process_times()来更新所耗费的各种节拍数:

update_process_times(user_mode(get_irq_regs()));

update_process_times()定义在 这里

/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
        struct task_struct *p = current;
        int cpu = smp_processor_id();

        /* Note: this timer irq context must be accounted for as well. */
        account_process_tick(p, user_tick);
        run_local_timers();
        rcu_check_callbacks(cpu, user_tick);
#ifdef CONFIG_IRQ_WORK
        if (in_irq())
                irq_work_run();
#endif
        scheduler_tick();
        run_posix_cpu_timers(p);
}

3.1

user_tick的值用来指明这次对进程更新的是系统时间还是用户时间。user_tick的值 通过查看系统寄存器来设置。也就是说,内核对进程进行时间计数时,是根据中断发生时 处理器所处的模式进行分类统计的,它把上一个节拍全部算给了进程。尽管在上个节拍内 ,进程可能多次进入和退出了内核模式,而且现在的进程也不一定是上一个节拍内唯一运 行的进程。

3.2

run_local_timers()标记了一个软中断:raise_softirq(TIMER_SOFTIRQ), 来运行所 有到期的定时器。

3.3

scheduler_tick()函数负责减少当前运行进程的时间片计数值并且在需要时设置 need_resched标志。在SMP机器中该函数还要负责平衡每个处理器上的运行队列。


参考资料: