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(); rq = cpu_rq(cpu); prev = rq->curr;
local_irq_disable();
rq_lock(rq, &rf);
update_rq_clock(rq);
if (prev不再Runnable) deactivate_task(rq, prev, ...);
next = pick_next_task(rq, prev, &rf);
clear_tsk_need_resched(prev);
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实际关联的地址空间。
上面的代码处理四种情况:
- 用户任务切换到用户任务
- 用户任务切换到内核线程
- 内核线程切换到用户任务
- 内核线程切换到内核线程

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