[toc]

kernel/time/timekeeping.c 内核时间保持(Kernel Timekeeping) 维护系统时间的核心与灵魂

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了解决操作系统中最基础、最核心的需求之一:以一种精确、稳定、单调的方式来跟踪和维护时间的流逝

kernel/time/timekeeping.c所管理的时间保持子系统,具体解决了以下几个关键问题:

  1. 时间的表示与更新:需要一个核心机制来管理系统的“官方时间”。这包括“墙上时间”(Wall Time,即CLOCK_REALTIME,自1970年Epoch以来的时间,可以被用户或NTP修改),以及“单调时间”(Monotonic Time,即CLOCK_MONOTONIC,从系统启动开始单调递增,不受时间调整影响)。
  2. 时钟源的抽象:硬件提供了各种各样的时间源(Clock Sources),例如TSC(CPU的时间戳计数器)、HPET(高精度事件定时器)、ACPI PM Timer等。这些时钟源的精度、稳定性和访问速度各不相同。时间保持子系统需要一个框架来抽象这些硬件差异,并从中选择一个最佳的时钟源来驱动系统时间。
  3. 节拍的产生与中断:传统的操作系统依赖于周期性的“节拍中断”(Tick Interrupt)来更新时间和执行调度。但这种方式在CPU空闲时会造成不必要的唤醒和功耗。时间保持子系统需要与定时器子系统协作,支持**“动态节拍”或“无节拍”(Tickless)**模式,即只在需要时才安排下一次时钟中断。
  4. 时间的平滑调整:当系统时间需要被调整时(例如,通过NTP进行网络时间同步),不能简单地向前或向后跳跃,因为这会导致应用程序的时间计算出现混乱。时间保持子系统必须能够以一种平滑、渐进的方式来纠正时间偏差,例如通过在一段时间内稍微加快或减慢内核的时钟频率。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux的时间保持系统经历了从简单到极其复杂的演进,以追求更高的精度和效率。

  1. 基于节拍(Tick-based)的时代:早期的Linux完全依赖于固定频率(如100Hz或1000Hz)的节拍中断。每次中断发生,时间保持代码就会将时间向前推进一个固定的增量。
  2. 高精度定时器(High-Resolution Timers, hrtimers)的引入:这是一个重要的里程碑,使得内核可以安排纳秒级精度的定时器事件,为时间保持的精度提升奠定了基础。
  3. 动态节拍/无节拍内核(Tickless Kernel):为了节能和降低虚拟化开销,内核引入了CONFIG_NO_HZ_IDLE(针对空闲CPU)和CONFIG_NO_HZ_FULL(针对被隔离用于高性能计算的CPU)。timekeeping.c的逻辑变得更加复杂,因为它不能再依赖周期性的更新,而是需要在每次CPU进入/退出idle状态时,精确地计算并累加经过的时间。
  4. 通用时间框架(Generic Time Framework):由Thomas Gleixner主导的对整个kernel/time子系统的大规模重构,将时钟源(clocksource)、时钟事件设备(clockevent device)、定时器和时间保持等松散相关的部分,整合成一个清晰、健壮的框架。timekeeping.c就是这个框架的核心。
  5. 64位时间表示:随着时间的推移,为了避免32位时间戳在2038年溢出的问题(Y2038),内核内部的时间表示全面转向64位。

目前该技术的社区活跃度和主流应用情况如何?

kernel/time/timekeeping.c是内核中最核心、最关键的部分之一,其代码由内核时间子系统的维护者们(主要是Thomas Gleixner)进行严格的审查和管理。

  • 绝对核心:系统中的每一次gettimeofday()clock_gettime()系统调用,每一次定时器事件,每一次调度,都直接或间接地依赖于timekeeping.c维护的时间。
  • 社区状态:代码库非常成熟和稳定。社区的活跃度主要体现在:
    • 为新的硬件架构和时钟源设备提供支持。
    • 持续优化无节拍内核在不同负载下的精度和性能。
    • 修复在极端情况下或与虚拟化交互时发现的精微的时间偏差问题。

核心原理与设计

它的核心工作原理是什么?

timekeeping.c的核心是基于一个选定的硬件时钟源,通过周期性或非周期性的更新,精确地维护一个多精度的时间值,并为内核其他部分提供读取时间的接口

  1. 时钟源(Clocksource)

    • 内核在启动时会探测所有可用的硬件时钟源(如TSC, HPET)。
    • 每个时钟源都被封装在一个struct clocksource中,该结构描述了其精度、稳定性和一个.read()函数,用于读取硬件计数器的原始值。
    • 时间保持子系统会根据一个评分系统,选择一个当前最佳的时钟源来使用。
  2. 核心数据结构 (struct timekeeper)

    • 所有的时间状态都集中在一个全局的struct timekeeper变量中。
    • 这个结构体包含了当前时间(墙上时间和单调时间)、时钟源的原始读数、用于转换的乘数和移位数(multshift)等关键信息。
  3. 时间更新 (timekeeping_update)

    • 时间的更新是核心所在。当一次时钟中断发生,或者在无节拍模式下需要更新时间时,timekeeping_update()会被调用。
    • 它会读取当前时钟源的计数器,计算出自上次更新以来经过了多少个“周期”(cycles)。
    • 然后,它使用timekeeper中存储的multshift值,通过一个高精度的数学运算((cycles * mult) >> shift),将硬件周期数精确地转换为纳秒(nanoseconds)
    • 最后,将计算出的纳秒增量累加到timekeeper中存储的当前时间上。
  4. 平滑调整(Time Slewing)

    • 当NTP守护进程需要调整时间时,它不会直接修改timekeeper中的时间值。
    • 相反,它会调整timekeeper中的一个“频率偏移”值。timekeeping.c的更新逻辑会在正常的增量计算之上,根据这个偏移值,额外地增加或减少一点点时间。这个微小的调整会随着时间的推移累积,从而以一种平滑的方式将系统时间逐渐“拉回”到正确的值。

它的主要优势体現在哪些方面?

  • 高精度和高分辨率:通过使用TSC等高频计数器和精确的数学转换,现代Linux内核的时间精度可以达到纳秒级别。
  • 抽象和可移植性clocksource框架完美地抽象了硬件差异,使得时间保持核心代码可以独立于具体的硬件平台。
  • 效率和节能:无节拍内核的设计,极大地降低了系统在空闲时的功耗和不必要的CPU活动。
  • 稳定性:平滑调整机制保证了系统时间的单调性和稳定性,对时间敏感的应用程序至关重要。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 对硬件的依赖:时间的最终精度和稳定性,严重依赖于底层硬件时钟源的质量。一个不稳定或有bug的TSC会给整个系统带来时间问题。
  • 虚拟化的挑战:在虚拟机中,精确的时间保持是一个众所周知的难题。虚拟机的暂停、恢复、迁移,以及与宿主机时钟的同步,都可能导致客户机操作系统内部的时间出现跳变或不一致。timekeeping.c中有大量复杂的代码就是为了处理这些虚拟化场景。
  • 复杂性:现代时间保持系统的内部逻辑极其复杂,涉及到高精度数学、原子操作、多种CPU状态(idle, nohz_full)和复杂的锁机制,是内核中最难理解和调试的部分之一。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是Linux内核中唯一且标准的时间管理解决方案,不存在替代品。所有需要时间的内核和用户空间功能都依赖于它。

  • gettimeofday() / clock_gettime():所有获取当前时间的系统调用,最终都是从timekeeping.c维护的timekeeper结构中读取时间。
  • 所有定时器:无论是nanosleep, select的超时,还是内核内部的hrtimertimerfd,它们的到期时间都是与timekeeper维护的当前时间进行比较来判断的。
  • 文件系统时间戳:文件的访问时间(atime)、修改时间(mtime)、创建时间(crtime)等,都是由timekeeping.c提供。
  • 网络协议:TCP协议中的时间戳选项、NTP客户端的时间同步等,都依赖于一个精确的本地时钟。
  • 调度器:内核调度器需要精确的时间来计算进程的运行时间片、CPU使用率等。

是否有不推荐使用该技术的场景?为什么?

这个提法不适用。在Linux内核的生态系统中,没有任何场景可以“不使用”这个核心的时间保持服务。

对比分析

请将其 与 其他相似技术 进行详细对比。

CLOCK_REALTIME vs. CLOCK_MONOTONIC

这是由timekeeping.c同时维护的两种最重要的时钟,理解它们的区别至关重要。

特性 CLOCK_REALTIME CLOCK_MONOTONIC
参考点 Unix Epoch (1970-01-01 00:00:00 UTC)。 系统启动时间
行为 代表**“墙上时间”**或日历时间。 代表自系统启动以来单调递增的时间。
可变性 不保证单调。可以被settimeofday()系统调用、NTP同步或管理员手动向前或向后调整 严格保证单调递增。它绝不会向后跳跃。
受闰秒影响 。(有一个CLOCK_MONOTONIC_RAW是完全不受NTP调整影响的硬件时间)。
适用场景 需要显示给用户的、与现实世界对应的日期和时间。 测量时间间隔、设置超时、性能分析等任何不应受墙上时间变化影响的场景。这是绝大多数程序内部逻辑应使用的时钟

read_persistent_wall_and_boot_offset 读取持久时钟并从引导中偏移

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
/**
* read_persistent_clock64 - 从持久时钟返回时间。
* @ts:指向读出值存储的指针
*
* 尚不支持它的拱门的虚拟功能较弱。从电池供电的持久时钟中读取时间。
* 返回 tv_sec=0 且 tv_nsec=0 的 timespec(如果不支持)。
*
* XXX - 一旦所有拱门都实施了它,请务必将其删除。
*/
void __weak read_persistent_clock64(struct timespec64 *ts)
{
ts->tv_sec = 0;
ts->tv_nsec = 0;
}

/**
* read_persistent_wall_and_boot_offset - 读取持久时钟,并从引导中偏移。
* @wall_time: 持久时钟返回的当前时间
* @boot_offset: 定义为 wall_time - boot_time 的偏移量
*
*对于尚不支持它的 arch 的 dummy 函数较弱。
*
* default 函数根据 local_clock() 的当前值计算 offset。这样,支持 sched_clock() 但不支持专用 boot time clock 的架构将提供最佳的 boot time 估计值。
*/
void __weak __init
read_persistent_wall_and_boot_offset(struct timespec64 *wall_time,
struct timespec64 *boot_offset)
{
read_persistent_clock64(wall_time);
*boot_offset = ns_to_timespec64(local_clock());
}

tk_clock_read 原子 clocksource read() 辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* tk_clock_read - 原子 clocksource read() 辅助函数
*
* 这个帮助程序在读取路径中是必要的,因为虽然 seqcount 确保我们在更新结构时不会返回错误的值,但它并不能防止潜在的崩溃。
* tkr 的 clocksource 有可能在 read reference 和传递给 read 函数的 clock reference 之间发生变化。
* 如果将错误的 clocksource 传递给错误的 read 函数,这可能会导致崩溃。
* 在持有 tk_core.lock 或读取 fast-timekeeper tkrs (受其自己的锁定和更新逻辑保护)时,不需要使用此功能。
*/
static inline u64 tk_clock_read(const struct tk_read_base *tkr)
{
struct clocksource *clock = READ_ONCE(tkr->clock);

return clock->read(clock);
}

tk_setup_internals 设置内部以使用 clocksource clock

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* tk_setup_internals - 设置内部以使用 clocksource clock。
*
* @tk:要设置的目标计时器。
* @clock:指向 clocksource 的指针。
*
* 计算给定 clocksource/adjustment 对和间隔请求的固定周期/nsec 间隔。
*
* 除非您是计时代码,否则您不应该使用它!
*/
static void tk_setup_internals(struct timekeeper *tk, struct clocksource *clock)
{
u64 interval;
u64 tmp, ntpinterval;
struct clocksource *old_clock;

/* 增加 cs_was_changed_seq,标记时钟源发生了变化 */
++tk->cs_was_changed_seq;
/* 保存旧的时钟源,并将新的时钟源赋值给 tkr_mono 和 tkr_raw */
old_clock = tk->tkr_mono.clock;
tk->tkr_mono.clock = clock;
tk->tkr_mono.mask = clock->mask;
/* 使用 tk_clock_read 读取当前时钟周期值,作为新的基准 */
tk->tkr_mono.cycle_last = tk_clock_read(&tk->tkr_mono);

tk->tkr_raw.clock = clock;
tk->tkr_raw.mask = clock->mask;
tk->tkr_raw.cycle_last = tk->tkr_mono.cycle_last;

/* 计算周期与纳秒的转换关系 */
/* 将 NTP 时间间隔(NTP_INTERVAL_LENGTH)转换为时钟周期 */
tmp = NTP_INTERVAL_LENGTH;
tmp <<= clock->shift;
ntpinterval = tmp;
tmp += clock->mult/2;
do_div(tmp, clock->mult);
/* 确保周期值至少为 1,避免无效的周期值 */
if (tmp == 0)
tmp = 1;

interval = (u64) tmp;
tk->cycle_interval = interval;

/* 计算纳秒间隔和剩余值
根据周期值计算纳秒间隔(xtime_interval)和剩余值(xtime_remainder)*/
tk->xtime_interval = interval * clock->mult;
tk->xtime_remainder = ntpinterval - tk->xtime_interval;
tk->raw_interval = interval * clock->mult;

/* 处理时钟源切换 */
if (old_clock) {
/* 调整 xtime_nsec 的单位以适应新的时钟源的 shift 值 */
int shift_change = clock->shift - old_clock->shift;
if (shift_change < 0) {
tk->tkr_mono.xtime_nsec >>= -shift_change;
tk->tkr_raw.xtime_nsec >>= -shift_change;
} else {
tk->tkr_mono.xtime_nsec <<= shift_change;
tk->tkr_raw.xtime_nsec <<= shift_change;
}
}

tk->tkr_mono.shift = clock->shift;
tk->tkr_raw.shift = clock->shift;

/* 设置 NTP 相关参数
初始化 NTP 误差值(ntp_error)和误差调整参数
计算 NTP 滴答值(ntp_tick),用于时间同步 */
tk->ntp_error = 0;
tk->ntp_error_shift = NTP_SCALE_SHIFT - clock->shift;
tk->ntp_tick = ntpinterval << tk->ntp_error_shift;

/*
* 保存时钟源的转换参数
* 保存当前时钟源的 mult 值,用于后续的时间转换
*/
tk->tkr_mono.mult = clock->mult;
tk->tkr_raw.mult = clock->mult;
tk->ntp_err_mult = 0;
tk->skip_second_overflow = 0;
}

tk_set_wall_to_mono 更新 timekeeper 结构体中的 wall_to_monotonic 偏移量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void tk_set_wall_to_mono(struct timekeeper *tk, struct timespec64 wtm)
{
struct timespec64 tmp;

/*
* 在修改任何内容之前,请验证以下各项的一致性:offset_real = -wall_to_monotonic
*/
set_normalized_timespec64(&tmp, -tk->wall_to_monotonic.tv_sec,
-tk->wall_to_monotonic.tv_nsec);
/* 检查 offs_real 是否与计算出的值一致 */
WARN_ON_ONCE(tk->offs_real != timespec64_to_ktime(tmp));
tk->wall_to_monotonic = wtm;
set_normalized_timespec64(&tmp, -wtm.tv_sec, -wtm.tv_nsec);
/* 与 READ_ONCE() 中的 ktime_mono_to_any() 配对*/
WRITE_ONCE(tk->offs_real, timespec64_to_ktime(tmp));
WRITE_ONCE(tk->offs_tai, ktime_add(tk->offs_real, ktime_set(tk->tai_offset, 0)));
}

tk_set_xtime 更新 xtime 和粗略计时器的纳秒部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* 更新粗略计时器的纳秒部分。
* 粗略时间(coarse time)是时间的低精度表示,通常用于对性能要求较高但不需要高精度的场景
* 由于 xtime_nsec 可能会因时钟乘法因子的调整而发生小的负向变化(例如在 timekeeping_apply_adjustment 中),
* 直接依赖 xtime_nsec 可能导致粗略时间出现倒退的现象
* 为了解决这个问题,coarse_nsec 作为一个独立的副本,仅在时钟被设置或时间累积时更新
*/
static inline void tk_update_coarse_nsecs(struct timekeeper *tk)
{
tk->coarse_nsec = tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift;
}

static void tk_set_xtime(struct timekeeper *tk, const struct timespec64 *ts)
{
tk->xtime_sec = ts->tv_sec;
tk->tkr_mono.xtime_nsec = (u64)ts->tv_nsec << tk->tkr_mono.shift;
tk_update_coarse_nsecs(tk);
}

update_fast_timekeeper 更新快速和 NMI 安全单调计时器

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
/**
* update_fast_timekeeper - 更新快速和 NMI 安全单调计时器。
* @tkr:我们进行更新的计时读数基础
* @tkf:指向 NMI 安全计时器的指针
*
* 我们希望从任何上下文中使用它,包括 NMI 和跟踪/检测计时代码本身。
*
* 采用闩锁技术;请参阅 @write_seqcount_latch。
*
* 因此,如果 NMI 命中 base[0] 的更新,那么它将使用 base[1],它仍然一致。在最坏的情况下,这可能会导致时间戳略微错误(几纳秒)。请参阅 @ktime_get_mono_fast_ns。
*/
static void update_fast_timekeeper(const struct tk_read_base *tkr,
struct tk_fast *tkf)
{
struct tk_read_base *base = tkf->base;

/* Force readers off to base[1] */
write_seqcount_latch_begin(&tkf->seq);

/* Update base[0] */
memcpy(base, tkr, sizeof(*base));

/* Force readers back to base[0] */
write_seqcount_latch(&tkf->seq);

/* Update base[1] */
memcpy(base + 1, base, sizeof(*base));

write_seqcount_latch_end(&tkf->seq);
}

timekeeping_update_from_shadow 将影子时间管理器(shadow_timekeeper)中的数据更新到实际的时间管理器(timekeeper)中

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
static void timekeeping_update_from_shadow(struct tk_data *tkd, unsigned int action)
{
struct timekeeper *tk = &tk_core.shadow_timekeeper;

lockdep_assert_held(&tkd->lock);

/*
* 使用序列计数器(seqcount)阻止读者在更新过程中访问时间数据。这样可以避免在更新过程中读者看到不一致的时间值
*/
write_seqcount_begin(&tkd->seq);

/* 清除 NTP 错误 */
if (action & TK_CLEAR_NTP) {
tk->ntp_error = 0;
ntp_clear();
}

tk_update_leap_state(tk); /* 更新闰秒状态 */
tk_update_ktime_data(tk); /* 更新内核时间数据 */

update_vsyscall(tk); /* 更新 VDSO(虚拟动态共享对象)中的时间数据 */
update_pvclock_gtod(tk, action & TK_CLOCK_WAS_SET); /* 更新 PV(Para-Virtualized)时钟的全局时间偏移 */

/* 计算单调时间(monotonic time)的实际基准时间,将其存储在 base_real 中 */
tk->tkr_mono.base_real = tk->tkr_mono.base + tk->offs_real;
/* 更新快速时间管理器:用于加速时间查询操作 */
update_fast_timekeeper(&tk->tkr_mono, &tk_fast_mono);
update_fast_timekeeper(&tk->tkr_raw, &tk_fast_raw);

/* 更新时钟设置序列 */
if (action & TK_CLOCK_WAS_SET)
tk->clock_was_set_seq++;

/*
* 将影子时间管理器的数据复制到实际时间管理器中。
* 这种设计避免了直接切换指针,从而保持缓存优化的数据布局。
*/
memcpy(&tkd->timekeeper, tk, sizeof(*tk));
write_seqcount_end(&tkd->seq);
}

timekeeping_init 初始化 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/* 此平台上是否有持久时钟的标志 */
static bool persistent_clock_exists;

/*
* timekeeping_init - 初始化 clocksource 和常用计时值
*/
void __init timekeeping_init(void)
{
struct timespec64 wall_time, /* 表示从持久时钟(persistent clock)读取的墙上时间(wall time) */
boot_offset, /* boot_offset:表示系统启动时间的偏移量 */
wall_to_mono; /* 表示墙上时间与单调时间(monotonic time)之间的差值 */
struct timekeeper *tks = &tk_core.shadow_timekeeper;
struct clocksource *clock;

tkd_basic_setup(&tk_core);

/* 从持久时钟中读取墙上时间和启动偏移量。
持久时钟是一个硬件时钟,能够在系统重启后保留时间信息 */
read_persistent_wall_and_boot_offset(&wall_time, &boot_offset);
/* 验证墙上时间 */
if (timespec64_valid_settod(&wall_time) &&
timespec64_to_ns(&wall_time) > 0) {
persistent_clock_exists = true;
} else if (timespec64_to_ns(&wall_time) != 0) {
pr_warn("Persistent clock returned invalid value");
wall_time = (struct timespec64){0};
}

/* 计算墙上时间与启动偏移的关系 */
if (timespec64_compare(&wall_time, &boot_offset) < 0)
boot_offset = (struct timespec64){0};

/*
* 我们想要设置wall_to_mono,因此以下内容为 true:
* 实际时间 wall_to_mono = 启动时间
*/
wall_to_mono = timespec64_sub(boot_offset, wall_time);

guard(raw_spinlock_irqsave)(&tk_core.lock);

ntp_init();

/* 设置默认时钟源 */
clock = clocksource_default_clock();
if (clock->enable)
clock->enable(clock);
tk_setup_internals(tks, clock);

tk_set_xtime(tks, &wall_time);
tks->raw_sec = 0;

tk_set_wall_to_mono(tks, wall_to_mono);

timekeeping_update_from_shadow(&tk_core, TK_CLOCK_WAS_SET);
}

timekeeping_get_ns 计时 get ns

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
* 这是一个静态内联函数,将硬件时钟周期数转换为纳秒。
* @tkr: 指向包含转换参数的tk_read_base结构体。
* @cycles: 当前读取到的原始硬件时钟周期数。
*/
static inline u64 timekeeping_cycles_to_ns(const struct tk_read_base *tkr, u64 cycles)
{
/* 计算自上次update_wall_time()以来的周期增量。*/
/* mask: 用于处理硬件计数器回绕的掩码。*/
/* delta: 存储计算出的周期增量。*/
u64 mask = tkr->mask, delta = (cycles - tkr->cycle_last) & mask;

/*
* 这个检查既能检测到负向的移动,也能检测到delta与tkr->mult的乘法会溢出的情况。
*/
/* unlikely()是编译器优化提示,表示这个分支不常发生。*/
if (unlikely(delta > tkr->clock->max_cycles)) {
/*
* 处理CPU之间的时钟源不一致性,通过检查掩码的最高有效位是否
* 在delta中被设置,来防止时间倒流。
*/
/* 如果delta的最高有效位被设置,这通常表示发生了时间倒流。*/
if (delta & ~(mask >> 1))
/* 在这种情况下,我们不能计算增量,只能返回上次更新时的纳秒余数,
* 避免让时间向后跳变。*/
return tkr->xtime_nsec >> tkr->shift;

/* 如果不是时间倒流,而是delta过大可能导致乘法溢出,
* 则调用一个安全的、开销更大的64位乘法版本。*/
return delta_to_ns_safe(tkr, delta);
}

/*
* 这是常规的、高效的快速路径。
* (delta * tkr->mult) 将周期增量转换为一个放大的、未归一化的纳秒值。
* + tkr->xtime_nsec 加上上次更新时剩余的纳秒,以保持精度。
* >> tkr->shift 将结果右移,归一化为最终的纳秒值。
*/
return ((delta * tkr->mult) + tkr->xtime_nsec) >> tkr->shift;
}

/*
* 这是一个静态且强制内联的函数,用于获取从上次基准更新以来所经过的纳秒数。
* @tkr: 指向一个tk_read_base结构体,其中包含了进行时间转换所需的所有参数。
*/
static __always_inline u64 timekeeping_get_ns(const struct tk_read_base *tkr)
{
/*
* 调用tk_clock_read(tkr),这是一个宏,它会调用tkr->clock->read(),
* 即当前系统选定的硬件时钟源(如TSC, HPET, ARM Arch Timer)的读周期函数,
* 以获取一个原始的、64位的硬件周期计数值。
*
* 然后,将这个原始周期值传递给timekeeping_cycles_to_ns()进行转换。
*/
return timekeeping_cycles_to_ns(tkr, tk_clock_read(tkr));
}

tk_fast_mono tk_fast_raw 快速单调和原始时间管理器初始化

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
/*
启动期间的作用:

在系统启动期间,正式的时钟源尚未安装,local_clock() 提供了一个简单的时间查询接口,直接返回纳秒值。
通过 FAST_TK_INIT 初始化,tk_fast_mono 和 tk_fast_raw 能够在启动期间正常工作,确保时间查询功能可用。
当第一个正式的时钟源安装后,这些快速时间管理器会被更新为使用新的时钟源。
*/
#define FAST_TK_INIT \
{ \
.clock = &dummy_clock, \
.mask = CLOCKSOURCE_MASK(64), \
.mult = 1, \
.shift = 0, \
}

static struct tk_fast tk_fast_mono ____cacheline_aligned = {
.seq = SEQCNT_LATCH_ZERO(tk_fast_mono.seq),
.base[0] = FAST_TK_INIT,
.base[1] = FAST_TK_INIT,
};

static struct tk_fast tk_fast_raw ____cacheline_aligned = {
.seq = SEQCNT_LATCH_ZERO(tk_fast_raw.seq),
.base[0] = FAST_TK_INIT,
.base[1] = FAST_TK_INIT,
};

ktime_get 获取当前单调时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ktime_t ktime_get(void)
{
struct timekeeper *tk = &tk_core.timekeeper;
unsigned int seq;
ktime_t base;
u64 nsecs;

WARN_ON(timekeeping_suspended);

do {
seq = read_seqcount_begin(&tk_core.seq);
base = tk->tkr_mono.base;
nsecs = timekeeping_get_ns(&tk->tkr_mono);

} while (read_seqcount_retry(&tk_core.seq, seq));

return ktime_add_ns(base, nsecs);
}
EXPORT_SYMBOL_GPL(ktime_get);

ktime_get_with_offset 获取带偏移的时间

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

static ktime_t *offsets[TK_OFFS_MAX] = {
[TK_OFFS_REAL] = &tk_core.timekeeper.offs_real,
[TK_OFFS_BOOT] = &tk_core.timekeeper.offs_boot,
[TK_OFFS_TAI] = &tk_core.timekeeper.offs_tai,
};

ktime_t ktime_get_with_offset(enum tk_offsets offs)
{
struct timekeeper *tk = &tk_core.timekeeper;
unsigned int seq;
ktime_t base, *offset = offsets[offs];
u64 nsecs;

WARN_ON(timekeeping_suspended);

do {
seq = read_seqcount_begin(&tk_core.seq);
base = ktime_add(tk->tkr_mono.base, *offset);
nsecs = timekeeping_get_ns(&tk->tkr_mono);

} while (read_seqcount_retry(&tk_core.seq, seq));

return ktime_add_ns(base, nsecs);

}
EXPORT_SYMBOL_GPL(ktime_get_with_offset);

ktime_get_real 以 ktime_t 格式获取真实 (wall-) 时间

1
2
3
4
5
6
7
8
9
/**
* ktime_get_real - 以 ktime_t 格式获取真实 (wall-) 时间
*
* 返回: ktime_t 格式的实 (wall) 时间
*/
static inline ktime_t ktime_get_real(void)
{
return ktime_get_with_offset(TK_OFFS_REAL);
}

ktime_get_update_offsets_now 获取当前时间并更新偏移量

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* ktime_get_update_offsets_now - hrtimer的辅助函数
* @cwsseq: 指向一个变量的指针,用于检查和存储“时钟被设置过”的序列号。
* @offs_real: 指向用于存储 monotonic -> realtime 偏移量的变量。
* @offs_boot: 指向用于存储 monotonic -> boottime 偏移量的变量。
* @offs_tai: 指向用于存储 monotonic -> clock tai 偏移量的变量。
*
* 返回值: 返回当前的单调时间。如果@cwsseq中的序列号与
* timekeeper.clock_was_set_seq不同,则更新这些偏移量。
*
* 从hrtimer_interrupt()或retrigger_next_event()中调用。
*/
ktime_t ktime_get_update_offsets_now(unsigned int *cwsseq, ktime_t *offs_real,
ktime_t *offs_boot, ktime_t *offs_tai)
{
/* tk: 指向内核核心的时间管理器结构体。*/
struct timekeeper *tk = &tk_core.timekeeper;
/* seq: 用于存储序列计数器的快照,以检测并发写入。*/
unsigned int seq;
/* base: 用于存储计算出的当前单调时间。*/
ktime_t base;
/* nsecs: 用于存储从上次tkr_mono.base更新以来,所经过的纳秒数。*/
u64 nsecs;

/*
* 这是一个do-while循环,配合序列计数器锁,以实现无锁的、乐观的
* 并发安全读取。如果读取期间有并发写入,循环会重试。
*/
do {
/*
* 调用read_seqcount_begin,读取并保存当前的序列计数值。
* 它还会检查计数器是否为奇数(表示有写者正在操作),如果是,则自旋等待。
*/
seq = read_seqcount_begin(&tk_core.seq);

/* 读取单调时钟的基准时间。*/
base = tk->tkr_mono.base;
/* 调用timekeeping_get_ns,根据硬件时钟源,计算自基准时间以来经过的纳秒数。*/
nsecs = timekeeping_get_ns(&tk->tkr_mono);
/* 将纳秒数加到基准时间上,得到当前精确的单调时间。*/
base = ktime_add_ns(base, nsecs);

/*
* 检查调用者本地的“时钟被设置过”序列号(*cwsseq),是否与全局的序列号不同。
*/
if (*cwsseq != tk->clock_was_set_seq) {
/*
* 如果不同,说明在两次调用之间,系统时间被修改过。
* 此时,需要将timekeeper中最新的全局偏移量,更新到调用者提供的地址中。
*/
*cwsseq = tk->clock_was_set_seq;
*offs_real = tk->offs_real;
*offs_boot = tk->offs_boot;
*offs_tai = tk->offs_tai;
}

/* 处理闰秒插入的调整。unlikely()是编译器优化提示。*/
if (unlikely(base >= tk->next_leap_ktime))
/* 如果当前时间已越过下一个闰秒点,则临时调整realtime的偏移量。*/
*offs_real = ktime_sub(tk->offs_real, ktime_set(1, 0));

/*
* 调用read_seqcount_retry,它会再次读取序列计数器,并与之前保存的seq比较。
* 如果计数器值发生变化或变为奇数,则函数返回true,do-while循环将重试。
*/
} while (read_seqcount_retry(&tk_core.seq, seq));

/* 在成功获取到一致的数据快照后,返回计算出的当前单调时间。*/
return base;
}

do_timer 更新 jiffies

1
2
3
4
5
6
7
8
/*
*必须持有jiffies_lock
*/
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
calc_global_load(); // 计算全局负载
}

timekeeping_valid_for_hres 检查时间是否适合高精度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* timekeeping_valid_for_hres - Check if timekeeping is suitable for hres
*/
int timekeeping_valid_for_hres(void)
{
struct timekeeper *tk = &tk_core.timekeeper;
unsigned int seq;
int ret;

do {
seq = read_seqcount_begin(&tk_core.seq);

ret = tk->tkr_mono.clock->flags & CLOCK_SOURCE_VALID_FOR_HRES;

} while (read_seqcount_retry(&tk_core.seq, seq));

return ret;
}

内核时间管理:在睡眠与唤醒之间维持时间的流逝

本代码片段定义了Linux内核时间保持子系统(Timekeeping Subsystem)与系统核心电源管理框架(syscore_ops)的接口。它的核心功能是,在系统进入睡眠(suspend)和从睡眠中唤醒(resume)这两个关键时刻,精确地管理系统时间,确保时间在操作系统“暂停”期间依然能够正确地“流逝”。

这个过程就像是一个精密的钟表匠,在需要给钟表(操作系统)上油保养(系统挂起)时,他会:

  1. 拍照留证 (timekeeping_suspend): 用一台高精度相机(persistent_clock,通常是RTC)拍下当前钟表的准确时间,并记下主发条(clocksource)停在哪个位置。然后他小心翼翼地让钟摆(tick_device)停下来。
  2. 校准时间 (timekeeping_resume): 保养结束后,他重新启动钟摆。然后拿出之前拍的照片,和现在的时间对比,计算出保养期间过去了多久。他会将钟表的指针(系统时间)向前拨动这段时间,确保时间的连续性。

实现原理分析

这个过程的实现依赖于一个多源校准策略,因为在系统挂起期间,主要的系统时钟源(clocksource)很可能会停止工作。内核必须有备用方案来恢复时间。

timekeeping_suspend 函数 (进入睡眠前的准备)

这个函数在系统即将进入低功耗状态之前被调用,它的任务是“冻结”时间状态并做好记录。

  1. 记录“墙上时间”: read_persistent_clock64(&timekeeping_suspend_time);是第一步。它会尝试从一个持久化时钟(persistent clock)读取当前时间。这种时钟通常是独立于主CPU的硬件,比如一个RTC(实时时钟)芯片或SoC内部一个在低功耗模式下也能运行的计数器。这个时间戳是恢复时间的最重要依据之一。
  2. 更新内部时间: timekeeping_forward_now(tks);是一个内部同步函数,它确保内核的时间状态(timekeeper结构体)与当前硬件时钟源(clocksource)完全同步,不留下任何未处理的时间片段。
  3. 标记状态: timekeeping_suspended = 1;设置一个全局标志,通知内核的其他部分时间子系统已暂停。
  4. 准备“不停歇”时钟: clocksource_start_suspend_timing(...)通知当前的时钟源,“我们要睡了,如果你是那种能在睡眠中继续计数的特殊时钟(suspend-nonstop clocksource),请开始记录”。这是一种优化,如果存在这种时钟,恢复时的精度会非常高。
  5. 漂移补偿: 接下来的if (persistent_clock_exists)块是一个非常精妙的防漂移算法
    • 问题: 每次挂起/恢复,由于各种延迟,可能会引入微小的误差(比如接近1秒)。如果频繁挂起/恢复,这种误差会累积,导致系统时间与真实的墙上时间(由RTC等提供)越差越远。
    • 解决方案: 内核会记住上一次挂起时,系统时间与持久化时钟之间的差值(old_delta)。在本次挂起时,它会再次计算这个差值(delta),并比较两次差值的变化(delta_delta)。如果变化不大,就认为这是累积误差,并微调本次记录的挂起时间戳(timekeeping_suspend_time),从而“吸收”掉这个误差,防止它传递到下一次。
  6. 关闭硬件: tick_suspend(), clocksource_suspend(), clockevents_suspend()会依次调用驱动程序,让节拍定时器(tick device)、时钟源等硬件进入低功耗模式。

timekeeping_resume 函数 (从睡眠中唤醒后的校准)

这个函数在系统刚刚从低功耗状态唤醒后被调用,它的任务是计算并补偿睡眠时间。

  1. 读取“墙上时间”: read_persistent_clock64(&ts_new);,和挂起时一样,再次读取持久化时钟,获取当前的准确时间。
  2. 开启硬件: clockevents_resume(), clocksource_resume()首先唤醒底层的时间硬件,让它们准备好工作。
  3. 计算睡眠时间 (核心): 这是整个恢复过程的核心,它按优先级顺序尝试三种方法来计算睡眠时间:
    • 方法一 (最佳): 不停歇时钟源: clocksource_stop_suspend_timing(clock, cycle_now)会询问时钟源:“你睡着了吗?”如果时钟源回答:“没有,我一直在数,这是我睡着期间数到的总数”,那么内核就得到了最精确的睡眠时间(nsec > 0)。
    • 方法二 (次佳): 持久化时钟: 如果方法一失败(时钟源睡着了),内核就会使用ts_delta = timespec64_sub(ts_new, timekeeping_suspend_time);,即用“现在墙上时间”减去“睡觉前记录的墙上时间”来计算睡眠时长。
    • 方法三 (备用): RTC核心: 如果前两种方法都失败了,还有一个备用方案,由RTC子系统的核心代码在稍晚的时候通过RTC硬件来校准时间(代码中未体现,但注释中提到了)。
  4. 注入睡眠时间: __timekeeping_inject_sleeptime(tks, &ts_delta);将计算出的睡眠时长ts_delta“注入”到内核时间保持器中,也就是将系统时间向前拨动相应的时长。
  5. 重置状态和重启服务:
    • 重置cycle_last等内部状态变量。
    • timekeeping_suspended = 0;清除暂停标志。
    • tick_resume()timerfd_resume()等函数会重新启动周期性的节拍中断,并通知所有等待时间的任务(如timerfd)时间可能发生了跳变。

代码分析

timekeeping_resume 函数

此函数在系统从挂起状态恢复后被调用,负责校准系统时间。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
void timekeeping_resume(void)
{
// 获取指向核心timekeeper结构的指针。
struct timekeeper *tks = &tk_core.shadow_timekeeper;
struct clocksource *clock = tks->tkr_mono.clock;
struct timespec64 ts_new, ts_delta; // 用于存储时间值的结构体。
bool inject_sleeptime = false; // 标志位,表示是否成功计算出了睡眠时间。
u64 cycle_now, nsec;
unsigned long flags;

// 步骤1: 立即从持久化时钟(如RTC)读取当前的“墙上时间”。
read_persistent_clock64(&ts_new);

// 步骤2: 恢复底层的时钟事件和时钟源硬件。
clockevents_resume();
clocksource_resume();

// 步骤3: 进入自旋锁保护的临界区,防止中断和其他任务干扰时间更新。
raw_spin_lock_irqsave(&tk_core.lock, flags);

/*
* 详细注释解释了计算睡眠时间的三种来源及其优先级:
* 1. suspend-nonstop clocksource (能在睡眠中计数的特殊时钟源) -> 最高优先级
* 2. persistent clock (RTC) -> 次高优先级
* 3. rtc 核心代码(作为备用)
*/
// 读取当前时钟源的计数值。
cycle_now = tk_clock_read(&tks->tkr_mono);
// 询问clocksource在睡眠期间记录了多少纳秒。
nsec = clocksource_stop_suspend_timing(clock, cycle_now);
if (nsec > 0) { // 方法1成功:如果clocksource是不停歇的,它会返回一个大于0的值。
ts_delta = ns_to_timespec64(nsec); // 将纳秒转换为timespec结构。
inject_sleeptime = true;
} else if (timespec64_compare(&ts_new, &timekeeping_suspend_time) > 0) {
// 方法2: 如果方法1失败,并且当前RTC时间大于挂起时的RTC时间。
ts_delta = timespec64_sub(ts_new, timekeeping_suspend_time); // 计算两者之差。
inject_sleeptime = true;
}

// 如果成功计算出了睡眠时间...
if (inject_sleeptime) {
suspend_timing_needed = false;
// 步骤4: 将计算出的睡眠时间“注入”到时间保持器中,原子地更新系统时间。
__timekeeping_inject_sleeptime(tks, &ts_delta);
}

// 步骤5: 重置内部状态,为下一次正常运行做准备。
tks->tkr_mono.cycle_last = cycle_now; // 更新最后一次读取的硬件计数值。
tks->tkr_raw.cycle_last = cycle_now;
tks->ntp_error = 0; // 清除NTP误差累积。
timekeeping_suspended = 0; // 清除挂起标志。
// 将影子timekeeper中的更新应用到主timekeeper,并标记为时钟被设置过。
timekeeping_update_from_shadow(&tk_core, TK_CLOCK_WAS_SET);

// 步骤6: 退出临界区,恢复之前的中断状态。
raw_spin_unlock_irqrestore(&tk_core.lock, flags);

// “喂狗”,防止软锁死检测器误报。
touch_softlockup_watchdog();

// 步骤7: 恢复高层的时间服务。
tick_resume(); // 重新启动周期性节拍中断。
// 通知timerfd子系统,时间可能发生了跳变,这与clock_settime()效果类似。
timerfd_resume();
}

timekeeping_suspend 函数

此函数在系统即将挂起前被调用,负责保存时间状态。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
int timekeeping_suspend(void)
{
struct timekeeper *tks = &tk_core.shadow_timekeeper;
struct timespec64 delta, delta_delta;
static struct timespec64 old_delta; // 静态变量,用于记住上一次的差值。
struct clocksource *curr_clock;
unsigned long flags;
u64 cycle_now;

// 步骤1: 记录挂起时的持久化时钟(RTC)时间。
read_persistent_clock64(&timekeeping_suspend_time);

/*
* 一个健壮性处理:某些系统在启动时可能无法检测到持久化时钟,
* 但如果在这里成功读取到了一个非零时间,就确信它存在。
*/
if (timekeeping_suspend_time.tv_sec || timekeeping_suspend_time.tv_nsec)
persistent_clock_exists = true;

suspend_timing_needed = true;

// 步骤2: 进入临界区。
raw_spin_lock_irqsave(&tk_core.lock, flags);
timekeeping_forward_now(tks); // 确保内部时间与硬件时钟同步到最新。
timekeeping_suspended = 1; // 设置挂起标志。

/*
* 步骤3: 通知当前的时钟源开始记录suspend时间(如果它支持的话)。
* 保存下当前硬件时钟的计数值。
*/
curr_clock = tks->tkr_mono.clock;
cycle_now = tks->tkr_mono.cycle_last;
clocksource_start_suspend_timing(curr_clock, cycle_now);

// 步骤4: 如果存在持久化时钟,则执行防漂移算法。
if (persistent_clock_exists) {
// 计算当前系统时间(tk_xtime)与RTC时间之间的差值。
delta = timespec64_sub(tk_xtime(tks), timekeeping_suspend_time);
// 计算本次差值与上次差值的变化量。
delta_delta = timespec64_sub(delta, old_delta);
if (abs(delta_delta.tv_sec) >= 2) {
// 如果变化太大(超过2秒),认为发生了NTP校时等事件,重置基准。
old_delta = delta;
} else {
// 如果变化不大,认为是累积误差,通过微调RTC挂起时间来补偿。
timekeeping_suspend_time =
timespec64_add(timekeeping_suspend_time, delta_delta);
}
}

timekeeping_update_from_shadow(&tk_core, 0);
halt_fast_timekeeper(tks); // 停止快速时间更新路径。
raw_spin_unlock_irqrestore(&tk_core.lock, flags); // 退出临界区。

// 步骤5: 从硬件层面挂起时钟服务。
tick_suspend();
clocksource_suspend();
clockevents_suspend();

return 0;
}

syscore_ops 结构体与初始化函数

这部分是将上述功能注册到内核电源管理框架的“胶水”代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 定义一个syscore_ops结构体实例,将我们的函数与之关联。 */
static struct syscore_ops timekeeping_syscore_ops = {
.resume = timekeeping_resume, // 系统恢复时调用 timekeeping_resume
.suspend = timekeeping_suspend, // 系统挂起时调用 timekeeping_suspend
};

// 初始化函数,在内核启动时被调用。
static int __init timekeeping_init_ops(void)
{
// 将我们的操作集注册到全局的syscore_ops链表中。
register_syscore_ops(&timekeeping_syscore_ops);
return 0;
}
// 使用device_initcall宏,确保在设备初始化阶段执行此注册。
// 这保证了在系统尝试挂起之前,我们的回调函数已经就绪。
device_initcall(timekeeping_init_ops);