从 scheduler_tick 到上下文切换:深入解析 Linux 内核的 TIF_NEED_RESCHED 标志设置流程

Linux 是如何决定何时进行上下文切换的?

在Linux中,CPU 上下文切换是指当操作系统将 CPU 从一个进程切换到另一个进程时,保存当前进程的执行状态,并加载新进程的执行状态的过程就称为上下文切换。

但在 Linux 内核中,是否切换进程 通常由一个关键标志位 TIF_NEED_RESCHED 来决定。

当该标志被设置时,内核会在合适的时机(例如从中断返回或系统调用结束时)调用schedule(),从而触发上下文切换。

TIF_NEED_RESCHED标志的设置过程

刚才我们提到切换进程或任务是由TIF_NEED_RESCHED标志位来判断,那这个标志位是如何设置的呢

接下来,我们将从 scheduler_tick() 函数开始,逐步揭示这个标志位是如何被设置,并最终触发上下文切换的。

1. scheduler_tick():调度的"心跳"

让我们先从**scheduler_tick()**函数开始 说起

scheduler_tick() 是由定时器中断(以 HZ 频率)触发的调度驱动函数。它会获取当前 CPU 上正在运行的进程,并调用该进程所属调度器的 task_tick() 方法,然后根据具体对应的调度策略判断是否需要重新调度。

复制代码
// kernel/sched/core.c
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;
    // ...

	rq_lock(rq, &rf); // 加锁以保护运行队列

	// ... 省略 ...

	// 这是核心调用:根据任务类型调用其对应的 task_tick 方法
	curr->sched_class->task_tick(rq, curr, 0);

    // ... 其他逻辑 ...

	rq_unlock(rq, &rf); // 解锁

    // ...
}

在上述代码中,最关键的一行是 curr->sched_class->task_tick(rq, curr, 0);

这行代码是调度器逻辑的入口,它根据当前运行任务 (curr) 所属的调度类 (sched_class),动态地调用其特有的 task_tick 方法。对于 CFS(完全公平调度器)任务,就会调用 task_tick_fair(),从而进入具体的调度决策流程。

2. task_tick_fair():CFS 的任务周期调度

刚才提到过每种调度策略都会通过一个sched_class结构体定义其行为,完全公平调度器(CFS)的调度类定义在**kernel/sched/fair.c** 文件中,它通过 DEFINE_SCHED_CLASS(fair) 结构体被绑定到 task_tick 接口上。

复制代码
/* kernel/sched/fair.c */
DEFINE_SCHED_CLASS(fair) = {
    .enqueue_task       = enqueue_task_fair,
    .dequeue_task       = dequeue_task_fair,
    .yield_task         = yield_task_fair,
    ...
    .task_tick          = task_tick_fair,  // 👈 心跳函数绑定在这里
    ...
    .update_curr        = update_curr_fair,
};

那我们接着就看一下task_tick_fair函数

复制代码
/*
 * scheduler tick hitting a task of our scheduling class.
 *
 * NOTE: This function can be called remotely by the tick offload that
 * goes along full dynticks. Therefore no local assumption can be made
 * and everything must be accessed through the @rq and @curr passed in
 * parameters.
 */
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);
}

首先CFS 把调度的最小单位抽象成 sched_entity,它即可以是线程也可以是进程组。每个sched_entity都会对应一个 运行队列

复制代码
struct cfs_rq *cfs_rq;
struct sched_entity *se = &curr->se;

接着**for_each_sched_entity循环会一直向上遍历到最顶层的调度实体**(例如:线程 → 进程组 → 父组),并在每一层都调用 entity_tick() 函数。

复制代码
for_each_sched_entity(se) {
	cfs_rq = cfs_rq_of(se);
	entity_tick(cfs_rq, se, queued);
}

3. entity_tick()update_deadline():判断是否"超时"

entity_tick() 函数中会调用 update_curr(),该函数会负责更新当前任务的运行时间统计信息,并在此过程中判断任务是否已超出其分配的时间片。

update_curr() 随后会调用 update_deadline(),这里便是我们寻找的触发点。该函数会更新当前任务的运行时间,并判断是否需要触发调度。我们先看一下update_deadline的具体代码

复制代码
/*
 * XXX: strictly: vd_i += N*r_i/w_i such that: vd_i > ve_i
 * this is probably good enough.
 */
static void update_deadline(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	if ((s64)(se->vruntime - se->deadline) < 0)
		return;

	/*
	 * For EEVDF the virtual time slope is determined by w_i (iow.
	 * nice) while the request time r_i is determined by
	 * sysctl_sched_base_slice.
	 */
	se->slice = sysctl_sched_base_slice;

	/*
	 * EEVDF: vd_i = ve_i + r_i / w_i
	 */
	se->deadline = se->vruntime + calc_delta_fair(se->slice, se);

	/*
	 * The task has consumed its request, reschedule.
	 */
	if (cfs_rq->nr_running > 1) {
		resched_curr(rq_of(cfs_rq));
		clear_buddies(cfs_rq, se);
	}
}

当Linux中任务的虚拟运行时间超过其截止时间,并且运行队列 (cfs_rq) 中有其他可运行的任务时,就会认为当前任务的时间片已用完。此时就会调用 resched_curr() 从而设置TIF_NEED_RESCHED 标志位了

复制代码
if (cfs_rq->nr_running > 1) {
	resched_curr(rq_of(cfs_rq));
	clear_buddies(cfs_rq, se); 
}

4. resched_curr():设置 TIF_NEED_RESCHED 标志

最后再看一下**resched_curr**的代码,代码在kernel/sched/core.c中

复制代码
/*
 * resched_curr - mark rq's current task 'to be rescheduled now'.
 *
 * On UP this means the setting of the need_resched flag, on SMP it
 * might also involve a cross-CPU call to trigger the scheduler on
 * the target CPU.
 */
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);
}

检查是否已标记 resched_curr函数首先会检查当前任务 (curr) 的 need_resched 标志是否已经被设置。如果已经被设置,说明任务已经被标记为需要调度就直接返回避免重复操作。

复制代码
if (test_tsk_need_resched(curr))
    return;

处理当前对应CPU核心上的调度

这段代码检查 resched_curr 是否在当前 CPU 上被调用。如果是,它会执行两个关键步骤:

  • set_tsk_need_resched(curr): 显式地设置当前任务的 TIF_NEED_RESCHED 标志。

  • set_preempt_need_resched(): 告诉抢占机制,在下一个安全点(例如从中断返回或系统调用结束时),应该检查该标志并立即进行一次上下文切换。

    if (cpu == smp_processor_id()) {
    set_tsk_need_resched(curr); // 标记当前任务需要被调度
    set_preempt_need_resched(); // 触发抢占检查
    return;
    }

总结:need_resched 的调用链

现在,我们可以清晰地梳理出 TIF_NEED_RESCHED 标志的完整设置流程:

这个调用链展示了 Linux 内核如何利用一个周期性的定时器中断,结合 CFS 调度器的公平性原则,最终实现了抢占式多任务的调度核心。

完整代码参考: 完整的源码可以在 Linux 内核源码的 kernel/sched/fair.ckernel/sched/core.c 文件中找到这些函数的完整实现。

linux/kernel/sched/fair.c at master · torvalds/linux · GitHublinux/kernel/sched/core.c at master · torvalds/linux · GitHub

相关推荐
阿让啊7 小时前
C语言strtol 函数使用方法
c语言·数据结构·c++·单片机·嵌入式硬件
方渐鸿8 小时前
【2024】k8s集群 图文详细 部署安装使用(两万字)
java·运维·容器·kubernetes·k8s·运维开发·持续部署
我爱云计算8 小时前
K8S详解(5万字详细教程)
linux·运维·云原生·容器·kubernetes
明明跟你说过8 小时前
【k8s】资源限制管理:Namespace、Deployment与Pod的实践
运维·docker·云原生·容器·kubernetes·k8s
2301_7943339111 小时前
实验室服务器配置|通过Docker实现Linux系统多用户隔离与安全防控
linux·服务器·docker·实验室
DebugKitty11 小时前
硬件开发1-51单片机4-DS18B20
单片机·嵌入式硬件·51单片机·ds18b20
Hello_Embed11 小时前
STM32HAL 快速入门(十九):UART 编程(二)—— 中断方式实现收发及局限分析
笔记·stm32·单片机·嵌入式硬件·学习
沐欣工作室_lvyiyi11 小时前
基于单片机的可燃性气体泄漏智能报警系统
stm32·单片机·嵌入式硬件·毕业设计
打码人的日常分享11 小时前
运维服务方案,运维巡检方案,运维安全保障方案文件
大数据·运维·安全·word·安全架构