Linux 时间子系统 (2):NO_HZ

上一篇:Linux 时间子系统 (1):基础框架概述

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. NO_HZ 概述

简单来说,NO_HZ 就是在一些特定的情形下,减少调度 tick 中断次数,从而起到提高能效降低操作系统抖动的目的。降低操作系统抖动对于某些计算密集型高性能计算(HPC)应用以及实时应用至关重要。

Linux 管理调度时钟中断(也称为调度时钟滴答或简称为滴答)主要有种方式:

  1. 永不省略调度时钟滴答(CONFIG_HZ_PERIODIC=y 或旧内核中的 CONFIG_NO_HZ=n)。通常不应选择此选项。

  2. 空闲 CPU省略调度时钟滴答CONFIG_NO_HZ_IDLE=y 或 旧内核中 CONFIG_NO_HZ=y)。这是最常见的做法,应作为默认设置

  3. 同时省略空闲 CPU仅有一个可运行任务的 CPU 的调度时钟滴答(CONFIG_NO_HZ_FULL=y)。除非运行实时应用特定 HPC 工作负载,否则通常不建议启用此选项。

接下来将分别描述这三种情况,随后将探讨 RCU 的特定考量,接着将讨论测试事宜,最后将列出已知问题。

2.1 NO_HZ 的各种模式

2.1.1 永不省略调度时钟滴答

1990 年代及 2000 年代初期的极早期 Linux 版本无法省略调度时钟滴答。事实证明,在某些场景下这种传统方法依然适用,例如处理大量任务的重负载场景——这些任务以短暂突发方式占用 CPU,存在高频空闲期但空闲时长极短(数十至数百微秒)。对于此类工作负载,调度时钟中断通常仍会正常触发,因为每个 CPU 核心往往存在多个可运行任务。此时尝试关闭调度时钟中断不仅无效,反而会增加空闲状态切换及用户模式/内核模式执行转换的开销

可通过设置 CONFIG_HZ_PERIODIC=y(或旧内核中的 CONFIG_NO_HZ=n)选择此运行模式。

然而,若你正在运行轻量级工作负载存在较长的空闲期,未能禁用调度时钟中断将导致功耗过高。这种情况在电池供电设备上尤为严重,会导致电池寿命极度缩短。因此,若您正在处理轻量级工作负载,请务必阅读以下章节。

此外,若你正在处理实时工作负载具有短迭代周期的 HPC 工作负载调度时钟中断可能降低应用程序性能。若你的工作负载符合上述描述,请阅读以下两节内容。

2.1.2 省略空闲 CPU 的调度时钟滴答

若 CPU 处于空闲状态,向其发送调度时钟中断便毫无意义。毕竟,调度时钟中断的主要目的是迫使繁忙的 CPU 在多项任务间切换,而空闲的 CPU 本就没有需要调度的任务。

未接收调度时钟中断的空闲 CPU 被称为无时钟空闲(dyntick-idle)处于无时钟空闲(dyntick-idle)模式【无休眠模式】【运行无时钟】。本文后续将统一使用【无时钟空闲模式】这一表述。

CONFIG_NO_HZ_IDLE=y(类似旧内核的 CONFIG_NO_HZ=y)配置选项可使内核避免向空闲 CPU 生成调度时钟中断,这对电池供电设备和高度虚拟化的主机系统都至关重要。运行 CONFIG_HZ_PERIODIC=y 内核的电池供电设备会极快耗尽电量,其耗电速度至少是运行 CONFIG_NO_HZ_IDLE=y 内核的同类设备的 2-3 倍。运行 1500 个操作系统实例的主机可能发现,其一半 CPU 时间被不必要的调度时钟中断消耗。在这些场景下,避免向空闲 CPU 发送调度时钟中断具有强烈必要性。但需注意,动态时钟空闲(dyntick-idle) 模式并非没有代价:

  1. 它增加了进入和退出空闲循环路径上执行的指令数量。

  2. 在许多架构中,动态时钟调整空闲模式还会增加耗费资源的时钟重编程操作次数。

因此,具有严格实时响应要求的系统通常运行 CONFIG_HZ_PERIODIC=y 内核(或旧版内核的 CONFIG_NO_HZ=n),以避免空闲状态切换导致的延迟恶化。

此外,还存在一个名为 nohz= 的启动参数,可在 CONFIG_NO_HZ_IDLE=y 内核中通过设置 nohz=off 来禁用动态时钟空闲模式。默认情况下,CONFIG_NO_HZ_IDLE=y内核会以 nohz=on 状态启动,从而启用动态时钟空闲(dyntick-idle)模式。

2.1.3 仅有一个可运行任务的 CPU,省略调度时钟滴答

如果某个 CPU 仅有一个可运行任务,向其发送调度时钟中断便毫无意义,因为没有其他任务可供切换。请注意,对仅有一个可运行任务的 CPU 省略调度时钟滴答,也意味着对空闲 CPU 省略这些滴答。

CONFIG_NO_HZ_FULL=y 配置选项会使内核避免向仅有一个可运行任务的 CPU 发送调度时钟中断,此类 CPU 被称为自适应时钟CPU(adaptive-tick)。这对具有严格实时响应要求的应用至关重要,因为它能使应用通过最大化调度时钟中断的持续时间来改善最坏情况下的响应时间。该机制对计算密集型短迭代工作负载同样关键:若某 CPU 在迭代过程中延迟,其余所有 CPU 将被迫空闲等待该延迟 CPU 完成。此时延迟时间将乘以 CPU 总数减一的倍数。此类场景下,避免发送调度时钟中断同样具有强烈的必要性。

默认情况下,没有 CPU 会被标记为自适应时钟周期 CPU。通过 nohz_full= 引导参数可指定自适应时钟周期 CPU。例如, nohz_full=1,6-8 表示将 CPU 1、6、7、8 设为自适应时钟周期 CPU。请注意,禁止将所有 CPU 标记为自适应时钟周期 CPU: 至少需保留一个非自适应时钟 CPU 在线处理计时任务,以确保 gettimeofday() 等系统调用在自适应时钟 CPU 上返回精确值。(CONFIG_NO_HZ_IDLE=y 时不存在此问题,因为没有运行中的用户进程会察觉时钟频率的微小漂移。) 请注意,这意味着你的系统必须至少配备两个 CPU,CONFIG_NO_HZ_FULL=y 选项才能发挥作用。

最后,自适应时钟 CPU 必须卸载其 RCU 回调。具体说明详见下文 RCU 相关章节。

通常情况下,CPU 会尽可能长时间保持自适应时钟模式(adaptive-tick mode)。特别需要说明的是,切换到内核模式不会自动改变该模式。相反,CPU 仅在必要时才会退出自适应时钟模式,例如当该 CPU 将 RCU 回调函数加入队列时

动态时钟空闲模式(dyntick-idle)类似,自适应时钟模式(adaptive-tick mode)的优势并非免费获得:

  1. CONFIG_NO_HZ_FULL 会选择 CONFIG_NO_HZ_COMMON,因此无法在不启用动态时钟空闲的情况下运行自适应时钟。这种依赖关系延伸至实现层面,因此 CONFIG_NO_HZ_IDLE 的所有开销,CONFIG_NO_HZ_FULL 也会有。

  2. 由于需要通知内核子系统(如 RCU)模式变更,用户/内核切换的开销略有增加。

  3. POSIX CPU 定时器会阻止 CPU 进入自适应时钟模式。需要基于 CPU 时间消耗采取行动的实时应用程序必须采用其他方式实现。

  4. 当待处理性能事件数量超过硬件承载能力时,通常采用 round-robined 调度机制。自适应时钟模式可能阻碍此循环调度机制。解决方案可能是禁止存在大量待处理性能事件的 CPU 进入自适应时钟模式。

  5. 自适应时钟模式 CPU 的调度器统计数据计算方式可能与非自适应模式存在细微差异,进而可能扰乱实时任务的负载均衡机制。

尽管随着时间推移有望得到改进,自适应时钟(adaptive-tick)对许多实时和计算密集型应用程序仍相当有用。然而,上述缺点意味着自适应时钟目前不应默认启用。

2.2 NO_HZ 和 RCU

某些情况下,空闲 CPU 不得进入动态时钟空闲模式(dyntick-idle)自适应时钟(adaptive-tick)模式,最常见的情况是该 CPU 存在待处理的 RCU 回调。

可通过 CONFIG_RCU_NOCB_CPU=y Kconfig 配置选项将 RCU 回调处理卸载至 rcuo 内核线程来规避此问题。可通过内核启动参数rcu_nocbs= 选择具体卸载的 CPU,该参数接受以逗号分隔的 CPU 列表或 CPU 范围,例如1,3-5表示卸载 CPU 1、3、4、5。需注意,由 nohz_full参数指定的 CPU 也会被自动卸载。

卸载的 CPU 永远不会为 RCU 回调建立队列,因此 RCU 机制不会阻止这些 CPU 进入动态时钟空闲模式自适应时钟模式。需要说明的是,若需将 rcuo 内核线程固定在特定 CPU 上,应由用户空间自行处理。否则调度器将自行决定运行位置,这可能与预期位置不符。

3. NO_HZ 实现

本小节按 NO_HZ 的各种模式,逐一解析它们的代码实现的主要细节。

3.1 永不省略调度时钟滴答

这个没有什么好说的,配置 CONFIG_HZ_PERIODIC=y 或旧内核中的 CONFIG_NO_HZ=n 即可。调度滴答定时器工作在 periodic 模式,永不停歇。此时每 CPU 的 tick_cpu_schednohz_mode 的值为 NOHZ_MODE_INACTIVE,也即系统默认的状态,NO_HZ 模式没激活。

3.2 省略调度时钟滴答模式

从前面了解到,省略调度时钟有以下两种模式:

  • 动态时钟空闲(dyntick-idle) [CONFIG_NO_HZ_IDLE=y]
  • 自适应时钟CPU(adaptive-tick) [CONFIG_NO_HZ_FULL=y]

这两种模式在省略时钟滴答上逐层递进的,也就是后一种模式省略更多的时钟滴答。下面来逐一分析。

3.2.1 省略空闲 CPU 的调度时钟滴答(dyntick-idle)

动态时钟空闲(dyntick-idle),这是较新 Linux 内核的默认的方式,配置 CONFIG_NO_HZ_IDLE=y,调度滴答定时器工作在 oneshot 模式,CPU 在没有任务可调度室进入空闲态,调度滴答定时器停止工作,需要广播定时器唤醒。

更多细节可参考如下两篇博文中进入 CPU idle 模式相关章节:

Linux 时间子系统 (1):基础框架概述
cpu idle 驱动简介

3.2.2 仅有一个可运行任务的 CPU,省略调度时钟滴答(adaptive-tick)

自适应时钟CPU(adaptive-tick),配置 CONFIG_NO_HZ_FULL=y,仅有一个可运行任务的 CPU(包括每 CPU 固有的 idle 进程,CPU 上一共有两个进程),这时候没有必要进行任务调度,一直运行非 idle 任务就行了,这样就可以减少该 CPU 一些非必要的调度时钟滴答中断。

3.2.2.1 初始化 adaptive-tick

start_kernel()
	tick_init()
		tick_nohz_init()
/* kernel/time/tick-sched.c */

/* Full dynaticks 功能 (nohz_full) 初始化 */
void __init tick_nohz_init(void)
{
	int cpu, ret;

	...

	/* 做 housekeeping 工作的 CPU 集合掩码 */
	if (!alloc_cpumask_var(&housekeeping_mask, GFP_KERNEL)) {
		...
	}

	/*
	 * Full dynticks uses irq work to drive the tick rescheduling on safe
	 * locking contexts. But then we need irq work to raise its own
	 * interrupts to avoid circular dependency on the tick
	 */
	if (!arch_irq_work_has_interrupt()) {
		pr_warn("NO_HZ: Can't run full dynticks because arch doesn't support irq work self-IPIs\n");
		cpumask_clear(tick_nohz_full_mask);
		cpumask_copy(housekeeping_mask, cpu_possible_mask);
		tick_nohz_full_running = false;
		return;
	}

	cpu = smp_processor_id();

	/*
	 * 当前场景, 除 BOOT CPU 外的其它所有 CPU 还没有运行起来,
	 * 所以当前函数运行于 BOOT CPU. 同时, 在 nohz_full 模式下, 
	 * 至少要保持有一个 CPU 一直在运行, 做必要的 housekeeping 
	 * 工作 (如时间系统更新维护). 而 BOOT CPU 负责 housekeeping 
	 * 工作, 所以这里要将 BOOT CPU 从 tick_nohz_full_mask
	 * CPU 掩码中移除.
	 */
	if (cpumask_test_cpu(cpu, tick_nohz_full_mask)) {
		pr_warn("NO_HZ: Clearing %d from nohz_full range for timekeeping\n",
			cpu);
		cpumask_clear_cpu(cpu, tick_nohz_full_mask);
	}

	/* 除 nohz_full=XXX 设定的 CPU 外,其它的 CPU 用来做 housekeeping 工作 */
	cpumask_andnot(housekeeping_mask,
		       cpu_possible_mask, tick_nohz_full_mask);

	ret = cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN,
					"kernel/nohz:predown", NULL,
					tick_nohz_cpu_down);
	WARN_ON(ret < 0);
	pr_info("NO_HZ: Full dynticks CPUs: %*pbl.\n",
		cpumask_pr_args(tick_nohz_full_mask));

	...
}

3.2.2.2 切换到 NO_HZ adaptive-tick 模式

首先看一下,NO_HZ adaptive-tick 模式的使能,有两种方式:

  • nohz_full= 内核命令行参数,指定可以进入 NO_HZ adaptive-tick 模式的 CPU 集合
  • CONFIG_NO_HZ_FULL_ALL=y,默认将所有 CPU 都指定为可以进入 NO_HZ adaptive-tick 模式(housekeeping CPU 除外)

逐一看一下这两种方式的代码。通过 nohz_full= 内核命令行参数 使能 NO_HZ adaptive-tick 模式

/* Parse the boot-time nohz CPU list from the kernel parameters. */
static int __init tick_nohz_full_setup(char *str)
{
	alloc_bootmem_cpumask_var(&tick_nohz_full_mask);
	if (cpulist_parse(str, tick_nohz_full_mask) < 0) {
		pr_warn("NO_HZ: Incorrect nohz_full cpumask\n");
		free_bootmem_cpumask_var(tick_nohz_full_mask);
		return 1;
	}
	tick_nohz_full_running = true;

	return 1;
}
__setup("nohz_full=", tick_nohz_full_setup);

通过配置 CONFIG_NO_HZ_FULL_ALL=y 使能 NO_HZ adaptive-tick 模式

start_kernel()
	tick_init()
		tick_nohz_init()

void __init tick_nohz_init(void)
{
	int cpu, ret;

	if (!tick_nohz_full_running) {
		if (tick_nohz_init_all() < 0)
			return;
	}

	...
}

static int tick_nohz_init_all(void)
{
	int err = -1;

#ifdef CONFIG_NO_HZ_FULL_ALL
	if (!alloc_cpumask_var(&tick_nohz_full_mask, GFP_KERNEL)) {
		WARN(1, "NO_HZ: Can't allocate full dynticks cpumask\n");
		return err;
	}
	err = 0;
	cpumask_setall(tick_nohz_full_mask); /* CONFIG_NO_HZ_FULL_ALL=y, 表示默认对所有的 CPU 启动用 nohz_full */
	tick_nohz_full_running = true; /* 标记启用了 nohz_full (NO_HZ adaptive-tick 模式) */
#endif
	return err;
}

接下来,分析如何切换到 NO_HZ adaptive-tick 模式。在支持和不支持 hrtimer 的系统上,切换到 NO_HZ adaptive-tick 模式的有着不同路径,下面来分析它们。

不支持 hrtimer 的系统上,切换到 NO_HZ adaptive-tick 模式路径,从每 CPU tick 设备中断开始:

tick_handle_periodic()
	int cpu = smp_processor_id();
	...
	tick_periodic(cpu);
		update_process_times()
			run_local_timers()
				hrtimer_run_queues()
					if (tick_check_oneshot_change(!hrtimer_is_hres_enabled())) {
						...
					}

int tick_check_oneshot_change(int allow_nohz)
{
	/*
	 * 检查是否有异步的 tick device 设备变更通知? 
	 * tick_oneshot_notify() / tick_clock_notify()
	 * 1) tick 设备注册 / replace
	 * 1.1) tick 设备注册 [为 cpu local 设备]
	 *    clockevents_register_device()
	 *        tick_check_new_device()
	 *            if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
	 *	               tick_oneshot_notify();
	 * 1.2) tick broadcast 注册 [为 broadcast 设备]
	 *    clockevents_register_device()
	 *        tick_check_new_device()
	 *        clockevents_notify_released()
	 *            tick_check_new_device()
	 *                tick_install_broadcast_device()
	 *                    if (dev->features & CLOCK_EVT_FEAT_ONESHOT)
	 *                        tick_clock_notify()
	 * 1.3) drivers/hv/hv.c (基本可以忽略的场景)???
	 *      hv_synic_clockevents_cleanup() / hv_synic_cleanup()
	 *        -> clockevents_unbind_device() -> ... 
	 *            -> tick_install_replacement() -> tick_oneshot_notify()
	 * 2) clocksource 更换
	 *    clocksource_select()
	 *        __clocksource_select()
	 *            timekeeping_notify()
	 *                tick_clock_notify()
	 */
	if (!test_and_clear_bit(0, &ts->check_clocks))
		return 0;

	if (ts->nohz_mode != NOHZ_MODE_INACTIVE) /* 该 CPU 已经切换为 nohz 模式, 不重复切换 */
		return 0;

	if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
		return 0;

	/* 如果 系统支持 hrtimer, */
	if (!allow_nohz)
		return 1; /* 则切换到  【高精度 timer nohz 模式 (NOHZ_MODE_HIGHRES) 】 */

	/*
	 * 某 CPU 在一次 CPU 本地 tick 中断中,切换为
	 * 【低精度 timer nohz 模式 (NOHZ_MODE_LOWRES) 】。
	 */
	tick_nohz_switch_to_nohz();
	return 0;
}

/**
 * tick_nohz_switch_to_nohz - switch to nohz mode
 */
static void tick_nohz_switch_to_nohz(void)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
	...

	if (tick_switch_to_oneshot(tick_nohz_handler))
		return;

	/*
	 * Recycle the hrtimer in ts, so we can share the
	 * hrtimer_forward with the highres code.
	 */
	/*
	 * 用 hrtimer 模拟 每 CPU 的 调度 tick 。
	 */
	hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
	/* Get the next period */
	next = tick_init_jiffy_update();

	hrtimer_set_expires(&ts->sched_timer, next);
	hrtimer_forward_now(&ts->sched_timer, tick_period);
	tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
	tick_nohz_activate(ts, NOHZ_MODE_LOWRES); /* 切换到 低精度 timer NOHZ 模式 */
}

/**
 * tick_switch_to_oneshot - switch to oneshot mode
 */
int tick_switch_to_oneshot(void (*handler)(struct clock_event_device *))
{
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
	struct clock_event_device *dev = td->evtdev;

	...

	td->mode = TICKDEV_MODE_ONESHOT; /* CPU 的 tick device 切换为 oneshot 模式 */
	/*
	 * 切换到 低精度 timer NOHZ 模式:
	 * NOHZ_MODE_LOWRES: tick_nohz_handler()
	 */
	dev->event_handler = handler;
	clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT); /* CPU 的 tick device 切换为 oneshot 状态 */
	tick_broadcast_switch_to_oneshot(); /* broadcast tick device 也要跟着切换为 oneshot 模式 */
	return 0;
}

支持 hrtimer 的系统上,切换到 NO_HZ adaptive-tick 模式路径,同样也是从每 CPU tick 设备中断开始:

tick_handle_periodic()
	int cpu = smp_processor_id();
	...
	tick_periodic(cpu);
		update_process_times()
			run_local_timers()
				hrtimer_run_queues()
					if (tick_check_oneshot_change(!hrtimer_is_hres_enabled())) {
						hrtimer_switch_to_hres();
						return;
					}

/*
 * Switch to high resolution mode
 */
static void hrtimer_switch_to_hres(void)
{
	struct hrtimer_cpu_base *base = this_cpu_ptr(&hrtimer_bases);

	/* hrtimer 模式下, 使用 oneshot 模式 */
	if (tick_init_highres()) {
		printk(KERN_WARNING "Could not switch to high resolution "
				    "mode on CPU %d\n", base->cpu);
		return;
	}
	base->hres_active = 1; /* 标记当前 CPU 上 hrtimer base 已经激活 */
	hrtimer_resolution = HIGH_RES_NSEC;

	/*
	 * 在切换到 hrtime 之前, 使用硬件 timer 来进行每 tick 调度;
	 * 在切换到 hrtime 之后, 使用一个 hrtimer 来模拟硬件 timer 
	 * 来进行每 tick 调度。模拟 hrtimer 回调接口为 tick_sched_timer() 。
	 */
	tick_setup_sched_timer();
	/* "Retrigger" the interrupt to get things going */
	retrigger_next_event(NULL);
}

tick_init_highres()
	tick_switch_to_oneshot(hrtimer_interrupt)

int tick_switch_to_oneshot(void (*handler)(struct clock_event_device *))
{
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
	struct clock_event_device *dev = td->evtdev;

	...

	td->mode = TICKDEV_MODE_ONESHOT; /* CPU 的 tick device 切换为 oneshot 模式 */
	/*
	 * 切换到 高精度 timer NOHZ 模式:
	 * NOHZ_MODE_HIGHRES: hrtimer_interrupt()
	 */
	dev->event_handler = handler;
	clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT); /* CPU 的 tick device 切换为 oneshot 状态 */
	tick_broadcast_switch_to_oneshot(); /* broadcast tick device 也要跟着切换为 oneshot 模式 */
	return 0;
}

void tick_setup_sched_timer(void)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
	ktime_t now = ktime_get();

	/*
	 * Emulate tick processing via per-CPU hrtimers:
	 */
	/* 用 hrtimer 模拟每 CPU 的 调度 tick */
	hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
	ts->sched_timer.function = tick_sched_timer;

	/* Get the next period (per-CPU) */
	hrtimer_set_expires(&ts->sched_timer, tick_init_jiffy_update());

	...

	hrtimer_forward(&ts->sched_timer, now, tick_period);
	hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED);
	tick_nohz_activate(ts, NOHZ_MODE_HIGHRES);
}

3.2.2.3 adaptive-tick 模式的 进入 和 退出

  • 进入 adaptive-tick 模式
static void do_idle(void)
{
	...
	tick_nohz_idle_enter();
	...
}

void tick_nohz_idle_enter(void)
{
	struct tick_sched *ts;

	...

	/*
	 * Update the idle state in the scheduler domain hierarchy
	 * when tick_nohz_stop_sched_tick() is called from the idle loop.
	 * State will be updated to busy during the first busy tick after
	 * exiting idle.
	 */
	set_cpu_sd_state_idle();

	local_irq_disable(); /* 禁用 CPU 本地中断 */

	ts = this_cpu_ptr(&tick_cpu_sched);
	ts->inidle = 1; /* 标记当前 CPU 进入了 [软件] idle 态 */
	__tick_nohz_idle_enter(ts);

	local_irq_enable(); /* 启用 CPU 本地中断 */
}

static void __tick_nohz_idle_enter(struct tick_sched *ts)
{
	ktime_t now, expires;
	int cpu = smp_processor_id();

	now = tick_nohz_start_idle(ts); /* 读取此次 @cpu 进入 idle 的开始时间 */

	if (can_stop_idle_tick(cpu, ts)) { /* 检查看 CPU 是否符合 停止 调度 tick 的条件, */
		/* 当前满足停止 @cpu 上 调度器 tick 的初步条件 */
		int was_stopped = ts->tick_stopped;

		ts->idle_calls++;

		expires = tick_nohz_stop_sched_tick(ts, now, cpu); /* 尝试停止 CPU 的 调度 tick */
		if (expires > 0LL) {
			ts->idle_sleeps++;
			ts->idle_expires = expires;
		}

		if (!was_stopped && ts->tick_stopped) {
			ts->idle_jiffies = ts->last_jiffies;
			nohz_balance_enter_idle(cpu);
		}
	}
}

static bool can_stop_idle_tick(int cpu, struct tick_sched *ts)
{
	...

	if (need_resched()) /* CPU 上有挂起的调度请求,不能停止 调度 tick */
		return false;

	if (unlikely(local_softirq_pending() && cpu_online(cpu))) { /* CPU 上有挂起的 softirq 要处理,不能停止 调度 tick */
		...
	}

	...

	return true;
}

static ktime_t tick_nohz_stop_sched_tick(struct tick_sched *ts,
					 ktime_t now, int cpu)
{
	struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);
	u64 basemono, next_tick, next_tmr, next_rcu, delta, expires;
	unsigned long seq, basejiff;
	ktime_t	tick;

	/* Read jiffies and the time when jiffies were updated last */
	/* @basmono: 存储当前 jiffies 计数 */
	do {
		seq = read_seqbegin(&jiffies_lock);
		basemono = last_jiffies_update;
		basejiff = jiffies;
	} while (read_seqretry(&jiffies_lock, seq));
	ts->last_jiffies = basejiff;

	/*
	 * Keep the periodic tick, when RCU, architecture or irq_work
	 * requests it.
	 * Aside of that check whether the local timer softirq is
	 * pending. If so its a bad idea to call get_next_timer_interrupt()
	 * because there is an already expired timer, so it will request
	 * immeditate expiry, which rearms the hardware timer with a
	 * minimal delta which brings us back to this place
	 * immediately. Lather, rinse and repeat...
	 */
	/*
	 * 下列情形,不能停掉 CPU 上的 tick: 
	 * . CPU 上有挂起的 RCU callback
	 * . CPU 上有挂起的 irq_work
	 * . CPU 上有挂起的 TIMER_SOFTIRQ
	 *
	 * @next_rcu: rcu_needs_cpu() 返回下一个 rcu 的时间点
	 */
	if (rcu_needs_cpu(basemono, &next_rcu) || arch_needs_cpu() ||
	    irq_work_needs_cpu() || local_timer_softirq_pending()) {
	    /* 不能停掉 CPU 上的 tick, 下一个 tick 时间点按正常计算 */
		next_tick = basemono + TICK_NSEC;
	} else {
		/*
		 * Get the next pending timer. If high resolution
		 * timers are enabled this only takes the timer wheel
		 * timers into account. If high resolution timers are
		 * disabled this also looks at the next expiring
		 * hrtimer.
		 */
		/*
		 * 可以停掉 CPU 上的 tick 的情形, 如果 CPU 上有 rcu 或 timer 
		 * 业务需要处理, 那么要计算下一个 tick 要在什么时间到来的时
		 * 间点,好到时重启 CPU 上的 tick.
		 */
		next_tmr = get_next_timer_interrupt(basejiff, basemono); /* 获取下一个最先到期的 高低精度 timer 时间 */
		ts->next_timer = next_tmr;
		/* Take the next rcu event into account */
		/* 下一个 rcu 和 timer, 谁先到期选谁的时间点, 作为下一个 tick 时间 */
		next_tick = next_rcu < next_tmr ? next_rcu : next_tmr;
	}

	/*
	 * If the tick is due in the next period, keep it ticking or
	 * force prod the timer.
	 */
	delta = next_tick - basemono; /* @delta: 当前时间 和 下一个 tick 的间隔 */
	if (delta <= (u64)TICK_NSEC) { /* 当前时间 和 下一个 tick 的间隔, 不足一个 tick 间隔, 不停掉 CPU 上的 tick */
		/*
		 * Tell the timer code that the base is not idle, i.e. undo
		 * the effect of get_next_timer_interrupt():
		 */
		timer_clear_idle();
		/*
		 * We've not stopped the tick yet, and there's a timer in the
		 * next period, so no point in stopping it either, bail.
		 */
		if (!ts->tick_stopped) {
			tick = 0;
			goto out;
		}
	}

	/*
	 * If this CPU is the one which updates jiffies, then give up
	 * the assignment and let it be taken by the CPU which runs
	 * the tick timer next, which might be this CPU as well. If we
	 * don't drop this here the jiffies might be stale and
	 * do_timer() never invoked. Keep track of the fact that it
	 * was the one which had the do_timer() duty last. If this CPU
	 * is the one which had the do_timer() duty last, we limit the
	 * sleep time to the timekeeping max_deferment value.
	 * Otherwise we can sleep as long as we want.
	 */
	/* 根据 timekeeper 的可能溢出的位宽,得到的 idle 最大值 */
	delta = timekeeping_max_deferment();
	if (cpu == tick_do_timer_cpu) { /* 如果当前 @cpu 负责维护时间,更新 jiffies, */
		tick_do_timer_cpu = TICK_DO_TIMER_NONE;
		ts->do_timer_last = 1; /* 标记 @cpu 是那个最后做 jiffies 更新的 CPU, @cpu 进入了 idle */
	} else if (tick_do_timer_cpu != TICK_DO_TIMER_NONE) { /* tick_do_timer_cpu 维护时间, 那当前 @cpu 不是维护时间的 CPU */
		delta = KTIME_MAX;
		ts->do_timer_last = 0;
	} else if (!ts->do_timer_last) {
		delta = KTIME_MAX;
	}

#ifdef CONFIG_NO_HZ_FULL
	/* Limit the tick delta to the maximum scheduler deferment */
	if (!ts->inidle)
		delta = min(delta, scheduler_tick_max_deferment());
#endif

	/* Calculate the next expiry time */
	/* 综合前述, 计算下一个 tick 超时时间点 */
	if (delta < (KTIME_MAX - basemono))
		expires = basemono + delta;
	else
		expires = KTIME_MAX;

	expires = min_t(u64, expires, next_tick);
	tick = expires;

	/* Skip reprogram of event if its not changed */
	if (ts->tick_stopped && (expires == ts->next_tick)) {
		/* Sanity check: make sure clockevent is actually programmed */
		if (tick == KTIME_MAX || ts->next_tick == hrtimer_get_expires(&ts->sched_timer))
			goto out;

		WARN_ON_ONCE(1);
		printk_once("basemono: %llu ts->next_tick: %llu dev->next_event: %llu timer->active: %d timer->expires: %llu\n",
			    basemono, ts->next_tick, dev->next_event,
			    hrtimer_active(&ts->sched_timer), hrtimer_get_expires(&ts->sched_timer));
	}

	/*
	 * nohz_stop_sched_tick can be called several times before
	 * the nohz_restart_sched_tick is called. This happens when
	 * interrupts arrive which do not cause a reschedule. In the
	 * first call we save the current tick time, so we can restart
	 * the scheduler tick in nohz_restart_sched_tick.
	 */
	if (!ts->tick_stopped) {
		calc_load_nohz_start();
		cpu_load_update_nohz_start();

		ts->last_tick = hrtimer_get_expires(&ts->sched_timer);
		ts->tick_stopped = 1; /* 标记 @cpu 的 调度 tick 停止了 */
		trace_tick_stop(1, TICK_DEP_MASK_NONE);
	}

	ts->next_tick = tick; /* @cpu 下一个调度 tick 时间点 */

	/*
	 * If the expiration time == KTIME_MAX, then we simply stop
	 * the tick timer.
	 */
	/* 如果下一调度 tick 超时时间为 KTIME_MAX, 则简单的停掉调度 tick hrtimer */
	if (unlikely(expires == KTIME_MAX)) {
		if (ts->nohz_mode == NOHZ_MODE_HIGHRES)
			hrtimer_cancel(&ts->sched_timer);
		goto out;
	}

	if (ts->nohz_mode == NOHZ_MODE_HIGHRES) { /* 高精度 nohz_full */
		/* 按计算得到得到调度 tick 下一次触发时间,启动 调度 tick hrtimer  */
		hrtimer_start(&ts->sched_timer, tick, HRTIMER_MODE_ABS_PINNED);
	} else { /* 低精度 nohz_full */
		hrtimer_set_expires(&ts->sched_timer, tick);
		tick_program_event(tick, 1);
	}

out:
	/*
	 * Update the estimated sleep length until the next timer
	 * (not only the tick).
	 */
	ts->sleep_length = ktime_sub(dev->next_event, now);
	return tick;
}
  • 退出 adaptive-tick 模式

CPU 进入 idle 状态后,被中断唤醒,继续执行进入 idle 时停止的后续代码流程:

do_idle()
	...
	tick_nohz_idle_enter();
	...
	while (!need_resched()) {
		...
		if (cpu_idle_force_poll || tick_check_broadcast_expired())
			...
		else
			cpuidle_idle_call(); /* CPU 在这里进入 idle 态,然后被唤醒,继续执行后续代码流程 */
		...
	}
	...
	tick_nohz_idle_exit();
	...

void tick_nohz_idle_exit(void)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
	ktime_t now;

	local_irq_disable(); /* 关闭 CPU 本地中断 */

	WARN_ON_ONCE(!ts->inidle);

	ts->inidle = 0; /* 标记 当前 CPU 退出了 idle 态 */

	/* CPU 从硬件 idle 态醒来 或 调度 tick 停止了 */
	if (ts->idle_active || ts->tick_stopped)
		now = ktime_get();

	if (ts->idle_active)
		tick_nohz_stop_idle(ts, now);

	/* 如果 调度 tick 停止了 */
	if (ts->tick_stopped) {
		tick_nohz_restart_sched_tick(ts, now); /* 重启 CPU 上的调度 tick */
		tick_nohz_account_idle_ticks(ts);
	}

	local_irq_enable(); /* 重启 CPU 本地中断 */
}

static void tick_nohz_stop_idle(struct tick_sched *ts, ktime_t now)
{
	update_ts_time_stats(smp_processor_id(), ts, now, NULL);
	ts->idle_active = 0;

	sched_clock_idle_wakeup_event();
}

static void tick_nohz_restart_sched_tick(struct tick_sched *ts, ktime_t now)
{
	/* Update jiffies first */
	tick_do_update_jiffies64(now); /* 更新 jiffies 计数 */
	cpu_load_update_nohz_stop();

	/*
	 * Clear the timer idle flag, so we avoid IPIs on remote queueing and
	 * the clock forward checks in the enqueue path:
	 */
	timer_clear_idle();

	calc_load_nohz_stop();
	touch_softlockup_watchdog_sched();
	/*
	 * Cancel the scheduled timer and restore the tick
	 */
	ts->tick_stopped  = 0; /* 清除 调度 tick 停止 标记 */
	ts->idle_exittime = now;

	tick_nohz_restart(ts, now); /* 重启 调度 tick timer */
}

/* 重启 调度 tick timer */
static void tick_nohz_restart(struct tick_sched *ts, ktime_t now)
{
	hrtimer_cancel(&ts->sched_timer);
	hrtimer_set_expires(&ts->sched_timer, ts->last_tick);

	/* Forward the time to expire in the future */
	hrtimer_forward(&ts->sched_timer, now, tick_period); /* 设置 调度 tick hrtimer 下一次 超时时间 */

	/* 重启 调度 tick timer */
	if (ts->nohz_mode == NOHZ_MODE_HIGHRES)
		hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED);
	else
		tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);

	/*
	 * Reset to make sure next tick stop doesn't get fooled by past
	 * cached clock deadline.
	 */
	ts->next_tick = 0;
}

3.2.3 adaptive-tick 模式 和 中断

从 3.2.1 和 3.2.2 两小节分析了 adaptive-tick 模式的进入和退出。adaptive-tick 模式的退出由中断触发,在 3.2.2 中刻意跳过了和中断相关的流程,在这里做统一的分析。在 CPU 的中断处理过程中,也需要做 adaptive-tick 模式的处理,因为被唤醒的 CPU 可能没有工作要做,不一定要重启调度 tick。

do_idle()
	...
	tick_nohz_idle_enter();
	...
	while (!need_resched()) {
		...
		/*
		 * 准备进入当前 CPU Idle 态,先禁用当前 CPU 中断; 
		 * CPU 退出 Idle 后再重启中断
		 */
		local_irq_disable(); // (1)
		...
		
		if (cpu_idle_force_poll || tick_check_broadcast_expired())
			...
		else
			cpuidle_idle_call(); /* CPU 在这里进入 idle 态,然后被唤醒,继续执行后续代码流程 */
		...
	}
	...
	tick_nohz_idle_exit(); // (2)
	...

cpuidle_idle_call()
	/* CPU 进入 idle 态 */
	call_cpuidle()
		cpuidle_enter()
			cpuidle_enter_state() // (3)
	 /* CPU 退出 idle 态后,重新使能中断 */
	local_irq_enable() // (4)

从上面代码片段看到,在 (1) 处禁用了 CPU 本地中断,然后在 (3) 进入 CPU idle 态,然后被 CPU 中断唤醒,退出 idle 态,最后在 (4) 处重启 CPU 本地中断。重启 CPU 本地中断后,可能是 (2) 的 tick_nohz_idle_exit() 先执行,也可能是中断处理回调先被执行。谁先执行不重要,中断的处理过程中会考虑这种先后关系情形。中断的处理过程中 adaptive-tick 模式相关逻辑分为两部分:

  • 进入中断上下文时
/*
 * Enter an interrupt context.
 */
void irq_enter(void)
{
	...

	/*
	 * 在同一 CPU 上, 同时满足如下条件:
	 * . 被中断的是 idle 进程 -> is_idle_task(current)
	 * . 没有产生中断抢占、嵌套,没有软中断在处理中 -> !in_interrupt()
	 * 即 Idle 态 CPU 上产生了中断被唤醒了。
	 *
	 * 没有产生中断抢占、嵌套的前提下, 这里 !in_interrupt()
	 * 是成立的, 因为 HARDIRQ_OFFSET 计数在后面的 __irq_enter()
	 * 里设置.
	 */
	if (is_idle_task(current) && !in_interrupt()) {
		/*
		 * Prevent raise_softirq from needlessly waking up ksoftirqd
		 * here, as softirq will be serviced on return from interrupt.
		 */
		local_bh_disable();
		tick_irq_enter();
		_local_bh_enable();
	}

	...
}

void tick_irq_enter(void)
{
	tick_check_oneshot_broadcast_this_cpu(); /* 重启 CPU 的 tick 设备[到 oneshot 模式] */
	tick_nohz_irq_enter();
}

static inline void tick_nohz_irq_enter(void)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
	ktime_t now;

	if (!ts->idle_active && !ts->tick_stopped)
		return;
	now = ktime_get();
	if (ts->idle_active)
		tick_nohz_stop_idle(ts, now);
	if (ts->tick_stopped)
		tick_nohz_update_jiffies(now); /* CPU 从 idle 退出后,重新更新 jiffies 计数 */
}
  • 退出中断上下文时
/*
 * Exit an interrupt context. Process softirqs if needed and possible:
 */
void irq_exit(void)
{
	...
	tick_irq_exit();
	...
}

static inline void tick_irq_exit(void)
{
#ifdef CONFIG_NO_HZ_COMMON
	int cpu = smp_processor_id();

	/* Make sure that timer wheel updates are propagated */
	if ((idle_cpu(cpu) && !need_resched()) || tick_nohz_full_cpu(cpu)) {
		if (!in_irq()) /* 此时, 在 irq_enter() 加上的 HARDIRQ_OFFSET 已经减掉 */
			tick_nohz_irq_exit();
	}
#endif
}

void tick_nohz_irq_exit(void)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);

	/*
	 * Idle CPU 上有中断进来后,唤醒 CPU 退出 idle 态, 接着:
	 * 1) 使能 CPU 本地中断
	 * 2) tick_nohz_idle_exit()
	 *        ts->inidle = 0
	 * 这里的调用上下文是中断:
	 * irq_exit() -> tick_irq_exit() -> tick_nohz_irq_exit()
	 * 没法知道 先执行 tick_nohz_idle_exit() 还是 中断回调,
	 * 所以这里可能 ts->inidle == 1, 也可能是 ts->inidle == 0.
	 */
	if (ts->inidle)
		__tick_nohz_idle_enter(ts);
	else
		tick_nohz_full_update_tick(ts);
}

static void tick_nohz_full_update_tick(struct tick_sched *ts)
{
#ifdef CONFIG_NO_HZ_FULL
	int cpu = smp_processor_id();

	if (!tick_nohz_full_cpu(cpu))
		return;

	if (!ts->tick_stopped && ts->nohz_mode == NOHZ_MODE_INACTIVE)
		return;

	if (can_stop_full_tick(cpu, ts)) /* 如果没必要重启 调度 tick, */
		tick_nohz_stop_sched_tick(ts, ktime_get(), cpu); /* 停止 调度 tick */
	else if (ts->tick_stopped)
		tick_nohz_restart_sched_tick(ts, ktime_get()); /* 重启 调度 tick */
#endif
}

4. 测试

尝试了本文所述的所有 NO_HZ 模式,你仍未观察到工作负载行为的任何变化。这究竟是因为工作负载受操作系统抖动影响较小,还是存在其他干扰因素?本节通过提供一个简单的操作系统抖动测试套件来解答此问题,该套件可在以下 Git 存档的主分支获取:

git://git.kernel.org/pub/scm/linux/kernel/git/frederic/dynticks-testing.git

克隆该存档并遵循 README 文件中的说明。此测试流程将生成一条跟踪记录,可用于评估是否成功从系统中移除了操作系统抖动。若该跟踪记录显示已尽可能消除操作系统抖动,则可得出结论:你的工作负载对操作系统抖动并不敏感。

注意:此测试要求系统至少配备两个 CPU。目前尚无有效方法从单 CPU 系统中消除操作系统抖动。

5. 参考资料

[1] NO_HZ: Reducing Scheduling-Clock Ticks
[2] Clockevents and dyntick
[3] Linux Time
[4] Linux时间子系统之八:动态时钟框架(CONFIG_NO_HZ、tickless)

posted @ 2025-11-25 17:24  JiMoKuangXiangQu  阅读(138)  评论(0)    收藏  举报