[toc]

kernel/time/jiffies.c 内核心跳(Kernel Heartbeat) 低分辨率定时器的基础

历史与背景

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

jiffies 是Linux内核中最古老、最基础的时间测量机制。它的诞生是为了解决内核内部需要一个**简单、廉价、全局统一的“心跳”或“滴答(tick)”**来驱动各种基于时间的活动。

在操作系统内核中,大量活动都不是由外部事件触发,而是需要在一个相对的时间点后发生。例如:

  1. 超时(Timeouts):一个网络驱动发送了一个数据包,它需要在一个超时时间(如200毫秒)后检查是否收到了确认。
  2. 调度(Scheduling):一个SCHED_RR(轮转)策略的进程,其运行时间片用完后需要被抢占。
  3. 周期性任务(Periodic Tasks):某些内核任务需要周期性地运行。
  4. 简单的延迟(Delays):代码中需要一个短暂的、非精确的延迟。

jiffies 提供了一个极其简单的解决方案:它是一个在系统启动后不断递增的全局计数器。通过读取这个计数器的值,内核的任何部分都可以获得一个关于“时间流逝”的基本概念,并以此来安排未来的事件。

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

jiffies 的发展与内核定时器子系统的演进紧密相连。

  • HZ 常量与周期性中断jiffies 的核心是与一个名为HZ的编译时常量绑定的。HZ代表每秒钟硬件定时器中断发生的次数,也就是jiffies计数器每秒增加的数值。HZ的值经历过多次变化(如100, 250, 1000),反映了在定时器精度和中断开销之间的权衡。
  • 32位回绕(Wraparound)问题:在32位系统上,jiffies是一个unsigned long(32位)变量。它会以一个可预测的时间间隔(例如,在HZ=100时,大约497天后)溢出并从0重新开始。这是一个严重的问题,因为简单地比较jiffies的值来判断时间是否到期会出错。为了解决这个问题,内核引入了一组宏,如time_after(a, b)time_before(a, b)。这些宏使用特殊的有符号整数算术来正确处理回绕,是正确使用jiffies的关键。
  • jiffies_64的引入:为了从根本上解决回绕问题,内核引入了一个64位的全局变量jiffies_64。在64位系统上,jiffies就是jiffies_64的别名,其回绕周期长得在宇宙生命周期内都不会发生。在32位系统上,jiffies仍然是32位的(为了性能和兼容性),但可以通过get_jiffies_64()安全地读取完整的64位值。
  • 角色的转变:随着高精度定时器(High-Resolution Timers, hrtimers)的引入,jiffies的角色从内核唯一的定时器机制,转变为低分辨率、低开销的定时器基础。对于需要微秒级或更高精度的应用,现在都使用hrtimers
  • 无滴答内核(Tickless Kernel, NO_HZ:在空闲(idle)状态下,周期性的时钟中断会不必要地唤醒CPU,消耗电力。Tickless内核允许在CPU空闲时停止这个周期性中断。这对jiffies的更新机制产生了影响:当CPU从长时间的空闲中被唤醒时,jiffies的值需要被一次性地“追赶”上已经流逝的真实时间。

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

jiffies是内核中一个极其稳定和成熟的“遗留但基础”的组件。

  • 主流应用:尽管有了hrtimersjiffies仍然被广泛用于所有不需要高精度定时的场景,因为它非常廉价。主要应用包括:
    • 内核定时器轮(Timer Wheels)struct timer_list所代表的传统内核定时器,其超时值就是基于jiffies的。
    • 网络协议栈:大量的超时,如TCP的重传定时器,都是基于jiffies
    • 调度器SCHED_RR的时间片。
    • 驱动程序:许多硬件的超时逻辑(如等待设备响应)并不需要高精度。

核心原理与设计

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

jiffies的原理极其简单:

  1. 硬件定时器中断:系统中的一个硬件定时器被编程为以HZ赫兹的频率周期性地触发中断。
  2. 中断服务程序:每次这个中断发生时,其中断服务程序会调用tick_periodic(),最终会执行do_timer()函数。
  3. 全局变量递增:在do_timer()中,全局的64位变量jiffies_64的值会被原子地加一。
  4. jiffies的访问jiffies变量本身是一个unsigned long,它被定义为jiffies_64的低32位或直接就是jiffies_64(取决于系统位数)。内核代码通过直接读取这个变量来获取当前的“滴答数”。
  5. 回绕安全比较:为了安全地比较两个jiffies值(尤其是在32位系统上),必须使用time_after(a, b)time_before(a, b)宏。这些宏的实现巧妙地将unsigned long转换为long进行比较,利用有符号数的溢出行为来确保即使在jiffies回绕点附近,比较结果也是正确的。例如,time_after(a, b)大致等价于 (long)(b) - (long)(a) < 0

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

  • 低开销:读取一个全局变量几乎是零开销的。其更新开销被分摊在每次时钟中断中,也非常低。
  • 简单性:模型简单直观,易于理解和使用(只要记住使用比较宏)。
  • 全局可用性:在内核的任何地方(除了极少数早期启动阶段)都可以安全地访问jiffies
  • 单调性:它是一个单调递增的计数器(在回绕前),非常适合用来测量时间间隔。

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

  • 低分辨率:这是其最大的缺点。jiffies的精度受限于HZ。例如,在HZ=250时,其分辨率只有4毫秒。任何需要比1/HZ秒更精确的定时都不能使用jiffies
  • 32位回绕:在32位系统上,如果开发者忘记使用time_after/before宏,就会引入非常难以调试的、与时间相关的bug。
  • 与Tickless内核的交互:在Tickless内核的空闲期间,jiffies的值不会变化。依赖于在循环中观察jiffies值变化的“忙等待”代码在这种情况下会失效。

使用场景

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

jiffies低分辨率、毫秒级定时的首选解决方案。

  • 设置timer_list定时器:这是最经典的使用场景。timer->expires = jiffies + msecs_to_jiffies(100); // 设置一个100毫秒后的定时器。
  • 检查超时if (time_after(jiffies, start_jiffies + timeout)) { /* 超时了 */ }
  • 驱动中的轮询超时:一个驱动在等待硬件状态位时,不能永远等下去,通常会使用jiffies来设置一个超时上限。

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

  • 高精度定时:任何需要亚毫秒级(sub-millisecond)精度的场景,都必须使用**高精度定时器(hrtimers)**和ktime API。
  • 测量真实世界时间(Wall Time)jiffies只与系统启动后的时间有关,不涉及年月日。需要真实世界时间的场景应使用专门的时间管理API。
  • 长时间间隔测量:在32位系统上测量超过jiffies回绕周期的长时间间隔,应始终使用get_jiffies_64()

对比分析

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

特性 jiffies 高精度定时器 (hrtimers / ktime) 真实世界时间 (Wall Time)
核心用途 低分辨率的内核内部时间流逝测量。 高分辨率的内核内部时间流逝测量。 与现实世界同步的绝对时间
分辨率 ,由HZ决定(通常是1-10毫秒)。 ,可达纳秒级,受硬件限制。 ,可达纳秒级。
单调性 (在回绕前)。 (可通过NTP或date命令向后调整)。
开销 极低 较高。涉及更复杂的数据结构(红黑树)和硬件编程。 较高。通常需要从ktime转换。
适用场景 传统内核定时器、网络超时、调度器时间片。 亚毫秒级延迟、nanosleep、需要精确触发的事件。 日志时间戳、文件时间戳、需要与外部世界同步的事件。

include/linux/jiffies.h

  • __jiffy_arch_data:这个修饰符是一个架构特定的标记,用于在不同的架构上处理 jiffies 变量的特殊需求。它可能在不同的架构上有不同的定义,但在这里它主要起到标记作用。
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* 64 位值在 32 位系统上不是原子值 - 您不得读取它
* 而不对 jiffies_lock 中的序列号进行采样。
* get_jiffies_64() 将根据需要为您执行此作。
*
* jiffies 和 jiffies_64 对于 little-endian 系统位于同一地址
* 和 64 位 big-endian 系统。
* 在 32 位 big-endian 系统上,jiffies 是 jiffies_64 的低 32 位
*(即地址 @jiffies_64 + 4)。
* 参见 arch/ARCH/kernel/vmlinux.lds.S
*/
extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;

INITIAL_JIFFIES

  1. jiffies自减 300*HZ, 使得jiffies在5分钟后换行从0开始
1
2
3
4
/*
* 在启动 5 分钟后将 32 位 jiffies 值换行,以便 jiffies 换行错误更早出现。
*/
#define INITIAL_JIFFIES ((unsigned long)(unsigned int) (-300*HZ))

time_eq 定时器时间比较

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
/**
* time_after - returns true if the time a is after time b.
* @a: first comparable as unsigned long
* @b: second comparable as unsigned long
*
* Do this with "<0" and ">=0" to only test the sign of the result. A
* good compiler would generate better code (and a really good compiler
* wouldn't care). Gcc is currently neither.
*
* Return: %true is time a is after time b, otherwise %false.
*/
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((b) - (a)) < 0))
/**
* time_before - returns true if the time a is before time b.
* @a: first comparable as unsigned long
* @b: second comparable as unsigned long
*
* Return: %true is time a is before time b, otherwise %false.
*/
#define time_before(a,b) time_after(b,a)

/**
* time_after_eq - 如果时间 a 晚于或等于时间 b,则返回 true。
* @a:首先与 unsigned long 进行比较
* @b:第二个与 unsigned long 相当
*
* 返回: %true 是时间 a 晚于或等于时间 b,否则为 lse。
*/
#define time_after_eq(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((a) - (b)) >= 0))
/**
* time_before_eq - 如果时间 a 早于时间 b 或等于时间 b,则返回 true。
* @a:首先与 unsigned long 进行比较
* @b:第二个与 unsigned long 相当
*
* 返回: %true 是时间 a 早于时间 b 或等于时间 b,否则为 lse。
*/
#define time_before_eq(a,b) time_after_eq(b,a)

kernel/time/jiffies.c

Jiffies Clocksource: 内核的基础回退时钟源

此代码片段定义了一个基于内核全局变量 jiffies 的时钟源(clocksource)。jiffies 是一个自系统启动以来发生的定时器滴答(tick)的总数, 它的更新频率由内核配置的 HZ 值决定。这个时钟源是内核时间子系统中最基础、最不可靠但保证存在的一个。它的主要作用是在系统启动早期、或在更高精度的硬件时钟源不可用或发生故障时, 提供一个最低限度的时间基准, 确保系统时间能够继续前进。

  • 在单核无MMU的STM32H750平台上的原理与作用

在STM32H750这样的微控制器上, 拥有多个高精度的硬件定时器(例如通用定时器TIMx, 或ARM Cortex-M核心自带的SysTick定时器)。Linux内核的ARM移植版本会专门为这些硬件定时器提供驱动, 并将它们注册为高精度的时钟源。

  • 角色定位clocksource_jiffies 在STM32平台上的角色是 “备胎”或“启动时钟”。当内核的定时器驱动(例如clocksource-systick.c)被初始化时, 它会注册一个基于SysTick硬件定时器的时钟源。这个硬件时钟源的评级(rating)会远高于jiffies的评级(1)。内核的时间管理框架会自动选择评级最高的时钟源作为当前活动的时钟源。
  • 工作流程
    1. 系统启动初期, 在高精度定时器驱动加载前, clocksource_default_clock() 会提供 clocksource_jiffies 作为默认时钟源, 保证内核有基本的时间概念。
    2. 当STM32的SysTick驱动初始化成功后, 它会注册一个高评级的clocksource
    3. 内核发现了一个更好的时钟源, 就会自动切换过去, 使用SysTick来驱动系统时间。
    4. 此后, jiffies 时钟源虽然仍然存在, 但不再被用于主要的计时任务, 仅作为一种安全回退机制保留。

因此, 尽管这段代码在STM32内核中存在, 但jiffies时钟源的实际工作时间非常短暂, 很快就会被精确得多的片上硬件定时器所取代。

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
82
83
84
85
86
/*
* jiffies_read: clocksource 的 .read 回调函数.
* 它的作用是读取当前时钟源的计数值.
*
* @cs: 指向当前 clocksource 结构体的指针, 在此场景下即为 clocksource_jiffies.
* @return: 返回一个64位的无符号整数, 代表当前的 jiffies 值.
*/
static u64 jiffies_read(struct clocksource *cs)
{
/*
* 直接返回全局变量 jiffies 的值.
* jiffies 是一个自系统启动以来累计的定时器滴答数.
* 将其强制类型转换为 u64, 以满足 clocksource 框架对返回类型的要求.
*/
return (u64) jiffies;
}

/*
* 这段注释解释了 Jiffies 时钟源的特性和缺点:
* - 它是所有系统都应该能正常工作的、最低标准的时钟源.
* - 它的分辨率很粗糙, 等同于定时器中断的频率 HZ.
* - 它会因为定时器中断的丢失或错过而产生不准确性.
* - 硬件本身也无法保证能精确地按照 HZ 值产生滴答.
* - 不推荐在 "tick-less" (无滴答) 的系统中使用它.
*/
static struct clocksource clocksource_jiffies = {
/*
* .name: 时钟源的名称. 这个名字会出现在 /proc/timer_list 和 sysfs 中.
*/
.name = "jiffies",
/*
* .rating: 时钟源的评级, 用于内核选择最佳的时钟源.
* 评级为 1 是最低的有效评级. 这意味着任何其他合法的时钟源都会比它优先.
* 这明确了它作为最终回退选项的角色.
*/
.rating = 1, /* 最低的有效评级 */
/*
* .uncertainty_margin: 时钟源的不确定性边际, 单位是纳秒.
* 表示两次连续读取之间可能存在的最大误差. 32毫秒是一个非常大的值, 反映了其低精度.
*/
.uncertainty_margin = 32 * NSEC_PER_MSEC,
/*
* .read: 一个函数指针, 指向用于读取时钟计数值的函数.
* 这里它指向我们上面定义的 jiffies_read 函数.
*/
.read = jiffies_read,
/*
* .mask: 一个位掩码, 用于处理时钟计数器的回绕.
* CLOCKSOURCE_MASK(32) 表示这是一个32位的计数器, 会在 0xffffffff 处回绕.
*/
.mask = CLOCKSOURCE_MASK(32),
/*
* .mult: 一个乘数因子, 用于将时钟的"周期"(在这里是jiffies滴答)转换为纳秒.
* TICK_NSEC 是每个滴答代表的纳秒数 (即 1/HZ * 10^9).
* << JIFFIES_SHIFT 是为了在计算中保留精度而进行的左移位操作.
* 转换公式是: nanoseconds = (cycles * mult) >> shift.
*/
.mult = TICK_NSEC << JIFFIES_SHIFT, /* 细节见上文注释 */
/*
* .shift: 一个移位因子, 与 .mult 配合使用, 完成从周期到纳秒的转换.
*/
.shift = JIFFIES_SHIFT,
/*
* .max_cycles: 在两次时钟同步(clock sync)之间, 这个时钟源可以计数的最大周期数.
* 设定一个较小的值(10)可以强制内核更频繁地进行时间校准, 以弥补 jiffies 的不稳定性.
*/
.max_cycles = 10,
};

/*
* clocksource_default_clock: 提供一个默认的时钟源.
* 这个函数在内核启动早期, 当其他高精度时钟源还未初始化时被调用.
* __init 属性表示它仅在初始化阶段使用.
* __weak 属性是一个链接器指令, 意味着如果内核的其他部分定义了一个同名的、非弱(strong)的函数,
* 链接器将会使用那个强函数, 而忽略这个弱函数. 这允许特定架构(如ARM)提供一个比jiffies更好的默认时钟源来覆盖它.
*
* @return: 返回一个指向 clocksource 结构体的指针.
*/
struct clocksource * __init __weak clocksource_default_clock(void)
{
/*
* 返回指向 clocksource_jiffies 全局实例的指针.
* 这确保了系统在任何情况下都至少有一个可用的时钟源.
*/
return &clocksource_jiffies;
}

jiffies_seq jiffies_lock

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
/*
* __cacheline_aligned_in_smp: 一个宏,仅在SMP配置下生效。它会强制
* 下面的变量地址对齐到CPU的一级缓存行边界,
* 以避免缓存伪共享(False Sharing)问题,提升性能。
*
* DEFINE_RAW_SPINLOCK: 一个宏,用于静态地声明并初始化一个原始自旋锁。
*
* jiffies_lock: 这是被声明的锁的名称。它是一个全局的原始自旋锁,专门用于
* 保护对全局变量jiffies_64的写操作,确保在任何时刻只有一个
* CPU可以修改jiffies_64。
*/
__cacheline_aligned_in_smp DEFINE_RAW_SPINLOCK(jiffies_lock);
/*
* __cacheline_aligned_in_smp: 同样地,在SMP配置下将变量对齐到缓存行。
*
* seqcount_raw_spinlock_t: 一种特殊的序列计数器类型,它与一个原始自旋锁绑定。
*
* jiffies_seq: 这是被声明的序列计数器的名称。它与jiffies_lock配合,
* 为jiffies_64变量提供了一种高效的读写保护机制。
*/
__cacheline_aligned_in_smp seqcount_raw_spinlock_t jiffies_seq =
/*
* SEQCNT_RAW_SPINLOCK_ZERO: 一个静态初始化宏。
* 它将jiffies_seq变量的序列计数器初始化为0,并将其内部的
* 锁指针指向全局的jiffies_lock。
*/
SEQCNT_RAW_SPINLOCK_ZERO(jiffies_seq, &jiffies_lock);

init_jiffies_clocksource

1
2
3
4
5
6
static int __init init_jiffies_clocksource(void)
{
return __clocksource_register(&clocksource_jiffies);
}

core_initcall(init_jiffies_clocksource);