深入理解linux进程切换
调度时机
进程的调度切换可以分为被动调度和主动调度。
被动调度主要与系统调用和中断相关。
系统调用<>用户空间
中断<>内核空间
主动调用是进程自己放弃cpu,调用调度函数。
调度函数
Linux内核通过schedule函数实现进程调度,schedule函数负责在运行队列中选择一个进程,然后把它切换到CPU上执行。
… set_current_state(TASK_INTERRUPTIBLE); schedule() …
schedule源码如下
void __sched schedule(void)
{
struct task_struct *tsk = current;
...
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}
schedule中调用_schedule进行实际上的调度。
通过检查need_resched()标记来决定是否调用schedule。
_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->curr;//现在的将变成过往
schedule_debug(prev, preempt);//此函数检查当前调用schedule是否合适,如不能在原子上下文(即抢占被关闭的情况下)中调用schedule,栈崩溃等.
if (sched_feat(HRTICK))
hrtick_clear(rq);
local_irq_disable();
rcu_note_context_switch(preempt);//通知RCU的相关操作
/*
* Make sure that signal_pending_state()->signal_pending() below
* can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
* done by the caller to avoid the race with signal_wake_up().
*
* The membarrier system call requires a full memory barrier
* after coming from user-space, before storing to rq->curr.
*/
rq_lock(rq, &rf);
smp_mb__after_spinlock();
/* Promote REQ to ACT */
rq->clock_update_flags <<= 1;//为什么要修改clock_update_flags呢?
update_rq_clock(rq);
switch_count = &prev->nivcsw; //根据上下文,switch_count此时指向的是非自愿切换计数器
if (!preempt && prev->state) {//如果不是被抢占的,且task处于非running状态
if (signal_pending_state(prev->state, prev)) {//如果有signal pending,则重新将prev task置为running状态
prev->state = TASK_RUNNING;
} else { //否则做dequeue的操作.
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
if (prev->in_iowait) { //如果prev task是在iowait状态,则增加当前runqueue iowait计数
atomic_inc(&rq->nr_iowait);
delayacct_blkio_start();
}
}
switch_count = &prev->nvcsw;//因为经过判断是自愿切换的,所以,将计数器指针指向自愿切换计数器
}
next = pick_next_task(rq, prev, &rf);//选择新的task出来
clear_tsk_need_resched(prev);//因为可能prev task是设置了need_reschedule,所以,重新置位
clear_preempt_need_resched();
if (likely(prev != next)) {
rq->nr_switches++;//nr_switches表示自愿与非自愿的总数
/*
* RCU users of rcu_dereference(rq->curr) may not see
* changes to task_struct made by pick_next_task().
*/
RCU_INIT_POINTER(rq->curr, next);
/*
* The membarrier system call requires each architecture
* to have a full memory barrier after updating
* rq->curr, before returning to user-space.
*
* Here are the schemes providing that barrier on the
* various architectures:
* - mm ? switch_mm() : mmdrop() for x86, s390, sparc, PowerPC.
* switch_mm() rely on membarrier_arch_switch_mm() on PowerPC.
* - finish_lock_switch() for weakly-ordered
* architectures where spin_unlock is a full barrier,
* - switch_to() for arm64 (weakly-ordered, spin_unlock
* is a RELEASE barrier),
*/
++*switch_count;
trace_sched_switch(preempt, prev, next);//这个trace吐出来的信息可以反映出一些信息,需要研究一下.
/* Also unlocks the rq: */
rq = context_switch(rq, prev, next, &rf);
} else {
rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
rq_unlock_irq(rq, &rf);
}
balance_callback(rq);//做一些回调动作.
}
该函数可以进行如下抽象
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
...
next = pick_next_task(rq, prev, &rf);
...
rq = context_switch(rq, prev, next, &rf);
...
}
进程执行环境的切换大致分为两大步,一是从就绪队列中选择一个进程(pick_next_task),也就是由进程调度算法决定选择哪一个进程作为下一个进程(next);二是完成进程上下文切换context_switch,进程上下文包含了进程执行需要的所有信息。
如何选择下一个进程——调度策略
根据进程的不同分类,Linux采用不同的调度策略。早期很多用户共享同一台小型机,调度算法追求吞吐率、利用率、公平性;现在的个人电脑更强调人机交互响应速度;而很多自动控制场合使用的嵌入式系统更强调实时性。当前Linux系统的解决方案是,对于实时进程,Linux采用FIFO(先进先出)或者Round Robin(时间片轮转)的调度策略。对其他进程,当前Linux采用CFS(Completely Fair Scheduler)调度器,核心思想是“完全公平”。这个设计理念不仅大大简化了调度器的代码复杂度,还对各种调度需求的提供了更完美支持。
Linux系统中常用的几种调度策略为SCHED_NORMAL、SCHED_FIFO、SCHED_RR、SCHED_BATCH。其中SCHED_NORMAL是用于普通进程的调度类,而SCHED_FIFO和SCHED_RR是用于实时进程的调度类,优先级高于SCHED_NORMAL。内核中根据进程的优先级来区分普通进程与实时进程,Linux内核进程优先级为0~139,数值越高,优先级越低,0为最高优先级。实时进程的优先级取值为0~99;而普通进程只具有nice值,nice值映射到优先级为100~139。子进程会继承父进程的优先级。对于实时进程,Linux系统会尽量使其调度延时在一个时间期限内,但是不能保证总是如此,不过正常情况下已经可以满足比较严格的时间要求了。
实时进程:SCHED_FIFO、SCHED_RR
普通进程:SCHED_NORMAL—>CFS(after 2.6)
CFS即为完全公平调度算法,其基本原理是基于权重的动态优先级调度算法。每个进程使用CPU的顺序由进程已使用的CPU虚拟时间(vruntime)决定,已使用的虚拟时间越少,进程排序就越靠前,进程再次被调度执行的概率也就越高。每个进程每次占用CPU后能够执行的时间(ideal_runtime)由进程的权重决定,并且保证在某个时间周期(__sched_period)内运行队列里的所有进程都能够至少被调度执行一次
进程切换的实质——进程上下文切换
进程上下文切换通过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)
{
prepare_task_switch(rq, prev, next);
...
/*
* kernel -> kernel lazy + transfer active
* user -> kernel lazy + mmgrab() active
*
* kernel -> user switch + mmdrop() active
* user -> user switch
*/
if (!next->mm) { // to kernel
...
} else { // to user
...
}
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();
return finish_task_switch(prev);
}
最核心的是几个关键寄存器的保存与变换。
- 进程页目录表(页表),即地址空间、数据。
-
内核堆栈栈顶寄存器sp代表进程内核堆栈(保存函数调用历史),进程描述符(最后的成员thread是关键)和内核堆栈存储于连续存取区域中,进程描述符存在内核堆栈的低地址,栈从高地址向低地址增长,因此通过栈顶指针寄存器还可以获取进程描述符的起始地址。
-
指令指针寄存器代表进程的CPU上下文,即要执行的下条指令地址。
这些寄存器从一个进程的状态切换到另一个进程的状态,进程切换的关键上下文就算完成了。