linux 时间子系统

  • 这篇草稿有了 4 年了吧,已经不是拖延症的问题了…

    • 篇幅还不少,丢掉可惜,稍微整理后发.
    • 因为当时动笔时属于应付 KPI,东拼西凑,无法保证内容准确性.
    • 时间实在太长了,资料来源有很多缺失了,版权问题请联系我.
  • 资料来源:

    http://www.wowotech.net/timer_subsystem/time_concept.html

  • 更新

1
2
2022.03.27 初始
2024.12.13 修正图片问题, 使用 GPT 做了一些内容的修正,但依旧无力核对所有内容, 请仅供参考.

导语

曾经以为对 linux 能刨根问底,于是轻率的进入了 linux 驱动/应用开发,最后却身心俱疲.个中原因😔.

  • 当时个人的基础并不能支撑想探寻的疑问.
  • 接手的工作环境…那些遗留代码…已经不回想了…
  • 兴趣被工作挟裹后,对当时的自己实在是个非常大的打击.

这篇草稿有了 3 年了吧,如今没有那个时间/精力重写,只能稍微整理后发出.

  • 因为当时动笔时属于应付 KPI,东拼西凑,无法保证内容准确性. 目前经过 O1 模型审核 😂(24.12.14)
  • 因时间实在太久了,很多文献的出处也记不清楚了,因此有很多部分无法标注来源…

概述

背景

时间在 linux 系统的概念 or 内容

时间标准

一般接触到的时间基准有两个:UTC 和 GMT。

  • UTC(Coordinated Universal Time,协调世界时):是以原子时秒为基础,并结合地球自转规律进行调整的时间系统。UTC 是当前国际上通用的时间标准,被广泛应用于互联网、通信等领域。UTC 与 UT1(基于地球自转的时间)之间的差异通过闰秒进行调整,通常差异在几毫秒范围内。
  • GMT(Greenwich Mean Time,格林尼治标准时间):是以伦敦皇家格林尼治天文台为基准的时间系统,传统上用于表示本初子午线通过时的时间。虽然 GMT 在技术上已经被 UTC 取代,但在日常用语和一些地区时区命名中仍然使用 GMT。

由于在大多数情况下,GMT 与 UTC 相差甚微,因此通常可近似认为:

北京时间 = UTC + 8 = GMT + 8;

Linux 时间精度

Linux 系统中传统的时间精度单位为秒,但已远远满足不了需求,进而拓展到了微秒、纳秒级别。Linux 系统中精度有 4 种表示,内核提供了不同类型的转换的接口。

  • __kernel_time_t: 精度为 1s,如使用被定义为 32 位的 time_t 的变量,存在 2038 年问题, 64 位系统不存在这个问题.

    1
    2
    typedef __kernel_long_t __kernel_time_t;
    typedef long __kernel_long_t;
  • Timeval(include/uapi/linux/time.h) 精度为 1us

    1
    2
    3
    4
    struct timeval {
    __kernel_time_t     tv_sec;     /* seconds */
    __kernel_suseconds_t    tv_usec;    /* microseconds */
    };
  • timespec(include/uapi/linux/time.h)精度 1ns

    1
    2
    3
    4
    struct timespec {
    __kernel_time_t tv_sec; /* seconds */
    long tv_nsec; /* nanoseconds */
    };
  • 64 位拓展 timespec64,定义在 time64.h 文件中。高精度计时器一般采用纳秒级别的计量。

    1
    2
    3
    4
    struct timespec64 {
    time64_t tv_sec; /_ seconds _/
    long tv_nsec; /_ nanoseconds */
    };
  • ktime(include/linux/ktime.h)精度 1ns,高精度计时器中常用,只能在内核中使用。

    1
    2
    3
    union ktime {
    s64 tv64;
    };

对应人类习惯年月日时分秒的表示方法,linux 内核提供了 struct tm 结构体。同时内核提供了各个时间类型的转换接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct tm {
/*
* the number of seconds after the minute, normally in the range
* 0 to 59, but can be up to 60 to allow for leap seconds
*/
int tm_sec;
/* the number of minutes after the hour, in the range 0 to 59*/
int tm_min;
/* the number of hours past midnight, in the range 0 to 23 */
int tm_hour;
/* the day of the month, in the range 1 to 31 */
int tm_mday;
/* the number of months since January, in the range 0 to 11 */
int tm_mon;
/* the number of years since 1900 */
long tm_year;
/* the number of days since Sunday, in the range 0 to 6 */
int tm_wday;
/* the number of days since January 1, in the range 0 to 365 */
int tm_yday;
};

POSIX 标准

POSIX(Portable Operating System Interface),是 IEEE 为要在各种 UNIX 操作系统上运行软件,而定义 API 的一系列互相关联的标准的总称,其正式称呼为 IEEE Std 1003,而国际标准名称为 ISO/IEC 9945。

Linux 基本上逐步实现了 POSIX 兼容,但并没有参加正式的 POSIX 认证。时间子系统接口部分符合 POSIX 标准。

Linux 系统时钟

这里指的是 clock(时钟)可以类比为手腕上的表,作为 linux 内核的计时工具存在。Linux 内核内存在多种 clock,各种类型的 timer(计时器)都是基于某个特定的系统时钟运行,特殊的计时器(如定时开关机所使用的定时器)需要基于特殊的系统时钟,保证关机时系统时钟仍然在运行。

时间是一个没有首尾的长线,任何有实际数值的时间都包含一个参考点。Linux 中时间参考点称为 linux Epoch,对应 1970 年 1 月 1 日 0 点 0 分 0 秒(UTC)时间点。Linux 系统内并非所有时间都是以 linux Epoch 为参考点。

  • RTC 时间:通常由专用的 RTC 硬件(如 RTC 芯片)维护,无论系统是否上电,RTC 都能通过后备电源保持时间信息。RTC 的精度依赖于具体硬件,一般能够满足系统启动前的时间同步需求。内核通过 RTC 驱动访问和设置 RTC 时间信息。
  • CLOCK_REALTIME:是内核维护的系统实时时间,表示自 1970 年 1 月 1 日 0 点 0 分 0 秒(UTC)以来的当前时间。CLOCK_REALTIME 的精度通常可以达到纳秒级别,取决于系统时钟源(clocksource)。CLOCK_REALTIME 可以被用户和系统程序修改,如通过 NTP(Network Time Protocol)进行时间同步和调整。
  • CLOCK_MONOTONIC:单调递增的计时源,自系统启动以来持续递增,不受系统时间修改影响,不计入系统休眠时间。
  • CLOCK_MONOTONIC_RAW:类似于 CLOCK_MONOTONIC,但不受 NTP 等时间调整的影响,直接反映硬件时钟的计时。
  • CLOCK_BOOTTIME:单调递增的计时源,自系统启动以来持续递增,包含系统休眠时间。

早期 Linux 内核通过全局变量(如 xtime)维护系统时间,随着内核的发展,自 Linux 4.x 版本起,xtime 已被移除,时间维护由 timekeeping 模块统一管理。

对应 Linux 系统内存在的系统时钟 ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* The IDs of the various system clocks (for POSIX.1b interval timers):
*/
#define CLOCK_REALTIME          0
#define CLOCK_MONOTONIC         1
#define CLOCK_PROCESS_CPUTIME_ID    2
#define CLOCK_THREAD_CPUTIME_ID     3
#define CLOCK_MONOTONIC_RAW     4
#define CLOCK_REALTIME_COARSE       5
#define CLOCK_MONOTONIC_COARSE      6
#define CLOCK_BOOTTIME          7
#define CLOCK_REALTIME_ALARM        8
#define CLOCK_BOOTTIME_ALARM        9
#define CLOCK_SGI_CYCLE         10  /* Hardware specific */
#define CLOCK_TAI           11

#define MAX_CLOCKS          16
#define CLOCKS_MASK         (CLOCK_REALTIME | CLOCK_MONOTONIC)
#define CLOCKS_MONO         CLOCK_MONOTONIC

CLOCK_PROCESS_CPUTIME_ID 和 CLOCK_THREAD_CPUTIME_ID 这两个 clock 是专门用来计算进程或者线程的执行时间的(用于性能剖析),一旦进程(线程)被切换出去,那么该进程(线程)的 clock 就会停下来。因此,这两种的 clock 都是 per-process 或者 per-thread 的,而其他的 clock 都是系统级别的。

Linux 系统时间有关变量

Linux 系统与时间有关的变量或结构。

  1. HZ:定义了 Linux 内核每秒生成多少次 timer 中断。常见的 HZ 值有 100、250、300、1000,具体取决于内核配置和系统需求。在内核编译时可通过配置选项指定 HZ 值。HZ 值越高,系统响应速度越快,计时精度越高,但会带来更多的中断开销。通常,桌面系统和实时系统会选择较高的 HZ 值,而低功耗嵌入式系统可能选择较低的 HZ 值。

  2. Tick:指的是内核每次 timer 中断之间的时间间隔,即 1/HZ 秒。在周期性 tick 模式下,系统会按照固定的 Tick 间隔生成中断,用于时间管理、进程调度等。在 tickless 系统(启用了 CONFIG_NO_HZ)中,系统在空闲时可以停止周期性 tick,以节省电力。

  3. jiffies:记录系统启动以来的 Tick 总数,是内核中用于计算时间间隔的基本变量。jiffies 通常是一个 unsigned long 类型,32 位系统在 HZ=100 时会在大约 50 天后溢出。为了避免溢出问题,内核提供了 jiffies_64 变量,它是一个 64 位的计数器,几乎不会溢出。此外,内核通过宏和函数(如 time_after 等)处理 jiffies 的比较,确保在溢出后依然能够正确计算时间差。

    1
    2
    extern u64 __jiffy_data jiffies_64;
    extern unsigned long volatile __jiffy_data jiffies;

时间子系统概述

大部分情况下硬件计时器只是一个按照固定间隔单调递增的计时器 (有些硬件计时器可能是递减的,或者可编程的),时间本身是一个没有首尾的直线,任何时间的度量都有一个参考点,对于 linux 系统而言,时间是自 Linux Epoch 以来的具体的数值。通过时间子系统 linux 将时间、计时事件与硬件计时器联系起来。

实现一个定时(timer),简单来说需要一个用来计时的腕表(clock)、表示时间的度量、在这个度量下需要计时的数值。在 linux 中表示时间的度量在 4.1 节提到了有 3 种,对应 s、us、ns。数值一般都会明确给出。

最大的问题是需要一个计时的腕表,linux 系统内与其功能相似的是硬件计时器,使用硬件计时器实现腕表的功能,linux 最开始确实是这样做的。但需要计时的地方越来越多,硬件计时器数量不够,之后出现了软件时钟的概念。

硬件计时器不再被那一个 timer 使用,取而代之的是内核维护一个软件时钟,随硬件计时器更新。系统其他的 timer 都是使用这个软件时钟。软件时钟的好处是时钟数量不再限制,系统可以维持多个全局的软件时钟(多个 system clock),相当于系统手里边又平白无故的多了很多可以用的腕表。

随着系统更进一步发展,将一个软件计时器作为了其他软件计时器的基准,其他的软件计时器都基于这个全局的软件计时器(jiffies),jiffies 时钟产生周期性中断,作为 linux 系统的时间参考,早期的时间子系统就是如此工作。

随着需要的需要的精度越来越高,这个全局的软件时钟(jiffies)越来越不能满足需求,系统时钟(system clock)开始直接基于硬件高精度计时器工作,不再依赖 jiffies 的周期计时。这部分的系统时钟(system clock)直接基于硬件计时器,使用这些系统时钟的 timer 可以摆脱 jiffies 时钟的精度限制。但系统内需要 jiffies 时钟的地方太多,没法移除这部分功能,于是选中了一个 timer 作为 sched timer 模拟早期硬件计时器产生周期中断,供 jiffies 时钟使用。这里对应的是新的时间子系统。

随着 tickless 系统(启用 CONFIG_NO_HZ)的引入,Linux 内核可以在系统空闲时停止周期性 tick 中断,从而减少不必要的中断开销,提升系统的电源效率和响应性能。tickless 系统尤其适用于移动设备和高性能服务器,能够更有效地管理资源。

timekeeping 模块是 Linux 时间子系统的核心,负责维护系统的各种 time line(如 CLOCK_REALTIME、CLOCK_MONOTONIC 等),并通过 clocksource 和 clockevent 设备提供高精度的时间服务。它确保系统时间的准确性和一致性,为用户空间和内核空间提供可靠的时间接口。

时间子系统文件系统

时间子系统文件源码在 kernel\time 文件夹下。具体整理如下:

  • time.c:提供系统时间的用户接口,如获取和设置系统时间(time, settimeofday 等),还有一些在其他内核模块使用时间格式转换的接口函数如 jiffes 和微秒之转换等。
  • timeconv.c:提供从日历时间到分解时间(broken-down time)的转换接口。
  • timer.c:实现低精度定时器的核心功能。
  • hrtimer.c:实现高精度定时器的核心功能。
  • itimer.c:实现间隔定时器(interval timer)的功能。
  • posix-timers.c、posix-cpu-timers.c、posix-clock.c:实现 POSIX 定时器和时钟的接口。
  • alarmtimer.c:实现闹钟定时器的功能。
  • clocksource.c:实现 clocksource 设备的注册、管理和选择逻辑。
  • jiffies.c:实现与 jiffies 相关的功能,如获取 jiffies 值、处理 jiffies 溢出等。
  • clockevent.c:实现 clockevent 设备的注册、配置和中断处理。
  • timekeeping.c、timekeeping_debug.c:实现 timekeeping 模块的核心逻辑和调试功能。
  • ntp.c:实现 NTP(Network Time Protocol)相关功能,用于时间同步。
  • tick-common.c、tick-oneshot.c、tick-sched.c:实现 tick 设备的通用逻辑、单次触发模式和调度相关功能, 属于 tick device layer,tick-common.c 是 periodic tick,管理周期性 tick 事件。tick-oneshot.c 管理高精度 tick 时间。 tick-sched.c 用于 dynamic tick。
  • tick-broadcast.c、tick-broadcast-hrtimer.c:实现广播 tick 模式的逻辑,确保多核系统中 tick 的一致性。
  • sched_clock.c:实现调度相关的高精度时间获取接口。这个模块主要是提供一个 sched_clock 的接口函数,调用该函数可以获取当前时间点到系统启动之间的纳秒值。 需要配置 CONFIG_GENERIC_SCHED_CLOCK。该模块扩展了 64-bit 的 counter,即使底层的 HW counter 比特数目。

时间子系统框架

Linux 内核中现有两种定时器:低分辨率(又称经典)定时器和 2.6 内核以后引入的高精度定时器。二者在使用上有一定差异。在 linux 发展早期低分辨率定时器毫秒级别的分辨率可以很好满足需要,但随着 linux 系统在多媒体等需要高精度定时的设备的应用,需要对 linux 内核时间子系统改进以满足需要,但低分辨率的时间系统已经很完善,引入高分辨率计时需要保证向前兼容,低分率时间系统涉及时间片轮等内容,试图整合高分辨率的尝试失败,考虑内核的健壮,内核针对高精度时钟实现了一个新的时间子系统。

高分/低分在使用上有差异。低精度的分辨率在毫秒级别,高精度计时的分辨率在纳秒级别。Linux 支持多核 cpu 后,新的时间子系统也做了相应处理。
整个时间子系统框图如图 5-1 所示:

由上到下分为三层:用户层、核心层、硬件相关层

硬件相关层:

  • 单核 cpu 时的 HW timer 在新的时间子系统功能上划分为了两部分,一部分是 free running 的 system counter,全局,不属于任何一个 CPU,与时间子系统框图相关的是 HW block,各个 cpu 的 cpu core 中都有对应的硬件 timer,称之为 CPU local Timer,这些 CPU local Timer 都是基于 Global counter 运行。为运行这些硬件提供驱动的就是 Clock Source driver。系统存在多个 HW timer 和 counter clock 时,对应多个 Clock Source driver。

核心层:与硬件无关,处理时间子系统核心功能。

clocksourceclockevent是内核中两个独立的抽象模块,分别用于提供计时源和可编程的计时事件。

  • clocksource:代表一个持续递增的计时源,通常基于硬件计时器(如 TSC, HPET 等)。它提供了一条基础的时间线,供内核用来维护系统时间。
  • clockevent:代表一个可编程的计时事件设备,可以被设置为在特定时间触发中断。内核通过 clockevent 设备来管理定时任务、中断调度等。
  • Clock event 对应实在 timeline 上的特定点尝试 clock even。Clock Source drive 通过 clock source 和 clock event 的向下提供的接口注册 clock sorece 和 clock event 设备。Clock event 事件的回掉是 Clock Source drive 申请中断调用 clock event 模块的 callback 函数实现的异步通知。

tick_device tick_device: 基于 clock event,但两者并非一一对应。

  • 硬件中存在几个计时器系统就会注册几个 clck event device。单个 cpu 拥有单独的 tick_device 用于进程统计、调度等,系统内有几个 cpu 就存在几个 tick_device。各个 cpu 的 tick_device 会选择合适的 clck event device。(这种情况下 tick_device 有时又称为 local tick_device)。
  • 面向整个系统的需求,在所有的 local tick_device 中会选定一个作为全局的 tick_device 使用,称为 global tick device,负责维护系统 jiffies、更新 wall clock、计算系统负荷等任务。
  • tick device 支持两种工作模式:周期模式(periodic mode)和单次触发模式(one-shot mode)。区别如下:
  • 周期模式(periodic mode):在该模式下,tick device 按照固定的周期自动产生中断,无需每次定时后重新配置。这种模式适用于需要频繁且规律性中断的场景,如传统的时间片调度。
  • 单次触发模式(one-shot mode):在该模式下,tick device 在每次定时中断后不会自动重新加载,需要系统根据新的定时时间手动配置下一次中断。这种模式适用于需要高精度定时的场景,因为定时时间可以根据实时需求动态调整。
  • 定时精度取决于所使用的 clock_event 设备和系统配置,而非工作模式本身。现代系统通过配置高精度 clock_event 设备并使用单次触发模式,可以实现纳秒级别的定时精度。

定时器的实现与 tick device 的工作模式相关:

  • 低精度 timer:通常基于 periodic mode 运行,依赖于固定周期的 tick 中断,实现较低的时间精度(如毫秒级)。适用于不需要高精度定时的任务。
  • 高精度 timer:通常基于 one-shot mode 运行,通过精确设置 tick 中断的触发时间,实现更高的时间精度(如纳秒级)。适用于需要高精度定时的任务。

低精度 timer 和高精度 timer

  • Timer 是基于 tick device 实现的,上文提到 tick device 支持 periodic mode 和 one shot mode 两种模式,低精度 timer 对应 periodic mode,高精度 timer 对应 one shot mode 模式。tick device 系统只能工作在一个模式下,对 linux 系统有 4 种定时器和 tick device 模式组合,4 种组合详情见下一节。
  • 高精度 timer+one shot mode 组合时,用户层依然可以使用低精度 timer 对应的 API,究其原因是低精度 timer 在内核中提供了大量重要功能,系统会特别设置一个处于 one shot mode 的 tick device,周期性的触发模拟传统的 periodic tick,这个 tick device 被称为 sched timer。
  • Linux 内核编译时即使没有选择高精度计时器,与高精度计时器有关的一部分代码依然会被编译入内核,保证即使没有设置 one shot mode,应用层依然可以正常调用高精度 timer 的接口,不过此时高精度 timer 的精度与低精度 timer 相同,都为毫秒级别。
  • 低精度时钟调度基于时间轮,高精度时钟调度基于红黑树,实现细节详情见各自章节。
  • Timekeeper: timekeeping 模块提供时间服务的基础模块。
  • Linux 内核提供各种 time line,real time clock,monotonic clock、monotonic raw clock 等,timekeeping 模块就是负责跟踪、维护这些 timeline 的,并且向其他模块(timer 相关模块、用户空间的时间服务等)提供服务,而 timekeeping 模块维护 timeline 的基础是基于 clocksource 模块和 tick 模块。通过 tick 模块的 tick 事件,可以周期性的更新 time line,通过 clocksource 模块、可以获取 tick 之间更精准的时间信息。开机后由 RTC 系统读取 RTC 时间,更新到系统时间。

用户层

  • 代码上可以划归成两方面:与 time 有关的接口和与 timer 有关的接口。功能上区分包括:系统时间相关(例:记录当前时间等)、进程休眠(典型数 sleep 等)、定时器有关(alert 进程等)。

时间子系统系统配置

系统编译时需要选择 CONFIG_GENERIC_CLOCKEVENTS 启用新的时间子系统(一般在 arch 中)

本文档默认均为普通的 dynamic tick 系统(tickless idle system)。

系统配置

  • tick device 配置: 在使用新的时间子系统的前提下,内核会提供 Timers subsystem 的配置选项,有 3 种选项。这 3 个选项,只能使能 1 项。
    • CONFIG_HZ_PERIODIC: 始终启用周期性 tick,即使系统处于 idle 时。
    • CONFIG_NO_HZ_IDLE: 对应 Idle dynticks system 模式,系统处于 idle 时,自动停止周期性 tick。启用改选项使,系统自动使能 CONFIG_NO_HZ_COMMON。
    • CONFIG_NO_HZ_FULL: 对应 Full dynticks system 模式,即便在非 idle 的状态下,也可能会停掉周期性 tick。启用改选项使,系统自动使能 CONFIG_NO_HZ_COMMON。
    • CONFIG_TICK_ONESHOT:该配置选项指定系统中的所有 tick device 均以 one-shot 模式运行。这意味着每次 tick 中断后,tick device 需要手动设置下一次 tick 中断的触发时间。这种模式下 tick device 能够提供更高的精度和更好的功耗管理,尤其适用于启用了动态 tick(CONFIG_NO_HZ)的系统。
      • 注意:启用 CONFIG_TICK_ONESHOT 通常需要同时启用 CONFIG_NO_HZ_COMMON,以确保 tick device 在动态 tick 模式下的正确运行。
  • timer 配置
    • 高精度 timer 只有一个 CONFIG_HIGH_RES_TIMERS 的配置项。如果配置了高精度 timer,或者配置了 NO_HZ_COMMON 的选项,那么一定需要配置 CONFIG_TICK_ONESHOT,表示系统支持支持 one-shot 类型的 tick device。

4 种组合方式

  • periodic tick+ 低精度 timer: 最为传统的组合方式,向前兼容性能最好。
    • 配置
      • tick device:CONFIG_HZ_PERIODIC,不配置 CONFIG_TICK_ONESHOT
      • Timer 不配置 CONFIG_HIGH_RES_TIMERS。
    • 注意:即使配置了 CONFIG_NO_HZ 和 CONFIG_TICK_ONESHOT,系统硬件未提供支持 one shot 的 clock event device,系统依然运行在周期性 tick 模式下。
  • periodic tick+ 高精度 timer: 一般不会选择这种组合,多半用于系统硬件无法支持 one shot 的 clock event device,系统依然运行在周期性 tick 下。
    • 配置
      • tick device:CONFIG_HZ_PERIODIC
      • timer:CONFIG_HIGH_RES_TIMERS
  • dynamic tick+ 低精度 timer
    • 系统启动时,仍处于周期模式 tick 中断,使用低精度的 tick_handle_periodic 作为事件处理程序。随着系统运行,内核会检测并选择支持 one-shot 模式的 clock_event 设备。当需要切换到动态 tick 模式时,内核会调用 tick_check_oneshot_change,在 tick device 中切换到 one-shot 模式,并将事件处理程序更新为 tick_nohz_handler。这种模式下,系统在非空闲状态下仍然使用 one-shot 模式 tick,提高了 tick 的灵活性和精度。
    • 系统正常运行时,event handler 每次都要重新对 clock event 设置,以此产生周期性 tick。系统处于 idle 时,clock event device 的 event handler 不在对 clock event 进行设置,周期性 tick 停滞。
    • 配置
      • tick device:CONFIG_NO_HZ_IDLE + CONFIG_TICK_ONESHOT
      • timer:CONFIG_HIGH_RES_TIMERS
  • dynamic tick + 高精度 timer: 除非为了绝对保证向前兼容,一般推荐使用 dynamic tick + 高精度 timer 组合。
    • 与 dynamic tick+ 低精度 timer 时相同,系统启动后会向处于周期 tick 模式下。
    • 进入 tick 软中断后进行如下切换:
      • hrtimer_switch_to_hres 将 timer 切换至高精度模式。
      • Tick device 的 clock event 设备切换到 oneshot mode
      • Tick device 的 clock event 设备的 event handler 会更新为 hrtimer_interrupt
      • 设定 sched timer 模拟周期性 tick。sched timer 会在系统进入 idle 时候停止,降低功耗。

时间子系统详细分析

在新的时间子系统中 Linux 系统将硬件计时器功能上抽象为了两个实体:clock source(时钟源) 与 clock_event(时钟事件)。

clock source(时钟源): 顾名思义,是系统时钟的源头。clock source 与硬件关联,硬件计时器单调计时递增 (当然有溢出问题,当 64 位计时器溢出时间相当长,可以忽略这种情况),在 clock source 的反应是提供了一条基础的 timeline。clock source 本身没有产生任何事件/中断的能力。clock source 在系统内主要功能是供 timekeep 使用,维持多个系统时钟。

clock_event(时钟事件): 是在 timeline 上特定时间点产生事件。tick device 正是基于 clock_event 工作的。

tick device 是基于 clock_event 设置定时时间,处理周期事件。

Timekeep 维护各种系统 time line,real time clock,monotonic clock、monotonic raw clock。维护 timeline 的基础是基于 clocksource 模块和 tick 模块。通过 tick 模块的 tick 事件,可以周期性的更新 time line,通过 clocksource 模块可以获取 tick 之间更精准的时间信息。系统的 time line 在高精度模式下,都是基于与硬件关联 clock source 提供的 time line。

Clock Source

clocksource 是对真实的时钟源进行软件抽象,留有注册/卸载接口,供驱动调用。

源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct clocksource {
    /*
     * Hotpath data, fits in a single cache line when the
     * clocksource itself is cacheline aligned.
     */
    cycle_t (*read)(struct clocksource *cs);
    cycle_t mask;
    u32 mult;
    u32 shift;
    u64 max_idle_ns;
    u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
    struct arch_clocksource_data archdata;
#endif
    u64 max_cycles;
    const char *name;
    struct list_head list;
    int rating;
    int (*enable)(struct clocksource *cs);
    void (*disable)(struct clocksource *cs);
    unsigned long flags;
    void (*suspend)(struct clocksource *cs);
    void (*resume)(struct clocksource *cs);

    /* private: */
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
    /* Watchdog related data, used by the framework */
    struct list_head wd_list;
    cycle_t cs_last;
    cycle_t wd_last;
#endif
    struct module *owner;
} ____cacheline_aligned;

我们只关注几个重要字段:

  • rating,代表了时钟源的精度范围,与每个时钟源晶振的频率有关,当有更好的时钟源注册时,timekeep 会主动切换到精度更好的时钟源。
    • 1–99: 不适合于用作实际的时钟源,只用于启动过程或用于测试;
    • 100–199:基本可用,可用作真实的时钟源,但不推荐;
    • 200–299:精度较好,可用作真实的时钟源;
    • 300–399:很好,精确的时钟源;
    • 400–499:理想的时钟源,如有可能就必须选择它作为时钟
  • cycle_t (*read)(struct clocksource *cs);
    获得时钟源的当前计数,只能通过调用 read 回调函数来获得当前的计数值,注意这里只能获得计数值(cycle)要获得相应的时间,必须要借助 clocksource 的 mult 和 shift 字段进行转换计算。
  • u32 mult;u32 shift;
    使用公式进行 cycle 和 t 的转换:
    t=(cyclemult)>>shift;t = (cycle * mult) >> shift;
Clocksource 的注册和初始化

clocksource 要在初始化阶段通过 clocksource_registeimages/r_hz 函数通知内核它的工作时钟的频率

|800

大部分工作在 clocksource_register_scale 完成,该函数首先完成对 mult 和 shift 值的计算,然后根据 mult 和 shift 值,clocksource_enqueue 函数负责按 clocksource 的 rating 的大小,把该 clocksource 挂载到全局链表 clocksource_list 上,rating 值越大,在链表上的位置越靠前。

每次新的 clocksource 注册进来,都会触发 clocksource_select 函数被调用,它按照 rating 值选择最好的 clocksource,并记录在全局变量 curr_clocksource 中,然后通过 timekeeping_notify 函数通知 timekeeping

Clocksource Watchdog

为了确保系统中注册的 clocksource 的稳定性和精度,内核引入了 watchdog 机制。当一个新的 clocksource 被注册时,除了被添加到 clocksource_list 链表外,还会被挂载到 watchdog_list 链表中。内核启动一个周期为 0.5 秒的定时器,定期检查 watchdog_list 中的所有 clocksource。

每次检查时,内核会比较 clocksource 的当前计数与上次记录的计数差异。如果在 0.5 秒内,clocksource 的计数偏差超过了 WATCHDOG_THRESHOLD(定义为 0.0625 秒,即相当于有多大偏差的纳秒数),则认为该 clocksource 不稳定。此时,定时器的回调函数会通过 clocksource_watchdog_kthread 线程对该 clocksource 进行标记,将其 rating 值设置为 0,表明该 clocksource 的精度极差,系统将不再选择它作为当前有效的 clocksource。

这样,内核能够动态筛选出最稳定和高精度的 clocksource,确保系统时间的准确性和可信赖性。

系统启动时 Clocksource 变化

系统的启动时,内核会注册了一个基于 jiffies 的 clocksource(kernel/time/jiffies.c),它精度只有 1/HZ 秒,rating 值为 1。

如果平台的代码没有提供定制的 clocksource_default_clock 函数,系统将返回这个基于 jiffies 的 clocksource。启动的后半段,clocksource 的代码会把全局变量 curr_clocksource 设置为 clocksource_default_clock 返回的 clocksource。

当然即使平台的代码没有提供 clocksource_default_clock 函数,在平台的硬件计时器注册时,经过 clocksource_select() 函数,系统还是会切换到精度更好的硬件计时器上。

clock_event

clocksource 不能被编程,clock_event 则是可编程的,它可以工作在周期触发或单次触发模式,系统通过 clock_event 确定下一次事件触发的时间,clock_event 主要用于实现普通定时器和高精度定时器,同时也用于产生 tick 事件,供给进程调度子系统使用。

多核系统内,每个 CPU 形成自己的一个小系统,有自己的调度、有自己的进程统计等,拥有自己的 tick 设备,而且是唯一的。clock event 有多少硬件 timer 注册多少 clock event device,各个 cpu 的 tick device 会选择自己适合的那个 clock event 设备,这个设备称为 clock_event_device。

源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct clock_event_device {
    void            (*event_handler)(struct clock_event_device *);
    int         (*set_next_event)(unsigned long evt, struct clock_event_device *);
    int         (*set_next_ktime)(ktime_t expires, struct clock_event_device *);
    ktime_t         next_event;
    u64         max_delta_ns;
    u64         min_delta_ns;
    u32         mult;
    u32         shift;
    enum clock_event_mode   mode;
    enum clock_event_state  state;
    unsigned int        features;
    unsigned long       retries;

    /*
     * State transition callback(s): Only one of the two groups should be
     * defined:
     * - set_mode(), only for modes <= CLOCK_EVT_MODE_RESUME.
     * - set_state_{shutdown|periodic|oneshot}(), tick_resume().
     */
    void            (*set_mode)(enum clock_event_mode mode, struct clock_event_device *);
    int         (*set_state_periodic)(struct clock_event_device *);
    int         (*set_state_oneshot)(struct clock_event_device *);
    int         (*set_state_shutdown)(struct clock_event_device *);
    int         (*tick_resume)(struct clock_event_device *);

    void            (*broadcast)(const struct cpumask *mask);
    void            (*suspend)(struct clock_event_device *);
    void            (*resume)(struct clock_event_device *);
    unsigned long       min_delta_ticks;
    unsigned long       max_delta_ticks;

    const char      *name;
    int         rating;
    int         irq;
    int         bound_on;
    const struct cpumask    *cpumask;
    struct list_head    list;
    struct module       *owner;
} ____cacheline_aligned;

clock_event_device 是 clock_event 的核心数据结构,这里只注释较重要部分。

  • event_handler 一个回调函数指针,通常由通用框架层设置,在时间中断到来时,硬件的中断服务程序会调用该回调,实现对时钟事件的处理。
  • set_next_event:设置下一次事件触发的周期数(cycle)。适用于基于 clocksource 周期数的定时管理,使用离当前周期数的差值作为参数。这种方式通常用于低精度定时器。
  • set_next_ktime:设置下一次事件触发的绝对 ktime 时间点。适用于高精度定时器,直接基于 ktime 进行精确的定时设置。
  • max_delta_ns 可设置的最大时间差,单位是纳秒。
  • min_delta_ns 可设置的最小时间差,单位是纳秒。
  • mult shift 与 clocksource 中的类似,只不过是用于把纳秒转换为 cycle。
  • mode 该时钟事件设备的工作模式,两种主要的工作模式分别是:
  • CLOCK_EVT_MODE_PERIODIC 周期触发,设置后按给定的周期不停地触发事件;
  • CLOCK_EVT_MODE_ONESHOT 单次触发,只在设置好的触发时刻触发一次;
  • set_mode 函数指针,用于设置时钟事件设备的工作模式。
  • rating 表示该设备的精度等级。
  • list 系统中注册的时钟事件设备用该字段挂在全局链表变量 clockevent_devices 上。
全局变量

除核心的结构体外,clock_event 同时还有两个相关的全局变量。

  • clockevent_devices: 定义在在 kernel/time/clockevents.c,系统内所有注册的 clock_event_device 都会挂载到该链表
  • clockevents_chain: 系统中的 clock_event 设备的状态发生变化时,利用该通知链通知系统的其它模块。
Clock Event 注册

clock event 留有注册函数 clockevents_register_notifier

|800

由 start_kernel 开始,调用 tick_init,调起 clockevents_register_notifier,同时把类型为 notifier_block 的 tick_notifier 作为参数传入。clockevents_register_notifier 注册了一个通知链,当系统中的 clock_event_device 状态发生变化时(新增,删除,挂起,唤醒等等),tick_notifier 中的 notifier_call 字段中设定的回调函数 tick_notify 就会被调用。

接下来 start_kernel 调用了 time_init 函数,该函数通常定义在体系相关的代码中,它主要完成机器对时钟系统的初始化工作,最终通过 clockevents_register_device 注册系统中的时钟事件设备,把每个时钟时间设备挂在 clockevent_device 全局链表上,最后通过 clockevent_do_notify 触发框架层事先注册好的通知链 clockevents_chain 上。


下面这段是 O1 给出的修改意见, 抱歉我没有太多时间核对…

clocksource_register_scale 函数负责注册一个新的 clocksource 设备,并根据其频率和精度计算 mult 和 shift 值,以便将 clocksource 的周期数转换为纳秒数。随后,clocksource_enqueue 函数将该 clocksource 按照其 rating 值插入到全局的 clocksource_list 链表中,rating 值越高,clocksource 的优先级越高,越可能被选为当前的最优 clocksource。

每当一个新的 clocksource 被注册,clocksource_select 函数会被调用以选择当前最佳的 clocksource,通常选择 rating 值最高的 clocksource 作为当前有效的 clocksource,并通过 timekeeping_notify 函数通知 timekeeping 模块进行相应的更新。


tick_device

tick_device 本身是对 clock event 的进一步封装

1
2
3
4
struct tick_device { 
struct clock_event_device *evtdev;
enum tick_device_mode mode;
};

tick device 其实是工作在某种模式下的 clock event 设备。工作模式体现在 tick device 的 mode 成员,evtdev 指向了和该 tick device 关联的 clock event 设备.

1
2
3
4
enum tick_device_mode { 
TICKDEV_MODE_PERIODIC,
TICKDEV_MODE_ONESHOT,
};

tick device 可以有两种模式,一种是周期性 tick 模式,另外一种是 one shot 模式

分类及 Cpu 的关系

Tick device 有 3 种分类。

  • Local tick device
    每个 CPU 有一个本地的 tick device,用于处理该 CPU 的任务调度、进程统计等。通过 DEFINE_PER_CPU(struct tick_device, tick_cpu_device); 为每个 CPU 定义一个 tick device。
  • Global tick device
    系统中存在一个全局的 tick device,负责维护系统的 jiffies、更新 wall clock 以及计算系统负载等全局性任务。通常由某一个本地 tick device 承担,例如第一个 CPU 的 tick device。
  • Broadcast tick device
    在多核系统中,当一个全局 tick device 由于 CPU 休眠等原因无法继续工作时,broadcast tick device 会接管 tick 中断的处理,确保系统时钟和调度功能的持续运行。Broadcast tick device 能够将 tick 事件广播到所有 CPU,确保时间的一致性。
  • 涉及 cpu 广播模式,当选用的 global tick device 因 cpu 休眠等原因而停止运行时,broadcast tick device 就会接入 global tick device 的 tick 处理程序,代替 global tick device 产生定期中断。但在系统看来 global tick device 依旧在运作。如图 5-3 所示。

Timekeeper

timekeeping 模块提供时间服务的基础模块。Linux 内核提供各种 time line,real time clock,monotonic clock、monotonic raw clock 等,timekeeping 模块就是负责跟踪、维护这些 timeline 的,并且向其他模块(timer 相关模块、用户空间的时间服务等)提供服务,而 timekeeping 模块维护 timeline 的基础是基于 clocksource 模块和 tick 模块。通过 tick 模块的 tick 事件,可以周期性的更新 time line,通过 clocksource 模块、可以获取 tick 之间更精准的时间信息。

源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct timekeeper {
    struct tk_read_base tkr_mono;
    struct tk_read_base tkr_raw;
    u64         xtime_sec;
    unsigned long       ktime_sec;
    struct timespec64   wall_to_monotonic;
    ktime_t         offs_real;
    ktime_t         offs_boot;
    ktime_t         offs_tai;
    s32         tai_offset;
    struct timespec64   raw_time;

    /* The following members are for timekeeping internal use */
    cycle_t         cycle_interval;
    u64         xtime_interval;
    s64         xtime_remainder;
    u32         raw_interval;
    /* The ntp_tick_length() value currently being used.
     * This cached copy ensures we consistently apply the tick
     * length for an entire tick, as ntp_tick_length may change
     * mid-tick, and we don't want to apply that new value to
     * the tick in progress.
     */
    u64         ntp_tick;
    /* Difference between accumulated time and NTP time in ntp
     * shifted nano seconds. */
    s64         ntp_error;
    u32         ntp_error_shift;
    u32         ntp_err_mult;
};

前面提及 Linux 系统内时钟,目前均为 timekeep 维护。

时间种类精度(统计单位)访问速度累计休眠时间受 NTP 调整的影响
xtimeYesYes
monotonicNoYes
raw monotonicNoNo
boot timeYesYes
Timekeep 初始化

timekeeper 的初始化由 timekeeping_init 完成,该函数在 start_kernel 的初始化序列中被调用,timekeeping_init 首先从 RTC 中获取当前时间。

  • 从 persistent clock 获取当前的时间值

    • 系统启动后,会从 persistent clock 中中取出当前时间值(例如 RTC),根据情况初始化各种 system clock。具体代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
          read_persistent_clock64(&now);
      if (!timespec64_valid_strict(&now)) {
          pr_warn("WARNING: Persistent clock returned invalid value!\n"
              " Check your CMOS/BIOS settings.\n");
          now.tv_sec = 0;
          now.tv_nsec = 0;
      } else if (now.tv_sec || now.tv_nsec)
          persistent_clock_exists = true;

      read_boot_clock64(&boot);
      if (!timespec64_valid_strict(&boot)) {
          pr_warn("WARNING: Boot clock returned invalid value!\n"
              " Check your CMOS/BIOS settings.\n");
          boot.tv_sec = 0;
          boot.tv_nsec = 0;
      }
    • read_persistent_clock 在 linux/arch/arm/kernel/time.c 文件中。主要功能就是从系统中的 HW clock(例如 RTC)中获取时间信息。

    • !timespec64_valid_strict 用来校验一个 timespec 是否是有效。需要满足 timespec 中的秒数值要大于等于 0,小于 KTIME_SEC_MAX,纳秒值要小于 NSEC_PER_SEC(10^9)。KTIME_SEC_MAX 这个宏定义了 ktime_t 这种类型的数据可以表示的最大的秒数值。

    • 设定 persistent_clock_exist flag,说明系统中存在 RTC 的硬件模块,timekeeping 模块会和 RTC 模块进行交互。例如:在 suspend 的时候,如果该 flag 是 true 的话,RTC driver 不能 sleep,因为 timekeeping 模块还需要在 resume 的时候通过 RTC 的值恢复其时间值呢。

  • 为 timekeeping 模块设置 default 的 clock source

    1
    2
    3
    4
    clock = clocksource_default_clock();
    if (clock->enable)
        clock->enable(clock);
    tk_setup_internals(tk, clock);
    • 在 timekeeping 初始化的时候,很难选择一个最好的 clock source。在平台没有定义 clocksource_default_clock 的情况下,默认就是采用一个基于 jiffies 的 clocksource。
    • 建立 default clocksource 和 timekeeping 伙伴关系。
  • 初始化 real time clock、monotonic clock 和 monotonic raw clock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    tk_set_xtime(tk, &now);
    tk->raw_time.tv_sec = 0;
    tk->raw_time.tv_nsec = 0;
    if (boot.tv_sec == 0 && boot.tv_nsec == 0)
        boot = tk_xtime(tk);

    set_normalized_timespec64(&tmp, -boot.tv_sec, -boot.tv_nsec);
    tk_set_wall_to_mono(tk, tmp);

    timekeeping_update(tk, TK_MIRROR);

    write_seqcount_end(&tk_core.seq);
    raw_spin_unlock_irqrestore(&timekeeper_lock, flags);
    • 根据从 RTC 中获取的时间值来初始化 timekeeping 中的 real time clock,如果没有获取到正确的 RTC 时间值,那么缺省时间为 linux epoch。
    • monotonic raw clock 被设定为从 0 开始。
    • 启动时将 monotonic clock 设定为负的 real time clock。
    • timekeeping_update 函数用于更新 timekeeper 结构体中的内部状态。参数 TK_MIRROR 指示该更新操作是镜像更新,确保 timekeeper 中的各个时间线(如 CLOCK_REALTIME、CLOCK_MONOTONIC 等)保持一致性。该函数会根据当前的 clocksource 和 clockevent 设备,调整和维护系统的时间线,确保系统时间的准确性和同步性。

低精度 Timer

低分辨率定时器准确来说指的是使用 jiffies 值计数的计时器,精度最高只有 1/HZ.

创建定时器

讨论定时器的实现原理前,我们先看看如何使用定时器。要在内核编程中使定时器,首先我们要定义一个 time_list 结构,该结构在 include/Linux/timer.h。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct timer_list {  
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;
struct tvec_base *base;

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

int slack;
......
};
  • entry  用于把一组定时器组成一个链表,低精度 timer 调度使用时间片轮。
  • expires  该定时器的到期时刻的 jiffies 计数值。
  • base  指向该定时器所属的 cpu 所对应 tvec_base 结构。
  • function  定时器到期时,调用该回调函数,用于响应该定时器的到期事件。
  • data  回调函数的参数。
  • slack  对到期时间精度不太敏感的定时器,到期时刻允许适当地延迟一小段时间,该字段用于计算每次延迟的 HZ 数。

定义一个 timer_list,可以使用静态和动态两种办法。

  • 静态方法使用 DEFINE_TIMER 宏:

#define DEFINE_TIMER(_name, _function, _expires, _data)
该宏将得到一个名字为 _name,并分别用 _function,_expires,_data 参数填充 timer_list 的相关字段。

  • 使用动态的方法,需要自行声明一个 timer_list 结构,然后手动初始化它的各个字段。

    1
    2
    3
    4
    5
    6
    struct timer_list timer;  
    ......
    init_timer(&timer);
    timer.function = _function;
    timer.expires = _expires;
    timer.data = _data;
定时器其他操作
  • 要激活一个定时器,调用 add_timer

add_timer(&timer);

  • 修改定时器的到期时间,调用 mod_timer 即可

mod_timer(&timer, jiffies+50);

  • 移除一个定时器,调用 del_timer

del_timer(&timer);

定时器系统还提供了以下这些 API 供我们使用:

  • void add_timer_on(struct timer_list *timer, int cpu);  // 在指定的 cpu 上添加定时器
  • int mod_timer_pending(struct timer_list *timer, unsigned long expires);  只有当 timer 已经处在激活状态时,才修改 timer 的到期时刻
  • void set_timer_slack(struct timer_list *time, int slack_hz);  设定 timer 允许的到期时刻的最大延迟,用于对精度不敏感的定时器
  • int del_timer_sync(struct timer_list *timer); 如果该 timer 正在被处理中,则等待 timer 处理完成才移除该 timer
低精度 Timer 的软件架构

对于大量的低精度定时器,内核采用时间轮(Time Wheel)算法进行统一管理。时间轮将定时器按照到期时间分配到不同的桶中,每当 tick 中断发生时,内核检查当前 tick 对应的桶,将其中的所有定时器触发执行。这种方法高效地管理大量定时器,减少了遍历和比较的开销。

上图的轮上由 8 个 bucket,可以将每一个 bucket 代表一秒,那么 bucket [1] 代表的时间点就是 "1 秒钟以后 ",bucket [8] 代表的时间点为 "8 秒之后 "。Bucket 存放着一个 timer 链表,链表中的所有 Timer 将在该 bucket 所代表的时间点触发。

每次时钟中断产生时,时间轮增加一格,然后中断处理代码检查 bucket,假如该 bucket 非空,则触发该 bucket 指向的 Timer 链表中的所有 Timer。

按照类似的做法,内核将时间轮上单一的 bucket 数组分成了几个不同的数组,每个数组表示不同的时间精度。如下图 5-5 所示,时间轮有三级,分别表示小时,分钟和秒。在 Hour 数组中,每个 bucket 代表一个小时,根据其定时器到期值,Timer 被放到不同的 bucket 数组中管理。

最终形成如下图所示的时间轮调度。


添加 Timer:

  • 根据其到期值,Timer 被放到不同的 bucket 数组中,这个比较简单。

删除 Timer:

  • Timer 本身有指向 bucket 的指针,因只需要从该 Timer 的 bucket 指针读取到 指向该 bucket 的指针,然后从该 List 中删除自己即可。

定时器处理:

  • 每个时钟中断产生时(假设时钟间隔为 1 秒),将 SECOND ARRAY 的 cursor 加一,假如 SECOND ARRAY 当前 cursor 指向的 bucket 非空,则触发其中的所有 Timer。

优点:

  • 确实基于时间轮的定时器调度,使得定时器处理相当高效,节省了系统开支。满足了低精度下定时器处理的要求。

缺点:

  • 系统计时的最高精度被限制为周期中断的精度,当然则与定时器本身也有关系。
  • 在时间轮走完一圈后,时间轮需要由上一级重填充,这个过程中无法相应中断处理,切换过程的系统开销较大。这在需要高精度实时响应的场合是不可接受的。

高精度 Timer

高精度 timer 克服了低精度 timer 的限制,实现了真正的高精度计时。

1
2
3
4
5
6
7
8
struct hrtimer {  
struct timerqueue_node node;
ktime_t _softexpires;
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
unsigned long state;
......
};

内核使用了一个 hrtimer 结构来表示一个高精度定时器。

  • _softexpires ktime_t 精度的时间,用来记录到期时间,到期后调用 function 指定的回调函数
  • (*function)(struct hrtimer *) 回调函数定时器到期时调用。函数的返回值为一个枚举值,它决定了该 hrtimer 是否需要被重新激活。
  • State 用于表示 hrtimer 当前的状态,有几种组合:
    • #define HRTIMER_STATE_INACTIVE 0x00 定时器未激活
    • #define HRTIMER_STATE_ENQUEUED 0x01 定时器已经被排入红黑树中
    • #define HRTIMER_STATE_CALLBACK 0x02 定时器的回调函数正在被调用
    • #define HRTIMER_STATE_MIGRATE 0x04 定时器正在 CPU 之间做迁移

hrtimer 的到期时间可以基于以下几种时间基准系统:

  • HRTIMER_BASE_MONOTONIC, 单调递增的 monotonic 时间,不包含休眠时间
  • HRTIMER_BASE_REALTIME, 平常使用的墙上真实时间
  • HRTIMER_BASE_BOOTTIME, 单调递增的 boottime,包含休眠时间
  • HRTIMER_MAX_CLOCK_BASES, 用于后续数组的定义

Hrtimer 其他操作:

  • hrtimer_init() 初始化一个 Timer 对象,
  • hrtimer_start() 设定到期时间和到期操作,并添加启动该 Timer。
  • remove_hrtimer() 删除一个 Timer。
高精度 Timer 的软件架构

与低精度 timer 使用时间轮作为调度不同,高精度 timer 使用红黑树作为调度手段,在高精度硬件计时器的基础上实现了 ns 级别的定时。

所有的 hrtimer 实例都被保存在红黑树中,添加 Timer 就是在红黑树中添加新的节点;删除 Timer 就是删除树节点。红黑树的键值为到期时间。

Timer 的触发和设置与定期的 tick 中断无关。当前 Timer 触发后,在中断处理的时候,将高精度时钟硬件的下次中断触发时间设置为红黑树中最早到期的 Timer 的时间。时钟到期后从红黑树中得到下一个 Timer 的到期时间,并设置硬件,如此循环反复。

高精度的硬件计时器与每个 cpu 紧密相关,其相关数据结构如下图所示

在多处理器系统中,每个 CPU 都保存和维护自己的高精度定时器,在每个 CPU 上,hrtimer 还分为两大类:

  • Monotonic:与系统时间无关,不可以被人修改。
  • Real time:实时时间即系统时间,可以被人修改。

每个 CPU 都需要两个 clock_base 数据结构:一个指向所有 monotonic hrtimer;另一个指向所有的 realtime hrtimer。

clock_base 数据结构中,active 指向一个红黑树,每个 hrtimer 都是该红黑树的一个节点,用到期时间作为 key。这样所有的定时器便按照到期时间的先后被顺序加入这棵平衡树。first 指向最近到期的 hrtimer, 即红黑树最左边的叶子节点。

添加 Timer,即在相应的 clock_base 指向的红黑树中增加一个新的节点,红黑树的 key 由 hrtimer 的到期时间表示,因此越早到期的 hrtimer 在树上越靠左。
删除 Timer,即从红黑树上删除该 hrtimer。

高精度时钟模式下,定时器直接由高精度定时器硬件产生的中断触发。以一个实例分析:

  • 假如 3 个 hrtimer,其到期时间分别为 10ns、100ns 和 1000ns。添加第一个 hrtimer 时,系统通过对应 clock_event_device 操作硬件将其下一次中断触发时间设置为 10ns。
  • 10ns 到期,中断产生,最终会调用到 hrtimer_interrrupt() 函数,该函数从红黑树中得到所有到期的 Timer,并负责调用 hrtimer 数据结构中维护的用户处理函数。
  • hrtimer_interrupt 读取下一个到期的 hrtimer,并且通过 clock_event_device 操作时钟硬件将下一次中断到期时间设置为 90ns ,如此反复操作。

Linux 内核在启用高精度定时器后,仍然需要一个特殊的 hrtimer 来模拟传统的 tick 时钟。这个模拟 tick 的 hrtimer 按照固定的 tick 间隔时间(例如 10ms)定期启动自身,生成 tick 中断,从而维护系统的时间片和调度。这种方式确保了在启用 tickless 模式(CONFIG_NO_HZ)时,系统依然能够在需要时生成 tick 中断,而在系统空闲时可以减少或跳过 tick 中断,以优化功耗和性能。

接口

除非特别情况下都应该将系统配置未 dynamic tick + 高精度 timer 模式。具体配置如下:

  • 编译时选择 CONFIG_GENERIC_CLOCKEVENTS 启用新的时间子系统。
  • tick device 配置为 CONFIG_NO_HZ_IDLE 和 CONFIG_TICK_ONESHOT
  • timer 配置 CONFIG_HIGH_RES_TIMERS,启用高精度计时器

时间子系统用户层接口

时间子系统面向用户层的接口主要有 3 种:系统时间相关、进程统计相关、timer 相关。以下一一说明。

系统时间相关

与系统时间相关的接口主要是获取/设定不同精度的当前系统时间、获取系统定时精度等。设定时间的进程必须拥有 CAP_SYS_TIME 权限。

  • time_t time(time_t *t);int stime(time_t *t);
    • 定义在 time.h,精度 s。返回/设定自 linux epoch 以来的秒数。编译内核时,需要启用 __ARCH_WANT_SYS_TIME,一般需要兼容旧版本应用时选用。系统内存在将 time_t 类型转换为其他时间表示的接口。
  • int gettimeofday(struct timeval *tv, struct timezone *tz);
  • int settimeofday(const struct timeval *tv, const struct timezone *tz);
    • 定义在<sys/time.h>,精度 us。
    • gettimeofday 将目前的时间用 tv 结构体返回,当地时区的信息则放到 tz 所指的结构中。Settimeofday 将对应时区和时间写入系统。
  • int clock_getres(clockid_t clk_id, struct timespec *res);
  • int clock_gettime(clockid_t clk_id, struct timespec *tp);
  • int clock_settime(clockid_t clk_id, const struct timespec *tp);
    • 定义在<time.h>,精度可以达到 ns 级别,但实际 clock_gettime 返回的时间值的粒度要比 ns 大。
    • clock ID 是指 system clock(系统时钟)ID,具见 4.1 节。

获取 clock ID 有两种方式,在进程中使用定义在<time.h>的 int clock_getcpuclockid(pid_t pid, clockid_t *clock_id) 函数,在线程中则需要包含 <pthread.h> 调用 int pthread_getcpuclockid(pthread_t thread, clockid_t *clock_id) 函数。

虽然 clock_xx 的精度可以达到 ns 级别但实际精度与系统所使用的计时器有关。clock_getres() 函数是用来获取系统时钟精度。

进程统计相关
  • unsigned int sleep(unsigned int seconds);
    • 定义在<unistd.h>,延时精度 s,基于 CLOCK_REALTIME 时钟。返回值为与设定相比进程未休眠时间。
  • int usleep(useconds_t usec);
    • 定义在<unistd.h>,延时精度 us,基于 CLOCK_REALTIME 时钟。返回值为 0 执行成功,返回值为 -1 执行失败。
  • int nanosleep(const struct timespec *req, struct timespec *rem);
    • 定义在<time.h>,延时精度 ns,基于 CLOCK_REALTIME 时钟。返回值为 0 执行成功,返回值 -1 执行失败,错误码定义在 errno 中。传入参数为延时的秒数和纳秒数。建议取代 sleep 和 usleep。
  • int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *request, struct timespec *remain);
    • 定义在<time.h>,延时精度 ns。
    • 传入参数:clock_id 为依赖的系统时钟 id,不仅仅是 CLOCK_REALTIME 时钟。Flags 取值 0/1 代表相对时间/绝对时间。request 和 remain 代表延时时间。
Timer 相关

unsigned int alarm(unsigned int seconds);

  • 定义在<unistd.h>,精度 s。指定时间过后,向进程发送 SIGALRM 信号。基于 CLOCK_REALTIME。

int getitimer(int which, struct itimerval *curr_value)
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

  • 定义在<sys/time.h>中。效果与 alarm() 函数类似,延迟精度 us。可以指定多长时间后执行任务,还可以设定间隔时间执行。已有更新的函数取代.

  • 传入参数:

    • Which,指明使用的 timer,有 3 中取值。
      • ITIMER_REAL。基于 CLOCK_REALTIME 计时,超时后发送 SIGALRM 信号
      • ITIMER_VIRTUAL。当该进程的用户空间代码执行的时候计时,超时后发送 SIGVTALRM 信号。
      • ITIMER_PROF。该进程执行的时候计时,不论是执行用户空间代码还是进入内核执行(例如系统调用),超时后发送 SIGPROF 信号。
    1
    2
    3
    4
    struct itimerval { 
    struct timeval it_interval; /* next value */
    struct timeval it_value; /* current value */
    };
    • 指定本次和下次超期后设定的时间值,可以工作在 one shot 类型的 timer 上。it_value 的值会在到期后充新加载为 it_interval。old_value 装载上次 setitimer 的设定值。
  • getitimer 函数获取当前的 Interval timer 的状态,其中的 it_value 成员可以得到当前时刻到下一次触发点的时间信息

POSIX timer 接口函数

  • 基础有 3 个函数,用于创建/设定/删除 timer。延时精度 ns 级别,是目前最复杂的定时器函数。
  • 创建 timer int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);
    • clockid 代表此 timer 使用的系统时钟
    • Timerid 为 timer ID 的句柄
    • struct sigevent 代表通知进程的方式
      • SIGEV_NONE。程序自己调用 timer_gettime 来轮询 timer 的当前状态
      • SIGEV_SIGNAL。使用 sinal 这样的异步通知方式。发送的信号由 sigev_signo 定义。如果发送的是 realtime signal,该信号的附加数据由 sigev_value 定义。
      • SIGEV_THREAD。创建一个线程执行 timer 超期 callback 函数,_attribute 定义了该线程的属性
      • SIGEV_THREAD_ID。行为和 SIGEV_SIGNAL 类似,不过发送的信号被送达进程内的一个指定的 thread,这个 thread 由 _tid 标识
  • 设定 timer int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec * old_value);
    • timerid 为创建的 timer。
    • flag 等于 0 或者 1,代表 new_value 参数设定的时间值是相对时间还是绝对时间。
    • new_value.it_value 是一个非 0 值,那么调用 timer_settime 可以启动该 timer。如果 new_value.it_value 是一个 0 值,那么调用 timer_settime 可以 stop 该 timer。
    • int timer_gettime(timer_t timerid, struct itimerspec *curr_value); 获取 timer 的剩余时间。
  • 删除 timer
    • timer_delete 用来删除指定的 timer,释放资源

具体使用

获取系统时间

使用 time() 函数,系统提供了常见时间格式之间的转换

|800

使用 gettimeofday() 以及 clock_gettime() 获取时间,暂时没有直接转换的库函数,一般的做法是将时间精度的个位提取,转换到 tm 格式,再进行输出。

进程休眠

建议使用 nanosleep() 函数,nanosleep() 函数的精度可以达到 ns 级别,在使用高精度时钟的系统上 nanosleep() 的精度可以得到充分利用。

如要求非常稳定的计时,换用 clock_nanosleep() 函数,指定比 nanosleep() 默认使用的 CLOCK_REALTIME 更稳定的系统时钟。
精度要求不高可以使用 sleep( int ) 函数实现简单的秒级别的暂停。Linux 系统内是调用 nanosleep() 实现的 sleep() 函数。

高精度计时器

需要定时器的场合下,上文提及的 setitimer() 函数可以满足大部分要求,但需要 us 甚至 ns 的定时场合下,其不再适用。这里需要使用符合 POSIX 标准的 POSIX Timer。

POSIX Timer 是针对有实时要求的应用所设计的,接口支持 ns 级别的时钟精度。

  • 创建一个 Timer。指定该 Timer 的一些特性 int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);
    • clock ID,即 timer 依赖的系统时钟,在 Posix Timer 中有 4 中取值
      • CLOCK_REALTIME 系统墙上时间,受修改系统时间影响
      • CLOCK_MONOTONIC 自开机后开始计数,调整时间不影响计数
      • CLOCK_PROCESS_CPUTIME_ID 只记录当前进程所实际花费的时间
      • CLOCK_THREAD_CPUTIME_ID 只记录当前线程所实际花费的时间
    • struct sigevent,即到期后,通知到达方式。
      • SIGEV_NONE 到期时不通知,应用 timer_gettime 查询处理
      • SIGEV_SIGNAL 到期时将给进程投递信号,可以用来指定信号
      • SIGEV_THREAD 到期时将启动新的线程进行处理
      • SIGEV_THREAD_ID 到期时将向指定线程发送信号
  • 启动定时器 int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value,struct itimerspec * old_value); and int timer_gettime(timer_t timerid, struct itimerspec *curr_value);
    • 调用 timer_settime() 函数指定定时器的时间间隔,启动该定时器。启动和停止.

      1
      2
      3
      4
      5
      struct itimerspec
      {
      struct timespec it_interval; //定时器周期值
      struct timespec it_value; //定时器到期值
      };
      • new_value->it_interval 为定时器的周期值,比如 1 秒,表示定时器每隔 1 秒到期;
      • new_value->it_value 如果大于 0,表示启动定时器,Timer 将在 it_value 这么长的时间过去后到期,此后每隔 it_interval 便到期一次。如果 it_value 为 0,表示停止该 Timer。
    • 应用程序会先启动用一个时间间隔启动定时器,随后又修改该定时器的时间间隔,这都可以通过修改 new_value 来实现;假如应用程序在修改了时间间隔之后希望了解之前的时间间隔设置,则传入一个非 NULL 的 old_value 指针,这样在 timer_settime() 调用返回时,old_value 就保存了上一次 Timer 的时间间隔设置。

直接使用 Hrtimer

用户态使用定时器因为优先级等问题,多少都会收到进程调度影响,如在用户层 POSIX Timer 依旧无法满足要求,则需要自行实现一个调用 hrtimer 的设备,在内核中直接调用 hrtime,到期后通过接口通知应用层。

实现一个挂载至 platform 总线的设备。

调用 hrtimer 的简单实现:

  • 需要定义一个 hrtimer 结构的实例
  • 用 hrtimer_init 函数对它进行初始化 void hrtimer_init(struct hrtimer *timer, clockid_t which_clock,enum hrtimer_mode mode);
    • which_clock 可以是 CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME
    • mode 则可以是相对时间 HRTIMER_MODE_REL,也可以是绝对时间 HRTIMER_MODE_ABS
    • 设定回调函数:timer.function = hr_callback;
  • 如果定时器无需指定一个到期范围,可以在设定回调函数后直接使用 hrtimer_start 激活该定时器;如果需要指定到期范围,则可以使用 hrtimer_start_range_ns 激活定时器。函数原型:
    • int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);
    • hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim, unsigned long range_ns, const enum hrtimer_mode mode);
  • 要取消一个 hrtimer,使用 hrtimer_cancel。

尾声

有错误请留意,尽力修补.