[toc]

kernel/time/tick-*.c 内核时钟滴答(The Kernel Tick) 驱动时间流逝与进程抢占

历史与背景

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

kernel/time/tick-*.c 目录下的文件集合构成了内核时钟滴答(Kernel Tick)的底层实现。这项技术是所有分时操作系统的心跳(Heartbeat),它的诞生是为了解决两个最根本的问题:

  1. 时间流逝的度量:内核需要一个机制来驱动内部的时间概念。没有一个周期性的“滴答”,像jiffies这样的计数器将无法更新,基于jiffies的传统定时器(timer_list)也将永远不会到期。
  2. 强制性进程抢占(Preemptive Multitasking):在一个协作式多任务系统中,一个进程会一直运行直到它自愿放弃CPU。这会导致一个死循环的进程就能锁死整个系统。为了实现抢占式多任务,内核需要一个周期性的、不可抗拒的事件来中断当前正在运行的进程,并给调度器一个机会去检查是否有其他进程应该运行。这个事件就是时钟滴答中断

简而言之,内核滴答是调度器得以强制执行时间片轮转内核传统时间得以向前流逝的基础驱动力。

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

内核滴答的实现方式经历了从简单到复杂的巨大演进,其核心驱动力是节能降低操作系统抖动(OS Jitter)

  • 固定周期性滴答(Periodic Tick):这是最原始、最简单的模型。一个硬件定时器被设置为以固定的HZ频率(例如每秒100、250或1000次)周期性地产生中断。这种模型的优点是实现简单、行为恒定。但其致命缺点是:即使系统完全空闲,CPU也会被这个中断毫无意义地、频繁地唤醒,这会阻止CPU进入深度睡眠状态,造成巨大的能源浪费。
  • 无滴答空闲(Tickless Idle / NO_HZ_IDLE:这是内核电源管理领域的一次革命。其核心思想是:当一个CPU核心即将进入空闲状态时,内核会取消周期性的滴答中断。取而代之,它会计算出下一个需要处理的定时事件(无论是timer_list还是hrtimer)的精确时间点,然后使用高精度定时器(hrtimer)来编程一个**一次性(one-shot)**的中断,恰好在那个时间点触发。这使得空闲的CPU可以深度睡眠数秒甚至更长时间,极大地节省了功耗。
  • 完全无滴答(Full Tickless / NO_HZ_FULL:这是NO_HZ的进一步演进。NO_HZ_IDLE只在CPU完全空闲时才停止滴答。但对于某些特定负载,如高性能计算(HPC)或硬实时任务,即使CPU上正忙于运行一个单一的用户空间进程,内核滴答本身也会构成一种不必要的干扰(OS Jitter)。NO_HZ_FULL允许在这种情况下也停止滴答,从而为该进程提供一个几乎“无干扰”的运行环境,将CPU完全让给应用程序。

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

内核滴答是内核时间子系统的绝对基础。其现代的、可配置的(周期性 vs. 无滴答)实现是所有现代Linux系统的标准配置。

  • 主流应用
    • NO_HZ_IDLE是几乎所有现代桌面、服务器和移动Linux系统的默认配置,是实现出色能效的关键。
    • NO_HZ_FULL则是一个更专业的选项,被用于延迟敏感的高性能计算、高频交易和硬实时系统中,以追求极致的低延迟和可预测性。

核心原理与设计

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

内核滴答的实现被分散在tick-common.c, tick-sched.c, tick-broadcast.c, tick-oneshot.c等文件中,其核心是围绕struct tick_sched对象展开的,并由高精度定时器(hrtimer)驱动。

  1. 滴答的“引擎”——tick_sched_timer

    • 在现代内核中,周期性滴答不再由一个独立的、低分辨率的硬件定时器驱动。取而代之的是,内核为每个CPU启动一个周期性的高精度定时器(hrtimer,名为tick_sched_timer
    • 这个hrtimer的回调函数是tick_sched_timer(),它就是滴答事件的核心处理函数
  2. 滴答事件处理 (tick_sched_timer() / tick_handle_periodic())

    • 当这个hrtimer到期时,它的回调函数会执行一系列关键的周期性任务:
      a. 更新jiffies:调用do_timer()来增加全局的jiffies计数。
      b. 运行传统定时器:检查并运行当前jiffies值对应的所有到期的timer_list定时器。
      c. 通知调度器:调用调度器scheduler_tick()函数。这是CFS实现时间片轮转的关键,scheduler_tick()会更新当前进程的vruntime,并检查是否需要设置TIF_NEED_RESCHED标志以触发抢占。
      d. 更新进程记账:更新当前进程所消耗的CPU时间统计(用户态/内核态)。
  3. 无滴答(Tickless)模式的实现

    • 进入空闲/隔离:当CPU进入空闲(NO_HZ_IDLE)或隔离(NO_HZ_FULL)状态时,内核会调用tick_stop_idle()tick_nohz_start_tick()等函数。这些函数的核心操作就是取消那个周期性的tick_sched_timer
    • 编程下一个事件:接着,内核会查询所有定时器子系统(hrtimer, timer_list),找出下一个最早需要触发的事件的时间点。
    • 然后,它会使用一个一次性(one-shot)的hrtimer,将硬件编程为在那个精确的时间点触发中断。
    • 退出:当CPU被唤醒时(无论是被那个一次性定时器还是其他外部中断唤醒),内核会调用tick_check_idle()tick_nohz_stop_tick()等函数,重新启动周期性的tick_sched_timer

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

  • 提供基础节拍:为所有依赖于周期性检查的传统内核子系统提供了驱动力。
  • 实现抢占:是调度器实现公平性的基础。
  • 节能:通过NO_HZ机制,在不牺牲功能的前提下,实现了巨大的功耗节省。
  • 降低抖动:通过NO_HZ_FULL,为性能敏感的应用提供了更稳定、可预测的运行环境。

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

  • 固定开销(周期性模式下):在传统的周期性模式下,无论系统负载如何,滴答中断都会持续产生固定的CPU开销。
  • 精度限制:滴答的频率HZ决定了jiffies的时间粒度,所有依赖它的子系统都无法实现比1/HZ秒更高的精度。
  • 复杂性(无滴答模式下)NO_HZ的实现逻辑,特别是NO_HZ_FULL,涉及到复杂的定时器管理、CPU间状态同步和记账,是内核中一个相当复杂的领域。

使用场景

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

内核滴答是一个内核内部的基础设施,用户或内核开发者不直接“选择”它,而是选择依赖于它的上层API。

  • 任何使用jiffiestimer_list的代码,都隐式地依赖于内核滴答来驱动它们。
  • 任何以SCHED_NORMAL(CFS)运行的进程,都依赖于内核滴答来保证其时间片能够被公平地轮转。

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

这个问题应该理解为“何时应该避免依赖滴答的行为?”

  • 高精度定时:任何需要亚毫秒级精度的场景,都不应该依赖jiffies或滴答周期,而应该直接使用**hrtimers**。
  • 高性能追踪:不应通过在循环中检查jiffies变化来进行性能测量,而应使用sched_clock()ktime

对比分析

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

特性 内核滴答 (Kernel Tick) hrtimer (高精度定时器) sched_clock() (调度器时钟)
核心用途 提供一个周期性的系统节拍,驱动低分辨率时间并触发抢占。 提供高精度的、一次性或周期性的未来事件调度 提供一个极低开销的、高精度的当前时间戳
角色关系 hrtimer的客户。现代滴答是通过一个周期性的hrtimer实现的。 是滴答的底层引擎。为滴答提供精确的触发能力。 与滴答互补。滴答是“事件”,sched_clock是“时间源”。
分辨率 (1/HZ秒,毫秒级)。 (纳秒级)。 (纳秒级)。
功能 触发一系列周期性任务。 调度一个特定的回调函数在未来执行。 读取当前时间。
开销 中等 (在周期性模式下是固定开aho销)。 较高 (比timer_list高)。 极低
适用场景 内核的“心跳”,驱动jiffies和CFS时间片。 nanosleep,实时应用,精确超时。 内核内部的性能测量和高精度记账。

kernel/time/tick-internal.h

tick_set_periodic_handler 设置周期性处理程序

1
2
3
4
5
6
7
8
9
10
11
12
/**
* tick_set_periodic_handler - 设置周期性处理程序
* @dev:时钟事件设备
* @broadcast:是否广播
*
* 设置周期性处理程序。
*
*/
static inline void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
dev->event_handler = tick_handle_periodic;
}

kernel/time/tick-common.c

tick_do_timer_cpu 存储负责调用 do_timer() 的 CPU 编号

  • tick_do_timer_cpu 是一个内核定时器核心的内部变量,用于存储负责调用 do_timer() 的 CPU 编号(CPU NR)。do_timer() 是一个与时间管理相关的函数,负责更新系统的时间信息。该变量的主要作用是协调多核系统中与时间管理相关的任务分配,避免资源竞争并支持动态的 CPU 状态变化
  1. 防止“惊群效应”:
    • 在多核系统中,如果没有协调机制,可能会有大量 CPU 同时尝试获取时间管理相关的锁(如时间保持锁 timekeeping lock),导致“惊群效应”(thundering herd problem)。
    • tick_do_timer_cpu 通过指定一个特定的 CPU 来负责 do_timer() 的调用,避免了多个 CPU 同时争抢锁的情况,从而提高了系统的效率
  2. 支持 NOHZ 空闲模式
    • 在 NOHZ(No HZ)模式下,当 CPU 进入空闲状态时,tick_do_timer_cpu 的值会被设置为 TICK_DO_TIMER_NONE,表示当前没有 CPU 负责时间管理任务
    • 这种设计允许下一个检查该变量的 CPU 自动接管时间管理任务,确保系统的时间保持功能不会中断
    • 这种机制还支持 CPU 热插拔(hotplug),即当某个 CPU 被移除或添加时,时间管理任务可以动态地分配给其他 CPU。
1
2
/* 表示在系统启动时,默认的时间管理任务由引导 CPU(boot CPU)负责 */
int tick_do_timer_cpu __read_mostly = TICK_DO_TIMER_BOOT;

clockevents_program_min_delta 将时钟事件设备设置为最小延迟

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
/**
* clockevents_program_min_delta - 将时钟事件设备设置为最小延迟。
* @dev:设备到程序
*
* 成功时返回 0,重试循环失败时返回 -ETIME。
*/
static int clockevents_program_min_delta(struct clock_event_device *dev)
{
unsigned long long clc;
int64_t delta = 0;
int i;

for (i = 0; i < 10; i++) {
delta += dev->min_delta_ns;
dev->next_event = ktime_add_ns(ktime_get(), delta);

if (clockevent_state_shutdown(dev))
return 0;

dev->retries++;
clc = ((unsigned long long) delta * dev->mult) >> dev->shift;
if (dev->set_next_event((unsigned long) clc, dev) == 0)
return 0;
}
return -ETIME;
}

tick_setup_periodic 设置为周期时钟

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
/*
* 为周期性滴答声设置设备
*/
void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
/* //在非广播模式下设置周期性处理程序
static inline void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
dev->event_handler = tick_handle_periodic;
} */
tick_set_periodic_handler(dev, broadcast);

/* 调用 tick_device_is_functional 检查设备是否处于功能正常的状态。
* 如果设备不可用,直接返回,不进行进一步配置。
*/
if (!tick_device_is_functional(dev))
return;

/* 如果设备支持周期模式(CLOCK_EVT_FEAT_PERIODIC)且当前未处于广播单次触发模式 */
if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) &&
!tick_broadcast_oneshot_active()) {
/* 将设备的状态切换为周期模式
在这种模式下,设备会自动生成周期性中断,无需进一步手动编程*/
clockevents_switch_state(dev, CLOCK_EVT_STATE_PERIODIC);
} else {
/* 配置为单次触发模式 */
unsigned int seq;
ktime_t next;
/* 使用序列计数器(jiffies_seq)读取 tick_next_period 的值,确保读取操作的一致性 */
do {
seq = read_seqcount_begin(&jiffies_seq);
next = tick_next_period;
} while (read_seqcount_retry(&jiffies_seq, seq));
/* 切换设备状态为单次触发模式: */
clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);

for (;;) {
/* 为设备设置下一个触发时间点。如果编程成功,退出循环*/
if (!clockevents_program_event(dev, next, false))
return;
/* 使用 ktime_add_ns 将当前触发时间点 next 增加一个滴答周期(TICK_NSEC),为下一个事件计算新的触发时间。 */
next = ktime_add_ns(next, TICK_NSEC);
}
}
}

检查新设备是否比 curdev 更合适

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
static bool tick_check_percpu(struct clock_event_device *curdev,
struct clock_event_device *newdev, int cpu)
{
if (!cpumask_test_cpu(cpu, newdev->cpumask))
return false;
if (cpumask_equal(newdev->cpumask, cpumask_of(cpu)))
return true;
/* Check if irq affinity can be set */
if (newdev->irq >= 0 && !irq_can_set_affinity(newdev->irq))
return false;
/* Prefer an existing cpu local device */
if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))
return false;
return true;
}


static bool tick_check_preferred(struct clock_event_device *curdev,
struct clock_event_device *newdev)
{
/* 更喜欢一击即发的设备e */
if (!(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) {
if (curdev && (curdev->features & CLOCK_EVT_FEAT_ONESHOT))
return false;
if (tick_oneshot_mode_active())
return false;
}

/*
* Use the higher rated one, but prefer a CPU local device with a lower
* rating than a non-CPU local device
*/
return !curdev ||
newdev->rating > curdev->rating ||
!cpumask_equal(curdev->cpumask, newdev->cpumask);
}

/*
* 检查新设备是否比 curdev 更合适。curdev 可以是 NULL !
*/
bool tick_check_replacement(struct clock_event_device *curdev,
struct clock_event_device *newdev)
{
if (!tick_check_percpu(curdev, newdev, smp_processor_id()))
return false;

return tick_check_preferred(curdev, newdev);
}

tick_setup_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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
* Setup the tick device
*/
static void tick_setup_device(struct tick_device *td,
struct clock_event_device *newdev, int cpu,
const struct cpumask *cpumask)
{
void (*handler)(struct clock_event_device *) = NULL;
ktime_t next_event = 0;

/*
* 首次设置设备 ?
*/
if (!td->evtdev) {
/*
* 如果没有 cpu 进行do_timer更新,请将其分配给此 cpu:
*/
if (READ_ONCE(tick_do_timer_cpu) == TICK_DO_TIMER_BOOT) {
WRITE_ONCE(tick_do_timer_cpu, cpu);
/* 获取当前时间并设置 */
tick_next_period = ktime_get();
}

/*
* 首先以周期模式启动。
*/
td->mode = TICKDEV_MODE_PERIODIC;
} else {
handler = td->evtdev->event_handler;
next_event = td->evtdev->next_event;
td->evtdev->event_handler = clockevents_handle_noop;
}

td->evtdev = newdev;

/*
*更新设备和中断亲和性
*/
if (!cpumask_equal(newdev->cpumask, cpumask))
/* 将中断绑定到当前 CPU */
irq_set_affinity(newdev->irq, cpumask);

/*
* 当全局广播处于活动状态时,检查当前设备是否注册为广播模式的占位符。
* 这允许我们以通用方式处理这个 x86 错误特性。
* 当我们保持此 CPU 的当前活动广播状态时,此函数也会返回 !=0。
*/

/* 检查新设备是否被注册为广播模式的占位符。如果是,则直接返回 */
if (tick_device_uses_broadcast(newdev, cpu))
return;

/* 根据设备模式(周期模式或单次触发模式),调用相应的设置函数 */
if (td->mode == TICKDEV_MODE_PERIODIC)
tick_setup_periodic(newdev, 0);
else
tick_setup_oneshot(newdev, handler, next_event);
}

tick_check_new_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
/*
* 检查是否应使用新注册的设备。在保持 clockevents_lock 并禁用中断的情况下调用。
*/
void tick_check_new_device(struct clock_event_device *newdev)
{
struct clock_event_device *curdev;
struct tick_device *td;
int cpu;

cpu = smp_processor_id();
td = &per_cpu(tick_cpu_device, cpu);
curdev = td->evtdev;

/* 检查设备替换条件 */
if (!tick_check_replacement(curdev, newdev))
goto out_bc;

if (!try_module_get(newdev->owner))
return;

/*
* 用 newdevice 替换最终存在的设备。
* 如果当前设备是广播设备,请不要将其返回给 clockevents 层!
*/
if (tick_is_broadcast_device(curdev)) {
clockevents_shutdown(curdev);
curdev = NULL;
}
clockevents_exchange_device(curdev, newdev);
tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
tick_oneshot_notify();
return;

out_bc:
/*
* 新设备可以用作广播设备吗?
*/
tick_install_broadcast_device(newdev, cpu);
}

tick_periodic 处理周期性时钟事件

  • tick_periodic是Linux内核中周期性时钟节拍(periodic tick)的核心处理函数。在配置为传统tick模式的系统上,这个函数在每一次硬件定时器中断发生时,都会被tick_handle_periodic回调函数调用。
    它的核心作用是:推进系统的jiffies计数,并触发所有与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
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
/*
* 下一次滴答事件:跟踪滴答时间。
* 它由处理滴答事件的CPU更新,并且受到 jiffies_lock 的保护。
* 对于它来说,不需要在写操作时持有 jiffies 的 seqcount。
*/
ktime_t tick_next_period;

/*
* 这是一个静态函数,是周期性tick的核心处理逻辑。
* 它在每个tick中断中被tick_handle_periodic调用。
* @cpu: 当前正在执行此中断的CPU的编号。
*/
static void tick_periodic(int cpu)
{
/*
* 检查当前CPU是否是被指定负责更新全局jiffies的那个CPU。
* tick_do_timer_cpu是一个全局变量,在启动时被设为某个固定的CPU。
* READ_ONCE用于确保原子地读取这个可能被并发访问的变量。
*/
if (READ_ONCE(tick_do_timer_cpu) == cpu) {
/*
* 进入jiffies更新的临界区。
* 获取专门保护jiffies_64的自旋锁。
*/
raw_spin_lock(&jiffies_lock);
/*
* 开始一次序列计数器的写操作。它会使jiffies_seq变为奇数,
* 以通知所有乐观的读者,数据正在被修改。
*/
write_seqcount_begin(&jiffies_seq);

/* 注释:追踪下一个tick事件。*/
/* 将下一个周期性tick的预期时间戳向前推进一个tick周期。*/
tick_next_period = ktime_add_ns(tick_next_period, TICK_NSEC);

/*
* 调用do_timer宏,它会将全局的jiffies_64计数器加1。
* 这是jiffies增长的直接执行点。
*/
do_timer(1);
/* 结束序列计数器的写操作,将计数器值加1使其变回偶数。*/
write_seqcount_end(&jiffies_seq);
/* 释放自旋锁。*/
raw_spin_unlock(&jiffies_lock);
/* 调用update_wall_time,根据流逝的tick,更新系统的墙上时间。*/
update_wall_time();
}

/*
* 以下是每个CPU在每次tick时都必须执行的本地操作。
*/
/*
* 调用update_process_times,更新当前CPU上正在运行的任务的时间统计。
* user_mode(get_irq_regs())会判断中断发生时CPU是处于用户态还是内核态,
* 以便将时间正确地记在utime或stime上。
* 此函数还会减少任务的时间片。
*/
update_process_times(user_mode(get_irq_regs()));
/*
* 调用profile_tick,如果内核开启了性能剖析,此函数会进行采样。
*/
// profile_tick(CPU_PROFILING);
}

tick_handle_periodic 处理周期性时钟事件

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
/*
* Event handler for periodic ticks
*/
void tick_handle_periodic(struct clock_event_device *dev)
{
int cpu = smp_processor_id(); // 获取当前 CPU ID
ktime_t next = dev->next_event; // 获取下一个时钟事件的时间

tick_periodic(cpu); // 处理当前时钟中断

// 检查是否启用了 TICK_ONESHOT 配置并且事件处理函数是否改变
if (IS_ENABLED(CONFIG_TICK_ONESHOT) && dev->event_handler != tick_handle_periodic)
return;

// 检查时钟事件设备是否处于单次模式
if (!clockevent_state_oneshot(dev))
return;

for (;;) {
// 设置下一个周期事件
next = ktime_add_ns(next, TICK_NSEC);

// 尝试编程时钟事件设备
if (!clockevents_program_event(dev, next, false))
return;

// 检查时间保持有效性,确保使用真实的硬件时钟源
if (timekeeping_valid_for_hres())
tick_periodic(cpu);
}
}

kernel/time/tick-oneshot.c

tick_oneshot_mode_active - 检查系统是否处于 OneShot 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* tick_oneshot_mode_active - 检查系统是否处于 OneShot 模式
*
* 启用 nohz 或 highres 时返回 1。否则为 0。
*/
int tick_oneshot_mode_active(void)
{
unsigned long flags;
int ret;

local_irq_save(flags);
ret = __this_cpu_read(tick_cpu_device.mode) == TICKDEV_MODE_ONESHOT;
local_irq_restore(flags);

return ret;
}

tick_setup_oneshot 将事件设备设置为 oneshot 模式(HRES 或 NOHZ)

1
2
3
4
5
6
7
8
9
10
11
/**
* tick_setup_oneshot - 将事件设备设置为 oneshot 模式(HRES 或 NOHZ)
*/
void tick_setup_oneshot(struct clock_event_device *newdev,
void (*handler)(struct clock_event_device *),
ktime_t next_event)
{
newdev->event_handler = handler;
clockevents_switch_state(newdev, CLOCK_EVT_STATE_ONESHOT);
clockevents_program_event(newdev, next_event, true);
}

tick_program_event

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
/**
* tick_program_event - 为下一个事件编程 CPU 本地计时器设备
*/
int tick_program_event(ktime_t expires, int force)
{
struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);

if (unlikely(expires == KTIME_MAX)) {
/*
* 我们不再需要 clock event 设备,停止它。
*/
clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT_STOPPED);
dev->next_event = KTIME_MAX;
return 0;
}

if (unlikely(clockevent_state_oneshot_stopped(dev))) {
/*
* 我们再次需要 clock 事件,在使用它之前在 ONESHOT 模式下配置它。
*/
clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);
}

return clockevents_program_event(dev, expires, force);
}

kernel/time/tick-sched.c

tick_nohz_start_idle 开始空闲状态

1
2
3
4
5
6
7
8
9
static void tick_nohz_start_idle(struct tick_sched *ts)
{
write_seqcount_begin(&ts->idle_sleeptime_seq);
ts->idle_entrytime = ktime_get();
tick_sched_flag_set(ts, TS_FLAG_IDLE_ACTIVE);
write_seqcount_end(&ts->idle_sleeptime_seq);

// sched_clock_idle_sleep_event();
}

tick_nohz_idle_enter 准备在当前 CPU 上进入空闲状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* tick_nohz_idle_enter - 准备在当前 CPU 上进入空闲状态
*
* 当我们开始空闲循环时调用。
*/
void tick_nohz_idle_enter(void)
{
struct tick_sched *ts;

lockdep_assert_irqs_enabled();

local_irq_disable();

ts = this_cpu_ptr(&tick_cpu_sched);

WARN_ON_ONCE(ts->timer_expires_base);

tick_sched_flag_set(ts, TS_FLAG_INIDLE);
tick_nohz_start_idle(ts);

local_irq_enable();
}

tick_nohz_stop_idle 停止空闲状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void tick_nohz_stop_idle(struct tick_sched *ts, ktime_t now)
{
ktime_t delta;

if (WARN_ON_ONCE(!tick_sched_flag_test(ts, TS_FLAG_IDLE_ACTIVE)))
return;

delta = ktime_sub(now, ts->idle_entrytime);

write_seqcount_begin(&ts->idle_sleeptime_seq);
if (nr_iowait_cpu(smp_processor_id()) > 0)
ts->iowait_sleeptime = ktime_add(ts->iowait_sleeptime, delta);
else
ts->idle_sleeptime = ktime_add(ts->idle_sleeptime, delta);

ts->idle_entrytime = now;
tick_sched_flag_clear(ts, TS_FLAG_IDLE_ACTIVE);
write_seqcount_end(&ts->idle_sleeptime_seq);

// sched_clock_idle_wakeup_event();
}

tick_nohz_idle_exit 在空闲任务退出时更新刻度

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
/**
* tick_nohz_idle_exit - 在空闲任务退出时更新刻度
*
* 当空闲任务退出时,根据
* 以下情况:
*
* 1) 如果 CPU 未处于 nohz_full 模式(大多数情况下),则
* 重新启动 Tick。
*
* 2) 如果 CPU 处于 nohz_full 模式(极端情况):
* 2.1) 如果 tick 可以保持停止(无 tick 依赖性)
* 然后重新评估下一个即时报价并尝试保持停止状态
* 尽可能长。
* 2.2) 如果 tick 有依赖关系,请重新启动 tick。
*
*/
void tick_nohz_idle_exit(void)
{
struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
bool idle_active, tick_stopped;
ktime_t now;

local_irq_disable();

WARN_ON_ONCE(!tick_sched_flag_test(ts, TS_FLAG_INIDLE));
WARN_ON_ONCE(ts->timer_expires_base);

tick_sched_flag_clear(ts, TS_FLAG_INIDLE);
idle_active = tick_sched_flag_test(ts, TS_FLAG_IDLE_ACTIVE);
tick_stopped = tick_sched_flag_test(ts, TS_FLAG_STOPPED);

if (idle_active || tick_stopped)
now = ktime_get();

if (idle_active)
tick_nohz_stop_idle(ts, now);

if (tick_stopped)
tick_nohz_idle_update_tick(ts, now);

local_irq_enable();
}

can_stop_idle_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
static bool can_stop_idle_tick(int cpu, struct tick_sched *ts)
{
WARN_ON_ONCE(cpu_is_offline(cpu));

if (unlikely(!tick_sched_flag_test(ts, TS_FLAG_NOHZ)))
return false;

if (need_resched())
return false;

if (unlikely(report_idle_softirq()))
return false;

if (tick_nohz_full_enabled()) {
int tick_cpu = READ_ONCE(tick_do_timer_cpu);

/*
* Keep the tick alive to guarantee timekeeping progression
* if there are full dynticks CPUs around
*/
if (tick_cpu == cpu)
return false;

/* Should not happen for nohz-full */
if (WARN_ON_ONCE(tick_cpu == TICK_DO_TIMER_NONE))
return false;
}

return true;
}

tick_nohz_idle_stop_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
31
32
33
34
35
36
37
38
39
40
/**
* tick_nohz_idle_stop_tick - 停止空闲任务的空闲刻度
*
* 当下一个事件距离未来超过一个即时报价时,停止空闲即时报价
*/
void tick_nohz_idle_stop_tick(void)
{
struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
int cpu = smp_processor_id();
ktime_t expires;

/*
* 如果 tick_nohz_get_sleep_length() 运行 tick_nohz_next_event(),则
* Tick 计时器到期时间已知。
*/
if (ts->timer_expires_base)
expires = ts->timer_expires;
else if (can_stop_idle_tick(cpu, ts))
expires = tick_nohz_next_event(ts, cpu);
else
return;

ts->idle_calls++;

if (expires > 0LL) {
int was_stopped = tick_sched_flag_test(ts, TS_FLAG_STOPPED);

tick_nohz_stop_tick(ts, cpu);

ts->idle_sleeps++;
ts->idle_expires = expires;

if (!was_stopped && tick_sched_flag_test(ts, TS_FLAG_STOPPED)) {
ts->idle_jiffies = ts->last_jiffies;
nohz_balance_enter_idle(cpu);
}
} else {
tick_nohz_retain_tick(ts);
}
}

tick_check_oneshot_change 检查是否需要重新编程单次刻度

  • tick_check_oneshot_change是一个在内核中被周期性调用的函数(如您提供的hrtimer_run_queues中),用于检查系统条件是否已经满足,可以从传统的周期性时钟节拍(periodic tick)模式,切换到更先进的、基于单次触发(one-shot)的模式。
    它的核心作用是:在一个安全的检查点,判断是否可以进行时钟模式的“升级”,如果可以,则返回一个信号或直接触发切换。
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
/*
* 检查是否发生了可以使one-shot模式成为可能的改变。
*
* 由hrtimer软中断(由timer软中断驱动)周期性地调用。
* 'allow_nohz'参数表示我们可以切换到低分辨率的NOHZ模式,因为高分辨率
* 定时器被禁用了(无论是编译时还是运行时)。
* 在中断禁用的情况下调用。
*
* 返回值:如果可以切换到高分辨率模式,返回1;否则返回0。
*/
int tick_check_oneshot_change(int allow_nohz)
{
/* ts: 指向当前CPU的tick_sched结构体,包含了所有与tick调度相关的状态。*/
struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);

/*
* 使用test_and_clear_bit原子地检查并清除check_clocks标志的第0位。
* 如果该位原本就是0,说明没有时钟源变更事件发生,无需检查,直接返回0。
* 这是一个高效的快速路径。
*/
if (!test_and_clear_bit(0, &ts->check_clocks))
return 0;

/*
* 使用tick_sched_flag_test检查当前CPU是否已经处于NOHZ模式。
* 如果是,则无需再次切换,返回0。
*/
if (tick_sched_flag_test(ts, TS_FLAG_NOHZ))
return 0;

/*
* 检查当前的时间维持(timekeeping)系统是否对高分辨率(hres)有效,
* 以及当前注册的时钟事件设备是否支持one-shot模式。
*/
if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
/* 如果任一条件不满足,则无法切换,返回0。*/
return 0;

/*
* 如果调用者不允许直接切换到NOHZ模式。
* (例如,hrtimer_run_queues调用时,因为高精度定时器还未禁用)
*/
if (!allow_nohz)
/* 返回1,通知调用者:“条件已满足,你可以进行切换到高分辨率模式的操作了”。*/
return 1;

/*
* 如果调用者允许,则直接在这里调用tick_nohz_switch_to_nohz(),
* 将当前CPU的时钟模式切换为NOHZ(tickless)。
*/
tick_nohz_switch_to_nohz();
/* 在这种情况下,切换已由本函数完成,返回0。*/
return 0;
}