【Linux内核三十】进程管理模块:CFS调度器周期性tick:entity_tick与check_preempt_tick
内核版本: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;
}
按权重分周期,三步:
- 算周期:
__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。 - 按权重折算:
__calc_delta(slice, se->load.weight, load)即slice × se权重 / 队列总权重,和 vruntime 折算用的是同一个函数。组调度时逐层折算。 - 保底:
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 的抢占路径走完了,整条链路:
scheduler_tick按调度类分发到task_tick_fair;entity_tick先update_curr记账(vruntime 增长的主战场),再判断是否需要抢占检查;check_preempt_tick两层判断:- 硬超时:本轮实际运行
delta_exec > ideal_runtime(时间片),必换,且清 buddy 防偏袒; - 软比较:跑够 min_granularity(默认 0.75ms)后,若当前进程 vruntime 领先红黑树最左节点超过一个时间片,换;
- 硬超时:本轮实际运行
- 时间片 = 调度周期(默认 6ms,进程数 >8 时按 0.75ms×进程数 拉长)× 权重占比,保底 0.75ms;
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` 被置上之后,进程切换到底是怎么发生的。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)