schedule() 主流程源码分析

schedule

task被调度的原因主要分为一下几类

我们看函数的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current; //保存当前任务

sched_submit_work(tsk);

do {
preempt_disable();
__schedule(SM_NONE); //核心
sched_preempt_enable_no_resched();
} while (need_resched());

sched_update_worker(tsk);
}
EXPORT_SYMBOL(schedule);

进入核心调度__schedule

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
static void __sched notrace
__schedule(unsigned int sched_mode)
{
struct task_struct *prev;
struct task_struct *next;
struct rq_flags rf;
struct rq *rq;
int cpu;

cpu = smp_processor_id(); //找到当前运行的CPU
rq = cpu_rq(cpu); // 获取当前CPU的rq
prev = rq->curr; // 当前CPU正在运行谁

/* 调度前检查 */

local_irq_disable(); // 关闭中断

rq_lock(rq, &rf); // 获取rq锁

update_rq_clock(rq);

/* 根据sched_mode和prev状态处理prev */
if (prev不再Runnable)
deactivate_task(rq, prev, ...);

next = pick_next_task(rq, prev, &rf);

clear_tsk_need_resched(prev); // 选择下一个task

if (prev != next) {
rq->curr = next;

context_switch(rq, prev, next, &rf); //上下文切换
} else {
rq_unlock(...);
}
}

上面是整个逻辑部分,其中还有些细节,我们看prev是什么时候被移除rq的

1
2
3
4
5
6
7
8
9
10
prev_state = READ_ONCE(prev->__state);

if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) {
if (signal_pending_state(prev_state, prev)) {
WRITE_ONCE(prev->__state, TASK_RUNNING);
} else {
deactivate_task(rq, prev,
DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
}
}

整个思路如下

接着向下看,接下来就是选择下一个要运行的任务pick_next_task

1
next = pick_next_task(rq, prev, &rf);

其选择的步骤是先选择调度类,再由调度类选择下一个任务

具体代码后续分析。

完整调度逻辑如下

pick_next_task

上一小节说了在__schedule里会执行pick_next_task选择要切换的task,这一节主要
讲深入分析这个函数。

1
2
3
4
next = pick_next_task(rq, prev, &rf);
rq:当前CPU的runqueue
prev:前一个任务
rf:runqueue锁状态

之前说过,内核中有很多种类的任务,pick_next_task是总入口。
在选择任务的时候分两大步,先根据调度类的优先级确定调度类,再根据调度类的
内部算法选择下一个任务。

1
2
3
4
5
6
7
static struct task_struct *
pick_next_task(struct rq *rq,
struct task_struct *prev,
struct rq_flags *rf)
{
return __pick_next_task(rq, prev, rf);
}

先屏蔽其他代码,跟schedule一样,真正业务在__pick_next_task里面。

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
static inline struct task_struct *
__pick_next_task(struct rq *rq,
struct task_struct *prev,
struct rq_flags *rf)
{
const struct sched_class *class; // 指向某个调度类
struct task_struct *p; // 选出来的具体任务

/* 公平调度类快速路径 */
if (likely(
!sched_class_above(prev->sched_class,
&fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {

p = pick_next_task_fair(rq, prev, rf);

if (unlikely(p == RETRY_TASK))
goto restart;

if (!p) {
put_prev_task(rq, prev);
p = pick_next_task_idle(rq);
}

return p;
}

restart:
put_prev_task_balance(rq, prev, rf);

...

/* 通用调度类遍历 */
for_each_class(class) {
p = class->pick_next_task(rq);

if (p)
return p;
}

BUG();
}

主要分了两条路,一条公平调度类快速路径,一条通用慢路径。
这样做的目的是为了优化全是普通任务的场景,不用每次都
判断到底是那个调度类。

整个选择逻辑如下

context_switch()

在选出next并与prev,会进行上下文切换,核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static __always_inline struct rq *
context_switch(struct rq *rq,
struct task_struct *prev,
struct task_struct *next,
struct rq_flags *rf)
{
prepare_task_switch(rq, prev, next); // 切换前准备

/*
* 切换或处理地址空间
*/
...

prepare_lock_switch(rq, next, rf);

/*
* 切换寄存器和内核栈
*/
switch_to(prev, next, prev);

barrier();

return finish_task_switch(prev); // 切换后收尾工作
}

一次完整的切换过程,并不只是切换几个寄存器,而是要让CPU从 prev 的地址空间、内核栈和线程状态,过渡到 next 的对应状态。主要有两部分,一部分是地址空间上下文,一部分是CPU执行上下文。

先看地址空间切换的核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!next->mm) {                     /* 切到内核线程 */
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;

if (prev->mm) /* 用户任务 → 内核线程 */
mmgrab(prev->active_mm);
else /* 内核线程 → 内核线程 */
prev->active_mm = NULL;
} else { /* 切到用户任务 */
membarrier_switch_mm(rq, prev->active_mm, next->mm);

switch_mm_irqs_off(prev->active_mm, next->mm, next);
lru_gen_use_mm(next->mm);

if (!prev->mm) { /* 内核线程 → 用户任务 */
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
}

task的地址相关信息页保存在task_struct中,有关切换的信息有两个

1
2
struct mm_struct *mm;
struct mm_struct *active_mm;

其中mm表示任务自己的虚拟空间地址

active_mm表示这个任务运行时,CPU实际关联的地址空间。

上面的代码处理四种情况:

  • 用户任务切换到用户任务
  • 用户任务切换到内核线程
  • 内核线程切换到用户任务
  • 内核线程切换到内核线程

当从用户任务切换到用户任务的时候,会走右边的分支
当从用户任务切换到内核线程时,走左边的分支


schedule() 主流程源码分析
https://cj0510.github.io/2026/08/23/Linux Kernel/Scheduling/调度主流程源码分析/
作者
CJ1018
发布于
2026年8月23日
许可协议
CC BY-NC-SA 4.0