日期 | 内核版本 | 架构 | 作者 | GitHub | CSDN |
---|---|---|---|---|---|
2016-06-30 | Linux-4.6 | X86 & arm | gatieme | LinuxDeviceDrivers | Linux进程管理与调度 |
我们前面提到linux有两种方法激活调度器:核心调度器和
一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU
另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要
因而内核提供了两个调度器主调度器,周期性调度器,分别实现如上工作, 两者合在一起就组成了核心调度器(core scheduler), 也叫通用调度器(generic scheduler).
他们都根据进程的优先级分配CPU时间, 因此这个过程就叫做优先调度, 我们将在本节主要讲解周期调度的设计和实现方式
在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule, 从系统调用返回后, 内核也会检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED
内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来.
调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换.
linux把进程区分为实时进程和非实时进程, 其中非实时进程进一步划分为交互式进程和批处理进程
根据进程的不同分类Linux采用不同的调度策略.
对于实时进程,采用FIFO, Round Robin或者Earliest Deadline First (EDF)最早截止期限优先调度算法|的调度策略.
字段 | 版本 |
---|---|
O(n)的始调度算法 | linux-0.11~2.4 |
O(1)调度器 | linux-2.5 |
CFS调度器 | linux-2.6~至今 |
2个调度器
可以用两种方法来激活调度
一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU
另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要
因此当前linux的调度程序由两个调度器组成:主调度器,周期性调度器(两者又统称为通用调度器(generic scheduler)或核心调度器(core scheduler))
并且每个调度器包括两个内容:调度框架(其实质就是两个函数框架)及调度器类
6种调度策略
linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能
SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程
SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程
SCHED_IDLE则在系统空闲时调用idle进程.
5个调度器类
而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程.
其所属进程的优先级顺序为
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
3个调度实体
调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度.
这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组.
linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体
sched_dl_entity 采用EDF算法调度的实时调度实体
sched_rt_entity 采用Roound-Robin或者FIFO算法调度的实时调度实体 rt_sched_class
sched_entity 采用CFS算法调度的普通非实时进程的调度实体
在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule, 从系统调用返回后, 内核也会检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED
例如, 前述的周期性调度器的scheduler_tick就会设置该标志, 如果是这样则内核会调用schedule, 该函数假定当前活动进程一定会被另一个进程取代.
在详细论述schedule之前, 需要说明一下__sched前缀, 该前缀可能用于调用schedule的函数, 包括schedule本身.
__sched前缀的声明, 在include/linux/sched.h, L416, 如下所示
/* Attach to any functions which should be ignored in wchan output. */
#define __sched __attribute__((__section__(".sched.text")))
attribute((_section(“…”)))是一个gcc的编译属性, 其目的在于将相关的函数的代码编译之后, 放到目标文件的以恶搞特定的段内, 即.sched.text中. 该信息使得内核在显示栈转储活类似信息时, 忽略所有与调度相关的调用. 由于调度哈书调用不是普通代码流程的一部分, 因此在这种情况下是没有意义的.
用它修饰函数的方式如下
void __sched some_function(args, ...)
{
......
schedule();
......
}
schedule就是主调度器的函数, 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule.
该函数完成如下工作
确定当前就绪队列, 并在保存一个指向当前(仍然)活动进程的task_struct指针
检查死锁, 关闭内核抢占后调用__schedule完成内核调度
恢复内核抢占, 然后检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED, 如果该进程被其他进程设置了TIF_NEED_RESCHED标志, 则函数重新执行进行调度
该函数定义在kernel/sched/core.c, L3243, 如下所示
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()); /* 如果该进程被其他进程设置了TIF_NEED_RESCHED标志,则函数重新执行进行调度 */
}
EXPORT_SYMBOL(schedule);
该函数定义在kernel/sched/core.c, L3231, 如下所示
static inline void sched_submit_work(struct task_struct *tsk)
{
/* 检测tsk->state是否为0 (runnable), 若为运行态时则返回,
* tsk_is_pi_blocked(tsk),检测tsk的死锁检测器是否为空,若非空的话就return
if (!tsk->state || tsk_is_pi_blocked(tsk))
return;
/*
* If we are going to sleep and we have plugged IO queued,
* make sure to submit it to avoid deadlocks.
*/
if (blk_needs_flush_plug(tsk)) /* 然后检测是否需要刷新plug队列,用来避免死锁 */
blk_schedule_flush_plug(tsk);
}
内核抢占
Linux除了内核态外还有用户态。用户程序的上下文属于用户态,系统调用和中断处理例程上下文属于内核态. 如果一个进程在用户态时被其他进程抢占了COU则成发生了用户态抢占, 而如果此时进程进入了内核态, 则内核此时代替进程执行, 如果此时发了抢占, 我们就说发生了内核抢占.
内核抢占是Linux 2.6以后引入的一个重要的概念
我们说:如果进程正执行内核函数时,即它在内核态运行时,允许发生内核切换(被替换的进程是正执行内核函数的进程),这个内核就是抢占的。
抢占内核的主要特点是:一个在内核态运行的进程,当且仅当在执行内核函数期间被另外一个进程取代。
这与用户态的抢占有本质区别.
内核为了支撑内核抢占, 提供了很多机制和结构, 必要时候开关内核抢占也是必须的, 这些函数定义在include/linux/preempt.h, L145
#define preempt_disable() \
do { \
preempt_count_inc(); \
barrier(); \
} while (0)
#define sched_preempt_enable_no_resched() \
do { \
barrier(); \
preempt_count_dec(); \
} while (0)
__schedule完成了真正的调度工作, 其定义在kernel/sched/core.c, L3103, 如下所示
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
/* ==1==
找到当前cpu上的就绪队列rq
并将正在运行的进程curr保存到prev中 */
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
/*
* do_exit() calls schedule() with preemption disabled as an exception;
* however we must fix that up, otherwise the next task will see an
* inconsistent (higher) preempt count.
*
* It also avoids the below schedule_debug() test from complaining
* about this.
*/
if (unlikely(prev->state == TASK_DEAD))
preempt_enable_no_resched_notrace();
/* 如果禁止内核抢占,而又调用了cond_resched就会出错
* 这里就是用来捕获该错误的 */
schedule_debug(prev);
if (sched_feat(HRTICK))
hrtick_clear(rq);
/* 关闭本地中断 */
local_irq_disable();
/* 更新全局状态,
* 标识当前CPU发生上下文的切换 */
rcu_note_context_switch();
/*
* 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().
*/
smp_mb__before_spinlock();
/* 锁住该队列 */
raw_spin_lock(&rq->lock);
lockdep_pin_lock(&rq->lock);
rq->clock_skip_update <<= 1; /* promote REQ to ACT */
/* 切换次数记录, 默认认为非主动调度计数(抢占) */
switch_count = &prev->nivcsw;
/*
* scheduler检查prev的状态state和内核抢占表示
* 如果prev是不可运行的, 并且在内核态没有被抢占
*
* 此时当前进程不是处于运行态, 并且不是被抢占
* 此时不能只检查抢占计数
* 因为可能某个进程(如网卡轮询)直接调用了schedule
* 如果不判断prev->stat就可能误认为task进程为RUNNING状态
* 到达这里,有两种可能,一种是主动schedule, 另外一种是被抢占
* 被抢占有两种情况, 一种是时间片到点, 一种是时间片没到点
* 时间片到点后, 主要是置当前进程的need_resched标志
* 接下来在时钟中断结束后, 会preempt_schedule_irq抢占调度
*
* 那么我们正常应该做的是应该将进程prev从就绪队列rq中删除,
* 但是如果当前进程prev有非阻塞等待信号,
* 并且它的状态是TASK_INTERRUPTIBLE
* 我们就不应该从就绪队列总删除它
* 而是配置其状态为TASK_RUNNING, 并且把他留在rq中
/* 如果内核态没有被抢占, 并且内核抢占有效
即是否同时满足以下条件:
1 该进程处于停止状态
2 该进程没有在内核态被抢占 */
if (!preempt && prev->state)
{
/* 如果当前进程有非阻塞等待信号,并且它的状态是TASK_INTERRUPTIBLE */
if (unlikely(signal_pending_state(prev->state, prev)))
{
/* 将当前进程的状态设为:TASK_RUNNING */
prev->state = TASK_RUNNING;
}
else /* 否则需要将prev进程从就绪队列中删除*/
{
/* 将当前进程从runqueue(运行队列)中删除 */
deactivate_task(rq, prev, DEQUEUE_SLEEP);
/* 标识当前进程不在runqueue中 */
prev->on_rq = 0;
/*
* If a worker went to sleep, notify and ask workqueue
* whether it wants to wake up a task to maintain
* concurrency.
*/
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
/* 如果不是被抢占的,就累加主动切换次数 */
switch_count = &prev->nvcsw;
}
/* 如果prev进程仍然在就绪队列上没有被删除 */
if (task_on_rq_queued(prev))
update_rq_clock(rq); /* 跟新就绪队列的时钟 */
/* 挑选一个优先级最高的任务将其排进队列 */
next = pick_next_task(rq, prev);
/* 清除pre的TIF_NEED_RESCHED标志 */
clear_tsk_need_resched(prev);
/* 清楚内核抢占标识 */
clear_preempt_need_resched();
rq->clock_skip_update = 0;
/* 如果prev和next非同一个进程 */
if (likely(prev != next))
{
rq->nr_switches++; /* 队列切换次数更新 */
rq->curr = next; /* 将next标记为队列的curr进程 */
++*switch_count; /* 进程切换次数更新 */
trace_sched_switch(preempt, prev, next);
/* 进程之间上下文切换 */
rq = context_switch(rq, prev, next); /* unlocks the rq */
}
else /* 如果prev和next为同一进程,则不进行进程切换 */
{
lockdep_unpin_lock(&rq->lock);
raw_spin_unlock_irq(&rq->lock);
}
balance_callback(rq);
}
STACK_FRAME_NON_STANDARD(__schedule); /* switch_to() */
内核从cpu的就绪队列中选择一个最合适的进程来抢占CPU
next = pick_next_task(rq);
全局的pick_next_task函数会从按照优先级遍历所有调度器类的pick_next_task函数, 去查找最优的那个进程, 当然因为大多数情况下, 系统中全是CFS调度的非实时进程, 因而linux内核也有一些优化的策略
其执行流程如下
如果当前cpu上所有的进程都是cfs调度的普通非实时进程, 则直接用cfs调度, 如果无程序可调度则调度idle进程
否则从优先级最高的调度器类sched_class_highest(目前是stop_sched_class)开始依次遍历所有调度器类的pick_next_task函数, 选择最优的那个进程执行
其定义在kernel/sched/core.c, line 3068, 如下所示
/*
* Pick up the highest-prio task:
*/
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
const struct sched_class *class = &fair_sched_class;
struct task_struct *p;
/*
* Optimization: we know that if all tasks are in
* the fair class we can call that function directly:
*
* 如果待被调度的进程prev是隶属于CFS的普通非实时进程
* 而当前cpu的全局就绪队列rq中的进程数与cfs_rq的进程数相等
* 则说明当前cpu上的所有进程都是由cfs调度的普通非实时进程
*
* 那么我们选择最优进程的时候
* 就只需要调用cfs调度器类fair_sched_class的选择函数pick_next_task
* 就可以找到最优的那个进程p
*/
/* 如果当前所有的进程都被cfs调度, 没有实时进程 */
if (likely(prev->sched_class == class &&
rq->nr_running == rq->cfs.h_nr_running))
{
/* 调用cfs的选择函数pick_next_task找到最优的那个进程p*/
p = fair_sched_class.pick_next_task(rq, prev);
/* #define RETRY_TASK ((void *)-1UL)有被其他调度气找到合适的进程 */
if (unlikely(p == RETRY_TASK))
goto again; /* 则遍历所有的调度器类找到最优的进程 */
/* assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p)) /* 如果没有进程可被调度 */
p = idle_sched_class.pick_next_task(rq, prev); /* 则调度idle进程 */
return p;
}
/* 进程中所有的调度器类, 是通过next域链接域链接在一起的
* 调度的顺序为stop -> dl -> rt -> fair -> idle
* again出的循环代码会遍历他们找到一个最优的进程 */
again:
for_each_class(class)
{
p = class->pick_next_task(rq, prev);
if (p)
{
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
BUG(); /* the idle class will always have a runnable task */
}
````
进程中所有的调度器类, 是通过next域链接域链接在一起的, 调度的顺序为
class="se-preview-section-delimiter">
```c
stop -> dl -> rt -> fair -> idle
其中for_each_class遍历所有的调度器类, 依次执行pick_next_task操作选择最优的进程
它会从优先级最高的sched_class_highest(目前是stop_sched_class)查起, 依次按照调度器类的优先级从高到低的顺序调用调度器类对应的pick_next_task_fair函数直到查找到一个能够被调度的进程
for_each_class定义在kernel/sched/sched.h, 如下所示
#define sched_class_highest (&stop_sched_class)
#define for_each_class(class) \
for (class = sched_class_highest; class; class = class->next)
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;
除了全局的pick_next_task函数, 每个调度器类都提供了pick_next_task函数用以查找对应调度器下的最优进程, 其定义如下所示
调度器类 | pick_next策略 | pick_next_task_fair函数 |
---|---|---|
stop_sched_class | kernel/sched/stop_task.c, line 121, pick_next_task_stop | |
dl_sched_class | kernel/sched/deadline.c, line 1782, pick_next_task_dl | |
rt_sched_class | 取出合适的进程后, dequeue_pushable_task从pushable队列里取出来 | /kernel/sched/rt.c, line 1508, pick_next_task_rt |
fail_sched_class | pick_next_task_fair,从红黑树里,选出vtime最小的那个进程,调用set_next_entity将其出队 | kernel/sched/fair.c, line 5441, pick_next_task_fail |
idle_sched_class | 直接调度idle进程 | kernel/sched/idle_task.c, line 26, pick_next_task_idle |
实际上,对于RT进程,put和pick并不操作运行队列
对于FIFO和RR的区别,在scheduler_tick中通过curr->sched_class->task_tick进入到task_tick_rt的处理, 如果是非RR的进程则直接返回,否则递减时间片,如果时间片耗完,则需要将当前进程放到运行队列的末尾, 这个时候才操作运行队列(FIFO和RR进程,是否位于同一个plist队列?),时间片到点,会重新移动当前进程requeue_task_rt,进程会被加到队列尾,接下来set_tsk_need_resched触发调度,进程被抢占进入schedule
问题1 : 为什么要多此一举判断所有的进程是否全是cfs调度的普通非实时进程?
加快经常性事件, 是程序开发中一个优化的准则, 那么linux系统中最普遍的进程是什么呢? 肯定是非实时进程啊, 其调度器必然是cfs, 因此
rev->sched_class == class && rq->nr_running == rq->cfs.h_nr_running
这种情形发生的概率是很大的, y也就是说多数情形下, 我们的linux中进程全是cfs调度的
而likely这个宏业表明了这点, 这也是gcc内建的一个编译选项, 它其实就是告诉编译器表达式很大的情况下为真, 编译器可以对此做出优化
// http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.6#L91
#ifndef likely
# define likely(x) (__builtin_expect(!!(x), 1))
#endif
#ifndef unlikely
# define unlikely(x) (__builtin_expect(!!(x), 0))
#endif
进程上下文的切换其实是一个很复杂的过程, 我们在这里不能详述, 但是我会尽可能说明白
具体的内容请参照
上下文切换(有时也称做进程切换或任务切换)是指CPU从一个进程或线程切换到另一个进程或线程
稍微详细描述一下,上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行以下的活动:
挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处,
在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复
跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程
因此上下文是指某一时间点CPU寄存器和程序计数器的内容, 广义上还包括内存中进程的虚拟地址映射信息.
上下文切换只能发生在内核态中, 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少.
context_switch函数完成了进程上下文的切换, 其定义在kernel/sched/core.c#L2711
context_switch( )函数建立next进程的地址空间。进程描述符的active_mm字段指向进程所使用的内存描述符,而mm字段指向进程所拥有的内存描述符。对于一般的进程,这两个字段有相同的地址,但是,内核线程没有它自己的地址空间而且它的 mm字段总是被设置为 NULL
context_switch( )函数保证:如果next是一个内核线程, 它使用prev所使用的地址空间
它主要执行如下操作
调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中
调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息
由于不同架构下地址映射的机制有所区别, 而寄存器等信息弊病也是依赖于架构的, 因此switch_mm和switch_to两个函数均是体系结构相关的
switch_mm主要完成了进程prev到next虚拟地址空间的映射, 由于内核虚拟地址空间是不许呀切换的, 因此切换的主要是用户态的虚拟地址空间
这个是一个体系结构相关的函数, 其实现在对应体系结构下的arch/对应体系结构/include/asm/mmu_context.h文件中, 我们下面列出了几个常见体系结构的实现
体系结构 | switch_mm实现 |
---|---|
x86 | arch/x86/include/asm/mmu_context.h, line 118 |
arm | arch/arm/include/asm/mmu_context.h, line 126 |
arm64 | arch/arm64/include/asm/mmu_context.h, line 183 |
其主要工作就是切换了进程的CR3
控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性
CR0中含有控制处理器操作模式和状态的系统控制标志;
CR1保留不用;
CR2含有导致页错误的线性地址;
CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)。
执行环境的切换是在switch_to()中完成的, switch_to完成最终的进程切换,它保存原进程的所有寄存器信息,恢复新进程的所有寄存器信息,并执行新的进程
调度过程可能选择了一个新的进程, 而清理工作则是针对此前的活动进程, 请注意, 这不是发起上下文切换的那个进程, 而是系统中随机的某个其他进程, 内核必须想办法使得进程能够与context_switch例程通信, 这就可以通过switch_to宏实现. 因此switch_to函数通过3个参数提供2个变量,
在新进程被选中时, 底层的进程切换冽程必须将此前执行的进程提供给context_switch, 由于控制流会回到陔函数的中间, 这无法用普通的函数返回值来做到, 因此提供了3个参数的宏
/*
* Saving eflags is important. It switches not only IOPL between tasks,
* it also protects other tasks from NT leaking through sysenter etc.
*/
#define switch_to(prev, next, last)
体系结构 | switch_to实现 |
---|---|
x86 | arch/x86/include/asm/switch_to.h中两种实现 定义CONFIG_X86_32宏 未定义CONFIG_X86_32宏 |
arm | arch/arm/include/asm/switch_to.h, line 25 |
通用 | include/asm-generic/switch_to.h, line 25 |
内核在switch_to中执行如下操作
进程切换, 即esp的切换, 由于从esp可以找到进程的描述符
硬件上下文切换, 设置ip寄存器的值, 并jmp到__switch_to函数
堆栈的切换, 即ebp的切换, ebp是栈底指针, 它确定了当前用户空间属于哪个进程
内核在即将返回用户空间时检查进程是否需要重新调度,如果设置了,就会发生调度, 这被称为用户抢占, 因此内核在thread_info的flag中设置了一个标识来标志进程是否需要重新调度, 即重新调度need_resched标识TIF_NEED_RESCHED
并提供了一些设置可检测的函数
函数 | 描述 | 定义 |
---|---|---|
set_tsk_need_resched | 设置指定进程中的need_resched标志 | include/linux/sched.h, L2920 |
clear_tsk_need_resched | 清除指定进程中的need_resched标志 | include/linux/sched.h, L2926 |
test_tsk_need_resched | 检查指定进程need_resched标志 | include/linux/sched.h, L2931 |
而我们内核中调度时常用的need_resched()函数检查进程是否需要被重新调度其实就是通过test_tsk_need_resched实现的, 其定义如下所示
// http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L3093
static __always_inline bool need_resched(void)
{
return unlikely(tif_need_resched());
}
// http://lxr.free-electrons.com/source/include/linux/thread_info.h?v=4.6#L106
#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)
当内核即将返回用户空间时, 内核会检查need_resched是否设置,如果设置,则调用schedule(),此时,发生用户抢占。
一般来说,用户抢占发生几下情况
从系统调用返回用户空间
从中断(异常)处理程序返回用户空间
当kerne(系统调用或者中断都在kernel中)l返回用户态时,系统可以安全的执行当前的任务,或者切换到另外一个任务.
当中断处理例程或者系统调用完成后, kernel返回用户态时, need_resched标志的值会被检查, 假如它为1, 调度器会选择一个新的任务并执行. 中断和系统调用的返回路径(return path)的实现在entry.S中(entry.S不仅包括kernel entry code,也包括kernel exit code)。
抢占时伴随着schedule()的执行, 因此内核提供了一个TIF_NEED_RESCHED标志来表明是否要用schedule()调度一次
根据抢占发生的时机分为用户抢占和内核抢占。
用户抢占发生在内核即将返回到用户空间的时候。内核抢占发生在返回内核空间的时候。
抢占类型 | 描述 | 抢占发生时机 |
---|---|---|
用户抢占 | 内核在即将返回用户空间时检查进程是否设置了TIF_NEED_RESCHED标志,如果设置了,就会发生用户抢占. | 从系统调用或中断处理程序返回用户空间的时候 |
内核抢占 | 在不支持内核抢占的内核中,内核进程如果自己不主动停止,就会一直的运行下去。无法响应实时进程. 抢占内核虽然牺牲了上下文切换的开销, 但获得 了更大的吞吐量和响应时间 2.6的内核添加了内核抢占,同时为了某些地方不被抢占,又添加了自旋锁. 在进程的thread_info结构中添加了preempt_count该数值为0,当进程使用一个自旋锁时就加1,释放一个自旋锁时就减1. 为0时表示内核可以抢占. |
从中断处理程序返回内核空间时,内核会检查preempt_count和TIF_NEED_RESCHED标志,如果进程设置了 TIF_NEED_RESCHED标志,并且preempt_count为0,发生内核抢占 2. 当内核再次用于可抢占性的时候,当进程所有的自旋锁都释 放了,释放程序会检查TIF_NEED_RESCHED标志,如果设置了就会调用schedule 3. 显示调用schedule时 4. 内核中的进程被堵塞的时候 |
schedule就是主调度器的函数, 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule, 该函数定义在kernel/sched/core.c, L3243, 如下所示
该函数完成如下工作
确定当前就绪队列, 并在保存一个指向当前(仍然)活动进程的task_struct指针
检查死锁, 关闭内核抢占后调用__schedule完成内核调度
恢复内核抢占, 然后检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED, 如果该进程被其他进程设置了TIF_NEED_RESCHED标志, 则函数重新执行进行调度
do {
preempt_disable(); /* 关闭内核抢占 */
__schedule(false); /* 完成调度 */
sched_preempt_enable_no_resched(); /* 开启内核抢占 */
} while (need_resched()); /* 如果该进程被其他进程设置了TIF_NEED_RESCHED标志,则函数重新执行进行调度 */
完成一些必要的检查, 并设置进程状态, 处理进程所在的就绪队列
调度全局的pick_next_task选择抢占的进程
如果当前cpu上所有的进程都是cfs调度的普通非实时进程, 则直接用cfs调度, 如果无程序可调度则调度idle进程
否则从优先级最高的调度器类sched_class_highest(目前是stop_sched_class)开始依次遍历所有调度器类的pick_next_task函数, 选择最优的那个进程执行
context_switch完成进程上下文切换
调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中
调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息
内核在完成调度的过程中总是先关闭内核抢占, 等待内核完成调度的工作后, 再把内核抢占开启, 如果在内核完成调度器过程中, 这时候如果发生了内核抢占, 我们的调度会被中断, 而调度却还没有完成, 这样会丢失我们调度的信息.
而同样我们可以看到, 在调度完成后, 内核会去判断need_resched条件, 如果这个时候为真, 内核会重新进程一次调度, 此次调度由于发生在内核态因此仍然是一次内核抢占
need_resched条件其实是判断need_resched标识TIF_NEED_RESCHED的值, 内核在thread_info的flag中设置了一个标识来标志进程是否需要重新调度, 即重新调度need_resched标识TIF_NEED_RESCHED, 内核在即将返回用户空间时会检查标识TIF_NEED_RESCHED标志进程是否需要重新调度,如果设置了,就会发生调度, 这被称为用户抢占,
而内核抢占是通过自旋锁preempt_count实现的, 同样当内核可以进行内核抢占的时候(比如从中断处理程序返回内核空间或内核中的进程被堵塞的时候),内核会检查preempt_count和TIF_NEED_RESCHED标志,如果进程设置了 TIF_NEED_RESCHED标志,并且preempt_count为0,发生内核抢占