低分辨率定时器的实现

定时器激活与进程统计

IA-32将timer_interrupt注册为中断处理程序,而AMD64使用的是timer_event_interrupt。这两个函数都通过调用所谓的全局时钟的事件处理程序,来通知内核中通用的、体系结构无关的时间处理层。无论如何,该处理程序都通过调用以下两个函数,使得周期性低分辨率计时设施开始运作。

  • do_timer
  • update_process_time

do_time:

  • jiffies_64 += ticks;这意味着 jiffies_64确定了系统启动以来时钟中断的准确数目。
  • 调用update_times

update_times调用update_wall_time:更新wall time,它指定了系统已经启动并运行了多长时间。该信息也是由jiffies机制提供,wall clock从当前时间源读取时间,并据此更新wall clock。与jiffies机制相反,wall clock使用了人类可读格式(纳秒)来表示当前时间。

update_times调用calc_load更新系统负载统计,确定在前1分钟、5分钟、15分钟内,平均有多少个就绪状态的进程在就绪队列上等待。举例来说,该状态可以使用 w 命令输出。

处理jiffies

由于 jiffies_64 在32位系统上是一个复合变量,它不能直接读取,而只能用辅助函数 get_jiffies_64 访问。这确保在所有系统上都能返回正确的值。

  • time_after(a, b) 返回true,如果时间 a 在时间 b 之后。 time_before(a, b) 返回true,如果时间 a 在时间 b 之前,读者应该已经猜到。
  • time_after_eq(a, b) 的工作方式类似于 time_after ,但在两个时间相等时也返回true。time_before_eq(a, b) 是 time_before 的类似变体。
  • time_in_range(a, b, c) 检查时间 a 是否包含在 [b, c] 时间间隔内。范围是包含边界的,因而 a 等于 b 或 c 也会返回true。

使用这些函数,可以确保正确处理jiffies计数器的回绕问题.

尽管比较由 jiffies_64 给出的64位时间问题较少,但内核针对64位时间提供了上述函数。除了time_in_range 以外,只要向其他函数名增加_64后缀,即可得到处理64位时间值的函数变体.

就时间间隔而言,jiffies在大多数程序员心里不是首选单位。对较短的时间间隔,更传统的方式是按照毫秒或微秒度量。因而内核提供了一些辅助函数,在这些单位和jiffies之间来回转换:

<jiffies.h>
unsigned int jiffies_to_msecs(const unsigned long j);
unsigned int jiffies_to_usecs(const unsigned long j);
unsigned long msecs_to_jiffies(const unsigned int m);
unsigned long usecs_to_jiffies(const unsigned int u);

数据结构

低分辨率定时器的实现。处理过程是由(update_process_times --> run_local_timers)发起的。

定时器按链表组织,以下数据结构表示链表上的一个定时器

<linux/timer.h>

struct timer_list {
    struct list_head entry;
    unsigned long expires;

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

    struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
};
  • entry连接定时器
  • function:回调函数指针
  • data:回调函数参数
  • expires 确定定时器到期的时间,单位是jiffies。
  • base 是一个指针,指向一个基元素,其中的定时器按到期时间排序。系统中的每个处理器对应于一个基元素,因而可使用 base 确定定时器在哪个CPU上运行。

宏 DEFINE_TIMER(_name, _function, _expires, _data) 用于声明一个静态的 timer_list 实例(_expires表示偏移量,)。

动态定时器

操作方式

主要的困难在于扫描即将到期和刚刚到期的定时器链表。因为只是将所有 timer_list 实例简单地串联在一起是不够的,内核创建了不同的组,根据定时器的到期时间进行分类。分类的基础是一个主数组,有5个数组项,都是数组。主数组的5个位置根据到期时间对定时器进行粗略的分类。第一组是到期时间在0到255(或2 8 - 1)个时钟周期之间的所有定时器。第二组包含了到期时间在256和2^(8+6) - 1= 2^14 - 1个时钟周期之间的所有定时器。第三组中定时器的到期时间范围是从2^14 到2^(8+2×6) - 1个时钟周期,依次类推。

主表中的各项,称为组(group),有时又称为桶(bucket)。

每个组本身由一个数组组成,定时器在其中再次排序。第一个组的数组有256个数组项,每个位置表示0到255个时钟周期之间一个可能的到期时间。如果系统中有几个定时器的到期时间相同,它们通过一个标准的双链表连接起来。

其余的组也由数组组成,但数组项数目较少,是64个。数组项包含的是 timer_list 的双链表。但每个数组项包含的 timer_list 的 expires 值不再只有一个,而是一个时间间隔。间隔的长度与组是相关的。对第二组来说,每个数组项可容许的时间间隔为256 = 2^8 个时钟周期,而对第三组来说是2^14个时钟周期,对第四组来说是2^20 ,对第五组来说是2^26 。

eg:

在每个时钟周期会逐一处理第一组的各个索引位置上的定时器,直至到达索引位置255。接下来,用第二组的索引位置表示的数组元素中的所有定时器,来补充第一组。在第二组的索引位置到达63时(从第二组开始,每组只包含64个数组项),则使用第三组第一个数组项的内容来补充第二组。最后,在第三组的索引位置到达最大值时,从第四组取得新的数据;同样的原则,也适用于第五组到第四组的数据传输。

数据结构

上述各组的内容是通过两个简单的数据结构生成的,其不同之处很少:

typedef struct tvec_s {
    struct list_head vec[TVN_SIZE];
} tvec_t;

typedef struct tvec_root_s {
    struct list_head vec[TVR_SIZE];
} tvec_root_t;

struct tvec_t_base_s {
    spinlock_t lock;
    struct timer_list *running_timer;
    unsigned long timer_jiffies;
    tvec_root_t tv1;
    tvec_t tv2;
    tvec_t tv3;
    tvec_t tv4;
    tvec_t tv5;
} ____cacheline_aligned;

typedef struct tvec_t_base_s tvec_base_t;

tvec_root_t 对应第一组,而 tvec_t 表示后续各组。两个结构的不同只在于数组项的个数。对第一组, TVR_SIZE 定义为256。所有其他组使用的数组长度为 TVN_SIZE ,默认值64。

系统中的每个处理器都有自身的数据结构(tvec_base_t),来管理运行于其上的定时器。其成员 tv1 到 tv5 表示各个组。timer_jiffies 成员。它记录了一个时间点(单位为jiffies),该结构中此前到期的定时器都已经执行。

实现定时器处理

时间中断-->update_process_times-->run_local_timers-->raise_softirq(TIMER_SOFTIRQ);-->run_timer_softirq-->__run_timers

内核并没有显示记录各组中的索引位置,而是使用tvec_base_t中的timer_jiffies成员来计算对应的值。为此定义了一下宏:

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

#define INDEX(N) ((base->timer_jiffies >> (TVR_BITS + (N) * TVN_BITS)) & TVN_MASK)

第一组的索引位置可通过 base->timer_jiffies & TVR_MASK 计算。

通常,可使用下列宏来计算组 N 的索引值(第二组的N值为0):

#define INDEX(N) (base->timer_jiffies >> (TVR_BITS + N * TVN_BITS)) & TVN_MASK
<kernel/timer.c>

static inline void __run_timers(tvec_base_t *base)
{
    struct timer_list *timer;

    spin_lock_irq(&base->lock);
    while (time_after_eq(jiffies, base->timer_jiffies)) {//处理上一个时间点到当前时间点之间到期的所有定时器
        struct list_head work_list;
        struct list_head *head = &work_list;
        int index = base->timer_jiffies & TVR_MASK;

        /*
         * Cascade timers:
         */
        if (!index &&
            (!cascade(base, &base->tv2, INDEX(0))) &&
                (!cascade(base, &base->tv3, INDEX(1))) &&
                    !cascade(base, &base->tv4, INDEX(2)))//cascade 函数用于从指定组取得定时器补充前一组
            cascade(base, &base->tv5, INDEX(3));
        ++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);

            set_running_timer(base, timer);
            detach_timer(timer, 1);
            spin_unlock_irq(&base->lock);
            {
                int preempt_count = preempt_count();
                fn(data);
                if (preempt_count != preempt_count()) {
                    printk(KERN_WARNING "huh, entered %p "
                           "with preempt_count %08x, exited"
                           " with %08x?\n",
                           fn, preempt_count,
                           preempt_count());
                    BUG();
                }
            }
            spin_lock_irq(&base->lock);
        }
    }
    set_running_timer(base, NULL);
    spin_unlock_irq(&base->lock);
}

激活定时器

add_timer 用于将一个完全设置好的 timer_list 实例插入到上述数据结构中。

通用时间子系统

通用时间框架提供了高分辨率定时器的基础。

高分辨率定时器使用人类的时间单位,即纳秒。

内核的第二个定时器子系统的核心实现可以在 kernel/time/hrtimer.c 中找到。

概述

  • 时钟源(由 struct clocksource 定义):时间管理的支柱。本质上每个时钟源都提供了一个单调增加的计数器,通用的内核代码只能进行只读访问。不同时钟源的精度取决于底层硬件的能力。
  • 时钟事件设备(由 struct clock_event_device 定义):向时钟增加了事件功能,在未来的某个时刻发生。请注意,由于历史原因,这种设备通常也称为时钟事件源(clock event source)。
  • 时钟设备(由 struct tick_device 定义):扩展了时钟事件源的功能,提供一个时钟事件的连续流,各个时钟事件定期触发。但可以使用动态时钟机制,在一定时间间隔内停止周期时钟。

内核区分如下两种时钟类型。

  • 全局时钟(global clock),负责提供周期时钟,主要用于更新jiffies值。在此前的内核版本中,此类型时钟在IA-32系统上是由PIT实现的,在其他体系结构上由类似芯片实现。
  • 每个CPU一个局部时钟(local clock),用来进行进程统计、性能剖析和实现高分辨率定时器。

全局时钟的角色,由一个明确选择的局部时钟承担。请注意,高分辨率定时器只能工作于提供了各CPU时钟源的系统上。否则,处理器之间的大量通信将大大降低系统性能。

时间表示

通用时间框架使用数据类型ktime_t来表示时间值。无论在何种底层体系结构下,该类型都是一个64位量。

内核定义了几个辅助函数来处理 ktime_t 对象。其中包括以下函数。

  • ktime_add 和 ktime_sub 分别用于加减 ktime_t 。
  • ktime_add_ns 向一个 ktime_t 变量加上给定数量的纳秒。 ktime_add_us 是另一种形式,加的单位是微秒。内核还提供了 ktime_sub_ns 和 ktime_sub_us 。
  • ktime_set 根据指定的秒和纳秒,来创建一个 ktime_t 变量。
  • 有各种形如 x_to_y 的函数,可以在 x 和 y 两种表示之间进行转换,其中 x 和 y 的类型可以是ktime_t 、 timeval clock_t 和 timespec 。

请注意,在64位机器上可以直接将 ktime_t 解释为纳秒数,但这在32位机器上将导致问题。因而提供了 ktime_to_ns 函数,来正确执行该转换。内核提供了辅助函数 ktime_equal ,判断两个 ktime_t是否相等。

用于时钟管理的对象

时钟源

<linux/clocksource.h>

/**
 * struct clocksource - hardware abstraction for a free running counter
 *  Provides mostly state-free accessors to the underlying hardware.
 *
 * @name:       ptr to clocksource name
 * @list:       list head for registration
 * @rating:     rating value for selection (higher is better)
 *          To avoid rating inflation the following
 *          list should give you a guide as to how
 *          to assign your clocksource a rating
 *          1-99: Unfit for real use
 *              Only available for bootup and testing purposes.
 *          100-199: Base level usability.
 *              Functional for real use, but not desired.
 *          200-299: Good.
 *              A correct and usable clocksource.
 *          300-399: Desired.
 *              A reasonably fast and accurate clocksource.
 *          400-499: Perfect
 *              The ideal clocksource. A must-use where
 *              available.
 * @read:       returns a cycle value
 * @mask:       bitmask for two's complement
 *          subtraction of non 64 bit counters
 * @mult:       cycle to nanosecond multiplier
 * @shift:      cycle to nanosecond divisor (power of two)
 * @flags:      flags describing special properties
 * @vread:      vsyscall based read
 * @resume:     resume function for the clocksource, if necessary
 * @cycle_interval: Used internally by timekeeping core, please ignore.
 * @xtime_interval: Used internally by timekeeping core, please ignore.
 */
struct clocksource {
    /*
     * First part of structure is read mostly
     */
    char *name;
    struct list_head list;
    int rating;
    cycle_t (*read)(void);
    cycle_t mask;
    u32 mult;
    u32 shift;
    unsigned long flags;
    cycle_t (*vread)(void);
    void (*resume)(void);
#ifdef CONFIG_IA64
    void *fsys_mmio;        /* used by fsyscall asm code */
#define CLKSRC_FSYS_MMIO_SET(mmio, addr)      ((mmio) = (addr))
#else
#define CLKSRC_FSYS_MMIO_SET(mmio, addr)      do { } while (0)
#endif

    /* timekeeping specific data, ignore */
    cycle_t cycle_interval;
    u64 xtime_interval;
    /*
     * Second part is written at each timer interrupt
     * Keep it in a different cache line to dirty no
     * more than one cache line.
     */
    cycle_t cycle_last ____cacheline_aligned_in_smp;
    u64 xtime_nsec;
    s64 error;

#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
    /* Watchdog related data, used by the framework */
    struct list_head wd_list;
    cycle_t wd_last;
#endif
};
  • name 为时钟源给出了一个人类可读的名称
  • list 是一个标准的链表元素,用于将所有的时钟源连接到一个标准的内核链表上。
  • rating 中指定时间源质量
  • read 成员用于读取时钟周期的当前计数值。请注意,并非所有时钟源的 read返回值都使用了统一的计时单位,因而需要分别转换为纳秒值。为此,需要分别使用 mult 和 shift 成员来乘/右移返回的时钟周期数(cyc2ns函数实现)
  • flags字段指定若干标志,只有一个标志是与我们的目的相关的。 CLOCK_SOURCE_CONTINUOUS 表示一个连续时钟,尽管其含义与数学上的“连续”不怎么相同。相反,如果该标志置位,则表示该时钟是自由振荡的,不能跳跃。如果没有置位,则可以丢失一些周期,即,如果上一个周期数为n,那么即使立即读取下一个周期数,也未必是n + 1。如果时钟源要用于高分辨率定时器,该标志必须置位。

在启动期间,如果计算机确实没有提供更好的选择(在启动后,决不会如此),内核提供了一个基于jiffies的时钟:

<kernel/time/jiffies.c>
struct clocksource clocksource_jiffies = {
    .name       = "jiffies",
    .rating     = 1, /* lowest valid rating*/
    .read       = jiffies_read,//无需与硬件交互,直接返回jiffies即可
    .mask       = 0xffffffff, /*32bits*/
    .mult       = NSEC_PER_JIFFY << JIFFIES_SHIFT, /* details above */
    .shift      = JIFFIES_SHIFT,
};

在IA-32和AMD64机器上,时间戳计数器通常提供了最佳时钟。

static struct clocksource clocksource_tsc = {
    .name           = "tsc",
    .rating         = 300,
    .read           = read_tsc,//read_tsc 使用一些汇编程序代码从硬件读出当前计数器值
    .mask           = CLOCKSOURCE_MASK(64),
    .mult           = 0, /* to be set */
    .shift          = 22,
    .flags          = CLOCK_SOURCE_IS_CONTINUOUS |
                  CLOCK_SOURCE_MUST_VERIFY,
};

使用时钟源

首先,它必须注册到内核。 clocksource_register 函数负责该工作。时钟源只是被添加到全局的 clocksource_list (定义在 kernel/time/clocksource.c ),其中根据 rating对所有可用的时钟源进行排序。可调用 select_clocksource 来选择最佳时钟源。通常该函数将选择rating最大的时钟,但也可以从用户层通过 /sys/devices/system/clocksource/clocksource0/current_clocksource 指定优先选择的时钟源,内核将优先使用。为此提供了如下两个全局变量。

  • current_clocksource 指向当前最佳时钟源。
  • next_clocksource 指向一个 struct clocksource 实例,它比当前使用的时钟源更好。在注册一个新的最佳时钟源时,内核将自动切换到最佳时钟源。

为读取时钟计时,内核提供了下列函数。

  • __get_realtime_clock_ts 以一个指向 struct timespec 实例的指针为参数,读取当前时钟,转换结果,并保存到 timespec 实例。
  • getnstimeofday 是 __get_realtime_clock_ts 的一个前端,如果系统没有提供高分辨率时钟,该函数也能工作。此时, getnstimeofday 被定义在 kernel/time.c 中(而不是 kernel/time/timekeeping.c) ,提供的 timespec 值只能满足低分辨率计时需求。

时钟事件设备

<linux/clockchips.h>

struct clock_event_device {
    const char      *name;
    unsigned int        features;
    unsigned long       max_delta_ns;
    unsigned long       min_delta_ns;
    unsigned long       mult;
    int         shift;
    int         rating;
    int         irq;
    cpumask_t       cpumask;
    int         (*set_next_event)(unsigned long evt,
                          struct clock_event_device *);
    void            (*set_mode)(enum clock_event_mode mode,
                        struct clock_event_device *);
    void            (*event_handler)(struct clock_event_device *);
    void            (*broadcast)(cpumask_t mask);
    struct list_head    list;
    enum clock_event_mode   mode;
    ktime_t         next_event;
};

时钟事件设备允许注册一个事件,在未来一个指定的时间点上发生。但与完备的定时器实现相比,它只能存储一个事件。每个 clock_event_device 的关键成员是 set_next_event和 event_handler ,其中设置事件将要发生的时间,后者在事件实际发生时调用。

  • name 是该事件设备的名称,是一个可读的字符串。它将显示在 /proc/timerlist 中.
  • max_delta_ns 和 min_delta_ns 指定了当前时间和下一次事件的触发时间之间的差值,分别是最大和最小值。
  • mult 和 shift 分别是一个乘数和位移数,用于在时钟周期数和纳秒值之间进行转换。
  • event_handler 指向的函数由硬件接口代码(通常是特定于体系结构的)调用,将时钟事件传递到通用时间子系统层。
  • irq 指定了该事件设备使用的IRQ编号。请注意,只有全局设备才需要该编号。各CPU的局部时钟使用不同的硬件机制来发送信号,将 irq 设置为1即可。
  • cpumask 指定了该事件设备所服务的CPU。为此使用了一个简单的位掩码。局部设备通常只负责一个CPU。
  • broadcast 是广播实现所需要的成员,它可以规避IA-32和AMD64系统上在省电模式下不工作的局部APIC设备
  • rating 的作用类似于时钟设备中相应的机制,时钟事件设备可以通过标称其精度来进行比较。
  • 所有 struct clock_event_device 的实例都保存在全局链表 clockevent_devices 上, list成员用作链表元素。辅助函数 clockevents_register_device 用于注册一个新的时钟事件设备。该函数将指定的设备置于上述全局链表上。
  • ktime_t 存储了下一个事件的绝对时间。
  • 每个事件设备的几个特性都可以根据 features 中存储的一个位串来判定:支持周期性事件的时钟事件设备由 CLOCK_EVT_FEAT_PERIODIC 标识。CLOCK_EVT_FEAT_ONESHOT 表示时钟能够发出单触发事件,只发生一次。
  • set_mode 指向一个函数,用来切换所需要的运行方式,即在周期模式和单触发模式之间切换。

通用代码并不需要直接调用 set_next_event ,因为内核为此提供了以下辅助函数:

kernel/time/clockevents.c
int clockevents_program_event(struct clock_event_device *dev,
                              ktime_t expires, ktime_t now)

expires 给出了设备dev的过期时间( 绝对值),而now表示当前时间。通常,调用者会将ktime_get()的结果传递给该参数。

为在x86系统上跟踪用于处理全局时钟事件的设备,采用了全局变量 global_clock_event ,定义在 arch/x86/kernel/i8253.c 中。它指向当前使用的全局时钟设备的 clock_event_device 实例。

内核通常会对每个时钟硬件设备注册一个时钟设备和一个时钟事件设备。例如,考虑IA-32和AMD64系统上的HPET设备。该设备作为时钟源的功能汇集到 clocksource_hpet ,而 hpet_clockevent 则是 clock_event_device 的一个实例。二者都定义在 arch/x86/kernel/hpet.c 中。 hpet_init 首先注册时钟源然后注册时钟事件设备。这向内核增加了两个时间管理对象,但只需要一个硬件。

时钟设备

<linux/tick.h>
enum tick_device_mode {
    TICKDEV_MODE_PERIODIC,
    TICKDEV_MODE_ONESHOT,
};

struct tick_device {
    struct clock_event_device *evtdev;
    enum tick_device_mode mode;
};

tick_device 只是 struct clock_event_device 的一个包装器,增加了一个额外的字段,用于指定设备的运行模式。模式可以是周期模式或单触发模式。现在,只要将时钟设备视为一种提供时钟事件连续流的机制即可。

内核仍然会区分全局和局部(各CPU)时钟设备。局部设备汇集在 tick_cpu_device 中。

请注意,在注册一个新的时钟事件设备时,内核会自动创建一个时钟设备。

此外,在 include/time/tick-internal.h 中还定义了如下几个全局变量

  • tick_cpu_device 是一个各CPU链表,包含了系统中每个CPU对应的 struct tick_device 实例。
  • tick_next_period 指定了下一个全局时钟事件发生的时间(单位为纳秒)。
  • tick_do_timer_cpu 包含了一个CPU编号,该CPU的时钟设备将承担全局时钟设备的角色。
  • tick_period 存储了时钟周期的长度,单位为纳秒。它与 HZ 相对,后者存储了时钟的频率。

为设置一个时钟设备,内核提供了 tick_setup_device 函数。

static void tick_setup_device(struct tick_device *td,
                  struct clock_event_device *newdev, int cpu,
                  cpumask_t cpumask)

参数 td 指定了将要设置的 tick_device 实例。它将绑定到时钟事件设备 newdev 。 cpu 表示该设备关联的处理器, cpumask 是一个位掩码,用于限制只有特定的CPU才能使用该时钟设备。

<kernel/time/tick-common.c>

static void tick_setup_device(struct tick_device *td,
                  struct clock_event_device *newdev, int cpu,
                  cpumask_t cpumask)
{
    ktime_t next_event;
    void (*handler)(struct clock_event_device *) = NULL;

    /*
     * First device setup ?
     */
    if (!td->evtdev) {
        /*
         * If no cpu took the do_timer update, assign it to
         * this cpu:
         */
        if (tick_do_timer_cpu == -1) {//如果没有选定时钟设备来承担全局时钟设备的角色,那么将选择当前设备来承担此职责
            tick_do_timer_cpu = cpu;
            tick_next_period = ktime_get();
            tick_period = ktime_set(0, NSEC_PER_SEC / HZ);//时钟周期,单位纳秒
        }

        /*
         * Startup in periodic mode first.
         */
        td->mode = TICKDEV_MODE_PERIODIC;//按周期方式运行
    } else {
        handler = td->evtdev->event_handler;
        next_event = td->evtdev->next_event;
    }

    td->evtdev = newdev;

    /*
     * When the device is not per cpu, pin the interrupt to the
     * current cpu:
     */
    if (!cpus_equal(newdev->cpumask, cpumask))
        irq_set_affinity(newdev->irq, cpumask);

    /*
     * When global broadcasting is active, check if the current
     * device is registered as a placeholder for broadcast mode.
     * This allows us to handle this x86 misfeature in a generic
     * way.
     */
    if (tick_device_uses_broadcast(newdev, cpu))
        return;
    //内核需要建立一个周期时钟,取决于时钟设备是运行于周期模式还是单触发模式
    if (td->mode == TICKDEV_MODE_PERIODIC)
        tick_setup_periodic(newdev, 0);
    else
        tick_setup_oneshot(newdev, handler, next_event);
}

根据所选择的配置,内核所需要处理的情形:

  • 没有动态时钟的低分辨率系统,总是使用周期时钟。该内核不包含任何对单触发操作的支持。
  • 启用了动态时钟特性的低分辨率系统,以单触发模式使用时钟设备。
  • 高分辨率系统总是使用单触发模式,无论是否启用了动态时钟特性。

实际上,如果时钟事件设备支持周期性事件,那么该函数的任务相当简单。在这种情况下,tick_set_periodic_handler 将 tick_handle_periodic 安装为处理程序函数,而 clockevents_set_mode 确保时钟事件设备以周期模式运行。

如果时钟事件设备不支持周期事件,那么内核必须用单触发事件来设法应付过去。 clockevents_set_mode 将事件设备设置为该模式,此外,需要使用 clockevents_program_event 来编程设置下一个事件。

在两种情况下,时钟设备的下一事件发生时,都会调用处理程序函数tick_handle_periodic。

tick_handle_periodic会调用tick_periodic。

tick_periodic负责给定CPU上的时钟周期信号:

  • 如果当前时钟设备负责全局时钟,那么将调用 do_timer 。
  • 每个时钟处理程序都会调用 update_process_times ,以及 profile_tick

高分辨率定时器

高分辨率定时器与低分辨率定时器相比,有如下两个根本性的不同:

  • 高分辨率定时器按时间在一颗红黑树上排序
  • 他们独立于周期时钟。他们不使用基于jiffies的时间规格,而是采用了纳秒时间戳。

数据结构

高分辨率定时器可以基于两种时钟(称为时钟基础,clock base)。单调时钟( CLOCK_MONOTONIC )在系统启动时从0开始。另一种时钟( CLOCK_REALTIME )表示系统的实际时间。后一种时钟的时间可能发生跳跃,例如在系统时间改变时,但单调时钟始终会单调地运行。

对系统中的每个CPU,都提供了一个包含了两种时钟基础的数据结构。每个时钟基础都有一个红黑树,来排序所有待决的高分辨率定时器。

所有定时器都按过期时间在红黑树上排序,如果定时器已经到期但其处理程序回调函数尚未执行,则从红黑树迁移到一个链表中。

时钟基础由以下数据结构定义:

<linux/hrtimer.h>
struct hrtimer_clock_base {
    struct hrtimer_cpu_base *cpu_base;
    clockid_t       index;
    struct rb_root      active;
    struct rb_node      *first;
    ktime_t         resolution;
    ktime_t         (*get_time)(void);
    ktime_t         (*get_softirq_time)(void);
    ktime_t         softirq_time;
#ifdef CONFIG_HIGH_RES_TIMERS
    ktime_t         offset;
    int         (*reprogram)(struct hrtimer *t,
                         struct hrtimer_clock_base *b,
                         ktime_t n);
#endif
};
  • hrtimer_cpu_base 指向该时钟基础所属的各CPU时钟基础结构。
  • index 用于区分 CLOCK_MONOTONIC 和 CLOCK_REALTIME 。
  • rb_root 是一个红黑树的根结点,所有活动的定时器都在该树中排序。
  • first 指向将第一个到期的定时器。
  • 对高分辨率定时器的处理,由相关的软中断 HRTIMER_SOFTIRQ 发起。softirq_time 存储了软中断发出的时间
  • get_time 读取细粒度的时间。这对单调时钟是比较简单的(可直接使用由当前时钟源提供的值),但需要进行一些简单的算术操作,才能将该值转换为实际的系统时间。
  • resolution 表示该定时器的分辩率,单位为纳秒。
  • 在调整实时时钟时,会造成存储在 CLOCK_REALTIME 时钟基础上的定时器的过期时间值与当前实际时间之间的偏差。 offset 字段有助于修正这种情况,它表示定时器需要校正的偏移量。
  • reprogram 是一个函数,用于对给定的定时器事件重新编程,即修改过期时间。

对每个CPU来说,都会使用以下数据结构建立两个时钟基础:

<linux/hrtimer.h>
struct hrtimer_cpu_base {
    spinlock_t          lock;
    struct lock_class_key       lock_key;
    struct hrtimer_clock_base   clock_base[HRTIMER_MAX_CLOCK_BASES];
#ifdef CONFIG_HIGH_RES_TIMERS
    ktime_t             expires_next;
    int             hres_active;
    struct list_head        cb_pending;
    unsigned long           nr_events;
#endif
};
  • HRTIMER_MAX_CLOCK_BASES 当前设置为2
  • expires_next 包含了将要到期的下一个事件的绝对时间
  • hres_active 用作一个布尔变量,表示高分辨率模式是否已经启用,还是只提供了低分辨率模式
  • 在定时器到期时,将从红黑树迁移到一个链表中,表头为 cb_pending 。 请注意,该链表上的定时器仍然需要进行处理。这由对应的软中断处理程序完成。
  • nr_events 用于跟踪记录时钟中断的总数。

DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) ,定义全局各CPU的hrtimer_cpu_base。

定时器数据结构:

<linux/hrtimer.h>
struct hrtimer {
    struct rb_node          node;
    ktime_t             expires;
    enum hrtimer_restart        (*function)(struct hrtimer *);
    struct hrtimer_clock_base   *base;
    unsigned long           state;
#ifdef CONFIG_HIGH_RES_TIMERS
    enum hrtimer_cb_mode        cb_mode;
    struct list_head        cb_entry;
#endif
#ifdef CONFIG_TIMER_STATS
    void                *start_site;
    char                start_comm[16];
    int             start_pid;
#endif
};
  • node 用于将定时器维持在上述的红黑树中
  • base 指向定时器的基础
  • expires表示到期时间
  • function 则是在定时器到期时调用的回调函数
  • cb_entry 是链表元素,可用于将定时器置于回调链表上

每个定时器都可以指定一些情况,在这些情况下,该定时器可能或必须运行。有下列选择可用:

<linux/hrtimer.h>
/*
 * hrtimer callback modes:
 *
 *  HRTIMER_CB_SOFTIRQ:     Callback must run in softirq context
 *  HRTIMER_CB_IRQSAFE:     Callback may run in hardirq context
 *  HRTIMER_CB_IRQSAFE_NO_RESTART:  Callback may run in hardirq context and
 *                  does not restart the timer
 *  HRTIMER_CB_IRQSAFE_NO_SOFTIRQ:  Callback must run in hardirq context
 *                  Special mode for tick emultation
 */
enum hrtimer_cb_mode {
    HRTIMER_CB_SOFTIRQ,
    HRTIMER_CB_IRQSAFE,
    HRTIMER_CB_IRQSAFE_NO_RESTART,
    HRTIMER_CB_IRQSAFE_NO_SOFTIRQ,
};

定时器当前的状态保存在 state 中。有下列可能值:

  • HRTIMER_STATE_INACTIVE 表示不活动的定时器
  • 在时钟基础上排队、等待到期的定时器,其状态为 HRTIMER_STATE_ENQUEUED
  • HRTIMER_STATE_CALLBACK 表示当前正在执行定时器的回调函数
  • 在定时器已经到期,正在回调链表上等待执行时,其状态为 HRTIMER_STATE_PENDING

回调函数本身值得进行一些专门的考虑。有两个可能的返回值:

<hrtimer.h>
enum hrtimer_restart {
    HRTIMER_NORESTART, /* 定时器无须重启 */
    HRTIMER_RESTART, /* 定时器必须重启 */
};

通常,回调函数结束执行时会返回 HRTIMER_NORESTART 。在这种情况下,该定时器将从系统消失。但定时器也可以选择重启。这需要在回调函数中执行如下两个步骤。

  • 回调函数的结果必须是 HRTIMER_RESTART 。
  • 定时器的到期时间必须设置为未来的某个时间点。

设置定时器

设置一个新的定时器需要如下步骤:

  • hrtimer_init用于初始化一个hrtimer实例
  • hrtimer_start设置定时器的到期时间,并启动定时器

为取消一个设置好的定时器,内核提供了hrtimer_cancel和hrtimer_try_to_cancel

如果要重启一个取消的定时器,可使用hrtimer_restart。

实现

高分辨率模式下的高分辨率定时器

在负责高分辨率定时器的时钟事件设备引发一个中断时,将调用 hrtimer_interrupt 作为事件处理程序。该函数负责选中所有到期的定时器,或者将其转移到过期链表(如果它们可以在软中断上下文执行),或者直接调用定时器的处理程序函数。在对时钟事件设备重新编程(使得在下一个待决定时器到期时可以引发一个中断)之后,将引发软中断 HRTIMER_SOFTIRQ 。在该软中断执行时,run_hrtimer_softirq 负责执行到期链表上所有定时器的处理程序函数。

<kernel/hrtimer.c>

void hrtimer_interrupt(struct clock_event_device *dev)
{
    struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);
    struct hrtimer_clock_base *base;
    ktime_t expires_next, now;
    int i, raise = 0;

    BUG_ON(!cpu_base->hres_active);
    cpu_base->nr_events++;
    dev->next_event.tv64 = KTIME_MAX;

 retry:
    now = ktime_get();

    expires_next.tv64 = KTIME_MAX;//expires_next保存下一次到期的时间,保存KTIME_MAX表示没有下一个到期的时间

    base = cpu_base->clock_base;

    for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {//遍历时钟基础
        ktime_t basenow;
        struct rb_node *node;

        spin_lock(&cpu_base->lock);

        basenow = ktime_add(now, base->offset);//basenow 表示当前时间。 base->offset 仅在已经重新调整了实时时钟时,才是非零值

        while ((node = base->first)) {
            struct hrtimer *timer;

            timer = rb_entry(node, struct hrtimer, node);

            if (basenow.tv64 < timer->expires.tv64) {//下一个到期的定时器是未来的。则跳过处理
                ktime_t expires;

                expires = ktime_sub(timer->expires,
                            base->offset);
                if (expires.tv64 < expires_next.tv64)
                    expires_next = expires;
                break;
            }

            /* Move softirq callbacks to the pending list */
            if (timer->cb_mode == HRTIMER_CB_SOFTIRQ) {//那么在允许在软中断上下文执行处理程序的情况下,会将该定时器移动到回调链表
                __remove_hrtimer(timer, base,
                         HRTIMER_STATE_PENDING, 0);//移除该定时器的同时,也通过更新 base->first 选择了下一个到期的候选定时器
                list_add_tail(&timer->cb_entry,
                          &base->cpu_base->cb_pending);
                raise = 1;
                continue;
            }

            __remove_hrtimer(timer, base,
                     HRTIMER_STATE_CALLBACK, 0);
            timer_stats_account_hrtimer(timer);

            /*
             * Note: We clear the CALLBACK bit after
             * enqueue_hrtimer to avoid reprogramming of
             * the event hardware. This happens at the end
             * of this function anyway.
             */
            //硬件中断上下文上执行
            if (timer->function(timer) != HRTIMER_NORESTART) {//HRTIMER_RESTART
                BUG_ON(timer->state != HRTIMER_STATE_CALLBACK);
                enqueue_hrtimer(timer, base, 0);
            }
            timer->state &= ~HRTIMER_STATE_CALLBACK;
        }
        spin_unlock(&cpu_base->lock);
        base++;
    }

    cpu_base->expires_next = expires_next;

    /* Reprogramming necessary ? */
    if (expires_next.tv64 != KTIME_MAX) {
        if (tick_program_event(expires_next, 0))//时钟事件设备重新编程,以便在下一个定时器到期时引发中断。
            goto retry;
    }

    /* Raise softirq ? */
    if (raise)
        raise_softirq(HRTIMER_SOFTIRQ);//启动软中断,run_hrtimer_softirq ,调用回调链表上的定时器
}

低分辨率模式下的高分辨率定时器

如果系统没有提供高分辨率时钟,高分辨率定时器的到期操作由hrtimer_run_queues 发起(run_timer_softirq-->hrtimer_run_queues)。

hrtimer_run_queues-->hrtimer_get_softirq_time:将粗粒度的时间值保存到定时器基础

hrtimer_run_queues-->run_hrtimer_queue:遍历时间基础中的定时器,并执行到期定时器

周期时钟仿真

高分辨率定时器提供一个等效的功能(周期时钟仿真)。

本质上, tick_sched 是一个专门的数据结构,用于管理周期时钟相关的所有信息,由全局变量tick_cpu_sched 为每个CPU分别提供了一个该结构的实例。

在内核切换到高分辨率模式时,将调用 tick_setup_sched_timer 来激活时钟仿真层。这将为每个CPU安装一个高分辨率定时器。所需的 struct hrtimer 实例保存在各CPU变量 tick_cpu_sched 中:

<tick.h>
struct tick_sched {
    struct hrtimer sched_timer;
...
}

该定时器的回调函数选择了 tick_sched_timer 。该函数与 tick_periodic 有些类似,但要复杂一些。

切换到高分辨率定时器

最初,高分辨率定时器并未启用,只有在已经初始化了适当的高分辨率时钟源并将其添加到通用时钟框架之后,才能启用高分辨率定时器。

在低分辨率定时器活动时,高分辨率队列由 hrtimer_run_queues 处理。

在队列运行前,该函数将检查系统中是否存在适用于高分辨率定时器的时钟事件设备。

<kernel/hrtimer.c>
void hrtimer_run_queues(void)
{
...
    if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
        if (hrtimer_switch_to_hres())
            return;
...
}

如果有一个支持单触发模式的时钟,而且其精度可以达到高分辨率定时器所要求的分辨率(即设置了 CLOCK_SOURCE_VALID_FOR_HRES 标志),那么 tick_check_oneshot_change 将通知内核可以使用高分辨率定时器。实际的切换由 hrtimer_switch_to_hres 执行

<kernel/hrtimer.c>

static int hrtimer_switch_to_hres(void)
{
    int cpu = smp_processor_id();
    struct hrtimer_cpu_base *base = &per_cpu(hrtimer_bases, cpu);
    unsigned long flags;

    if (base->hres_active)
        return 1;

    local_irq_save(flags);

    if (tick_init_highres()) {//将其转换为单触发模式,并将hrtimer_interrupt注册为事件处理程序
        local_irq_restore(flags);
        printk(KERN_WARNING "Could not switch to high resolution "
                    "mode on CPU %d\n", cpu);
        return 0;
    }
    base->hres_active = 1;
    base->clock_base[CLOCK_REALTIME].resolution = KTIME_HIGH_RES;
    base->clock_base[CLOCK_MONOTONIC].resolution = KTIME_HIGH_RES;

    tick_setup_sched_timer();//激活周期时钟仿真

    /* "Retrigger" the interrupt to get things going */
    retrigger_next_event(NULL);
    local_irq_restore(flags);
    printk(KERN_DEBUG "Switched to high resolution mode on CPU %d\n",
           smp_processor_id());
    return 1;
}

动态时钟

由于时钟可以根据当前的需要来激活或停用,因而“动态时钟”这个术语就很适用。

每当选中idle进程运行时,都将禁用周期时钟,直至下一个定时器即将到期为止。在经过这样一段时间之后,或者有中断发生时,将重新启用周期时钟。

在讨论动态时钟的实现之前,我们先要注意,单触发时钟是实现动态时钟的先决条件。因为动态时钟的一个关键特性是可以根据需要来停止或重启时钟机制,纯粹周期性的定时器根本就不适用于该机制。

下文提到周期时钟时,是指时钟的实现没有使用动态时钟。这决不能与工作于周期模式的时钟事件设备相混淆。

数据结构

<linux/tick.h>
/**
 * struct tick_sched - sched tick emulation and no idle tick control/stats
 * @sched_timer:    hrtimer to schedule the periodic tick in high
 *          resolution mode
 * @idle_tick:      Store the last idle tick expiry time when the tick
 *          timer is modified for idle sleeps. This is necessary
 *          to resume the tick timer operation in the timeline
 *          when the CPU returns from idle
 * @tick_stopped:   Indicator that the idle tick has been stopped
 * @idle_jiffies:   jiffies at the entry to idle for idle time accounting
 * @idle_calls:     Total number of idle calls
 * @idle_sleeps:    Number of idle calls, where the sched tick was stopped
 * @idle_entrytime: Time when the idle call was entered
 * @idle_sleeptime: Sum of the time slept in idle with sched tick stopped
 * @sleep_length:   Duration of the current idle sleep
 */
struct tick_sched {
    struct hrtimer          sched_timer;
    unsigned long           check_clocks;
    enum tick_nohz_mode     nohz_mode;
    ktime_t             idle_tick;
    int             tick_stopped;
    unsigned long           idle_jiffies;
    unsigned long           idle_calls;
    unsigned long           idle_sleeps;
    ktime_t             idle_entrytime;
    ktime_t             idle_sleeptime;
    ktime_t             sleep_length;
    unsigned long           last_jiffies;
    unsigned long           next_jiffies;
    ktime_t             idle_expires;
};
  • sched_timer 表示用于实现时钟的定时器。
  • 当前运作模式保存在 nohz_mode 中。有3种可能值(NOHZ_MODE_INACTIVE:周期时钟处于活动状态,NOHZ_MODE_LOWRES、NOHZ_MODE_HIGHRES:两个值表示所使用的动态时钟是基于低/高分辨率的定时器)
  • idle_tick 存储在禁用周期时钟之前,上一个时钟信号的到期时间。
  • 如果周期时钟已经停用,则 tick_stopped 为1
  • idle_jiffies 存储了周期时钟禁用时的 jiffies 值。
  • idle_calls 统计了内核试图停用周期时钟的次数。
  • idle_sleeps 统计了实际上成功停用周期时钟的次数。
  • idle_sleeptime 存储了周期时钟上一次禁用的准确时间
  • sleep_length 存储了周期时钟将禁用的时间长度,即从时钟禁用起,到预定将发生的下一个时钟信号为止,这一段时间的长度。
  • idle_sleeptime 累计了时钟停用的总的时间
  • next_jiffies 存储了下一个定时器到期时间的jiffy值。
  • idle_expires 存储了下一个将到期的经典定时器的到期时间。与上一个值不同,这个值的分辨率会尽可能高,其单位不是jiffies。

tick_cpu_sched 是一个全局各CPU变量,提供了一个 struct tick_sched 实例。这是必须的,因为对时钟的禁用是按CPU指定的,而不是对整个系统指定。

低分辨率系统下的动态时钟

hrtimer_run_queues 调用 tick_check_oneshot_change 来判断是否可以激活高分辨率定时器。此外,该函数还检查是否可以在低分辨率系统上启用动态时钟。在两种情况下,这是可能的。

  • 提供了支持单触发模式的时钟事件设备。
  • 未启用高分辨率模式。

启用过程:

run_timer_softirq-->hrtimer_run_queues-->tick_check_oneshot_change-->tick_nohz_switch_to_nohz

tick_nohz_switch_to_nohz:改变是将时钟事件设备设置为单触发模式。并安装一个适当的时钟定时器处理程序(tick_nohz_handler)。

动态时钟处理程序

新的时钟定时器处理程序 tick_nohz_handler 需要承担如下两个职责。

  • 执行时钟机制所需的所有操作。
  • 对时钟设备重新编程,使得下一个时钟信号在适当的时候到期。
<kernel/time/tick-sched.c>
static void tick_nohz_handler(struct clock_event_device *dev)
{
    struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
    struct pt_regs *regs = get_irq_regs();
    int cpu = smp_processor_id();
    ktime_t now = ktime_get();

    dev->next_event.tv64 = KTIME_MAX;

    /*
     * Check if the do_timer duty was dropped. We don't care about
     * concurrency: This happens only when the cpu in charge went
     * into a long sleep. If two cpus happen to assign themself to
     * this duty, then the jiffies update is still serialized by
     * xtime_lock.
     */
    if (unlikely(tick_do_timer_cpu == -1))//如果一个CPU要进入比较长时间的休眠,不能继续负责全局时钟,需要撤销其职责。如果是这样,那么接下来如果有哪个CPU的时钟定时器处理程序被调用,该CPU必须承担该职责:
        tick_do_timer_cpu = cpu;

    /* Check, if the jiffies need an update */
    if (tick_do_timer_cpu == cpu)
        tick_do_update_jiffies64(now);

    /*
     * When we are idle and the tick is stopped, we have to touch
     * the watchdog as we might not schedule for a really long
     * time. This happens on complete idle SMP systems while
     * waiting on the login prompt. We also increment the "start
     * of idle" jiffy stamp so the idle accounting adjustment we
     * do when we go busy again does not account too much ticks.
     */
    if (ts->tick_stopped) {
        touch_softlockup_watchdog();
        ts->idle_jiffies++;
    }

    update_process_times(user_mode(regs));
    profile_tick(CPU_PROFILING);

    /* Do not restart, when we are in the idle loop */
    if (ts->tick_stopped)
        return;

    while (tick_nohz_reprogram(ts, now)) {//重编程,设置为在下一个jiffy到期
        now = ktime_get();
        tick_do_update_jiffies64(now);
    }
}

更新jiffies

全局时钟设备调用 tick_do_update_jiffies64 来更新全局 jiffies_64 变量。

在使用周期时钟时,这相对简单,因为每过一个jiffy,都会调用该函数。在启用动态时钟时,可能出现这种情况:系统的所有CPU都处于idel状态,系统处于没有全局时钟的状态。tick_do_update_jiffies64 需要考虑这种情况。

<kernel/time/tick-sched.c>
static void tick_do_update_jiffies64(ktime_t now)
{
    unsigned long ticks = 0;
    ktime_t delta;

    /* Reevalute with xtime_lock held */
    write_seqlock(&xtime_lock);

    delta = ktime_sub(now, last_jiffies_update);
    if (delta.tv64 >= tick_period.tv64) {//自上次更新jiffy值以来,已经过去了一个时钟周期

        delta = ktime_sub(delta, tick_period);
        last_jiffies_update = ktime_add(last_jiffies_update,
                        tick_period);

        /* Slow path for long timeouts */
        if (unlikely(delta.tv64 >= tick_period.tv64)) {//更新也可能是在多于一个时钟周期
            s64 incr = ktime_to_ns(tick_period);

            ticks = ktime_divns(delta, incr);

            last_jiffies_update = ktime_add_ns(last_jiffies_update,
                               incr * ticks);//调用 do_timer 来更新全局jiffies值,
        }
        do_timer(++ticks);
    }
    write_sequnlock(&xtime_lock);
}

高分辨率系统下的动态定时器

内核使用高分辨率时,时钟事件设备以单触发模式运行。对动态时钟的支持比低分辨率情形更为容易实现。

停止和启动周期时间

很自然的一种做法是,在调度idle进程时停止时钟:这表明处理器确实没什么可做。动态时钟框架提供了 tick_nohz_stop_sched_tick ,用于停止时钟。

在调用 tick_nohz_stop_sched_tick 关闭时钟后,系统进入一个无限循环,在该处理器上有一个进程可调度时,循环才结束。时钟接下来就需要使用了,可通过 tick_nohz_restart_sched_tick 重激活。

有两种情况需要重启时钟:

  • 一个外部中断使某个进程变为可运行,这要求时钟机制恢复工作。 在这种情况下,时钟的恢复,比最初计划的时间要早一些。
  • 下一个时钟信号即将到期,而时钟中断表明到期时间已经到来。在这种情况下,时钟机制的恢复与此前的计划相同。

停止时钟

tick_nohz_stop_sched_tick需要执行以下3个任务

  • 检查下一个定时器轮事件是否在一个时钟周期之后。
  • 如果是这样,则重新编程时钟设备,忽略下一个时钟周期信号,直至有必要时才恢复。这将自动忽略所有不需要的时钟信号。
  • 在 tick_sched 中更新统计信息
< kernel/time/tick-sched.c >
void tick_nohz_stop_sched_tick(void)
{
    unsigned long seq, last_jiffies, next_jiffies, delta_jiffies, flags;
    struct tick_sched *ts;
    ktime_t last_update, expires, now, delta;
    struct clock_event_device *dev = __get_cpu_var(tick_cpu_device).evtdev;
    int cpu;

    local_irq_save(flags);

    cpu = smp_processor_id();
    ts = &per_cpu(tick_cpu_sched, cpu);

    /*
     * If this cpu is offline and it is the one which updates
     * jiffies, then give up the assignment and let it be taken by
     * the cpu which runs the tick timer next. If we don't drop
     * this here the jiffies might be stale and do_timer() never
     * invoked.
     */
    if (unlikely(!cpu_online(cpu))) {
        if (cpu == tick_do_timer_cpu)
            tick_do_timer_cpu = -1;
    }

    if (unlikely(ts->nohz_mode == NOHZ_MODE_INACTIVE))
        goto end;

    if (need_resched())
        goto end;

    cpu = smp_processor_id();
    if (unlikely(local_softirq_pending())) {
        static int ratelimit;

        if (ratelimit < 10) {
            printk(KERN_ERR "NOHZ: local_softirq_pending %02x\n",
                   local_softirq_pending());
            ratelimit++;
        }
    }

    now = ktime_get();//当前时间
    /*
     * When called from irq_exit we need to account the idle sleep time
     * correctly.
     */
    if (ts->tick_stopped) {
        delta = ktime_sub(now, ts->idle_entrytime);
        ts->idle_sleeptime = ktime_add(ts->idle_sleeptime, delta);
    }

    ts->idle_entrytime = now;
    ts->idle_calls++;

    /* Read jiffies and the time when jiffies were updated last */
    do {
        seq = read_seqbegin(&xtime_lock);
        last_update = last_jiffies_update;//上一次更新jiffies的时间
        last_jiffies = jiffies;//当前jiffies的值
    } while (read_seqretry(&xtime_lock, seq));

    /* Get the next timer wheel timer */
    next_jiffies = get_next_timer_interrupt(last_jiffies);//找到下一次事件到期的jiffies
    delta_jiffies = next_jiffies - last_jiffies;//当前时间与下一次事件到期时间的差值

    if (rcu_needs_cpu(cpu))
        delta_jiffies = 1;
    /*
     * Do not stop the tick, if we are only one off
     * or if the cpu is required for rcu
     */
    if (!ts->tick_stopped && delta_jiffies == 1)
        goto out;

    /* Schedule the tick, if we are at least one jiffie off */
    if ((long)delta_jiffies >= 1) {//如果下一个时钟信号至少在一个jiffy以后,则时钟设备需要据此重新编程:

        if (delta_jiffies > 1)
            cpu_set(cpu, nohz_cpu_mask);
        /*
         * nohz_stop_sched_tick can be called several times before
         * the nohz_restart_sched_tick is called. This happens when
         * interrupts arrive which do not cause a reschedule. In the
         * first call we save the current tick time, so we can restart
         * the scheduler tick in nohz_restart_sched_tick.
         */
        if (!ts->tick_stopped) {
            if (select_nohz_load_balancer(1)) {
                /*
                 * sched tick not stopped!
                 */
                cpu_clear(cpu, nohz_cpu_mask);
                goto out;
            }

            ts->idle_tick = ts->sched_timer.expires;
            ts->tick_stopped = 1;
            ts->idle_jiffies = last_jiffies;
        }

        /*
         * If this cpu is the one which updates jiffies, then
         * give up the assignment and let it be taken by the
         * cpu which runs the tick timer next, which might be
         * this cpu as well. If we don't drop this here the
         * jiffies might be stale and do_timer() never
         * invoked.
         */
        if (cpu == tick_do_timer_cpu)//如果当前CPU必须提供全局时钟,那么此职责必须转交给另一CPU。
            tick_do_timer_cpu = -1;

        ts->idle_sleeps++;

        /*
         * delta_jiffies >= NEXT_TIMER_MAX_DELTA signals that
         * there is no timer pending or at least extremly far
         * into the future (12 days for HZ=1000). In this case
         * we simply stop the tick timer:
         */
        if (unlikely(delta_jiffies >= NEXT_TIMER_MAX_DELTA)) {
            ts->idle_expires.tv64 = KTIME_MAX;
            if (ts->nohz_mode == NOHZ_MODE_HIGHRES)
                hrtimer_cancel(&ts->sched_timer);
            goto out;
        }

        /*
         * calculate the expiry time for the next timer wheel
         * timer
         */
        //最后,将对时钟设备重新编程,以便在未来的适当时间点提供下一个事件的信号
        expires = ktime_add_ns(last_update, tick_period.tv64 *
                       delta_jiffies);
        ts->idle_expires = expires;

        if (ts->nohz_mode == NOHZ_MODE_HIGHRES) {
            hrtimer_start(&ts->sched_timer, expires,
                      HRTIMER_MODE_ABS);
            /* Check, if the timer was already in the past */
            if (hrtimer_active(&ts->sched_timer))
                goto out;
        } else if(!tick_program_event(expires, 0))
                goto out;
        /*
         * We are past the event already. So we crossed a
         * jiffie boundary. Update jiffies and raise the
         * softirq.
         */
        tick_do_update_jiffies64(ktime_get());
        cpu_clear(cpu, nohz_cpu_mask);
    }
    raise_softirq_irqoff(TIMER_SOFTIRQ);
out:
    ts->next_jiffies = next_jiffies;
    ts->last_jiffies = last_jiffies;
    ts->sleep_length = ktime_sub(dev->next_event, now);
end:
    local_irq_restore(flags);
}

重启时钟

tick_nohz_restart_sched_tick

同样,该函数的实现被各种技术细节搞得很复杂,但其一般原理是很简单的。首先调用我们熟悉的 tick_do_updates_jiffies64 。在正确地统计空闲时间之后,将 tick_sched->tick_stopped 设置为0,因为时钟现在再次激活了。最后,需要对下一个时钟事件编程。这是必要的,因为外部中断的存在,可能导致空闲时间的结束早于预期。

定时器相关系统调用的实现

在使用定时器时,有3个选项可以区分如何计算经过的时间,或定时器所处的时间基准:

  • ITIMER_REAL 测量定时器激活以来实际流逝的时间,以便在超时时间达到时发出信号。在这种情况下,定时器会继续运转,而不管系统是处于核心态还是用户态,或使用该定时器的应用程序当前是否在运行。在定时器到期时将发出 SIGALRM 类型的信号。
  • ITIMER_VIRTUAL 只在定时器的拥有者进程在用户态消耗的时间内运行。在这种情况下,在核心态(或处理器忙于另一个应用程序)消耗的时间将忽略。定时器到期通过 SIGVTALRM 信号表示。
  • ITIMER_PROF 计算进程在用户态和核心态消耗的时间,在内核代表该进程执行系统调用时,仍然会计算时间的消耗。系统其他进程消耗的时间将忽略。定时器到期时发送的信号是 SIGPROF 。

alarm 和 setitimer 系统调用

alarm 安装 ITIMER_REAL 类型的定时器(实时定时器),而 setitimer 不仅可用于安装实时定时器,还可以安装虚拟和剖析定时器。这两个系统调用都结束于 do_setitimer 。

<kernel/itimer.c>
int do_setitimer(int which, struct itimerval *value, struct itimerval *ovalue)
  • which 指定了定时器类型.可以是 ITIMER_REAL 、 ITIMER_VIRTUAL 或ITIMER_PROF 。
  • value 包含有关新定时器的所有信息
  • 如果定时器将替换某个现存定时器,那么可使用 ovalue 来返回此前活动的定时器的描述。

管理进程时间

task_struct 实例包含了两个与进程时间有关的成员,在这里比较重要:

<sched.h>
struct task_struct {
...
    cputime_t utime, stime;
...
}

update_process_times 用于管理特定于进程的时间数据,从局部时钟调用。

update_process_times->account_process_tick:使用 account_user_time 或 account_sys_time 来更新进程在用户态或核心态消耗的CPU时间,即 task_struct 中的 utime 或 stime 成员。如果进程超出了 Rlimit 指定的CPU份额限制,那么还会每隔1秒发送 SIGXCPU 信号。

update_process_times->run_local_timers 激活低分辨率定时器,或对其进行到期操作。

update_process_times->scheduler_tick 是一个辅助函数,用于CPU调度器,

update_process_times->run_posix_cpu_timers 使当前注册的 POSIX 定时器开始运行.

总结

时钟中断:timer_interrupt

时钟源:struct clocksource

时钟事件设备:struct clock_event_device

时钟设备:struct clock_event_device

低分辨率动态时钟的 clock_event_device.event_handler = tick_nohz_handler

低分辨率周期时钟的 clock_event_device.event_handler = tick_periodic

高分辨率动态时钟的 clock_event_device.event_handler = hrtimer_interrupt.由tick_sched结构模拟的定时器来实现动态时钟

高分辨率周期时钟的 clock_event_device.event_handler = hrtimer_interrupt.由tick_sched结构模拟的定时器来实现周期时钟

Linux内核入门到放弃-时间管理-《深入Linux内核架构》笔记的更多相关文章

  1. Linux内核入门到放弃-内存管理-《深入Linux内核架构》笔记

    概述 内存管理的实现涵盖了许多领域: 内存中的物理内存页管理 分配大块内存的伙伴系统 分配较小内存块的slab.slub和slob分配器 分配非连续内存块的vmalloc机制 进程的地址空间 在IA- ...

  2. Linux内核入门到放弃-进程管理和调度-《深入Linux内核架构》笔记

    进程优先级 硬实时进程 软实时进程 普通进程 O(1)调度.完全公平调度器 抢占式多任务处理(preemptive multitasking):各个进程都分配到一定的时间段可以执行.时间段到期后,内核 ...

  3. Linux内核入门到放弃-网络-《深入Linux内核架构》笔记

    网络命名空间 struct net { atomic_t count; /* To decided when the network * namespace should be freed. */ a ...

  4. Linux内核入门到放弃-模块-《深入Linux内核架构》笔记

    使用模块 依赖关系 modutils标准工具集中的depmod工具可用于计算系统的各个模块之间的依赖关系.每次系统启动时或新模块安装后,通常都会运行该程序.找到的依赖关系保存在一个列表中.默认情况下, ...

  5. Linux从入门到放弃、零基础入门Linux(第四篇):在虚拟机vmware中安装centos7.7

    如果是新手,建议安装带图形化界面的centos,这里以安装centos7.7的64位为例 一.下载系统镜像 镜像文件下载链接https://wiki.centos.org/Download 阿里云官网 ...

  6. Linux从入门到放弃、零基础入门Linux(第三篇):在虚拟机vmware中安装linux(二)超详细手把手教你安装centos6分步图解

    一.继续在vmware中安装centos6.9 本次安装是进行最小化安装,即没有图形化界面的安装,如果是新手,建议安装带图形化界面的centos, 具体参考Linux从入门到放弃.零基础入门Linux ...

  7. Linux内核入门到放弃-无持久存储的文件系统-《深入Linux内核架构》笔记

    proc文件系统 proc文件系统是一种虚拟的文件系统,其信息不能从块设备读取.只有在读取文件内容时,才动态生成相应的信息. /proc的内容 内存管理 系统进程的特征数据 文件系统 设备驱动程序 系 ...

  8. Linux内核入门到放弃-锁与进程间通信-《深入Linux内核架构》笔记

    内核锁机制 对整数的原子操作 <asm-arch/atomic.h> typedef struct {volatile int counter;} atomic_t; //初始化只能借助于 ...

  9. Linux从入门到放弃

    Ch.0 几点Linux常识 Linux严格区分大小写,不像windows中命令是不区分大小写的 Linux中所有内容以文件形式保存,包括硬件 Linux不靠扩展名区分文件类型,所有扩展名只是为了方便 ...

随机推荐

  1. 数据结构之哈希(hash)表

    最近看PHP数组底层结构,用到了哈希表,所以还是老老实实回去看结构,在这里去总结一下. 1.哈希表的定义 这里先说一下哈希(hash)表的定义:哈希表是一种根据关键码去寻找值的数据映射结构,该结构通过 ...

  2. Go语言系列文章

    这个系列写的不是很好,未来重构. Go基础系列 Go基础 Go基础 1.Go简介 2.Go数据结构struct 3.构建Go程序 4.import导包和初始化阶段 5.array 6.Slice详解 ...

  3. Spring框架浅析

    一.一个简单的示例 1.引入依赖和配置 pom.xml <?xml version="1.0" encoding="UTF-8"?> <pro ...

  4. Haskell复习笔记(二)

    Haskell中的递归 递归就是定义函数以调用自身的方式,关于递归解决问题的实例有很多,如斐波那契数列,还有汉诺塔问题,递归也正是Haskell中用来解决循环问题的关键. 自定义maxinum函数 m ...

  5. C#_实现冒泡排序

    //排序方法类public class Bubble { ; public static void SBubble(ref int[] intArr) { ; outSize < intArr. ...

  6. CSS 渐变色

    CSS linear-gradient() 函数 http://www.runoob.com/cssref/func-linear-gradient.html CSS radial-gradient( ...

  7. Vue.js如何在一个页面调用另一个同级页面的方法

    使用场景: 页面分为header.home.footer三部分,需要在home中调用header中的方法,这两个没有相互引入 官方给出方法: 需要在展示页里调用顶部导航栏页里的方法,两者之间没有引用关 ...

  8. 在 Apex 中得到 sObject 的信息

    Salesforce 的数据模型是基于 sObject 的.在 Apex 中,所有的标准对象.自定义对象都是继承自 sObject 的. 关于在 Apex 中得到 sObject 的信息,我们要基于两 ...

  9. Windbg学习笔记

    下载winsdksetup.exe ,双击,选择Debugging Tools for Windows安装. 64位系统抓64位进程dump,用64位windbg来分析.64位系统抓32位进程dump ...

  10. github、git软件安装、pycharm下使用git配置、git GUI相关

    1.GitHub: 官网:直接搜索,排名很靠前,需要注册: 注册完之后,会有指引.新建项目两个选项(看不懂的问YOUDAO等翻译软件啦,大段复制进去就行) 2.Git安装: (https://git- ...