linux 时间子系统

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

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

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

  • 更新

    1
    2022.03.27 初始

导语

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

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

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

  • 因为当时动笔时属于应付 KPI,东拼西凑,无法保证内容准确性.
  • 因时间实在太久了,很多文献的出处也记不清楚了,因此有很多部分无法标注来源..

概述

背景

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

时间标准

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

  • UTC(Coordinated Universal Time),又称世界标准时间。是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。这套时间系统被应用于许多互联网和万维网的标准中,linux 系统中联网状态下同步时间即同步 UTC 时间。
  • GMT (Greenwich Mean Time,GMT)格林尼治标准时间(Greenwich Mean Time,GMT)是指位于伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。地球的椭圆轨道里的运动速度不均匀,这个时刻可能和实际的太阳时相差 16 分钟。格林尼治时间已经不再被作为标准时间使用。现在的标准时间——世界标准时间(UTC)——由原子钟提供,UTC 是基于标准的 GMT 提供的准确时间。

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

Linux 时间精度

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

  • __kernel_time_t: 精度为 1s,如使用被定义为 32 位的 time_t 的变量,存在 2038 年问题

    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 硬件来获取或设置时间信息。
  • realtime 和 RTC 一样,都是人们日常所使用的墙上时间,只是 RTC 时钟的精度通常比较低,只能达到毫秒级别的精度,外部的 RTC 芯片,访问速度也比较慢,为此,内核维护了另外一个 wall time 时间:realtime,取决于用于对计时的 clocksource,它的精度可以达到纳秒级别, realtime 存在于内存中,它的访问速度很快。realtime 记录的是自 1970 年 1 月 1 日 0 点 0 时到当前时刻所经历的纳秒数。(linux4.x 以前内核对应为全局变量 xtime,4.x 以后不再有全局 xtime 定义)
  • monotonic time  该时钟自系统开机后就一直单调地增加,不因用户的调整时间而产生跳变,该时间不计算系统休眠的时间。
  • raw monotonic time  与 monotonic 类似,属于单调递增的时钟,raw monotonic time 不会受到 NTP 调整,它代表着系统独立时钟硬件对时间的统计。
  • boot time  与 monotonic 相同,系统休眠时同样增加,它代表着系统上电后的总时间。

早期 Linux 系统中是通过全局变量形式更改对应时间,而随着 Linux 内核迭代以上提及的时间类型被定义在 timekeeper 结构体中统一管理。

对应 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 interrupt (IRQ 0)

    • HZ 是用来定义每一秒有几次 timer interrupts。Linux 内核 2.6 及以后版本可以在内核编译时指定 HZ 的值。HZ 取值一般为 100、250、300 或 1000。对于 linux 而言 HZ 一般为固定值。
    • 高 HZ 对系统而言是更快的响应速度、更精准的进程调度、更准确的计时精度。随之带来的是系统处理 timer interrupt 中断开销的上升。需要根据实际需求合理选择 HZ 大小。
  2. Tick:翻译为 “ 节拍 “,数值上为 HZ 的倒数

    • 对应 1 次 timer interrupt 的间隔长短,定期的 tick 事件或者用于全局的时间管理(jiffies 和时间的更新),或者用于本地 cpu 的进程统计、时间轮定时器框架
    • 大部分情况下 LInux 系统都会尽量维持周期性 tick。但 Linux2.6 内核以后,引入了动态时钟,在系统处于 idle 模式时,可以关闭周期性 tick 以节省电量。这一特性需要配置内核选项 CONFIG_NO_HZ 来激活,这一特性也被叫做 tickless。
  3. jiffies:记录系统启动以来的节拍总数,作为内核内计算时间间隔的一个很重要变量。

    1
    2
    extern u64 __jiffy_data jiffies_64;
    extern unsigned long volatile __jiffy_data jiffies;
    • 定义上看 jiffies 为 32 位变量,对应 100HZ 的系统溢出间隔 50 天左右,内核定义了专门的宏处理比较时间长短时 jiffies 溢出的问题。同时还存在 jiffies_64 变量,jiffies_64 的低 32 位对应 jiffies,溢出时间间隔忽略不计。一般应用内 jiffies 已足够。

时间子系统概述

硬件计时器只是一个按照固定间隔单调递增的计时器,时间本身是一个没有首尾的直线,任何时间的度量都有一个参考点,对于 linux 系统而言,时间是自 linux exposed 以来的具体的数值。通过时间子系统 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 时钟使用。这里对应的是新的时间子系统。

时间子系统文件系统

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

  • time.c :向用户空间提供时间接口。包括:time, stime, gettimeofday, settimeofday,adjtime。还有一些在其他内核模块使用时间格式转换的接口函数如 jiffes 和微秒之转换等。
  • timeconv.c:从 calendar time 转换 broken-down time 转换接口。
  • timer.c:低精度 timer 模块
  • time_list.c 与 timer_status.c:用户空间提供的调试接口。
  • hrtimer.c:高精度 timer 模块。
  • itimer.c:interval timer 模块。
  • posix-timers.c 、posix-cpu-timers.c 、posix-clock.c:OSIX timer 模块和 POSIX clock 模块。
  • alarmtimer.c:alarmtimer 模块
  • clocksource.c:clocksource.c 是通用 clocksource driver。
  • jiffies.c:全局 system tick 对应的 clocksource。
  • clockevent.c:clockevent 模块。
  • timekeeping.c、timekeeping_debug.c:timekeeping 模块。
  • ntp.c:ntp 模块。
  • tick-common.c、tick-oneshot.c、tick-sched.c:属于 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:用于广播模式。
  • sched_clock.c:通用 sched clock 模块。这个模块主要是提供一个 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。

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

  • clock event 和 clock source:
    • clock event 和 clock source 位于核心层底层,是内核抽象出来与硬件无关的模块 clock source 对应硬件设备的 system free running counter 提供一个基础的 timeline,64 位的计时器,在 ns 级别的溢出间隔对于应用而言,完全可以满足.
    • 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 下只需要配置一次定时器,之后等事情就会周期性产生中断,是内核早期基于的模式。one shot mode 下定时器每产生一次中断,系统需要再次对定时器进行配置,才可以触发下一次定时器中断。periodic mode 定时精度较低在毫秒级别,one shot mode 定时精度可以达到纳秒级别。
  • 低精度 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。
    • 除此之外还有一个用来配置 tick 模式的选项: CONFIG_TICK_ONESHOT 表示系统内所有的 tick 设备都是 oneshot mode。
  • 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
    • 系统开始时并不是直接进入 dynamic tick mode,系统开始会运行在周期 tick 模式下,各个 cpu 对应的 tick device 的 event handler 为 tick_handle_periodic。在 timer 的软中断上下文中,系统调用 tick_check_oneshot_change 检查支持 one shot 的 clock event device 发生 tick mode 的切换。tick device 切换到 one shot 模式,event handler 设置为 tick_nohz_handler。
    • 系统正常运行时,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 = (cycle * mult) >> shift;$
Clocksource 的注册和初始化

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

clocksource的注册和初始化

大部分工作在 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 不止一个,为了筛选 clocksource。内核启用了一个周期为 0.5 秒的定时器。

clocksource 被注册时,除 clocksource_list 外,还会同时挂载到 watchdog_list 链表。定时器每 0.5 秒检查 watchdog_list 上的 clocksource,WATCHDOG_THRESHOLD 的值定义为 0.0625 秒,如果在 0.5 秒内,clocksource 的偏差大于这个值就表示这个 clocksource 是不稳定的,定时器的回调函数通过 clocksource_watchdog_kthread 线程标记该 clocksource,并把它的 rate 修改为 0,表示精度极差。

系统启动时 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 差值作为参数。
  • set_next_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

由 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 上。

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 DEFINE_PER_CPU(struct tick_device, tick_cpu_device);
    • 在多核架构下,系统会为每一个 cpu 建立了一个 tick device。每一个 cpu 就像是一个小系统一样运行在各自的 tick 上,实现任务调度等工作。
  • global tick device int tick_do_timer_cpu __read_mostly = TICK_DO_TIMER_BOOT;
  • 有些全局的系统任务,不适合使用 local tick device,如更新 jiffies、更新 wall time 等。这时系统会选择一个 local tick device,在 tick_do_timer_cpu 中指明,作为 global tick device 负责这些全局任务。
  • broadcast tick device static struct tick_device tick_broadcast_device;
  • 涉及 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。

低精度 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 的软件架构

系统中有可能有成百上千个定时器,对于这些定时器的处理,早期时间子系统采用了时间轮进行统一管理即按照定时器到期的时间按照时间轮的方式排列,统一处理。

上图的轮上由 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 ,如此反复操作。

系统依然创建一个模拟 tick 时钟的特殊 hrtimer,并且该时钟按照 tick 的间隔时间(比如 10ms)定期启动自己,从而模拟出 tick 时钟,不过在 tickless 情况下,会跳过一些 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() 函数,系统提供了常见时间格式之间的转换

使用 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。

尾声

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