<audio id="audio" title="16 | 调度（中）：主动调度是如何发生的？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9c/53/9c66465f00cbc22674ca5ac1220ef853.mp3"></audio>

上一节，我们为调度准备了这么多的数据结构，这一节我们来看调度是如何发生的。

所谓进程调度，其实就是一个人在做A项目，在某个时刻，换成做B项目去了。发生这种情况，主要有两种方式。

**方式一**：A项目做着做着，发现里面有一条指令sleep，也就是要休息一下，或者在等待某个I/O事件。那没办法了，就要主动让出CPU，然后可以开始做B项目。

**方式二**：A项目做着做着，旷日持久，实在受不了了。项目经理介入了，说这个项目A先停停，B项目也要做一下，要不然B项目该投诉了。

## 主动调度

我们这一节先来看方式一，主动调度。

这里我找了几个代码片段。**第一个片段是Btrfs，等待一个写入**。[B](https://zh.wikipedia.org/wiki/Btrfs)[trfs](https://zh.wikipedia.org/wiki/Btrfs)（B-Tree）是一种文件系统，感兴趣你可以自己去了解一下。

这个片段可以看作写入块设备的一个典型场景。写入需要一段时间，这段时间用不上CPU，还不如主动让给其他进程。

```
static void btrfs_wait_for_no_snapshoting_writes(struct btrfs_root *root)
{
......
	do {
		prepare_to_wait(&amp;root-&gt;subv_writers-&gt;wait, &amp;wait,
				TASK_UNINTERRUPTIBLE);
		writers = percpu_counter_sum(&amp;root-&gt;subv_writers-&gt;counter);
		if (writers)
			schedule();
		finish_wait(&amp;root-&gt;subv_writers-&gt;wait, &amp;wait);
	} while (writers);
}

```

另外一个例子是，**从Tap网络设备等待一个读取**。Tap网络设备是虚拟机使用的网络设备。当没有数据到来的时候，它也需要等待，所以也会选择把CPU让给其他进程。

```
static ssize_t tap_do_read(struct tap_queue *q,
			   struct iov_iter *to,
			   int noblock, struct sk_buff *skb)
{
......
	while (1) {
		if (!noblock)
			prepare_to_wait(sk_sleep(&amp;q-&gt;sk), &amp;wait,
					TASK_INTERRUPTIBLE);
......
		/* Nothing to read, let's sleep */
		schedule();
	}
......
}

```

你应该知道，计算机主要处理计算、网络、存储三个方面。计算主要是CPU和内存的合作；网络和存储则多是和外部设备的合作；在操作外部设备的时候，往往需要让出CPU，就像上面两段代码一样，选择调用schedule()函数。

接下来，我们就来看**schedule函数的调用过程**。

```
asmlinkage __visible void __sched schedule(void)
{
	struct task_struct *tsk = current;


	sched_submit_work(tsk);
	do {
		preempt_disable();
		__schedule(false);
		sched_preempt_enable_no_resched();
	} while (need_resched());
}

```

这段代码的主要逻辑是在__schedule函数中实现的。这个函数比较复杂，我们分几个部分来讲解。

```
static void __sched notrace __schedule(bool preempt)
{
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct rq_flags rf;
	struct rq *rq;
	int cpu;


	cpu = smp_processor_id();
	rq = cpu_rq(cpu);
	prev = rq-&gt;curr;
......

```

首先，在当前的CPU上，我们取出任务队列rq。

task_struct *prev指向这个CPU的任务队列上面正在运行的那个进程curr。为啥是prev？因为一旦将来它被切换下来，那它就成了前任了。

接下来代码如下：

```
next = pick_next_task(rq, prev, &amp;rf);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();

```

第二步，获取下一个任务，task_struct *next指向下一个任务，这就是**继任**。

pick_next_task的实现如下：

```
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;
	/*
	 * Optimization: we know that if all tasks are in the fair class we can call that function directly, but only if the @prev task wasn't of a higher scheduling class, because otherwise those loose the opportunity to pull in more work from other CPUs.
	 */
	if (likely((prev-&gt;sched_class == &amp;idle_sched_class ||
		    prev-&gt;sched_class == &amp;fair_sched_class) &amp;&amp;
		   rq-&gt;nr_running == rq-&gt;cfs.h_nr_running)) {
		p = fair_sched_class.pick_next_task(rq, prev, rf);
		if (unlikely(p == RETRY_TASK))
			goto again;
		/* Assumes fair_sched_class-&gt;next == idle_sched_class */
		if (unlikely(!p))
			p = idle_sched_class.pick_next_task(rq, prev, rf);
		return p;
	}
again:
	for_each_class(class) {
		p = class-&gt;pick_next_task(rq, prev, rf);
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}
}

```

我们来看again这里，就是咱们上一节讲的依次调用调度类。但是这里有了一个优化，因为大部分进程是普通进程，所以大部分情况下会调用上面的逻辑，调用的就是fair_sched_class.pick_next_task。

根据上一节对于fair_sched_class的定义，它调用的是pick_next_task_fair，代码如下：

```
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
	struct cfs_rq *cfs_rq = &amp;rq-&gt;cfs;
	struct sched_entity *se;
	struct task_struct *p;
	int new_tasks;

```

对于CFS调度类，取出相应的队列cfs_rq，这就是我们上一节讲的那棵红黑树。

```
		struct sched_entity *curr = cfs_rq-&gt;curr;
		if (curr) {
			if (curr-&gt;on_rq)
				update_curr(cfs_rq);
			else
				curr = NULL;
......
		}
		se = pick_next_entity(cfs_rq, curr);

```

取出当前正在运行的任务curr，如果依然是可运行的状态，也即处于进程就绪状态，则调用update_curr更新vruntime。update_curr咱们上一节就见过了，它会根据实际运行时间算出vruntime来。

接着，pick_next_entity从红黑树里面，取最左边的一个节点。这个函数的实现我们上一节也讲过了。

```
	p = task_of(se);


	if (prev != p) {
		struct sched_entity *pse = &amp;prev-&gt;se;
......
		put_prev_entity(cfs_rq, pse);
		set_next_entity(cfs_rq, se);
	}


	return p

```

task_of得到下一个调度实体对应的task_struct，如果发现继任和前任不一样，这就说明有一个更需要运行的进程了，就需要更新红黑树了。前面前任的vruntime更新过了，put_prev_entity放回红黑树，会找到相应的位置，然后set_next_entity将继任者设为当前任务。

第三步，当选出的继任者和前任不同，就要进行上下文切换，继任者进程正式进入运行。

```
if (likely(prev != next)) {
		rq-&gt;nr_switches++;
		rq-&gt;curr = next;
		++*switch_count;
......
		rq = context_switch(rq, prev, next, &amp;rf);

```

## 进程上下文切换

上下文切换主要干两件事情，一是切换进程空间，也即虚拟内存；二是切换寄存器和CPU上下文。

我们先来看context_switch的实现。

```
/*
 * context_switch - switch to the new MM and the new thread's register state.
 */
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct rq_flags *rf)
{
	struct mm_struct *mm, *oldmm;
......
	mm = next-&gt;mm;
	oldmm = prev-&gt;active_mm;
......
	switch_mm_irqs_off(oldmm, mm, next);
......
	/* Here we just switch the register state and the stack. */
	switch_to(prev, next, prev);
	barrier();
	return finish_task_switch(prev);
}

```

这里首先是内存空间的切换，里面涉及内存管理的内容比较多。内存管理后面我们会有专门的章节来讲，这里你先知道有这么一回事就行了。

接下来，我们看switch_to。它就是寄存器和栈的切换，它调用到了__switch_to_asm。这是一段汇编代码，主要用于栈的切换。

对于32位操作系统来讲，切换的是栈顶指针esp。

```
/*
 * %eax: prev task
 * %edx: next task
 */
ENTRY(__switch_to_asm)
......
	/* switch stack */
	movl	%esp, TASK_threadsp(%eax)
	movl	TASK_threadsp(%edx), %esp
......
	jmp	__switch_to
END(__switch_to_asm)

```

对于64位操作系统来讲，切换的是栈顶指针rsp。

```
/*
 * %rdi: prev task
 * %rsi: next task
 */
ENTRY(__switch_to_asm)
......
	/* switch stack */
	movq	%rsp, TASK_threadsp(%rdi)
	movq	TASK_threadsp(%rsi), %rsp
......
	jmp	__switch_to
END(__switch_to_asm)

```

最终，都返回了__switch_to这个函数。这个函数对于32位和64位操作系统虽然有不同的实现，但里面做的事情是差不多的。所以我这里仅仅列出64位操作系统做的事情。

```
__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
	struct thread_struct *prev = &amp;prev_p-&gt;thread;
	struct thread_struct *next = &amp;next_p-&gt;thread;
......
	int cpu = smp_processor_id();
	struct tss_struct *tss = &amp;per_cpu(cpu_tss, cpu);
......
	load_TLS(next, cpu);
......
	this_cpu_write(current_task, next_p);


	/* Reload esp0 and ss1.  This changes current_thread_info(). */
	load_sp0(tss, next);
......
	return prev_p;
}

```

这里面有一个Per CPU的结构体tss。这是个什么呢？

在x86体系结构中，提供了一种以硬件的方式进行进程切换的模式，对于每个进程，x86希望在内存里面维护一个TSS（Task State Segment，任务状态段）结构。这里面有所有的寄存器。

另外，还有一个特殊的寄存器TR（Task Register，任务寄存器），指向某个进程的TSS。更改TR的值，将会触发硬件保存CPU所有寄存器的值到当前进程的TSS中，然后从新进程的TSS中读出所有寄存器值，加载到CPU对应的寄存器中。

下图就是32位的TSS结构。

<img src="https://static001.geekbang.org/resource/image/df/64/dfa9762cfec16822ec74d53350db4664.png" alt="">

但是这样有个缺点。我们做进程切换的时候，没必要每个寄存器都切换，这样每个进程一个TSS，就需要全量保存，全量切换，动作太大了。

于是，Linux操作系统想了一个办法。还记得在系统初始化的时候，会调用cpu_init吗？这里面会给每一个CPU关联一个TSS，然后将TR指向这个TSS，然后在操作系统的运行过程中，TR就不切换了，永远指向这个TSS。TSS用数据结构tss_struct表示，在x86_hw_tss中可以看到和上图相应的结构。

```
void cpu_init(void)
{
	int cpu = smp_processor_id();
	struct task_struct *curr = current;
	struct tss_struct *t = &amp;per_cpu(cpu_tss, cpu);
    ......
    load_sp0(t, thread);
	set_tss_desc(cpu, t);
	load_TR_desc();
    ......
}


struct tss_struct {
	/*
	 * The hardware state:
	 */
	struct x86_hw_tss	x86_tss;
	unsigned long		io_bitmap[IO_BITMAP_LONGS + 1];
} 

```

在Linux中，真的参与进程切换的寄存器很少，主要的就是栈顶寄存器。

于是，在task_struct里面，还有一个我们原来没有注意的成员变量thread。这里面保留了要切换进程的时候需要修改的寄存器。

```
/* CPU-specific state of this task: */
	struct thread_struct		thread;

```

所谓的进程切换，就是将某个进程的thread_struct里面的寄存器的值，写入到CPU的TR指向的tss_struct，对于CPU来讲，这就算是完成了切换。

例如__switch_to中的load_sp0，就是将下一个进程的thread_struct的sp0的值加载到tss_struct里面去。

## 指令指针的保存与恢复

你是不是觉得，这样真的就完成切换了吗？是的，不信我们来**盘点**一下。

从进程A切换到进程B，用户栈要不要切换呢？当然要，其实早就已经切换了，就在切换内存空间的时候。每个进程的用户栈都是独立的，都在内存空间里面。

那内核栈呢？已经在__switch_to里面切换了，也就是将current_task指向当前的task_struct。里面的void *stack指针，指向的就是当前的内核栈。

内核栈的栈顶指针呢？在__switch_to_asm里面已经切换了栈顶指针，并且将栈顶指针在__switch_to加载到了TSS里面。

用户栈的栈顶指针呢？如果当前在内核里面的话，它当然是在内核栈顶部的pt_regs结构里面呀。当从内核返回用户态运行的时候，pt_regs里面有所有当时在用户态的时候运行的上下文信息，就可以开始运行了。

唯一让人不容易理解的是指令指针寄存器，它应该指向下一条指令的，那它是如何切换的呢？这里有点绕，请你仔细看。

这里我先明确一点，进程的调度都最终会调用到__schedule函数。为了方便你记住，我姑且给它起个名字，就叫“**进程调度第一定律**”。后面我们会多次用到这个定律，你一定要记住。

我们用最前面的例子仔细分析这个过程。本来一个进程A在用户态是要写一个文件的，写文件的操作用户态没办法完成，就要通过系统调用到达内核态。在这个切换的过程中，用户态的指令指针寄存器是保存在pt_regs里面的，到了内核态，就开始沿着写文件的逻辑一步一步执行，结果发现需要等待，于是就调用__schedule函数。

这个时候，进程A在内核态的指令指针是指向__schedule了。这里请记住，A进程的内核栈会保存这个__schedule的调用，而且知道这是从btrfs_wait_for_no_snapshoting_writes这个函数里面进去的。

__schedule里面经过上面的层层调用，到达了context_switch的最后三行指令（其中barrier语句是一个编译器指令，用于保证switch_to和finish_task_switch的执行顺序，不会因为编译阶段优化而改变，这里咱们可以忽略它）。

```
switch_to(prev, next, prev);
barrier();
return finish_task_switch(prev);

```

当进程A在内核里面执行switch_to的时候，内核态的指令指针也是指向这一行的。但是在switch_to里面，将寄存器和栈都切换到成了进程B的，唯一没有变的就是指令指针寄存器。当switch_to返回的时候，指令指针寄存器指向了下一条语句finish_task_switch。

但这个时候的finish_task_switch已经不是进程A的finish_task_switch了，而是进程B的finish_task_switch了。

这样合理吗？你怎么知道进程B当时被切换下去的时候，执行到哪里了？恢复B进程执行的时候一定在这里呢？这时候就要用到咱的“进程调度第一定律”了。

当年B进程被别人切换走的时候，也是调用__schedule，也是调用到switch_to，被切换成为C进程的，所以，B进程当年的下一个指令也是finish_task_switch，这就说明指令指针指到这里是没有错的。

接下来，我们要从finish_task_switch完毕后，返回__schedule的调用了。返回到哪里呢？按照函数返回的原理，当然是从内核栈里面去找，是返回到btrfs_wait_for_no_snapshoting_writes吗？当然不是了，因为btrfs_wait_for_no_snapshoting_writes是在A进程的内核栈里面的，它早就被切换走了，应该从B进程的内核栈里面找。

假设，B就是最前面例子里面调用tap_do_read读网卡的进程。它当年调用__schedule的时候，是从tap_do_read这个函数调用进去的。

当然，B进程的内核栈里面放的是tap_do_read。于是，从__schedule返回之后，当然是接着tap_do_read运行，然后在内核运行完毕后，返回用户态。这个时候，B进程内核栈的pt_regs也保存了用户态的指令指针寄存器，就接着在用户态的下一条指令开始运行就可以了。

假设，我们只有一个CPU，从B切换到C，从C又切换到A。在C切换到A的时候，还是按照“进程调度第一定律”，C进程还是会调用__schedule到达switch_to，在里面切换成为A的内核栈，然后运行finish_task_switch。

这个时候运行的finish_task_switch，才是A进程的finish_task_switch。运行完毕从__schedule返回的时候，从内核栈上才知道，当年是从btrfs_wait_for_no_snapshoting_writes调用进去的，因而应该返回btrfs_wait_for_no_snapshoting_writes继续执行，最后内核执行完毕返回用户态，同样恢复pt_regs，恢复用户态的指令指针寄存器，从用户态接着运行。

到这里你是不是有点理解为什么switch_to有三个参数呢？为啥有两个prev呢？其实我们从定义就可以看到。

```
#define switch_to(prev, next, last)					\
do {									\
	prepare_switch_to(prev, next);					\
									\
	((last) = __switch_to_asm((prev), (next)));			\
} while (0)

```

在上面的例子中，A切换到B的时候，运行到__switch_to_asm这一行的时候，是在A的内核栈上运行的，prev是A，next是B。但是，A执行完__switch_to_asm之后就被切换走了，当C再次切换到A的时候，运行到__switch_to_asm，是从C的内核栈运行的。这个时候，prev是C，next是A，但是__switch_to_asm里面切换成为了A当时的内核栈。

还记得当年的场景“prev是A，next是B”，__switch_to_asm里面return prev的时候，还没return的时候，prev这个变量里面放的还是C，因而它会把C放到返回结果中。但是，一旦return，就会弹出A当时的内核栈。这个时候，prev变量就变成了A，next变量就变成了B。这就还原了当年的场景，好在返回值里面的last还是C。

通过三个变量switch_to(prev = A, next=B, last=C)，A进程就明白了，我当时被切换走的时候，是切换成B，这次切换回来，是从C回来的。

## 总结时刻

这一节我们讲主动调度的过程，也即一个运行中的进程主动调用__schedule让出CPU。在__schedule里面会做两件事情，第一是选取下一个进程，第二是进行上下文切换。而上下文切换又分用户态进程空间的切换和内核态的切换。

<img src="https://static001.geekbang.org/resource/image/9f/64/9f4433e82c78ed5cd4399b4b116a9064.png" alt="">

## 课堂练习

你知道应该用什么命令查看进程的运行时间和上下文切换次数吗？

欢迎留言和我分享你的疑惑和见解，也欢迎你收藏本节内容，反复研读。你也可以把今天的内容分享给你的朋友，和他一起学习、进步。


