内核版本:linux-5.15.140
涉及文件:kernel/sched/core.c、kernel/sched/fair.c、kernel/sched/features.h

前言

在前面几篇里,我们已经把 CFS 的两条抢占路径看过了:

  • enqueue 路径:新进程入队时 check_preempt_curr(见 enqueue 系列);
  • wakeup 路径:进程被唤醒时 check_preempt_wakeup(上一篇)。

本篇看第三条、也是平时触发最频繁的一条:周期性时钟中断(tick)里的抢占检查。一个进程如果在 CPU 上跑得太久,不能等它自己让出,tick 会定期检查"你跑够了吗?跑够了就换下一个"。这条链路的调用关系是:

scheduler_tick()                    kernel/sched/core.c:5284
  └── curr->sched_class->task_tick()  →  task_tick_fair()   kernel/sched/fair.c:11449
        └── entity_tick()             kernel/sched/fair.c:4789
              └── check_preempt_tick() kernel/sched/fair.c:4629

我们自下而上,逐个函数拆开看。


一、入口:scheduler_tick

每个 CPU 的周期性时钟中断都会走到 scheduler_tick(core.c:5284):

void scheduler_tick(void)
{
	int cpu = smp_processor_id();
	struct rq *rq = cpu_rq(cpu);
	struct task_struct *curr = rq->curr;
	struct rq_flags rf;
	unsigned long thermal_pressure;
	u64 resched_latency;

	arch_scale_freq_tick();
	sched_clock_tick();

	rq_lock(rq, &rf);

	update_rq_clock(rq);
	thermal_pressure = arch_scale_thermal_pressure(cpu_of(rq));
	update_thermal_load_avg(rq_clock_thermal(rq), rq, thermal_pressure);
	curr->sched_class->task_tick(rq, curr, 0);
	...
	rq_unlock(rq, &rf);
	...
}

关键的一行是 curr->sched_class->task_tick(rq, curr, 0)。这就是我们在第七篇讲过的 sched_class 多态:当前进程属于哪个调度类,就调哪个调度类的 task_tick 钩子。对普通进程来说就是 CFS 的 task_tick_fair(挂在 fair.c:12051 的 fair_sched_class 定义里)。

第三个参数 queued 传 0,表示这是周期性 tick(而不是 hrtick 高精度定时器触发的 tick),这个参数后面有用。

二、task_tick_fair:遍历调度实体层级

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &curr->se;

	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		entity_tick(cfs_rq, se, queued);
	}

	if (static_branch_unlikely(&sched_numa_balancing))
		task_tick_numa(rq, curr);

	update_misfit_status(curr, rq);
	update_overutilized_status(task_rq(curr));

	task_tick_core(rq, curr);
}

for_each_sched_entity(se) 这个循环我们在 enqueue 系列里见过:如果开了组调度(CONFIG_FAIR_GROUP_SCHED),一个进程的调度实体可能嵌套在多层 task_group 的 cfs_rq 里,需要自底向上逐层处理;没开组调度时就只有一层,循环只执行一次。

后面几行是其他子系统的 tick 钩子,本篇不展开:

  • task_tick_numa:NUMA balancing 相关,只有开了自动 NUMA 均衡才走;
  • update_misfit_status / update_overutilized_status:EAS(能耗感知调度)用,标记该 CPU 上的任务是否"放错了核"或核是否过载;
  • task_tick_core:CONFIG_SCHED_CORE(core scheduling,防侧信道攻击特性)的 tick 处理。

主线在 entity_tick。

三、entity_tick:先记账,再检查抢占

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
	/*
	 * Update run-time statistics of the 'current'.
	 */
	update_curr(cfs_rq);

	/*
	 * Ensure that runnable average is periodically updated.
	 */
	update_load_avg(cfs_rq, curr, UPDATE_TG);
	update_cfs_group(curr);

#ifdef CONFIG_SCHED_HRTICK
	/*
	 * queued ticks are scheduled to match the slice, so don't bother
	 * validating it and just reschedule.
	 */
	if (queued) {
		resched_curr(rq_of(cfs_rq));
		return;
	}
	/*
	 * don't let the period tick interfere with the hrtick preemption
	 */
	if (!sched_feat(DOUBLE_TICK) &&
			hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))
		return;
#endif

	if (cfs_rq->nr_running > 1)
		check_preempt_tick(cfs_rq, curr);
}

三件事:

1. update_curr(cfs_rq):把从上次记账到现在流逝的时间记到当前进程头上——累加 sum_exec_runtime、按权重折算累加 vruntime。这个函数我们在"enqueue(四)"那篇已经逐行看过,此处不再展开,只需要记住:tick 是当前进程 vruntime 增长的主要时机之一。

2. update_load_avg:PELT 负载衰减累加,为负载均衡提供数据。这块是个大专题,留给后面负载均衡系列再拆。

3. CONFIG_SCHED_HRTICK 分支:高精度 tick 特性。如果开了它,内核会按时间片精确安排一个 hrtimer,到点直接 resched_curr 强制重调度(queued 为真时)。而普通的周期 tick 若撞见 hrtick 定时器还在跑,就直接返回不重复检查(DOUBLE_TICK feature 默认关闭,features.h:42)。一般发行版默认不开这个配置,了解即可。

最后一句是本篇正题:只有 cfs_rq 上可运行的进程数大于 1,才有"抢占"可言,否则就一个进程,抢谁的去。

四、check_preempt_tick:抢占判断的核心逻辑

static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;
	struct sched_entity *se;
	s64 delta;

	ideal_runtime = sched_slice(cfs_rq, curr);
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
	if (delta_exec > ideal_runtime) {
		resched_curr(rq_of(cfs_rq));
		/*
		 * The current task ran long enough, ensure it doesn't get
		 * re-elected due to buddy favours.
		 */
		clear_buddies(cfs_rq, curr);
		return;
	}

	/*
	 * Ensure that a task that missed wakeup preemption by a
	 * narrow margin doesn't have to wait for a full slice.
	 * This also mitigates buddy induced latencies under load.
	 */
	if (delta_exec < sysctl_sched_min_granularity)
		return;

	se = __pick_first_entity(cfs_rq);
	delta = curr->vruntime - se->vruntime;

	if (delta < 0)
		return;

	if (delta > ideal_runtime)
		resched_curr(rq_of(cfs_rq));
}

这个函数只有二十几行,但分了两层判断,一层"硬超时",一层"软比较"。先解释两个变量:

  • ideal_runtime:当前进程本轮应该跑的时间片,由 sched_slice 算出;
  • delta_exec:当前进程本轮实际已经跑的时间,即 sum_exec_runtime - prev_sum_exec_runtime。prev_sum_exec_runtime 是它被 set_next_entity 摘上 CPU 时记下的起点。

4.1 第一层:跑超时间片了,无条件换

	if (delta_exec > ideal_runtime) {
		resched_curr(rq_of(cfs_rq));
		clear_buddies(cfs_rq, curr);
		return;
	}

实际运行时间超过应得时间片,直接 resched_curr 给当前进程打上 TIF_NEED_RESCHED 标记。注意这里只是打标记,真正的切换发生在之后中断返回/抢占检查时的 __schedule(第十三篇的坑,下一篇填)。

clear_buddies 是清掉 CFS 的 “buddy” 缓存(last/next/skip 三个指针)。buddy 机制会在某些场景下偏袒上一个运行的进程(缓存亲和),但既然它都跑超时间片了,就不能再让它靠 buddy 优惠立刻被选回来,注释里说的 “buddy favours” 就是这个意思。

4.2 第二层:最小粒度保护 + vruntime 比较

如果没超时,并不马上高枕无忧,还有第二道检查:

	if (delta_exec < sysctl_sched_min_granularity)
		return;

sysctl_sched_min_granularity 默认 750000ns(0.75ms,fair.c:59,还会按 1+ilog(ncpus) 缩放),对应 proc 接口 /proc/sys/kernel/sched_min_granularity_ns。进程没跑够最小粒度,tick 阶段一律不抢。这是防止两个进程被频繁"乒乓"切换:如果抢占太敏感,切换开销本身(TLB 刷、cache 变冷)会把吞吐拖垮。

跑够了最小粒度,则和红黑树最左边的节点比一比:

	se = __pick_first_entity(cfs_rq);
	delta = curr->vruntime - se->vruntime;

	if (delta < 0)
		return;

	if (delta > ideal_runtime)
		resched_curr(rq_of(cfs_rq));
  • __pick_first_entity:取 cfs_rq 红黑树中 vruntime 最小的进程,我们在"pick_next_task_fair(三)"里拆过;
  • delta < 0:当前进程的 vruntime 比树里最左边的还小,说明它本来就该继续跑,返回;
  • delta > ideal_runtime:当前进程的 vruntime 已经领先最左节点超过一个时间片了,说明它"占"得太多,换最左节点上来,打重调度标记。

这个第二层判断对应注释里的场景:唤醒抢占(check_preempt_wakeup)有 wakeup granularity 的保护,一个新唤醒的进程可能以微弱差距没抢到 CPU;如果没有这层检查,它就得等当前进程跑完一整个时间片。tick 里的这次 vruntime 比较给了"差一点没抢成"的进程一次补抢的机会。

五、时间片怎么算:sched_slice 与 __sched_period

ideal_runtime 来自 sched_slice(fair.c:674),而它又依赖 __sched_period(fair.c:660):

/*
 * The idea is to set a period in which each task runs once.
 *
 * When there are too many tasks (sched_nr_latency) we have to stretch
 * this period because otherwise the slices get too small.
 *
 * p = (nr <= nl) ? l : l*nr/nl
 */
static u64 __sched_period(unsigned long nr_running)
{
	if (unlikely(nr_running > sched_nr_latency))
		return nr_running * sysctl_sched_min_granularity;
	else
		return sysctl_sched_latency;
}

设计思想写在注释里:设定一个周期 period,让每个可运行进程在周期内都能轮到一次。

  • sysctl_sched_latency 默认 6ms(fair.c:38),即默认调度周期;
  • sched_nr_latency 默认 8(fair.c:65),等于 latency / min_granularity,即"6ms 周期内按每人 0.75ms 最多能体面地容纳 8 个进程";
  • 进程数 ≤ 8:周期就是 6ms,每人分 6ms × (权重/总权重);
  • 进程数 > 8:6ms 不够分了,每人分到的会小于最小粒度,于是把周期拉长为 nr_running × 0.75ms,保证每人至少 0.75ms。
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	unsigned int nr_running = cfs_rq->nr_running;
	u64 slice;

	if (sched_feat(ALT_PERIOD))
		nr_running = rq_of(cfs_rq)->cfs.h_nr_running;

	slice = __sched_period(nr_running + !se->on_rq);

	for_each_sched_entity(se) {
		struct load_weight *load;
		struct load_weight lw;

		cfs_rq = cfs_rq_of(se);
		load = &cfs_rq->load;

		if (unlikely(!se->on_rq)) {
			lw = cfs_rq->load;
			update_load_add(&lw, se->load.weight);
			load = &lw;
		}
		slice = __calc_delta(slice, se->load.weight, load);
	}

	if (sched_feat(BASE_SLICE))
		slice = max(slice, (u64)sysctl_sched_min_granularity);

	return slice;
}

按权重分周期,三步:

  1. 算周期:__sched_period(nr_running + !se->on_rq)。ALT_PERIOD 特性默认开启(features.h:97),用整个 rq 的层级进程数 h_nr_running 而不是单个 cfs_rq 的 nr_running。+ !se->on_rq 是因为 se 若还没入队(比如在 enqueue 路径算时间片),统计 nr_running 时没算上它,要补一个;tick 路径里 curr 在运行中、on_rq 为 0(cfs 的 current 不在树上),实际是 nr_running + 1。
  2. 按权重折算:__calc_delta(slice, se->load.weight, load) 即 slice × se权重 / 队列总权重,和 vruntime 折算用的是同一个函数。组调度时逐层折算。
  3. 保底:BASE_SLICE 特性默认开启(features.h:98),时间片最低不小于 min_granularity(0.75ms),避免 nice 值太低(权重太小)的进程拿到过于碎的时间片。

六、resched_curr:只打标记,不做切换

两层判断的最终出口都是 resched_curr(core.c:962):

void resched_curr(struct rq *rq)
{
	struct task_struct *curr = rq->curr;
	int cpu;

	lockdep_assert_rq_held(rq);

	if (test_tsk_need_resched(curr))
		return;

	cpu = cpu_of(rq);

	if (cpu == smp_processor_id()) {
		set_tsk_need_resched(curr);
		set_preempt_need_resched();
		return;
	}

	if (set_nr_and_not_polling(curr))
		smp_send_reschedule(cpu);
	else
		trace_sched_wake_idle_without_ipi(cpu);
}

逻辑很直白:

  • 已经打过标记了就直接返回(幂等);
  • tick 一般就在本 CPU 上触发,走 set_tsk_need_resched:给当前进程的 thread_info.flags 置上 TIF_NEED_RESCHED 位,并设置抢占计数中的 need_resched 标记;
  • 如果被标记的进程在别的 CPU 上(跨核场景,比如负载均衡、唤醒路径调过来),就要发 IPI 核间中断 smp_send_reschedule 踢那个 CPU 一脚。

再次强调:这里没有任何进程切换。它只是在进程身上插了面旗子,等中断返回用户态、或内核抢占点检查时,旗子被看到,才进入 __schedule 做真正的切换。这就是 Linux"延迟调度"的经典设计——抢占决策可以发生在任何上下文,但切换只在安全点发生。

小结

本篇把 CFS 周期性 tick 的抢占路径走完了,整条链路:

  1. scheduler_tick 按调度类分发到 task_tick_fair;
  2. entity_tick 先 update_curr 记账(vruntime 增长的主战场),再判断是否需要抢占检查;
  3. check_preempt_tick 两层判断:
    • 硬超时:本轮实际运行 delta_exec > ideal_runtime(时间片),必换,且清 buddy 防偏袒;
    • 软比较:跑够 min_granularity(默认 0.75ms)后,若当前进程 vruntime 领先红黑树最左节点超过一个时间片,换;
  4. 时间片 = 调度周期(默认 6ms,进程数 >8 时按 0.75ms×进程数 拉长)× 权重占比,保底 0.75ms;
  5. resched_curr 只打 TIF_NEED_RESCHED 标记,真正的切换留给 __schedule。

至此,CFS 的三条抢占路径(enqueue / wakeup / tick)就齐了。下一篇我们回到第十三篇挖的坑——__schedule 主流程,看看 TIF_NEED_RESCHED 被置上之后,进程切换到底是怎么发生的。

内核版本:linux-5.15.140> 涉及文件:kernel/sched/core.c、kernel/sched/fair.c、kernel/sched/features.h## 前言在前面几篇里,我们已经把 CFS 的两条抢占路径看过了:- enqueue 路径:新进程入队时 check_preempt_curr(见 enqueue 系列);- wakeup 路径:进程被唤醒时 check_preempt_wakeup(上一篇)。本篇看第三条、也是平时触发最频繁的一条:周期性时钟中断(tick)里的抢占检查。一个进程如果在 CPU 上跑得太久,不能等它自己让出,tick 会定期检查"你跑够了吗?跑够了就换下一个"。这条链路的调用关系是:scheduler_tick() kernel/sched/core.c:5284 └── curr->sched_class->task_tick() → task_tick_fair() kernel/sched/fair.c:11449 └── entity_tick() kernel/sched/fair.c:4789 └── check_preempt_tick() kernel/sched/fair.c:4629我们自下而上,逐个函数拆开看。—## 一、入口:scheduler_tick每个 CPU 的周期性时钟中断都会走到 scheduler_tick(core.c:5284):cvoid scheduler_tick(void){ int cpu = smp_processor_id(); struct rq *rq = cpu_rq(cpu); struct task_struct *curr = rq->curr; struct rq_flags rf; unsigned long thermal_pressure; u64 resched_latency; arch_scale_freq_tick(); sched_clock_tick(); rq_lock(rq, &rf); update_rq_clock(rq); thermal_pressure = arch_scale_thermal_pressure(cpu_of(rq)); update_thermal_load_avg(rq_clock_thermal(rq), rq, thermal_pressure); curr->sched_class->task_tick(rq, curr, 0); ... rq_unlock(rq, &rf); ...}关键的一行是 curr->sched_class->task_tick(rq, curr, 0)。这就是我们在第七篇讲过的 sched_class 多态:当前进程属于哪个调度类,就调哪个调度类的 task_tick 钩子。对普通进程来说就是 CFS 的 task_tick_fair(挂在 fair.c:12051 的 fair_sched_class 定义里)。第三个参数 queued 传 0,表示这是周期性 tick(而不是 hrtick 高精度定时器触发的 tick),这个参数后面有用。## 二、task_tick_fair:遍历调度实体层级cstatic void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued){ struct cfs_rq *cfs_rq; struct sched_entity *se = &curr->se; for_each_sched_entity(se) { cfs_rq = cfs_rq_of(se); entity_tick(cfs_rq, se, queued); } if (static_branch_unlikely(&sched_numa_balancing)) task_tick_numa(rq, curr); update_misfit_status(curr, rq); update_overutilized_status(task_rq(curr)); task_tick_core(rq, curr);}````for_each_sched_entity(se)` 这个循环我们在 enqueue 系列里见过:如果开了组调度(CONFIG_FAIR_GROUP_SCHED),一个进程的调度实体可能嵌套在多层 task_group 的 cfs_rq 里,需要自底向上逐层处理;没开组调度时就只有一层,循环只执行一次。后面几行是其他子系统的 tick 钩子,本篇不展开:- `task_tick_numa`:NUMA balancing 相关,只有开了自动 NUMA 均衡才走;- `update_misfit_status` / `update_overutilized_status`:EAS(能耗感知调度)用,标记该 CPU 上的任务是否"放错了核"或核是否过载;- `task_tick_core`:CONFIG_SCHED_CORE(core scheduling,防侧信道攻击特性)的 tick 处理。主线在 `entity_tick`。## 三、entity_tick:先记账,再检查抢占cstatic voidentity_tick(struct cfs_rq *cfs_rq, struct sched_entity curr, int queued){ / * Update run-time statistics of the ‘current’. / update_curr(cfs_rq); / * Ensure that runnable average is periodically updated. / update_load_avg(cfs_rq, curr, UPDATE_TG); update_cfs_group(curr);#ifdef CONFIG_SCHED_HRTICK / * queued ticks are scheduled to match the slice, so don’t bother * validating it and just reschedule. / if (queued) { resched_curr(rq_of(cfs_rq)); return; } / * don’t let the period tick interfere with the hrtick preemption */ if (!sched_feat(DOUBLE_TICK) && hrtimer_active(&rq_of(cfs_rq)->hrtick_timer)) return;#endif if (cfs_rq->nr_running > 1) check_preempt_tick(cfs_rq, curr);}三件事:**1. `update_curr(cfs_rq)`**:把从上次记账到现在流逝的时间记到当前进程头上——累加 `sum_exec_runtime`、按权重折算累加 `vruntime`。这个函数我们在"enqueue(四)"那篇已经逐行看过,此处不再展开,只需要记住:**tick 是当前进程 vruntime 增长的主要时机之一**。**2. `update_load_avg`**:PELT 负载衰减累加,为负载均衡提供数据。这块是个大专题,留给后面负载均衡系列再拆。**3. CONFIG_SCHED_HRTICK 分支**:高精度 tick 特性。如果开了它,内核会按时间片精确安排一个 hrtimer,到点直接 `resched_curr` 强制重调度(`queued` 为真时)。而普通的周期 tick 若撞见 hrtick 定时器还在跑,就直接返回不重复检查(`DOUBLE_TICK` feature 默认关闭,features.h:42)。一般发行版默认不开这个配置,了解即可。最后一句是本篇正题:**只有 cfs_rq 上可运行的进程数大于 1,才有"抢占"可言**,否则就一个进程,抢谁的去。## 四、check_preempt_tick:抢占判断的核心逻辑cstatic voidcheck_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr){ unsigned long ideal_runtime, delta_exec; struct sched_entity se; s64 delta; ideal_runtime = sched_slice(cfs_rq, curr); delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; if (delta_exec > ideal_runtime) { resched_curr(rq_of(cfs_rq)); / * The current task ran long enough, ensure it doesn’t get * re-elected due to buddy favours. / clear_buddies(cfs_rq, curr); return; } / * Ensure that a task that missed wakeup preemption by a * narrow margin doesn’t have to wait for a full slice. * This also mitigates buddy induced latencies under load. */ if (delta_exec < sysctl_sched_min_granularity) return; se = __pick_first_entity(cfs_rq); delta = curr->vruntime - se->vruntime; if (delta < 0) return; if (delta > ideal_runtime) resched_curr(rq_of(cfs_rq));}这个函数只有二十几行,但分了两层判断,一层"硬超时",一层"软比较"。先解释两个变量:- `ideal_runtime`:当前进程本轮**应该**跑的时间片,由 `sched_slice` 算出;- `delta_exec`:当前进程本轮**实际已经**跑的时间,即 `sum_exec_runtime - prev_sum_exec_runtime`。`prev_sum_exec_runtime` 是它被 `set_next_entity` 摘上 CPU 时记下的起点。### 4.1 第一层:跑超时间片了,无条件换c if (delta_exec > ideal_runtime) { resched_curr(rq_of(cfs_rq)); clear_buddies(cfs_rq, curr); return; }实际运行时间超过应得时间片,直接 `resched_curr` 给当前进程打上 `TIF_NEED_RESCHED` 标记。注意这里只是**打标记**,真正的切换发生在之后中断返回/抢占检查时的 `__schedule`(第十三篇的坑,下一篇填)。`clear_buddies` 是清掉 CFS 的 "buddy" 缓存(last/next/skip 三个指针)。buddy 机制会在某些场景下偏袒上一个运行的进程(缓存亲和),但既然它都跑超时间片了,就不能再让它靠 buddy 优惠立刻被选回来,注释里说的 "buddy favours" 就是这个意思。### 4.2 第二层:最小粒度保护 + vruntime 比较如果没超时,并不马上高枕无忧,还有第二道检查:c if (delta_exec < sysctl_sched_min_granularity) return;````sysctl_sched_min_granularity默认 750000ns(0.75ms,fair.c:59,还会按1+ilog(ncpus)缩放),对应 proc 接口/proc/sys/kernel/sched_min_granularity_ns。**进程没跑够最小粒度,tick 阶段一律不抢**。这是防止两个进程被频繁"乒乓"切换:如果抢占太敏感,切换开销本身(TLB 刷、cache 变冷)会把吞吐拖垮。跑够了最小粒度,则和红黑树最左边的节点比一比:```c se = __pick_first_entity(cfs_rq); delta = curr->vruntime - se->vruntime; if (delta < 0) return; if (delta > ideal_runtime) resched_curr(rq_of(cfs_rq));```- __pick_first_entity:取 cfs_rq 红黑树中 vruntime 最小的进程,我们在"pick_next_task_fair(三)"里拆过;- delta < 0:当前进程的 vruntime 比树里最左边的还小,说明它本来就该继续跑,返回;- delta > ideal_runtime:当前进程的 vruntime 已经领先最左节点超过一个时间片了,说明它"占"得太多,换最左节点上来,打重调度标记。这个第二层判断对应注释里的场景:唤醒抢占(check_preempt_wakeup)有 wakeup granularity 的保护,一个新唤醒的进程可能以微弱差距没抢到 CPU;如果没有这层检查,它就得等当前进程跑完一整个时间片。tick 里的这次 vruntime 比较给了"差一点没抢成"的进程一次补抢的机会。## 五、时间片怎么算:sched_slice 与 __sched_periodideal_runtime来自sched_slice(fair.c:674),而它又依赖 __sched_period(fair.c:660):```c/* * The idea is to set a period in which each task runs once. * * When there are too many tasks (sched_nr_latency) we have to stretch * this period because otherwise the slices get too small. * * p = (nr <= nl) ? l : l*nr/nl */static u64 __sched_period(unsigned long nr_running){ if (unlikely(nr_running > sched_nr_latency)) return nr_running * sysctl_sched_min_granularity; else return sysctl_sched_latency;}```设计思想写在注释里:**设定一个周期 period,让每个可运行进程在周期内都能轮到一次**。- sysctl_sched_latency默认 6ms(fair.c:38),即默认调度周期;-sched_nr_latency默认 8(fair.c:65),等于 latency / min_granularity,即"6ms 周期内按每人 0.75ms 最多能体面地容纳 8 个进程";- 进程数 ≤ 8:周期就是 6ms,每人分 6ms × (权重/总权重);- 进程数 > 8:6ms 不够分了,每人分到的会小于最小粒度,于是把周期拉长为nr_running × 0.75ms,保证每人至少 0.75ms。```cstatic u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se){ unsigned int nr_running = cfs_rq->nr_running; u64 slice; if (sched_feat(ALT_PERIOD)) nr_running = rq_of(cfs_rq)->cfs.h_nr_running; slice = __sched_period(nr_running + !se->on_rq); for_each_sched_entity(se) { struct load_weight *load; struct load_weight lw; cfs_rq = cfs_rq_of(se); load = &cfs_rq->load; if (unlikely(!se->on_rq)) { lw = cfs_rq->load; update_load_add(&lw, se->load.weight); load = &lw; } slice = __calc_delta(slice, se->load.weight, load); } if (sched_feat(BASE_SLICE)) slice = max(slice, (u64)sysctl_sched_min_granularity); return slice;}```按权重分周期,三步:1. **算周期**:__sched_period(nr_running + !se->on_rq)。ALT_PERIOD特性默认开启(features.h:97),用整个 rq 的层级进程数h_nr_running 而不是单个 cfs_rq 的 nr_running。+ !se->on_rq是因为 se 若还没入队(比如在 enqueue 路径算时间片),统计 nr_running 时没算上它,要补一个;tick 路径里 curr 在运行中、on_rq 为 0(cfs 的 current 不在树上),实际是nr_running + 1。2. **按权重折算**:__calc_delta(slice, se->load.weight, load)即slice × se权重 / 队列总权重,和 vruntime 折算用的是同一个函数。组调度时逐层折算。3. **保底**:BASE_SLICE特性默认开启(features.h:98),时间片最低不小于 min_granularity(0.75ms),避免 nice 值太低(权重太小)的进程拿到过于碎的时间片。## 六、resched_curr:只打标记,不做切换两层判断的最终出口都是resched_curr(core.c:962):```cvoid resched_curr(struct rq *rq){ struct task_struct *curr = rq->curr; int cpu; lockdep_assert_rq_held(rq); if (test_tsk_need_resched(curr)) return; cpu = cpu_of(rq); if (cpu == smp_processor_id()) { set_tsk_need_resched(curr); set_preempt_need_resched(); return; } if (set_nr_and_not_polling(curr)) smp_send_reschedule(cpu); else trace_sched_wake_idle_without_ipi(cpu);}```逻辑很直白:- 已经打过标记了就直接返回(幂等);- tick 一般就在本 CPU 上触发,走 set_tsk_need_resched:给当前进程的 thread_info.flags置上TIF_NEED_RESCHED位,并设置抢占计数中的 need_resched 标记;- 如果被标记的进程在别的 CPU 上(跨核场景,比如负载均衡、唤醒路径调过来),就要发 IPI 核间中断smp_send_reschedule踢那个 CPU 一脚。再次强调:**这里没有任何进程切换**。它只是在进程身上插了面旗子,等中断返回用户态、或内核抢占点检查时,旗子被看到,才进入__schedule做真正的切换。这就是 Linux"延迟调度"的经典设计——抢占决策可以发生在任何上下文,但切换只在安全点发生。## 小结本篇把 CFS 周期性 tick 的抢占路径走完了,整条链路:1.scheduler_tick按调度类分发到task_tick_fair;2. entity_tick先update_curr记账(vruntime 增长的主战场),再判断是否需要抢占检查;3.check_preempt_tick两层判断: - **硬超时**:本轮实际运行delta_exec > ideal_runtime(时间片),必换,且清 buddy 防偏袒; - **软比较**:跑够 min_granularity(默认 0.75ms)后,若当前进程 vruntime 领先红黑树最左节点超过一个时间片,换;4. 时间片 = 调度周期(默认 6ms,进程数 >8 时按 0.75ms×进程数 拉长)× 权重占比,保底 0.75ms;5. resched_curr只打TIF_NEED_RESCHED标记,真正的切换留给__schedule。至此,CFS 的三条抢占路径(enqueue / wakeup / tick)就齐了。下一篇我们回到第十三篇挖的坑——__schedule主流程,看看TIF_NEED_RESCHED` 被置上之后,进程切换到底是怎么发生的。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐