[toc]

kernel/time/clocksource.c 时钟源管理(Clocksource Management) 内核时间的原始动力

历史与背景

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

这项技术是为了在Linux内核中创建一个统一的、可插拔的硬件时钟源抽象层。内核需要一个可靠的方式来读取时间的流逝,而不同的硬件平台提供了种类繁多的硬件定时器/计数器,例如x86上的TSC和HPET,ARM上的System Counter等。这些硬件在精度、稳定性、访问开销等方面都千差万别。

kernel/time/clocksource.c的诞生就是为了解决这个硬件异构性的问题:

  • 抽象硬件差异:它提供了一个标准的struct clocksource接口,将所有不同硬件定时器的具体实现(如如何读取其计数器)封装起来,为上层的timekeeping子系统提供一个统一的、与硬件无关的视图。
  • 选择最佳时钟源:系统中可能同时存在多个可用的时钟源。内核需要一种机制来评估它们各自的优劣(基于频率、分辨率、稳定性等),并从中自动选择一个最佳的来驱动整个系统的时间。
  • 保证可靠性:某些时钟源(特别是x86的TSC)虽然速度极快,但在一些老旧的或配置不当的硬件上可能不稳定(例如,频率会变,或者在多核之间不同步)。clocksource.c实现了一个看门狗(watchdog)机制,用于在运行时持续监控当前选定的时钟源,如果发现其行为异常,可以自动切换到一个更可靠的备用时钟源,从而保证系统时间的正确性。

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

clocksource.c是内核通用时间框架(Generic Time Framework)的核心组成部分,其发展与整个时间子系统的现代化同步。

  1. 框架的引入:在2.6.18内核左右,作为对时间子系统进行大规模重构的一部分,clocksource框架被正式引入。这标志着Linux从依赖于特定硬件(如PIT)和简单的jiffies计数,转向一个更加通用和高精度的模型。
  2. 评级(Rating)系统的建立:为了实现时钟源的自动选择,引入了一个评级系统。每个时钟源根据其特性被赋予一个分数,分数越高的越优先。例如,TSC通常有最高的评级,而HPET次之,ACPI PM Timer则更低。
  3. 时钟源看门狗(Clocksource Watchdog):为了解决TSC等高速时钟源的稳定性问题,clocksource_watchdog被引入。它会定期将当前选定的高精度时钟源与一个已知的稳定(但可能较慢)的时钟源进行比较。如果两者之间的偏差超过了某个阈值,看门狗就会认为当前时钟源不稳定,并触发一次重新选择。
  4. timekeeping的深度集成clocksource.c的职责被明确定义为“原始时间节拍的提供者”,而kernel/time/timekeeping.c则作为“消费者”,负责使用clocksource提供的节拍来计算和维护最终的墙上时间和单调时间。

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

clocksource.c是内核时间子系统中极其稳定和基础的部分。

  • 绝对核心:任何一个Linux系统,从嵌入式设备到大型服务器,其时间的精确流逝都始于clocksource.c所管理和选择的那个硬件时钟源。
  • 社区状态:其核心框架非常稳定。社区的活跃度主要体现在为新的CPU架构或SoC添加新的clocksource驱动,以及对看门狗算法和时钟源切换逻辑进行微调,以适应更复杂的硬件和虚拟化环境。

核心原理与设计

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

clocksource.c的核心是一个注册、评级、选择和监控的管理框架。

  1. 注册 (clocksource_register)

    • 在系统启动时,各种硬件定时器的驱动程序会初始化并填充一个struct clocksource结构体。
    • 这个结构体描述了硬件时钟源的所有属性,包括:
      • .name:名称,如”tsc”, “hpet”。
      • .rating:评级分数,越高越好。
      • .read:一个函数指针,指向读取硬件原始计数器的函数。这是最核心的部分。
      • .mask:一个位掩码,用于处理计数器的回绕(wrap-around)。
      • .mult.shift:预先计算好的乘数和移位数,用于将原始计数器读数快速转换为纳秒。
    • 驱动调用clocksource_register()将这个结构体添加到一个全局的链表中。
  2. 选择 (clocksource_select)

    • 当需要选择一个新的时钟源时(例如在启动时,或者当前时钟源失效时),clocksource_select()会被调用。
    • 它会遍历全局链表中的所有已注册时钟源,并简单地选择评级(.rating)最高的那个作为当前的主时钟源。
    • 选定的时钟源会被激活,并将其指针传递给timekeeping子系统。
  3. 使用

    • timekeeping子系统在需要更新时间时,会调用当前选定clocksource.read()函数来获取一个原始的、单调递增的计数值。timekeeping随后会负责将这个计数值转换为实际的时间。
  4. 监控 (clocksource_watchdog)

    • 一个内核定时器会定期触发clocksource_watchdog函数。
    • 该函数会同时读取当前选定的时钟源(如TSC)和另一个作为“校验标准”的备用时钟源(如HPET)。
    • 它会比较在一段时间内两者计数的增量。如果增量比例严重偏离预期,说明当前时钟源可能不稳定(例如,CPU频率发生了意外变化)。
    • 如果发现不稳定,看门狗会将该时钟源的评级大幅降低,并调用clocksource_select()来切换到一个更可靠的时钟源。

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

  • 硬件解耦:完美地将时间保持逻辑与具体的硬件实现分离开来。
  • 自动化和最优选择:能够自动为当前系统选择性能最佳的可用时钟源。
  • 健壮性和自愈能力:通过看门狗机制,系统能够从某些类型的硬件时钟故障中自动恢复,而无需人工干预。

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

  • 对硬件的根本依赖:软件框架再好,也无法凭空创造时间。如果一个系统上所有可用的硬件时钟源都有问题,那么系统时间仍然会不准确。
  • 看门狗的开销:看门狗的定期检查会带来轻微的性能开销,并且在极少数情况下,如果校验时钟源本身出现问题,可能会错误地将一个好的主时钟源标记为不稳定。
  • 作为内部框架:它是一个纯粹的内核内部机制,用户空间程序不直接与其交互。

使用场景

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

它是Linux内核中管理硬件时间源的唯一且标准的解决方案。任何需要在内核中注册一个硬件计数器作为潜在系统时间驱动者的场景,都必须使用此框架。

  • CPU架构支持代码arch/x86/kernel/tsc.c使用此框架注册TSC。arch/arm64/kernel/time.c使用此框架注册ARM architected timer。
  • 平台设备驱动drivers/clocksource/hpet.c驱动程序使用此框架注册HPET。
  • 虚拟化drivers/clocksource/kvm_clock.c为在KVM虚拟机中运行的客户机提供一个高效的、半虚拟化的时钟源,它也通过此框架注册。

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

这个提法不适用。它是一个基础框架,而不是一个可选的应用。任何希望其硬件定时器能被内核考虑作为系统时间源的驱动,都必须使用它。

对比分析

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

clocksource vs. clock_event_device (时钟事件设备)

这是通用时间框架中最重要的两个概念,它们角色互补,经常被一起提及,但也最容易混淆。

特性 clocksource (时钟源) clock_event_device (时钟事件设备)
核心作用 回答“现在是什么时间?” 实现“在将来的某个时间点叫醒我。”
数据流向 硬件 -> 内核。内核读取一个自由运行的计数器。 内核 -> 硬件。内核编程一个硬件定时器,让它在未来的某个时刻触发一个中断。
工作模式 被动读取 (Passive Read) 主动事件触发 (Active Event Trigger)
核心功能 提供单调递增的时间刻度 提供定时中断
关键回调函数 .read() .set_next_event(), .set_mode()
典型用途 作为timekeeping的输入,驱动系统时间的更新。 作为高精度定时器(hrtimers)和系统节拍(tick)的后端。
硬件举例 TSC, HPET (作为计数器), ARM System Counter。 APIC Timer, HPET (作为比较器), ARM Arch Timer。

总结clocksource尺子,用来测量时间的流逝。clock_event_device闹钟,用来在特定时间点触发事件。一个精确的系统需要同时拥有这两者。

clocks_calc_mult_shift 计算 mult/shift 因子

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
/**
* clocks_calc_mult_shift - 计算 clocks 缩放数学的 mult/shift 因子
* @mult:指向存储乘数的变量的指针
* @shift:指向存储移位因子的变量的指针
* @from:源频率(以 Hz 为单位),表示需要转换的频率。
* @to:目标频率(以 Hz 为单位),表示转换后的频率
* @maxsec:保证的运行时间转换范围(以秒为单位)
*
* 该函数评估 clocksources 和 clockevents 的缩放数学运算的 shift/mult 对。
*
* @to 和 @from 是以 HZ 为单位的频率值。对于 clock sources @to 是 NSEC_PER_SEC == 1GHz ,
* @from 是计数器频率。对于 clock event @to 是计数器频率,@from 是 NSEC_PER_SEC。
*
* @maxsec conversion range 参数控制以秒为单位的时间范围,运行时转换必须使用计算的 mult 和 shift 因子覆盖该时间范围。
* 这保证了当转换的输入值与计算的倍数相乘时,不会发生 64 位溢出。较大的范围可能会通过选择较小的 mult 和 shift 因子来降低转换精度。
*/
void
clocks_calc_mult_shift(u32 *mult, u32 *shift, u32 from, u32 to, u32 maxsec)
{
u64 tmp;
u32 sft, sftacc= 32;

/*
* 计算限制转换范围的偏移因子:
* 这一步的目的是确定在给定的最大时间范围内,转换操作不会导致溢出
*/
tmp = ((u64)maxsec * from) >> 32;
while (tmp) {
tmp >>=1;
sftacc--;
}

/*
* 找到具有最佳精度且适合 maxsec 转换范围的转换移位/多对:
*/
for (sft = 32; sft > 0; sft--) {
tmp = (u64) to << sft;
tmp += from / 2;
do_div(tmp, from);
if ((tmp >> sftacc) == 0)
break;
}
*mult = tmp;
*shift = sft;
}
EXPORT_SYMBOL_GPL(clocks_calc_mult_shift);

clocksource_max_adjustment 计算最大调整值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* clocksource_max_adjustment- 返回最大调整金额
* @cs:指向 clocksource 的指针
*
*/
static u32 clocksource_max_adjustment(struct clocksource *cs)
{
u64 ret;
/*
* 限制时钟源的调整范围,确保调整幅度不会超过 11%(即 110,000 ppm,百万分比
*/
ret = (u64)cs->mult * 11;
do_div(ret,100);
return (u32)ret;
}

__clocksource_update_freq_scale 更新时钟源(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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* __clocksource_update_freq_scale - 使用具有新频率的更新 clocksource
* @cs:要注册的 clocksource
* @scale:比例因子乘以 freq 得到 clocksource hz
* @freq:clocksource 频率(每秒周期数)除以刻度
*
* 这只能从 clocksource->enable() 方法调用。
*
* 这个 * 不应该 * 直接调用!请使用
* __clocksource_update_freq_hz() 或 __clocksource_update_freq_khz() 辅助函数。
*/
void __clocksource_update_freq_scale(struct clocksource *cs, u32 scale, u32 freq)
{
u64 sec;

/*
* 默认的 clocksources 是 *special* 并自行定义它们的 mult/shift。
* 但是,您并不特殊,因此您应该指定一个 freq 值。
*/
if (freq) {
/*
* 计算最大运行时间:
* 如果提供了 freq 值,函数会计算时钟源在不发生溢出的情况下可以运行的最大秒数。
* 对于掩码大于 32 位的时钟源,会限制最大运行时间以确保转换精度。
* 默认情况下,最大运行时间被限制为 10 分钟(600 秒),以平衡精度和性能。
*/
sec = cs->mask;
do_div(sec, freq);
do_div(sec, scale);
if (!sec)
sec = 1;
else if (sec > 600 && cs->mask > UINT_MAX)
sec = 600;
/* 计算 mult 和 shift:这些参数用于将时钟周期转换为纳秒 */
clocks_calc_mult_shift(&cs->mult, &cs->shift, freq,
NSEC_PER_SEC / scale, sec * scale);
}

/*
* 设置不确定性边界:
* 如果未指定不确定性边界(uncertainty_margin),函数会根据 scale 和 freq 计算一个默认值。
* 默认值下限为 2 * WATCHDOG_MAX_SKEW(通常为 500ppm),以确保时钟源的精度。
* 如果 scale 或 freq 为零,则使用更保守的 WATCHDOG_THRESHOLD 值。
*/
if (scale && freq && !cs->uncertainty_margin) {
cs->uncertainty_margin = NSEC_PER_SEC / (scale * freq);
if (cs->uncertainty_margin < 2 * WATCHDOG_MAX_SKEW)
cs->uncertainty_margin = 2 * WATCHDOG_MAX_SKEW;
} else if (!cs->uncertainty_margin) {
cs->uncertainty_margin = WATCHDOG_THRESHOLD;
}
WARN_ON_ONCE(cs->uncertainty_margin < 2 * WATCHDOG_MAX_SKEW);

/*
* 防止溢出:
* 函数检查 mult 值是否可能在调整时发生溢出。
* 如果存在溢出风险,会通过右移 mult 和减少 shift 的方式进行调整,直到溢出风险消除
*/
cs->maxadj = clocksource_max_adjustment(cs);
while (freq && ((cs->mult + cs->maxadj < cs->mult)
|| (cs->mult - cs->maxadj > cs->mult))) {
cs->mult >>= 1;
cs->shift--;
cs->maxadj = clocksource_max_adjustment(cs);
}

/*
* 仅对 *特殊* 时钟源发出警告,这些时钟源自定义其 mult/shift 值并且不指定 freq。
*/
WARN_ONCE(cs->mult + cs->maxadj < cs->mult,
"timekeeping: Clocksource %s might overflow on 11%% adjustment\n",
cs->name);
/* 更新最大延迟: 调用 clocksource_update_max_deferment 更新时钟源的最大延迟参数。 */
clocksource_update_max_deferment(cs);

pr_info("%s: mask: 0x%llx max_cycles: 0x%llx, max_idle_ns: %lld ns\n",
cs->name, cs->mask, cs->max_cycles, cs->max_idle_ns);
}
EXPORT_SYMBOL_GPL(__clocksource_update_freq_scale);

clocksource_enqueue 将时钟源(clocksource)按照其评分(rating)的顺序插入到全局时钟源列表(clocksource_list)中

  • clocksource_enqueue 的设计目的是维护一个按评分排序的时钟源列表。
  • 这种排序机制使得内核可以快速选择评分最高的时钟源作为系统的主要时钟源,从而提高时间管理的效率和可靠性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 将按评级排序的 clocksource 排队
*/
static void clocksource_enqueue(struct clocksource *cs)
{
struct list_head *entry = &clocksource_list;
struct clocksource *tmp;

list_for_each_entry(tmp, &clocksource_list, list) {
/*跟踪位置,插入位置 */
/* 如果当前时钟源的评分小于待插入时钟源的评分,则找到插入位置,并退出循环 */
if (tmp->rating < cs->rating)
break;
entry = &tmp->list;
}
list_add(&cs->list, entry);
}

clocksource_find_best 从全局时钟源列表(clocksource_list)中选择最合适的时钟源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static struct clocksource *clocksource_find_best(bool oneshot, bool skipcur)
{
struct clocksource *cs;

if (!finished_booting || list_empty(&clocksource_list))
return NULL;

/*
* 我们选择评分最高的时钟源。如果启用了单次模式,
* 我们选择具有最佳评分的高分辨率有效时钟源。
*/
list_for_each_entry(cs, &clocksource_list, list) {
if (skipcur && cs == curr_clocksource)
continue;
if (oneshot && !(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES))
continue;
return cs;
}
return NULL;
}

clocksource_select 选择最佳时钟源

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
static void __clocksource_select(bool skipcur)
{
/* 单次模式通常用于高分辨率计时(high-resolution timing)或无滴答模式(NOHZ),这对时钟源的选择有特殊要求 */
bool oneshot = tick_oneshot_mode_active();
struct clocksource *best, *cs;

/* 找到最合适的 clocksource*/
/* 根据时钟源的评分(rating)和其他属性(如是否支持高分辨率)进行选择 */
best = clocksource_find_best(oneshot, skipcur);
if (!best)
return;

if (!strlen(override_name))
goto found;

/* 查找与 override_name 匹配的时钟源 */
list_for_each_entry(cs, &clocksource_list, list) {
if (skipcur && cs == curr_clocksource)
continue;
if (strcmp(cs->name, override_name) != 0)
continue;
/*
* Check to make sure we don't switch to a non-highres
* capable clocksource if the tick code is in oneshot
* mode (highres or nohz)
*/
if (!(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES) && oneshot) {
/* Override clocksource cannot be used. */
if (cs->flags & CLOCK_SOURCE_UNSTABLE) {
pr_warn("Override clocksource %s is unstable and not HRT compatible - cannot switch while in HRT/NOHZ mode\n",
cs->name);
override_name[0] = 0;
} else {
/*
* The override cannot be currently verified.
* Deferring to let the watchdog check.
*/
pr_info("Override clocksource %s is not currently HRT compatible - deferring\n",
cs->name);
}
} else
/* Override clocksource can be used. */
best = cs;
break;
}

found:
if (curr_clocksource != best && !timekeeping_notify(best)) {
pr_info("Switched to clocksource %s\n", best->name);
curr_clocksource = best;
}
}

/**
* clocksource_select - 选择最好的时钟源
*
* 私人活动。调用时必须保持 clocksource_mutex。
*
* 选择评分最高的 clocksource,或者由 userspace override 选择的 clocksource。
*/
static void clocksource_select(void)
{
__clocksource_select(false);
}

__clocksource_suspend_select 在系统进入挂起(suspend)状态时选择一个适合的时钟源(clocksource)作为挂起时钟源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void __clocksource_suspend_select(struct clocksource *cs)
{
/*
* 跳过非持续运行的时钟源
*/
if (!(cs->flags & CLOCK_SOURCE_SUSPEND_NONSTOP))
return;

/*
* The nonstop clocksource can be selected as the suspend clocksource to
* calculate the suspend time, so it should not supply suspend/resume
* interfaces to suspend the nonstop clocksource when system suspends.
*/
if (cs->suspend || cs->resume) {
pr_warn("Nonstop clocksource %s should not supply suspend/resume interfaces\n",
cs->name);
}

/* Pick the best rating. */
if (!suspend_clocksource || cs->rating > suspend_clocksource->rating)
suspend_clocksource = cs;
}

__clocksource_register_scale 注册新的时钟源(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
/**
* __clocksource_register_scale - 注册新的时钟源(clocksource)
* @cs:指向需要注册的时钟源结构体
* @scale:时钟源的频率(以每秒周期数为单位),需要除以比例因子。
* @freq:clocksource 频率(每秒周期数)除以刻度
*
* 如果注册成功,返回 0。
* 如果注册失败(例如由于资源冲突),返回 -EBUSY
*
* 注意事项:
* 该函数设计为内部使用,调用时需要确保时钟源结构体已正确初始化。
* 不应直接调用此函数,而是通过更高层的辅助函数(如 clocksource_register_hz)使用,以简化调用过程并避免错误。
*/
int __clocksource_register_scale(struct clocksource *cs, u32 scale, u32 freq)
{
unsigned long flags;

clocksource_arch_init(cs);
/* 验证时钟源 ID 和 VDSO 模式 */
if (WARN_ON_ONCE((unsigned int)cs->id >= CSID_MAX))
cs->id = CSID_GENERIC;
/* 验证时钟源的 VDSO(Virtual Dynamic Shared Object)模式是否有效 */
if (cs->vdso_clock_mode < 0 ||
cs->vdso_clock_mode >= VDSO_CLOCKMODE_MAX) {
pr_warn("clocksource %s registered with invalid VDSO mode %d. Disabling VDSO support.\n",
cs->name, cs->vdso_clock_mode);
/* 如果模式无效,会打印警告信息并禁用 VDSO 支持 */
cs->vdso_clock_mode = VDSO_CLOCKMODE_NONE;
}

/* 初始化 mult/shift 和 max_idle_ns */
__clocksource_update_freq_scale(cs, scale, freq);

/* 将时钟源添加到管理列表 */
mutex_lock(&clocksource_mutex);

clocksource_watchdog_lock(&flags);
/* 将时钟源添加到全局时钟源列表 */
clocksource_enqueue(cs);
clocksource_enqueue_watchdog(cs);
clocksource_watchdog_unlock(&flags);

/* 处理与挂起相关的时钟源选择 */
clocksource_select();
clocksource_select_watchdog(false);
__clocksource_suspend_select(cs);
mutex_unlock(&clocksource_mutex);
return 0;
}
EXPORT_SYMBOL_GPL(__clocksource_register_scale);

static inline int clocksource_register_hz(struct clocksource *cs, u32 hz)
{
return __clocksource_register_scale(cs, 1, hz);
}

clocks_calc_max_nsecs 计算最大纳秒数

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
/**
* clocks_calc_max_nsecs - 返回可转换的最大纳秒数
* @mult:周期到纳秒乘数
* @shift:周期到纳秒除数(2 的幂)
* @maxadj:最大调整值为 mult (~11%)
* @mask:非 64 位计数器的 2 补码减法的位掩码
* @max_cyc:潜在溢流前的最大循环值(不包括
* 任何安全边际)
*
* 注意:此函数包括 50% 的安全裕度,换句话说,我们返回硬件计数器在技术上可以覆盖的纳秒数的一半。
* 这样做是为了让我们有可能检测到由延迟计时器或不良硬件引起的问题,
* 这可能会导致时间间隔大于所使用的数学可以处理而不会溢出的时间间隔。
*/
u64 clocks_calc_max_nsecs(u32 mult, u32 shift, u32 maxadj, u64 mask, u64 *max_cyc)
{
u64 max_nsecs, max_cycles;

/*
* 计算最大周期数:
* 计算在不发生 64 位溢出的情况下,cyc2ns 函数可以处理的最大周期数
* 使用 do_div 函数将 ULLONG_MAX 除以 mult + maxadj,确保结果不会超出 64 位整数的范围
*/
max_cycles = ULLONG_MAX;
do_div(max_cycles, mult+maxadj);

/*
* 限制最大周期数
* 将计算出的最大周期数限制在 mask 范围内
* 减去 maxadj,以避免由于负调整值导致的过长延迟
*/
max_cycles = min(max_cycles, mask);
/* 转换为纳秒
* 这里使用 mult - maxadj 作为乘数,进一步确保时间计算的安全性
*/
max_nsecs = clocksource_cyc2ns(max_cycles, mult - maxadj, shift);

/* return the max_cycles value as well if requested */
if (max_cyc)
*max_cyc = max_cycles;

/* 应用安全裕度
* 为了增加安全性,函数将计算出的最大纳秒数减半(右移 1 位)。
* 这样可以检测由于硬件问题或延迟导致的异常时间值
*/
max_nsecs >>= 1;

return max_nsecs;
}

Clocksource最终选择与稳定化:系统启动后期的时钟源评定

本代码片段的功能是在内核启动流程的后期,对系统中所有已经注册的时钟源(clocksources)进行一次最终的检验和选择。其核心作用是将系统从启动阶段可能使用的临时或不稳定的时钟源,切换到经过验证的最佳可用时钟源上。这个过程对于确保内核时间戳(如ktime)的准确性、单调性和稳定性至关重要。

实现原理分析

该函数的实现逻辑是一个精心设计的状态转换过程,确保在系统进入稳定运行状态之前,其时间基准是可靠的。

  1. 执行时机 (fs_initcall):

    • 此函数通过fs_initcall宏注册,这决定了它在一个非常特定的时间点执行。在Linux的initcall顺序中,fs_initcall晚于subsys_initcall(大多数核心子系统已初始化,此时许多基础的时钟源驱动,如体系结构相关的定时器,已经注册完毕),但早于device_initcall(大多数挂载在具体总线上的设备驱动尚未开始探测)。
    • 选择这个时机是为了“收集”到足够多的候选时钟源,同时又避免因等待慢速设备(如I2C设备上的时钟芯片)而过久地使用一个次优的时钟源。
  2. 原子化操作 (mutex_lock):

    • 整个函数体被clocksource_mutex互斥锁保护。时钟源的选择和切换是内核中的一个核心且敏感的操作,必须保证是原子的,以防止在切换过程中,其他CPU或中断上下文看到不一致的时间状态。
  3. 状态标记 (finished_booting = 1):

    • 设置finished_booting标志位是一个明确的状态转换信号。它通知内核的时间保持(timekeeping)子系统,启动阶段的“时钟源动荡期”已经结束。在此之后,时钟源的选择和管理策略可能会变得更加严格。
  4. 质量检验 (__clocksource_watchdog_kthread):

    • 在进行选择之前,函数首先显式触发时钟源“看门狗”(watchdog)的检查。这个看门狗是一个内核线程,它会遍历所有已注册的时钟源,通过读取多次并比较其差值与流逝的时间,来验证它们的稳定性和单调性。
    • 任何被发现不稳定(如频率漂移过大)或非单调(时间倒流)的时钟源,其评分(rating)会被大幅降低或被标记为不可用。这是确保最终只在“好”的时钟源中进行选择的关键一步。
  5. 最优选择 (clocksource_select):

    • 在看门狗清除了不可靠的选项之后,clocksource_select函数被调用。它会遍历所有剩余的、有效的时钟源,并选择其中rating值最高的一个作为系统当前的主时钟源(curr_clocksource)。rating是一个综合评分,通常由时钟源的频率(分辨率)和稳定性标志决定。

代码分析

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
/*
* clocksource_done_booting - 在核心启动过程接近尾声时被调用
*
* 这是一个技巧,用于避免在启动期间发生大量的时钟源切换。
* 我们使用 fs_initcall 是因为我们希望它在 device_initcall 之前,
* 但在 subsys_initcall 之后开始。
*/
static int __init clocksource_done_booting(void)
{
// 获取全局时钟源互斥锁,以保证操作的原子性。
mutex_lock(&clocksource_mutex);
// 首先,临时将当前时钟源设置为一个已知的、安全的默认时钟。
curr_clocksource = clocksource_default_clock();
// 设置全局标志,表示内核启动阶段已经完成,可以进行最终的时钟源选择了。
finished_booting = 1;
/*
* 首先运行看门狗,以剔除不稳定的时钟源。
*/
// 显式调用时钟源看门狗的检查函数,它会验证所有已注册时钟源的可靠性。
__clocksource_watchdog_kthread();
// 调用选择函数,它会从所有通过看门狗检查的时钟源中,挑选出评分最高的一个。
clocksource_select();
// 释放互斥锁。
mutex_unlock(&clocksource_mutex);
return 0;
}
// 将 clocksource_done_booting 注册为文件系统级别的初始化调用。
fs_initcall(clocksource_done_booting);

内核时钟源的仪表盘与控制台:Sysfs接口

本代码片段的功能是为Linux内核的时钟源(clocksource)子系统创建一个sysfs接口。Sysfs是一个虚拟文件系统,它将内核的数据结构和属性以文件的形式暴露到用户空间。这段代码所创建的接口,就如同汽车的仪表盘和控制面板,允许用户在系统运行时:

  1. 查看当前仪表 (current_clocksource): 检查内核当前正在使用哪个硬件定时器作为最高精度的时间基准。
  2. 查看可用选项 (available_clocksource): 列出系统中所有已注册并被认为是可用的硬件定时器。
  3. 手动切换挡位 (current_clocksource): 强制内核切换到一个不同的、可用的硬件定时器。
  4. 禁用某个选项 (unbind_clocksource): 从时钟源列表中移除一个特定的定时器,使其不再可用。

这套接口位于/sys/devices/system/clocksource/clocksource0/目录下,是内核调试、性能分析和解决与时间相关问题的关键工具。

实现原理分析

这段代码的实现是Linux内核中创建sysfs接口的一个非常典型的范例,它遵循一套标准的“样板代码”模式。

  1. Show/Store 函数:

    • 对于每一个需要在sysfs中创建的文件,内核都需要提供一或两个回调函数。
    • _show 函数(例如 current_clocksource_show): 当用户cat这个文件时,此函数被调用。它的任务是读取相应的内核变量,将其格式化为字符串,然后放入用户提供的缓冲区buf中。
    • _store 函数(例如 current_clocksource_store): 当用户echo一个值到这个文件时,此函数被调用。它的任务是解析用户写入的字符串buf,并用这个值去修改相应的内核状态。
  2. 属性宏 (DEVICE_ATTR_...):

    • DEVICE_ATTR_RW(current_clocksource)这样的宏是连接show/store函数和sysfs文件系统的“胶水”。它会根据前缀current_clocksource自动找到current_clocksource_showcurrent_clocksource_store这两个函数,并将它们与一个名为dev_attr_current_clocksourcestruct device_attribute变量绑定。这个结构体就是sysfs中一个文件的内部表示。_RO表示只读(只有show),_WO表示只写(只有store)。
  3. 对象层次结构:

    • sysfs的组织是分层的。这段代码创建了这样一个层次:
      a. 总线/子系统 (clocksource_subsys): 定义了一个名为clocksource的子系统。这会在/sys/bus/下创建clocksource目录。
      b. 虚拟设备 (device_clocksource): 创建了一个名为clocksource0的虚拟设备。它不代表任何真实的硬件,只是一个“容器”,用来挂载我们的属性文件。这会在/sys/devices/system/clocksource/下创建clocksource0目录。
      c. 属性组 (clocksource_groups): 将所有由DEVICE_ATTR宏创建的属性(文件)集合到一个组里,然后挂载到上述的虚拟设备上。
  4. 初始化与注册:

    • init_clocksource_sysfs函数在内核启动时被调用(通过device_initcall)。它执行两个关键的注册操作:subsys_system_register注册子系统,device_register注册虚拟设备。正是这两步,使得上述的所有目录和文件在/sys文件系统中被真正创建出来。
  5. 同步:

    • 所有对全局时钟源列表(clocksource_list)或当前时钟源(curr_clocksource)的访问,都必须由clocksource_mutex互斥锁来保护。这是为了防止当一个用户正在读取可用列表时,另一个用户(或内核的其他部分)恰好在修改这个列表(例如,加载一个新驱动注册了一个新的clocksource),从而导致数据竞争或系统崩溃。

代码分析

current_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
// current_clocksource_show: 当'cat /sys/.../current_clocksource'时调用。
static ssize_t current_clocksource_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
ssize_t count = 0;

// 锁住互斥锁,保护对全局变量 curr_clocksource 的访问。
mutex_lock(&clocksource_mutex);
// 使用sysfs_emit(一个安全的snprintf变种)将当前时钟源的名字写入缓冲区。
count = sysfs_emit(buf, "%s\n", curr_clocksource->name);
mutex_unlock(&clocksource_mutex); // 解锁。

return count;
}

// sysfs_get_uname: 一个辅助函数,用于清理从sysfs传入的字符串。
ssize_t sysfs_get_uname(const char *buf, char *dst, size_t cnt)
{
size_t ret = cnt;

// 用户写入的字符串不保证以'\0'结尾,所以必须严格处理长度。
if (!cnt || cnt >= CS_NAME_LEN) // 长度无效或过长。
return -EINVAL;

// 用户'echo'时,字符串末尾通常会带一个换行符'\n',需要去掉。
if (buf[cnt-1] == '\n')
cnt--;
if (cnt > 0)
memcpy(dst, buf, cnt); // 拷贝有效内容。
dst[cnt] = 0; // 手动添加字符串结束符'\0'。
return ret; // 返回原始的计数值,这是sysfs store函数的惯例。
}

// current_clocksource_store: 当'echo "name" > .../current_clocksource'时调用。
static ssize_t current_clocksource_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
ssize_t ret;

mutex_lock(&clocksource_mutex); // 加锁。

// 使用辅助函数清理用户输入的字符串,并存入全局变量 override_name。
ret = sysfs_get_uname(buf, override_name, count);
if (ret >= 0)
// 调用时钟源核心函数,它会根据override_name去查找并切换时钟源。
clocksource_select();

mutex_unlock(&clocksource_mutex); // 解锁。

return ret;
}
// 将上述show/store函数与名为"current_clocksource"的sysfs文件绑定。
static DEVICE_ATTR_RW(current_clocksource);

unbind_clocksourceavailable_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
56
57
58
59
60
61
62
63
// unbind_clocksource_store: 'echo "name" > .../unbind_clocksource'
static ssize_t unbind_clocksource_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct clocksource *cs;
char name[CS_NAME_LEN];
ssize_t ret;

ret = sysfs_get_uname(buf, name, count); // 清理输入的时钟源名字。
if (ret < 0)
return ret;

ret = -ENODEV; // 默认错误码:没有这个设备。
mutex_lock(&clocksource_mutex);
// 遍历全局的clocksource_list链表。
list_for_each_entry(cs, &clocksource_list, list) {
if (strcmp(cs->name, name)) // 比较名字。
continue; // 不是这个,继续找。
ret = clocksource_unbind(cs); // 找到了,调用核心函数来解绑。
break; // 完成,退出循环。
}
mutex_unlock(&clocksource_mutex);

return ret ? ret : count; // 如果成功,返回count;否则返回错误码。
}
// 绑定为一个只写的sysfs文件。
static DEVICE_ATTR_WO(unbind_clocksource);

// available_clocksource_show: 'cat .../available_clocksource'
static ssize_t available_clocksource_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct clocksource *src;
ssize_t count = 0;

mutex_lock(&clocksource_mutex);
// 遍历全局的clocksource_list链表。
list_for_each_entry(src, &clocksource_list, list) {
/*
* 一个重要的过滤:如果系统处于高精度定时器模式(HRES)或
* 无动态节拍(NOHZ)模式,就只显示那些被标记为适合高精度
* 模式的时钟源。这可以防止用户切换到一个不兼容的、
* 可能导致系统时间行为异常的时钟源。
*/
if (!tick_oneshot_mode_active() ||
(src->flags & CLOCK_SOURCE_VALID_FOR_HRES))
// 将时钟源名字追加到缓冲区,名字之间用空格隔开。
count += snprintf(buf + count,
max((ssize_t)PAGE_SIZE - count, (ssize_t)0),
"%s ", src->name);
}
mutex_unlock(&clocksource_mutex);

// 在末尾添加一个换行符。
count += snprintf(buf + count,
max((ssize_t)PAGE_SIZE - count, (ssize_t)0), "\n");

return count;
}
// 绑定为一个只读的sysfs文件。
static DEVICE_ATTR_RO(available_clocksource);

Sysfs 注册的“样板代码”

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
// 创建一个属性指针数组,包含了上面所有由DEVICE_ATTR宏创建的变量。
static struct attribute *clocksource_attrs[] = {
&dev_attr_current_clocksource.attr,
&dev_attr_unbind_clocksource.attr,
&dev_attr_available_clocksource.attr,
NULL // 数组必须以NULL结尾。
};

// 使用宏将属性数组包装成一个属性组。
ATTRIBUTE_GROUPS(clocksource);

// 定义一个总线(子系统)类型。
static const struct bus_type clocksource_subsys = {
.name = "clocksource",
.dev_name = "clocksource",
};

// 定义一个虚拟设备。
static struct device device_clocksource = {
.id = 0,
.bus = &clocksource_subsys, // 将设备归属到clocksource子系统。
.groups = clocksource_groups, // 将属性组挂载到此设备上。
};

// 初始化函数,在内核启动时被调用。
static int __init init_clocksource_sysfs(void)
{
int error = subsys_system_register(&clocksource_subsys, NULL);

if (!error)
error = device_register(&device_clocksource);

return error;
}

// 确保在设备初始化阶段调用我们的初始化函数。
device_initcall(init_clocksource_sysfs);
#endif /* CONFIG_SYSFS */